You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
4.6 KiB
JavaScript
161 lines
4.6 KiB
JavaScript
3 years ago
|
"use strict";
|
||
|
|
||
|
const defaultValue = require("default-value");
|
||
|
const flatten = require("flatten");
|
||
|
const addCommonFields = require("../event-add-common-fields");
|
||
|
const mapMaybeRedacted = require("../map-maybe-redacted");
|
||
|
const numberDerivedEvents = require("../number-derived-events");
|
||
|
|
||
|
function mapProfile(eventContent) {
|
||
|
return {
|
||
|
avatar: normalizeProfileField(eventContent.avatar_url),
|
||
|
displayName: normalizeProfileField(eventContent.displayname)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function normalizeProfileField(value) {
|
||
|
if (value === null || value === undefined) {
|
||
|
// We make this an *explicit* null, as this makes it easier for the consuming code to distinguish between "this user has unset the profile field" and "we just haven't seen a profile field yet".
|
||
|
return null;
|
||
|
} else {
|
||
|
return value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = function mapMemberEvent(event, _context) {
|
||
|
let previousContent = defaultValue(event.unsigned.prev_content, { membership: "leave" });
|
||
|
|
||
|
let oldState = previousContent.membership;
|
||
|
let newState = event.content.membership;
|
||
|
let oldProfile = mapProfile(previousContent);
|
||
|
let newProfile = mapProfile(event.content);
|
||
|
|
||
|
let voluntaryChange = (event.state_key === event.sender);
|
||
|
|
||
|
let transitions = {
|
||
|
invite: {
|
||
|
invite: null,
|
||
|
join: "_userAcceptedInviteAndJoined",
|
||
|
leave: (voluntaryChange)
|
||
|
? "userRejectedInvite"
|
||
|
: "userInviteWasRevoked",
|
||
|
ban: "userWasBanned"
|
||
|
},
|
||
|
join: {
|
||
|
join: "_userChangedProfile",
|
||
|
leave: (voluntaryChange)
|
||
|
? "userLeft"
|
||
|
: "userWasKicked",
|
||
|
ban: "_userWasKickedAndBanned"
|
||
|
},
|
||
|
leave: {
|
||
|
invite: "userWasInvited",
|
||
|
join: "userJoined",
|
||
|
leave: null,
|
||
|
ban: "userWasBanned"
|
||
|
},
|
||
|
ban: {
|
||
|
leave: "userWasUnbanned",
|
||
|
ban: null
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// userChangedProfile -> userChangedDisplayName / userChangedAvatar
|
||
|
// use prev_content to determine diff
|
||
|
|
||
|
let typesWithProfileData = new Set([ "_userChangedProfile", "userJoined", "_userAcceptedInviteAndJoined" ]);
|
||
|
|
||
|
if (transitions[oldState] !== undefined && transitions[oldState][newState] !== undefined) {
|
||
|
let type = transitions[oldState][newState];
|
||
|
let hasProfileData = typesWithProfileData.has(type);
|
||
|
let deltaEvents = [];
|
||
|
|
||
|
if (hasProfileData) {
|
||
|
if (oldProfile.avatar != newProfile.avatar) {
|
||
|
deltaEvents.push({
|
||
|
type: "userChangedAvatar",
|
||
|
user: event.state_key,
|
||
|
sender: event.sender,
|
||
|
... mapMaybeRedacted(event, () => {
|
||
|
return {
|
||
|
url: newProfile.avatar,
|
||
|
previousURL: oldProfile.avatar
|
||
|
};
|
||
|
})
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (oldProfile.displayName != newProfile.displayName) {
|
||
|
deltaEvents.push({
|
||
|
type: "userChangedDisplayName",
|
||
|
user: event.state_key,
|
||
|
sender: event.sender,
|
||
|
... mapMaybeRedacted(event, () => {
|
||
|
return {
|
||
|
name: newProfile.displayName,
|
||
|
previousName: oldProfile.displayName
|
||
|
};
|
||
|
})
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We ignore any "no change" transitions, as well as any profile changes (since those will have been handled in full above)
|
||
|
if (type !== null && type !== "_userChangedProfile") {
|
||
|
let membershipFields = {
|
||
|
user: event.state_key,
|
||
|
sender: event.sender,
|
||
|
... mapMaybeRedacted(event, () => {
|
||
|
return {
|
||
|
reason: event.content.reason,
|
||
|
// FIXME: is_direct flag, translate into "is direct message" event if prev_content does not contain it
|
||
|
// FIXME: third_party_invite.display_name
|
||
|
};
|
||
|
})
|
||
|
};
|
||
|
|
||
|
// NOTE: We have a few special cases below, where certain membership changes result in multiple logical actions. To leave it up to the consumer whether to care about the cause of eg. a kick or join, we represent the logical actions as individual events, annotating them with metadata so that a cause-interested client can ignore the implicitly-generated events.
|
||
|
|
||
|
if (type === "_userWasKickedAndBanned") {
|
||
|
deltaEvents.push([
|
||
|
addCommonFields(event, {
|
||
|
type: "userWasKicked",
|
||
|
causedByBan: true,
|
||
|
... membershipFields
|
||
|
}),
|
||
|
addCommonFields(event, {
|
||
|
type: "userWasBanned",
|
||
|
... membershipFields
|
||
|
}),
|
||
|
]);
|
||
|
} else if (type === "_userAcceptedInviteAndJoined") {
|
||
|
deltaEvents.push([
|
||
|
addCommonFields(event, {
|
||
|
type: "userAcceptedInvite",
|
||
|
... membershipFields
|
||
|
}),
|
||
|
addCommonFields(event, {
|
||
|
type: "userJoined",
|
||
|
causedByInvite: true,
|
||
|
... membershipFields
|
||
|
}),
|
||
|
]);
|
||
|
} else {
|
||
|
deltaEvents.push(addCommonFields(event, {
|
||
|
type: type,
|
||
|
... membershipFields
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let flattenedDeltaEvents = flatten(deltaEvents);
|
||
|
|
||
|
numberDerivedEvents(event.event_id, flattenedDeltaEvents);
|
||
|
|
||
|
return flattenedDeltaEvents;
|
||
|
} else {
|
||
|
// FIXME: Error type
|
||
|
throw new Error(`Membership transition not allowed: ${oldState} => ${newState}`);
|
||
|
}
|
||
|
};
|