"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}`); } };