Implement direct sign-up to an event with registration derived from meeting point memberships

This commit is contained in:
Sylvain Duchesne
2026-11-06 23:16:29 +01:00
parent af0cba6920
commit 5540d361c7
13 changed files with 385 additions and 147 deletions
+26 -28
View File
@@ -35,17 +35,16 @@ const firstStringOf = value =>
.find(v => typeof v === 'string' && v.trim().length > 0);
/**
* What a Join/Leave targets: the stored object must be a meeting point — an `as:Event` (checked by
* `verifiedDelivery`, and by `meetingPointOf` on the creator's copy) anchored to an event by
* `as:context` — whose creator is its `dc:creator`, required to own the pod the meeting point lies
* in. Only on the copy the creator's pod serves (`meetingPointOf`) is that creator believed (the pod
* What a Join/Leave targets: the stored object must be an `as:Event` (checked by `verifiedDelivery`,
* and by `meetingPointOf` on the creator's copy) — a meeting point when anchored to an event by
* `as:context`, otherwise an event signed up to directly, which is its own anchor — whose creator is
* its `dc:creator`, required to own the pod it lies in. Only on the copy the creator's pod serves (`meetingPointOf`) is that creator believed (the pod
* stamps the creator, and nobody can store a resource in someone else's pod); on the copy read
* through the participant's pod, it only names where to read that one. Resolves
* `{ ok, eventUri, creatorUri }` or a refusal.
*/
function meetingPointTarget(meetingPoint, meetingPointUri) {
const eventUri = linkOf(meetingPoint, 'context', 'as:context', `${AS}context`);
if (!eventUri) return { ok: false, reason: `the meeting point ${meetingPointUri} names no single as:context (its event)` };
const eventUri = linkOf(meetingPoint, 'context', 'as:context', `${AS}context`) ?? meetingPointUri;
const creatorUri = linkOf(meetingPoint, 'dc:creator', DC_CREATOR);
if (!creatorUri) return { ok: false, reason: `the meeting point ${meetingPointUri} names no single dc:creator` };
if (!isInActorPod(meetingPointUri, creatorUri)) {
@@ -55,9 +54,9 @@ function meetingPointTarget(meetingPoint, meetingPointUri) {
}
/**
* The notification template for the creator of a meeting point (`pod-notifications.send`, rendered in
* the creator's language): always ANONYMOUS, the meeting point named through `meetingPointName`, the
* link to its event's meeting points (`eventUri`). Every value travels as a template PARAMETER, never
* The notification template for the creator of a target (`pod-notifications.send`, rendered in the
* creator's language): always ANONYMOUS, the target named through `meetingPointName` or `eventName`, the link to the
* event (`eventUri`), or to its meeting points when the target is one. Every value travels as a template PARAMETER, never
* inside the template, so no user text is compiled.
*
* Never names the participant: naming would need a trustworthy signal that the creator is one of the
@@ -66,21 +65,18 @@ function meetingPointTarget(meetingPoint, meetingPointUri) {
* request checked; platform defect reported upstream). Until that list can be trusted, every
* notification is anonymous.
*/
function notificationTemplate(change) {
const title =
change === 'join'
? {
en: 'A participant joined your meeting point "{{{meetingPointName}}}"',
fr: 'Un participant a rejoint votre point de rencontre « {{{meetingPointName}}} »'
}
: {
en: 'A participant left your meeting point "{{{meetingPointName}}}"',
fr: 'Un participant a quitté votre point de rencontre « {{{meetingPointName}}} »'
};
return {
title,
actions: [{ caption: { en: 'View', fr: 'Voir' }, link: '/events/{{encodeUri eventUri}}/meeting-points' }]
function notificationTemplate(change, isMeetingPoint) {
const joined = change === 'join';
const nameParam = isMeetingPoint ? 'meetingPointName' : 'eventName';
const subject = isMeetingPoint
? { en: 'your meeting point', fr: 'votre point de rencontre' }
: { en: 'your event', fr: 'votre événement' };
const title = {
en: `A participant ${joined ? 'joined' : 'left'} ${subject.en} "{{{${nameParam}}}}"`,
fr: `Un participant a ${joined ? 'rejoint' : 'quitté'} ${subject.fr} « {{{${nameParam}}}} »`
};
const link = isMeetingPoint ? '/events/{{encodeUri eventUri}}/meeting-points' : '/events/{{encodeUri eventUri}}';
return { title, actions: [{ caption: { en: 'View', fr: 'Voir' }, link }] };
}
// Participation (decision_2026-10-29_participation-privacy-mechanism; its delivery:
@@ -366,25 +362,27 @@ module.exports = {
await expectItem(ctx, { collectionUri, itemUri: meetingPointUri, actorUri: participantUri, present: joining });
},
/**
* Tells the creator of the meeting point that a participant joined or left it — ALWAYS anonymously.
* Tells the creator of the target (a meeting point, or an event signed up to directly) that a
* participant joined or left it — ALWAYS anonymously.
* The participant is never named: the only membership signal the app can read (the participant's
* `apods:contacts` list) is forgeable, so naming cannot be trusted (platform defect reported
* upstream, see `notificationTemplate`). The notification carries no `activity`: given one,
* `pod-notifications.send` fetches the emitter's profile. A creator acting on their own meeting
* point is not notified. A failure is logged, never thrown: the act stands.
* point or event is not notified. A failure is logged, never thrown: the act stands.
*/
async notifyCreator(ctx, { change, meetingPointUri, meetingPoint, eventUri, creatorUri, participantUri }) {
const isMeetingPoint = eventUri !== meetingPointUri;
try {
if (creatorUri === participantUri) {
this.logger.info(`${participantUri} acted on their own meeting point ${meetingPointUri}: no notification`);
this.logger.info(`${participantUri} acted on their own ${meetingPointUri}: no notification`);
return;
}
this.logger.info(`Notifying ${creatorUri} of a ${change} on ${meetingPointUri}, anonymously`);
await ctx.call('pod-notifications.send', {
template: notificationTemplate(change),
template: notificationTemplate(change, isMeetingPoint),
recipientUri: creatorUri,
context: meetingPointUri,
meetingPointName: firstStringOf(meetingPoint.name ?? meetingPoint['as:name'] ?? meetingPoint[`${AS}name`]) ?? '',
[isMeetingPoint ? 'meetingPointName' : 'eventName']: firstStringOf(meetingPoint.name ?? meetingPoint['as:name'] ?? meetingPoint[`${AS}name`]) ?? '',
eventUri
});
} catch (e) {