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'); }, methods: { /** * The Event's `apods:attendees` collection is attached asynchronously by * PodCollectionsHandlerMixin on create, so a Join/Leave processed in that window sees no * collection. Returning immediately would drop the activity forever (the client overlay * shows joined, nothing reconciles). Re-fetch the Event a bounded number of times until the * collection appears; only then give up. */ async fetchAttendeesCollectionUri(ctx, event, actorUri) { if (event['apods:attendees'] || !event.id) return event['apods:attendees']; for (let attempt = 0; attempt < 3; attempt++) { await new Promise(resolve => setTimeout(resolve, 500)); try { const { ok, body } = await ctx.call('pod-resources.get', { resourceUri: event.id, actorUri }); if (ok && body && body['apods:attendees']) return body['apods:attendees']; } catch (e) { this.logger.warn(`Could not re-fetch ${event.id} while waiting for its attendees collection: ${e.message}`); } } return undefined; } }, 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 = await this.fetchAttendeesCollectionUri(ctx, event, actorUri); 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 = await this.fetchAttendeesCollectionUri(ctx, event, actorUri); 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 }); } } } };