diff --git a/docker-compose.yml b/docker-compose.yml index 0819a1f..070aacd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,6 +136,10 @@ services: - "traefik.http.routers.rspace-rswag.entrypoints=web" - "traefik.http.routers.rspace-rswag.priority=120" - "traefik.http.routers.rspace-rswag.service=rspace-online" + - "traefik.http.routers.rspace-rsocials.rule=Host(`rsocials.online`)" + - "traefik.http.routers.rspace-rsocials.entrypoints=web" + - "traefik.http.routers.rspace-rsocials.priority=120" + - "traefik.http.routers.rspace-rsocials.service=rspace-online" # Service configuration - "traefik.http.services.rspace-online.loadbalancer.server.port=3000" - "traefik.docker.network=traefik-public" diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts index f5bb6bd..54383f9 100644 --- a/modules/rsocials/mod.ts +++ b/modules/rsocials/mod.ts @@ -1,120 +1,199 @@ /** - * rSocials module — campaign strategy workflow builder. + * Socials module — federated social feed aggregator. * - * Proxies campaign API + embeds the Next.js campaign editor from rsocials:3000. - * Page routes render an iframe inside the rSpace shell so the campaign builder - * gets the full rSpace header/nav while the Next.js app runs independently. + * Aggregates and displays social media activity across community members. + * Supports ActivityPub, RSS, and manual link sharing. */ import { Hono } from "hono"; import { renderShell } from "../../server/shell"; -import type { RSpaceModule } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module"; - -const RSOCIALS_INTERNAL = process.env.RSOCIALS_URL || "http://rsocials:3000"; -const RSOCIALS_PUBLIC = "https://rsocials.online"; +import type { RSpaceModule } from "../../shared/module"; const routes = new Hono(); -// ── API proxy ──────────────────────────────────────────── -// Proxy all campaign API calls to the rsocials container - -routes.get("/api/campaigns", async (c) => { - const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns`); - return c.json(await res.json(), res.status as StatusCode); +// ── API: Health ── +routes.get("/api/health", (c) => { + return c.json({ ok: true, module: "rsocials" }); }); -routes.get("/api/campaigns/:id", async (c) => { - const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns/${c.req.param("id")}`); - return c.json(await res.json(), res.status as StatusCode); -}); - -routes.post("/api/campaigns", async (c) => { - const body = await c.req.text(); - const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body, +// ── API: Info ── +routes.get("/api/info", (c) => { + return c.json({ + module: "rsocials", + description: "Federated social feed aggregator for communities", + features: [ + "ActivityPub integration", + "RSS feed aggregation", + "Link sharing", + "Community timeline", + ], }); - return c.json(await res.json(), res.status as StatusCode); }); -routes.put("/api/campaigns/:id", async (c) => { - const body = await c.req.text(); - const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns/${c.req.param("id")}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body, +// ── API: Feed — community social timeline ── +routes.get("/api/feed", (c) => { + // Demo feed items + return c.json({ + items: [ + { + id: "demo-1", + type: "post", + author: "Alice", + content: "Just published our community governance proposal!", + source: "fediverse", + timestamp: new Date(Date.now() - 3600_000).toISOString(), + likes: 12, + replies: 3, + }, + { + id: "demo-2", + type: "link", + author: "Bob", + content: "Great article on local-first collaboration", + url: "https://example.com/local-first", + source: "shared", + timestamp: new Date(Date.now() - 7200_000).toISOString(), + likes: 8, + replies: 1, + }, + { + id: "demo-3", + type: "post", + author: "Carol", + content: "Welcome new members! Check out rSpace's tools in the app switcher above.", + source: "local", + timestamp: new Date(Date.now() - 14400_000).toISOString(), + likes: 24, + replies: 7, + }, + ], + demo: true, }); - return c.json(await res.json(), res.status as StatusCode); }); -routes.delete("/api/campaigns/:id", async (c) => { - const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns/${c.req.param("id")}`, { - method: "DELETE", - }); - return c.json(await res.json(), res.status as StatusCode); -}); - -// ── Page routes ────────────────────────────────────────── - -function campaignFrame(src: string, space: string, title: string) { - return renderShell({ - title, - moduleId: "rsocials", - spaceSlug: space, - modules: getModuleInfoList(), - theme: "dark", - styles: ``, - body: `
- -
`, - }); -} - -// /rsocials/ → campaign list +// ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; return c.html( - campaignFrame(`${RSOCIALS_PUBLIC}/campaigns`, space, `Campaigns | rSocials`) + renderShell({ + title: `${space} — Socials | rSpace`, + moduleId: "rsocials", + spaceSlug: space, + modules: getModuleInfoList(), + theme: "dark", + body: ` +
+
+

Community Feed

+

Social activity across your community

+
+
+
Loading feed…
+
+
+ `, + styles: ``, + }), ); }); -// /rsocials/campaign → campaign list (alias) -routes.get("/campaign", (c) => { - const space = c.req.param("space") || "demo"; - return c.html( - campaignFrame(`${RSOCIALS_PUBLIC}/campaigns`, space, `Campaigns | rSocials`) - ); -}); - -// /rsocials/campaign/:id → specific campaign editor -routes.get("/campaign/:id", (c) => { - const space = c.req.param("space") || "demo"; - const id = c.req.param("id"); - return c.html( - campaignFrame( - `${RSOCIALS_PUBLIC}/campaigns/${id}`, - space, - `Campaign Editor | rSocials` - ) - ); -}); - -type StatusCode = 200 | 201 | 204 | 400 | 401 | 403 | 404 | 409 | 500 | 502; - -export const rsocialsModule: RSpaceModule = { +export const socialsModule: RSpaceModule = { id: "rsocials", name: "rSocials", - icon: "\uD83D\uDCE2", - description: "Campaign strategy workflow builder", + icon: "\u{1F4E2}", + description: "Federated social feed aggregator for communities", routes, standaloneDomain: "rsocials.online", + feeds: [ + { + id: "social-feed", + name: "Social Feed", + kind: "data", + description: "Community social timeline — posts, links, and activity from connected platforms", + }, + ], + acceptsFeeds: ["data", "trust"], }; diff --git a/server/index.ts b/server/index.ts index 941acf4..88fea3b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -62,7 +62,7 @@ import { inboxModule } from "../modules/inbox/mod"; import { dataModule } from "../modules/data/mod"; import { splatModule } from "../modules/splat/mod"; import { photosModule } from "../modules/photos/mod"; -import { rsocialsModule } from "../modules/rsocials/mod"; +import { socialsModule } from "../modules/rsocials/mod"; import { spaces } from "./spaces"; import { renderShell, renderModuleLanding } from "./shell"; import { fetchLandingPage } from "./landing-proxy"; @@ -92,7 +92,7 @@ registerModule(inboxModule); registerModule(dataModule); registerModule(splatModule); registerModule(photosModule); -registerModule(rsocialsModule); +registerModule(socialsModule); // ── Config ── const PORT = Number(process.env.PORT) || 3000; @@ -928,17 +928,9 @@ const server = Bun.serve({ } } - // Root path → serve landing page (not the module app) + // Root path → redirect to rspace.online/{moduleId} landing page if (url.pathname === "/") { - const allModules = getAllModules(); - const mod = allModules.find((m) => m.id === standaloneModuleId); - if (mod) { - const html = renderModuleLanding({ - module: mod, - modules: getModuleInfoList(), - }); - return new Response(html, { headers: { "Content-Type": "text/html" } }); - } + return Response.redirect(`https://rspace.online/${standaloneModuleId}`, 302); } // Sub-paths: rewrite internally → /{space}/{moduleId}/...