Implement direct sign-up to an event with registration derived from meeting point memberships
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -955,6 +955,17 @@ describe('registration — a direct sign-up: a Join naming an event itself', ()
|
||||
}
|
||||
});
|
||||
|
||||
test('a declarer signing up to, then leaving, their own event is not notified', async () => {
|
||||
const j = join(PLAIN, T0, ALICE);
|
||||
const l = leave(PLAIN, T1, ALICE);
|
||||
const w = plainWorld([j, l]);
|
||||
await emitJoin(w, j);
|
||||
await emitLeave(w, l);
|
||||
expect(attendees(w, PLAIN)).toEqual([]);
|
||||
expect(ledgerOf(w, PLAIN, ALICE).change).toBe('leave');
|
||||
expect(w.state.notifications).toEqual([]);
|
||||
});
|
||||
|
||||
test('the same person carries a different URN on another event (R7e)', async () => {
|
||||
const OTHER = `${ALICE}/data/events/other`;
|
||||
const a = join(PLAIN, T0, BOB, 1);
|
||||
|
||||
Reference in New Issue
Block a user