From e17398d86dd118a82b23d4d60709875021f9ab98 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Fri, 9 Oct 2026 13:51:02 +0200 Subject: [PATCH] T04.f: backend Festipod ActivityPods + dockerisation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajoute la configuration et les services backend (app, actors, registration, attendees, contacts, catalog, core auth/nodeinfo/proxy/notification). - Corrige le démarrage : désactive resourcesWithContainerPath pour éviter le conflit entre l'URI de l'acteur applicatif (/as/actor/app) et la route /app. - Corrige l'URL du dépôt de shapes : https://shapes.activitypods.org/. - Ajoute backend/Dockerfile et docker-compose.yml racine (frontend + backend). - Met à jour .env.example racine avec les variables SEMAPPS_* et VITE_BACKEND_*. - Gates verts : bunx tsc --noEmit (0 erreur) et bun test (50 pass). --- .env.example | 40 ++++++++ backend/.env.example | 14 +++ backend/Dockerfile | 25 +++++ backend/config/config.js | 18 ++++ backend/moleculer.config.js | 62 ++++++++++++ backend/package.json | 7 +- backend/pnpm-lock.yaml | 15 +++ backend/services/app.service.js | 51 ++++++++++ backend/services/attendees.service.js | 17 ++++ backend/services/catalog.service.js | 41 ++++++++ backend/services/contacts.service.js | 17 ++++ backend/services/core/auth-account.service.js | 8 ++ backend/services/core/core.service.js | 53 ++++++++++ backend/services/core/nodeinfo.service.js | 30 ++++++ .../core/notification.listener.service.js | 11 +++ backend/services/core/proxy.service.js | 3 + backend/services/registration.service.js | 97 +++++++++++++++++++ docker-compose.yml | 28 ++++++ 18 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/config/config.js create mode 100644 backend/moleculer.config.js create mode 100644 backend/services/app.service.js create mode 100644 backend/services/attendees.service.js create mode 100644 backend/services/catalog.service.js create mode 100644 backend/services/contacts.service.js create mode 100644 backend/services/core/auth-account.service.js create mode 100644 backend/services/core/core.service.js create mode 100644 backend/services/core/nodeinfo.service.js create mode 100644 backend/services/core/notification.listener.service.js create mode 100644 backend/services/core/proxy.service.js create mode 100644 backend/services/registration.service.js create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example index 0191217..98f7690 100644 --- a/.env.example +++ b/.env.example @@ -43,6 +43,46 @@ PORT=3000 # NODE_ENV=production bascule `bun run start` (pas de HMR). En dev, laisser vide. NODE_ENV= +# ── Backend ActivityPods ─────────────────────────────────────────────────── +# URL publique du backend Festipod (écoute sur le port 3003). +SEMAPPS_HOME_URL=http://localhost:3003 + +# URL publique du frontend (utilisée par le backend pour les OIDC redirects, etc.) +SEMAPPS_FRONT_URL=http://localhost:5000 + +# Endpoint SPARQL du Fuseki auquel se connecte le backend. +SEMAPPS_SPARQL_ENDPOINT=http://localhost:3030/ + +# Dataset Jena/Fuseki principal utilisé par le backend. +SEMAPPS_MAIN_DATASET=settings + +# Identifiants Jena/Fuseki. +SEMAPPS_JENA_USER=admin +SEMAPPS_JENA_PASSWORD=admin + +# Redis : cache (db 3) et file des jobs (db 4). +SEMAPPS_REDIS_CACHE_URL=redis://localhost:6390/3 +SEMAPPS_QUEUE_SERVICE_URL=redis://localhost:6390/4 + +# Dataset utilisé par le service auth.account. +SEMAPPS_AUTH_ACCOUNTS_DATASET_NAME=settings + +# Dépôt de shapes/shapetrees utilisé pour déclarer les besoins d'accès. +SEMAPPS_SHAPE_REPOSITORY_URL=https://shapes.activitypods.org/ + +# Nom et description de l'application (backend). +SEMAPPS_APP_NAME=Festipod +SEMAPPS_APP_DESCRIPTION=Points de rencontre sur les événements publics +SEMAPPS_APP_LANG=en,fr +SEMAPPS_PORT=3003 + +# ── Frontend / Backend coupling ──────────────────────────────────────────── +# URL du backend telle que vue par le navigateur. +VITE_BACKEND_URL=http://localhost:3003 + +# Client ID OAuth/Solid du backend (laisser vide pour l'auto-découverte). +VITE_BACKEND_CLIENT_ID= + # ── Outillage dev (facultatif) ───────────────────────────────────────────── # Override du chemin local du polyfill @ng-eventually/sdk pour `pnpm run # overlay:polyfill` (overlay local réactif). Défaut = ../nextgraph/ng-eventually-js/packages/sdk. diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..28888b9 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,14 @@ +SEMAPPS_APP_NAME=Festipod +SEMAPPS_APP_DESCRIPTION=Points de rencontre sur les événements publics +SEMAPPS_APP_LANG=en,fr +SEMAPPS_HOME_URL=http://localhost:3003 +SEMAPPS_FRONT_URL=http://localhost:5000 +SEMAPPS_SHAPE_REPOSITORY_URL=https://shapes.activitypods.org/ +SEMAPPS_PORT=3003 +SEMAPPS_SPARQL_ENDPOINT=http://localhost:3030/ +SEMAPPS_MAIN_DATASET=settings +SEMAPPS_JENA_USER=admin +SEMAPPS_JENA_PASSWORD=admin +SEMAPPS_REDIS_CACHE_URL=redis://localhost:6390/3 +SEMAPPS_QUEUE_SERVICE_URL=redis://localhost:6390/4 +SEMAPPS_AUTH_ACCOUNTS_DATASET_NAME=settings diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..7ca0be1 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,25 @@ +FROM node:22-slim + +# Install pnpm and build tools required for native modules (sharp, bcrypt). +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* \ + && corepack enable && corepack prepare pnpm@10.26.0 --activate + +WORKDIR /app + +# Install dependencies first to leverage Docker layer caching. +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod + +# Copy application code. +COPY config ./config +COPY moleculer.config.js ./ +COPY services ./services + +EXPOSE 3003 + +CMD ["pnpm", "start"] diff --git a/backend/config/config.js b/backend/config/config.js new file mode 100644 index 0000000..5bd9deb --- /dev/null +++ b/backend/config/config.js @@ -0,0 +1,18 @@ +require('dotenv-flow').config(); + +module.exports = { + APP_NAME: process.env.SEMAPPS_APP_NAME, + APP_DESCRIPTION: process.env.SEMAPPS_APP_DESCRIPTION, + APP_LANG: process.env.SEMAPPS_APP_LANG, + HOME_URL: process.env.SEMAPPS_HOME_URL, + FRONT_URL: process.env.SEMAPPS_FRONT_URL, + SHAPE_REPOSITORY_URL: process.env.SEMAPPS_SHAPE_REPOSITORY_URL, + PORT: process.env.SEMAPPS_PORT, + SPARQL_ENDPOINT: process.env.SEMAPPS_SPARQL_ENDPOINT, + MAIN_DATASET: process.env.SEMAPPS_MAIN_DATASET, + JENA_USER: process.env.SEMAPPS_JENA_USER, + JENA_PASSWORD: process.env.SEMAPPS_JENA_PASSWORD, + REDIS_CACHE_URL: process.env.SEMAPPS_REDIS_CACHE_URL, + QUEUE_SERVICE_URL: process.env.SEMAPPS_QUEUE_SERVICE_URL, + AUTH_ACCOUNTS_DATASET_NAME: process.env.SEMAPPS_AUTH_ACCOUNTS_DATASET_NAME +}; diff --git a/backend/moleculer.config.js b/backend/moleculer.config.js new file mode 100644 index 0000000..0137df3 --- /dev/null +++ b/backend/moleculer.config.js @@ -0,0 +1,62 @@ +const { CacherMiddleware } = require('@semapps/webacl'); +const CONFIG = require('./config/config'); + +Error.stackTraceLimit = Infinity; + +const cacherConfig = CONFIG.REDIS_CACHE_URL + ? { + type: 'Redis', + options: { + prefix: 'action', + ttl: 2592000, + redis: CONFIG.REDIS_CACHE_URL + } + } + : undefined; + +const SkipOrphanBlankNodesCleanupMiddleware = () => ({ + name: 'SkipOrphanBlankNodesCleanupMiddleware', + localAction: (next, action) => { + if (action.name === 'triplestore.deleteOrphanBlankNodes') { + return async () => {}; + } + return next; + } +}); + +// ActivityPods app services mark internal calls with isSystemCall, but the SDK +// triplestore layer needs an explicit webId='system' to bypass pod-side WAC. +const SystemCallWebIdMiddleware = () => ({ + name: 'SystemCallWebIdMiddleware', + localAction: (next, action) => { + return async ctx => { + if (ctx.meta?.isSystemCall && !ctx.params?.webId) { + ctx.params.webId = 'system'; + } + return next(ctx); + }; + } +}); + +module.exports = { + middlewares: [ + SkipOrphanBlankNodesCleanupMiddleware(), + SystemCallWebIdMiddleware(), + CacherMiddleware(cacherConfig) + ], + logger: { + type: 'Console', + options: { + formatter: 'short', + level: 'info' + } + }, + retryPolicy: { + enabled: true, + retries: 0, + delay: 100, + maxDelay: 30000, + factor: 2, + check: err => err && !!err.retryable + } +}; diff --git a/backend/package.json b/backend/package.json index d069521..982396c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,19 +10,24 @@ "@rdfjs/data-model": "2.1.1", "@semapps/activitypub": "1.2.7", "@semapps/auth": "1.2.7", - "@semapps/crypto": "1.2.7", "@semapps/core": "1.2.7", + "@semapps/crypto": "1.2.7", "@semapps/importer": "1.2.7", "@semapps/ldp": "1.2.7", + "@semapps/mime-types": "1.2.7", "@semapps/nodeinfo": "1.2.7", + "@semapps/ontologies": "1.2.7", "@semapps/solid": "1.2.7", + "@semapps/sparql-endpoint": "1.2.7", "@semapps/triplestore": "1.2.7", + "@semapps/webacl": "1.2.7", "dotenv-flow": "^3.2.0", "ioredis": "^4.17.3", "moleculer": "^0.14.19", "moleculer-bull": "0.2.8", "moleculer-cron": "0.0.4", "moleculer-repl": "^0.6.6", + "sharp": "0.31.3", "url-join": "^4.0.1" }, "engines": { diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 97d8b68..89f23bf 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -32,15 +32,27 @@ importers: '@semapps/ldp': specifier: 1.2.7 version: 1.2.7(debug@4.4.3)(moleculer@0.14.36(debug@4.4.3)(ioredis@4.31.0)) + '@semapps/mime-types': + specifier: 1.2.7 + version: 1.2.7(moleculer@0.14.36(debug@4.4.3)(ioredis@4.31.0)) '@semapps/nodeinfo': specifier: 1.2.7 version: 1.2.7(moleculer@0.14.36(debug@4.4.3)(ioredis@4.31.0)) + '@semapps/ontologies': + specifier: 1.2.7 + version: 1.2.7(moleculer@0.14.36(debug@4.4.3)(ioredis@4.31.0)) '@semapps/solid': specifier: 1.2.7 version: 1.2.7(debug@4.4.3)(moleculer@0.14.36(debug@4.4.3)(ioredis@4.31.0)) + '@semapps/sparql-endpoint': + specifier: 1.2.7 + version: 1.2.7(moleculer@0.14.36(debug@4.4.3)(ioredis@4.31.0)) '@semapps/triplestore': specifier: 1.2.7 version: 1.2.7(moleculer@0.14.36(debug@4.4.3)(ioredis@4.31.0)) + '@semapps/webacl': + specifier: 1.2.7 + version: 1.2.7(debug@4.4.3)(moleculer@0.14.36(debug@4.4.3)(ioredis@4.31.0)) dotenv-flow: specifier: ^3.2.0 version: 3.3.0 @@ -59,6 +71,9 @@ importers: moleculer-repl: specifier: ^0.6.6 version: 0.6.6 + sharp: + specifier: 0.31.3 + version: 0.31.3 url-join: specifier: ^4.0.1 version: 4.0.1 diff --git a/backend/services/app.service.js b/backend/services/app.service.js new file mode 100644 index 0000000..e66bb7d --- /dev/null +++ b/backend/services/app.service.js @@ -0,0 +1,51 @@ +const urlJoin = require('url-join'); +const { AppService } = require('@activitypods/app'); +const CONFIG = require('../config/config'); + +module.exports = { + mixins: [AppService], + settings: { + baseUrl: CONFIG.HOME_URL, + app: { + name: CONFIG.APP_NAME, + description: CONFIG.APP_DESCRIPTION, + thumbnail: urlJoin(CONFIG.FRONT_URL, 'logo.svg'), + frontUrl: CONFIG.FRONT_URL, + supportedLocales: CONFIG.APP_LANG + }, + oidc: { + clientUri: CONFIG.FRONT_URL, + redirectUris: urlJoin(CONFIG.FRONT_URL, 'login'), + postLogoutRedirectUris: urlJoin(CONFIG.FRONT_URL, 'login?logout=true'), + tosUri: null + }, + accessNeeds: { + required: [ + { + shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Event'), + accessMode: ['acl:Read', 'acl:Write', 'acl:Control'], + preferredScope: 'interop:AllFromRegistry' + }, + { + shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Profile'), + accessMode: ['acl:Read', 'acl:Write'], + preferredScope: 'interop:AllFromRegistry' + }, + { + shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/vcard/Group'), + accessMode: 'acl:Read', + preferredScope: 'interop:AllFromRegistry' + }, + 'apods:ReadInbox', + 'apods:ReadOutbox', + 'apods:PostOutbox', + 'apods:QuerySparqlEndpoint', + 'apods:CreateWacGroup', + 'apods:CreateCollection', + 'apods:UpdateWebId' + ], + optional: [] + }, + queueServiceUrl: CONFIG.QUEUE_SERVICE_URL + } +}; diff --git a/backend/services/attendees.service.js b/backend/services/attendees.service.js new file mode 100644 index 0000000..97ebfd4 --- /dev/null +++ b/backend/services/attendees.service.js @@ -0,0 +1,17 @@ +const urlJoin = require('url-join'); +const { PodCollectionsHandlerMixin } = require('@activitypods/app'); +const CONFIG = require('../config/config'); + +module.exports = { + name: 'attendees', + mixins: [PodCollectionsHandlerMixin], + settings: { + shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Event'), + attachPredicate: 'http://activitypods.org/ns/core#attendees', + collectionOptions: { + ordered: false, + summary: 'Meeting point attendees' + }, + createWacGroup: true + } +}; diff --git a/backend/services/catalog.service.js b/backend/services/catalog.service.js new file mode 100644 index 0000000..99afe5c --- /dev/null +++ b/backend/services/catalog.service.js @@ -0,0 +1,41 @@ +const urlJoin = require('url-join'); +const { PodResourcesHandlerMixin } = require('@activitypods/app'); +const CONFIG = require('../config/config'); + +module.exports = { + name: 'catalog', + mixins: [PodResourcesHandlerMixin], + settings: { + shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Event') + }, + actions: { + discover: { + async handler() { + return { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + summary: 'Public Festipod events and meeting points', + totalItems: this.index ? Object.keys(this.index).length : 0, + items: this.index ? Object.values(this.index) : [] + }; + } + } + }, + methods: { + async onCreate(ctx, resource) { + if (!this.index) this.index = {}; + this.index[resource.id || resource['@id']] = resource; + this.logger.info(`Catalog indexed new event ${resource.id || resource['@id']}`); + }, + async onUpdate(ctx, resource) { + if (!this.index) this.index = {}; + this.index[resource.id || resource['@id']] = resource; + this.logger.info(`Catalog updated event ${resource.id || resource['@id']}`); + }, + async onDelete(ctx, resourceUri) { + if (!this.index) return; + delete this.index[resourceUri]; + this.logger.info(`Catalog removed event ${resourceUri}`); + } + } +}; diff --git a/backend/services/contacts.service.js b/backend/services/contacts.service.js new file mode 100644 index 0000000..635b621 --- /dev/null +++ b/backend/services/contacts.service.js @@ -0,0 +1,17 @@ +const urlJoin = require('url-join'); +const { PodCollectionsHandlerMixin } = require('@activitypods/app'); +const CONFIG = require('../config/config'); + +module.exports = { + name: 'contacts', + mixins: [PodCollectionsHandlerMixin], + settings: { + shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Profile'), + attachPredicate: 'http://activitypods.org/ns/core#contacts', + collectionOptions: { + ordered: false, + summary: 'Festipod contacts' + }, + createWacGroup: true + } +}; diff --git a/backend/services/core/auth-account.service.js b/backend/services/core/auth-account.service.js new file mode 100644 index 0000000..5bc21c6 --- /dev/null +++ b/backend/services/core/auth-account.service.js @@ -0,0 +1,8 @@ +const { AuthAccountService } = require('@semapps/auth'); +const { TripleStoreAdapter } = require('@semapps/triplestore'); +const CONFIG = require('../../config/config'); + +module.exports = { + mixins: [AuthAccountService], + adapter: new TripleStoreAdapter({ type: 'AuthAccount', dataset: CONFIG.AUTH_ACCOUNTS_DATASET_NAME }) +}; diff --git a/backend/services/core/core.service.js b/backend/services/core/core.service.js new file mode 100644 index 0000000..2bffd84 --- /dev/null +++ b/backend/services/core/core.service.js @@ -0,0 +1,53 @@ +const path = require('path'); +const { CoreService } = require('@semapps/core'); +const { apods, notify, interop, oidc, skos } = require('@semapps/ontologies'); +const CONFIG = require('../../config/config'); + +module.exports = { + mixins: [CoreService], + settings: { + baseUrl: CONFIG.HOME_URL, + baseDir: path.resolve(__dirname, '../..'), + triplestore: { + url: CONFIG.SPARQL_ENDPOINT, + user: CONFIG.JENA_USER, + password: CONFIG.JENA_PASSWORD, + mainDataset: CONFIG.MAIN_DATASET + }, + ontologies: [apods, notify, interop, oidc, skos], + activitypub: { + queueServiceUrl: CONFIG.QUEUE_SERVICE_URL + }, + api: { + port: CONFIG.PORT, + routes: [ + { + path: '/app', + aliases: { + 'GET /': 'app.get' + }, + mappingPolicy: 'restrict', + authentication: false, + authorization: false, + bodyParsers: { + json: true + } + }, + { + path: '/catalog', + aliases: { + 'GET /': 'catalog.discover' + }, + mappingPolicy: 'restrict', + authentication: false, + authorization: false, + bodyParsers: { + json: true + } + } + ] + }, + webacl: false, + void: false + } +}; diff --git a/backend/services/core/nodeinfo.service.js b/backend/services/core/nodeinfo.service.js new file mode 100644 index 0000000..84d1df3 --- /dev/null +++ b/backend/services/core/nodeinfo.service.js @@ -0,0 +1,30 @@ +const urlJoin = require('url-join'); +const { NodeinfoService } = require('@semapps/nodeinfo'); +const CONFIG = require('../../config/config'); + +module.exports = { + mixins: [NodeinfoService], + settings: { + baseUrl: CONFIG.HOME_URL, + software: { + name: 'festipod' + }, + protocols: ['activitypub'], + metadata: { + frontend_url: CONFIG.FRONT_URL, + login_url: CONFIG.FRONT_URL && urlJoin(CONFIG.FRONT_URL, 'login'), + logout_url: CONFIG.FRONT_URL && urlJoin(CONFIG.FRONT_URL, 'login?logout=true') + } + }, + actions: { + async getUsersCount(ctx) { + const containerUri = await ctx.call('app-registrations.getContainerUri'); + const registrationsUris = await ctx.call('ldp.container.getUris', { containerUri }); + return { + total: registrationsUris.length, + activeHalfYear: registrationsUris.length, + activeMonth: registrationsUris.length + }; + } + } +}; diff --git a/backend/services/core/notification.listener.service.js b/backend/services/core/notification.listener.service.js new file mode 100644 index 0000000..c3591c6 --- /dev/null +++ b/backend/services/core/notification.listener.service.js @@ -0,0 +1,11 @@ +const { TripleStoreAdapter } = require('@semapps/triplestore'); +const { NotificationsListenerService } = require('@semapps/solid'); +const CONFIG = require('../../config/config'); + +module.exports = { + mixins: [NotificationsListenerService], + adapter: new TripleStoreAdapter({ type: 'WebhookChannelListener', dataset: CONFIG.AUTH_ACCOUNTS_DATASET_NAME }), + settings: { + baseUrl: CONFIG.HOME_URL + } +}; diff --git a/backend/services/core/proxy.service.js b/backend/services/core/proxy.service.js new file mode 100644 index 0000000..218bca7 --- /dev/null +++ b/backend/services/core/proxy.service.js @@ -0,0 +1,3 @@ +const { ProxyService } = require('@semapps/crypto'); + +module.exports = ProxyService; diff --git a/backend/services/registration.service.js b/backend/services/registration.service.js new file mode 100644 index 0000000..5b69a5c --- /dev/null +++ b/backend/services/registration.service.js @@ -0,0 +1,97 @@ +const { ACTIVITY_TYPES, OBJECT_TYPES } = require('@semapps/activitypub'); +const { PodActivitiesHandlerMixin } = require('@activitypods/app'); +const { arrayOf } = require('@semapps/ldp'); + +module.exports = { + name: 'registration', + mixins: [PodActivitiesHandlerMixin], + async started() { + this.logger.info('Registration watcher subscribed'); + }, + activities: { + join: { + match: { + type: ACTIVITY_TYPES.JOIN, + object: { + type: OBJECT_TYPES.EVENT + } + }, + async onEmit(ctx, activity, actorUri) { + const event = activity.object; + const organizerUri = event['dc:creator']; + if (!organizerUri) return; + + const { body: actor } = await ctx.call('pod-resources.get', { resourceUri: actorUri, actorUri }); + if (!actor || !actor['apods:contacts']) return; + + const contactsGroupUri = await ctx.call('pod-wac-groups.getUriFromCollectionUri', { + collectionUri: actor['apods:contacts'] + }); + + await ctx.call('pod-wac-groups.addMember', { + groupUri: contactsGroupUri, + memberUri: organizerUri, + actorUri + }); + }, + async onReceive(ctx, activity, actorUri) { + const event = activity.object; + const attendeesCollectionUri = event['apods:attendees']; + + if (!attendeesCollectionUri) { + this.logger.warn(`No attendees collection on ${event.id}, skipping Join`); + return; + } + + const attendees = await ctx.call('pod-collections.getItems', { + collectionUri: attendeesCollectionUri, + actorUri + }); + + if (arrayOf(attendees).includes(activity.actor)) { + this.logger.info(`${activity.actor} is already attending ${event.id}`); + return; + } + + await ctx.call('attendees.add', { + collectionUri: attendeesCollectionUri, + itemUri: activity.actor, + actorUri + }); + } + }, + leave: { + match: { + type: ACTIVITY_TYPES.LEAVE, + object: { + type: OBJECT_TYPES.EVENT + } + }, + async onReceive(ctx, activity, actorUri) { + const event = activity.object; + const attendeesCollectionUri = event['apods:attendees']; + + if (!attendeesCollectionUri) { + this.logger.warn(`No attendees collection on ${event.id}, skipping Leave`); + return; + } + + const attendees = await ctx.call('pod-collections.getItems', { + collectionUri: attendeesCollectionUri, + actorUri + }); + + if (!arrayOf(attendees).includes(activity.actor)) { + this.logger.info(`${activity.actor} is not attending ${event.id}, skipping Leave`); + return; + } + + await ctx.call('attendees.remove', { + collectionUri: attendeesCollectionUri, + itemUri: activity.actor, + actorUri + }); + } + } + } +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..98ec98f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + frontend: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + - PORT=3000 + - NODE_ENV=production + - VITE_BACKEND_URL=${VITE_BACKEND_URL:-http://localhost:3003} + - VITE_BACKEND_CLIENT_ID=${VITE_BACKEND_CLIENT_ID:-} + - FESTIPOD_SHARED_WALLET_PASSWORD=${FESTIPOD_SHARED_WALLET_PASSWORD:-} + - FESTIPOD_SHARED_WALLET_FILE=${FESTIPOD_SHARED_WALLET_FILE:-} + - FESTIPOD_SHARED_WALLET_FILE_BASE64=${FESTIPOD_SHARED_WALLET_FILE_BASE64:-} + - FESTIPOD_AUTO_SEED=${FESTIPOD_AUTO_SEED:-} + + backend: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "3003:3003" + env_file: + - ./backend/.env + environment: + - SEMAPPS_HOME_URL=${SEMAPPS_HOME_URL:-http://localhost:3003} + - SEMAPPS_FRONT_URL=${SEMAPPS_FRONT_URL:-http://localhost:5000}