diff --git a/lib/shared/messages/add-members-message-spec.js b/lib/shared/messages/add-members-message-spec.js index 2f1f0db18..28b40ef41 100644 --- a/lib/shared/messages/add-members-message-spec.js +++ b/lib/shared/messages/add-members-message-spec.js @@ -1,159 +1,162 @@ // @flow import invariant from 'invariant'; import type { CreateMessageInfoParams, MessageSpec } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - AddMembersMessageData, - AddMembersMessageInfo, - RawAddMembersMessageInfo, +import { + type AddMembersMessageData, + type AddMembersMessageInfo, + type RawAddMembersMessageInfo, + rawAddMembersMessageInfoValidator, } from '../../types/messages/add-members.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; export const addMembersMessageSpec: MessageSpec< AddMembersMessageData, RawAddMembersMessageInfo, AddMembersMessageInfo, > = Object.freeze({ messageContentForServerDB( data: AddMembersMessageData | RawAddMembersMessageInfo, ): string { return JSON.stringify(data.addedUserIDs); }, messageContentForClientDB(data: RawAddMembersMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawAddMembersMessageInfo { return { type: messageTypes.ADD_MEMBERS, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), addedUserIDs: JSON.parse(row.content), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawAddMembersMessageInfo { const content = clientDBMessageInfo.content; invariant( content !== undefined && content !== null, 'content must be defined for AddMembers', ); const rawAddMembersMessageInfo: RawAddMembersMessageInfo = { type: messageTypes.ADD_MEMBERS, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, addedUserIDs: JSON.parse(content), }; return rawAddMembersMessageInfo; }, createMessageInfo( rawMessageInfo: RawAddMembersMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): AddMembersMessageInfo { const addedMembers = params.createRelativeUserInfos( rawMessageInfo.addedUserIDs, ); return { type: messageTypes.ADD_MEMBERS, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, addedMembers, }; }, rawMessageInfoFromMessageData( messageData: AddMembersMessageData, id: ?string, ): RawAddMembersMessageInfo { invariant(id, 'RawAddMembersMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: AddMembersMessageInfo): EntityText { const users = messageInfo.addedMembers; invariant(users.length !== 0, 'added who??'); const creator = ET.user({ userInfo: messageInfo.creator }); const addedUsers = pluralizeEntityText( users.map(user => ET`${ET.user({ userInfo: user })}`), ); return ET`${creator} added ${addedUsers}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const addedMembersObject = {}; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.ADD_MEMBERS, 'messageInfo should be messageTypes.ADD_MEMBERS!', ); for (const member of messageInfo.addedMembers) { addedMembersObject[member.id] = member; } } const addedMembers = values(addedMembersObject); const mostRecentMessageInfo = messageInfos[0]; invariant( mostRecentMessageInfo.type === messageTypes.ADD_MEMBERS, 'messageInfo should be messageTypes.ADD_MEMBERS!', ); const mergedMessageInfo = { ...mostRecentMessageInfo, addedMembers }; const robotext = notifRobotextForMessageInfo(mergedMessageInfo, threadInfo); const merged = ET`${robotext} to ${ET.thread({ display: 'shortName', threadInfo, })}`; return { merged, title: threadInfo.uiName, body: robotext, }; }, notificationCollapseKey(rawMessageInfo: RawAddMembersMessageInfo): string { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, ); }, generatesNotifs: async () => undefined, userIDs(rawMessageInfo: RawAddMembersMessageInfo): $ReadOnlyArray { return rawMessageInfo.addedUserIDs; }, + + validator: rawAddMembersMessageInfoValidator, }); diff --git a/lib/shared/messages/change-role-message-spec.js b/lib/shared/messages/change-role-message-spec.js index b5f4da24b..a28966828 100644 --- a/lib/shared/messages/change-role-message-spec.js +++ b/lib/shared/messages/change-role-message-spec.js @@ -1,177 +1,180 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type RobotextParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - ChangeRoleMessageData, - ChangeRoleMessageInfo, - RawChangeRoleMessageInfo, +import { + type ChangeRoleMessageData, + type ChangeRoleMessageInfo, + type RawChangeRoleMessageInfo, + rawChangeRoleMessageInfoValidator, } from '../../types/messages/change-role.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; export const changeRoleMessageSpec: MessageSpec< ChangeRoleMessageData, RawChangeRoleMessageInfo, ChangeRoleMessageInfo, > = Object.freeze({ messageContentForServerDB( data: ChangeRoleMessageData | RawChangeRoleMessageInfo, ): string { return JSON.stringify({ userIDs: data.userIDs, newRole: data.newRole, }); }, messageContentForClientDB(data: RawChangeRoleMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawChangeRoleMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.CHANGE_ROLE, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), userIDs: content.userIDs, newRole: content.newRole, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawChangeRoleMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for ChangeRole', ); const content = JSON.parse(clientDBMessageInfo.content); const rawChangeRoleMessageInfo: RawChangeRoleMessageInfo = { type: messageTypes.CHANGE_ROLE, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, userIDs: content.userIDs, newRole: content.newRole, }; return rawChangeRoleMessageInfo; }, createMessageInfo( rawMessageInfo: RawChangeRoleMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ChangeRoleMessageInfo { const members = params.createRelativeUserInfos(rawMessageInfo.userIDs); return { type: messageTypes.CHANGE_ROLE, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, members, newRole: rawMessageInfo.newRole, }; }, rawMessageInfoFromMessageData( messageData: ChangeRoleMessageData, id: ?string, ): RawChangeRoleMessageInfo { invariant(id, 'RawChangeRoleMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: ChangeRoleMessageInfo, params: RobotextParams, ): EntityText { const users = messageInfo.members; invariant(users.length !== 0, 'changed whose role??'); const creator = ET.user({ userInfo: messageInfo.creator }); const affectedUsers = pluralizeEntityText( users.map(user => ET`${ET.user({ userInfo: user })}`), ); const { threadInfo } = params; invariant(threadInfo, 'ThreadInfo should be set for CHANGE_ROLE message'); const verb = threadInfo.roles[messageInfo.newRole].isDefault ? 'removed' : 'added'; const noun = users.length === 1 ? 'an admin' : 'admins'; return ET`${creator} ${verb} ${affectedUsers} as ${noun}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const membersObject = {}; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.CHANGE_ROLE, 'messageInfo should be messageTypes.CHANGE_ROLE!', ); for (const member of messageInfo.members) { membersObject[member.id] = member; } } const members = values(membersObject); const mostRecentMessageInfo = messageInfos[0]; invariant( mostRecentMessageInfo.type === messageTypes.CHANGE_ROLE, 'messageInfo should be messageTypes.CHANGE_ROLE!', ); const mergedMessageInfo = { ...mostRecentMessageInfo, members }; const robotext = notifRobotextForMessageInfo(mergedMessageInfo, threadInfo); const merged = ET`${robotext} of ${ET.thread({ display: 'shortName', threadInfo, })}`; return { merged, title: threadInfo.uiName, body: robotext, }; }, notificationCollapseKey(rawMessageInfo: RawChangeRoleMessageInfo): string { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, rawMessageInfo.newRole, ); }, generatesNotifs: async () => pushTypes.NOTIF, + + validator: rawChangeRoleMessageInfoValidator, }); diff --git a/lib/shared/messages/change-settings-message-spec.js b/lib/shared/messages/change-settings-message-spec.js index 1e6051e89..243b6c054 100644 --- a/lib/shared/messages/change-settings-message-spec.js +++ b/lib/shared/messages/change-settings-message-spec.js @@ -1,176 +1,179 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec, type RobotextParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - ChangeSettingsMessageData, - ChangeSettingsMessageInfo, - RawChangeSettingsMessageInfo, +import { + type ChangeSettingsMessageData, + type ChangeSettingsMessageInfo, + type RawChangeSettingsMessageInfo, + rawChangeSettingsMessageInfoValidator, } from '../../types/messages/change-settings.js'; import type { NotifTexts } from '../../types/notif-types.js'; import { assertThreadType } from '../../types/thread-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { validHexColorRegex } from '../account-utils.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; import { threadLabel } from '../thread-utils.js'; export const changeSettingsMessageSpec: MessageSpec< ChangeSettingsMessageData, RawChangeSettingsMessageInfo, ChangeSettingsMessageInfo, > = Object.freeze({ messageContentForServerDB( data: ChangeSettingsMessageData | RawChangeSettingsMessageInfo, ): string { return JSON.stringify({ [data.field]: data.value, }); }, messageContentForClientDB(data: RawChangeSettingsMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawChangeSettingsMessageInfo { const content = JSON.parse(row.content); const field = Object.keys(content)[0]; return { type: messageTypes.CHANGE_SETTINGS, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), field, value: content[field], }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawChangeSettingsMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for ChangeSettings', ); const content = JSON.parse(clientDBMessageInfo.content); const field = Object.keys(content)[0]; const rawChangeSettingsMessageInfo: RawChangeSettingsMessageInfo = { type: messageTypes.CHANGE_SETTINGS, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, field, value: content[field], }; return rawChangeSettingsMessageInfo; }, createMessageInfo( rawMessageInfo: RawChangeSettingsMessageInfo, creator: RelativeUserInfo, ): ChangeSettingsMessageInfo { return { type: messageTypes.CHANGE_SETTINGS, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, field: rawMessageInfo.field, value: rawMessageInfo.value, }; }, rawMessageInfoFromMessageData( messageData: ChangeSettingsMessageData, id: ?string, ): RawChangeSettingsMessageInfo { invariant(id, 'RawChangeSettingsMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: ChangeSettingsMessageInfo, params: RobotextParams, ): EntityText { const creator = ET.user({ userInfo: messageInfo.creator }); const thread = ET.thread({ display: 'alwaysDisplayShortName', threadID: messageInfo.threadID, threadType: params.threadInfo?.type, parentThreadID: params.threadInfo?.parentThreadID, possessive: true, }); if ( (messageInfo.field === 'name' || messageInfo.field === 'description') && messageInfo.value.toString() === '' ) { return ET`${creator} cleared ${thread} ${messageInfo.field}`; } if (messageInfo.field === 'avatar') { return ET`${creator} updated ${thread} ${messageInfo.field}`; } let value; if ( messageInfo.field === 'color' && messageInfo.value.toString().match(validHexColorRegex) ) { value = ET.color({ hex: `#${messageInfo.value}` }); } else if (messageInfo.field === 'type') { invariant( typeof messageInfo.value === 'number', 'messageInfo.value should be number for thread type change ', ); const newThreadType = assertThreadType(messageInfo.value); value = threadLabel(newThreadType); } else { value = messageInfo.value.toString(); } return ET`${creator} updated ${thread} ${messageInfo.field} to "${value}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const mostRecentMessageInfo = messageInfos[0]; invariant( mostRecentMessageInfo.type === messageTypes.CHANGE_SETTINGS, 'messageInfo should be messageTypes.CHANGE_SETTINGS!', ); const body = notifRobotextForMessageInfo(mostRecentMessageInfo, threadInfo); return { merged: body, title: threadInfo.uiName, body, }; }, notificationCollapseKey( rawMessageInfo: RawChangeSettingsMessageInfo, ): string { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, rawMessageInfo.field, ); }, generatesNotifs: async () => pushTypes.NOTIF, + + validator: rawChangeSettingsMessageInfoValidator, }); diff --git a/lib/shared/messages/create-entry-message-spec.js b/lib/shared/messages/create-entry-message-spec.js index d7b5abc8b..8366696f3 100644 --- a/lib/shared/messages/create-entry-message-spec.js +++ b/lib/shared/messages/create-entry-message-spec.js @@ -1,122 +1,125 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - CreateEntryMessageData, - CreateEntryMessageInfo, - RawCreateEntryMessageInfo, +import { + type CreateEntryMessageData, + type CreateEntryMessageInfo, + type RawCreateEntryMessageInfo, + rawCreateEntryMessageInfoValidator, } from '../../types/messages/create-entry.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { prettyDate } from '../../utils/date-utils.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { notifTextsForEntryCreationOrEdit } from '../notif-utils.js'; export const createEntryMessageSpec: MessageSpec< CreateEntryMessageData, RawCreateEntryMessageInfo, CreateEntryMessageInfo, > = Object.freeze({ messageContentForServerDB( data: CreateEntryMessageData | RawCreateEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawCreateEntryMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawCreateEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.CREATE_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawCreateEntryMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for CreateEntry', ); const content = JSON.parse(clientDBMessageInfo.content); const rawCreateEntryMessageInfo: RawCreateEntryMessageInfo = { type: messageTypes.CREATE_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawCreateEntryMessageInfo; }, createMessageInfo( rawMessageInfo: RawCreateEntryMessageInfo, creator: RelativeUserInfo, ): CreateEntryMessageInfo { return { type: messageTypes.CREATE_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: CreateEntryMessageData, id: ?string, ): RawCreateEntryMessageInfo { invariant(id, 'RawCreateEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: CreateEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} created an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { return notifTextsForEntryCreationOrEdit(messageInfos, threadInfo); }, notificationCollapseKey(rawMessageInfo: RawCreateEntryMessageInfo): string { return joinResult(rawMessageInfo.creatorID, rawMessageInfo.entryID); }, generatesNotifs: async () => pushTypes.NOTIF, + + validator: rawCreateEntryMessageInfoValidator, }); diff --git a/lib/shared/messages/create-sidebar-message-spec.js b/lib/shared/messages/create-sidebar-message-spec.js index 608d3fdad..570059b84 100644 --- a/lib/shared/messages/create-sidebar-message-spec.js +++ b/lib/shared/messages/create-sidebar-message-spec.js @@ -1,230 +1,233 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type NotificationTextsParams, type RobotextParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - CreateSidebarMessageData, - CreateSidebarMessageInfo, - RawCreateSidebarMessageInfo, +import { + type CreateSidebarMessageData, + type CreateSidebarMessageInfo, + type RawCreateSidebarMessageInfo, + rawCreateSidebarMessageInfoValidator, } from '../../types/messages/create-sidebar.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { notifTextsForSidebarCreation } from '../notif-utils.js'; import { hasMinCodeVersion } from '../version-utils.js'; export const createSidebarMessageSpec: MessageSpec< CreateSidebarMessageData, RawCreateSidebarMessageInfo, CreateSidebarMessageInfo, > = Object.freeze({ messageContentForServerDB( data: CreateSidebarMessageData | RawCreateSidebarMessageInfo, ): string { return JSON.stringify({ ...data.initialThreadState, sourceMessageAuthorID: data.sourceMessageAuthorID, }); }, messageContentForClientDB(data: RawCreateSidebarMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawCreateSidebarMessageInfo { const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse( row.content, ); return { type: messageTypes.CREATE_SIDEBAR, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), sourceMessageAuthorID, initialThreadState, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawCreateSidebarMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for CreateSidebar', ); const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse( clientDBMessageInfo.content, ); const rawCreateSidebarMessageInfo: RawCreateSidebarMessageInfo = { type: messageTypes.CREATE_SIDEBAR, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, sourceMessageAuthorID: sourceMessageAuthorID, initialThreadState: initialThreadState, }; return rawCreateSidebarMessageInfo; }, createMessageInfo( rawMessageInfo: RawCreateSidebarMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?CreateSidebarMessageInfo { const { threadInfos } = params; const parentThreadInfo = threadInfos[rawMessageInfo.initialThreadState.parentThreadID]; const sourceMessageAuthor = params.createRelativeUserInfos([ rawMessageInfo.sourceMessageAuthorID, ])[0]; if (!sourceMessageAuthor) { return null; } return { type: messageTypes.CREATE_SIDEBAR, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, sourceMessageAuthor, initialThreadState: { name: rawMessageInfo.initialThreadState.name, parentThreadInfo, color: rawMessageInfo.initialThreadState.color, otherMembers: params.createRelativeUserInfos( rawMessageInfo.initialThreadState.memberIDs.filter( (userID: string) => userID !== rawMessageInfo.creatorID, ), ), }, }; }, rawMessageInfoFromMessageData( messageData: CreateSidebarMessageData, id: ?string, ): RawCreateSidebarMessageInfo { invariant(id, 'RawCreateSidebarMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: CreateSidebarMessageInfo, params: RobotextParams, ): EntityText { let text = ET`started ${ET.thread({ display: 'alwaysDisplayShortName', threadID: messageInfo.threadID, threadType: params.threadInfo?.type, parentThreadID: params.threadInfo?.parentThreadID, })}`; const users = messageInfo.initialThreadState.otherMembers.filter( member => member.id !== messageInfo.sourceMessageAuthor.id, ); if (users.length !== 0) { const initialUsers = pluralizeEntityText( users.map(user => ET`${ET.user({ userInfo: user })}`), ); text = ET`${text} and added ${initialUsers}`; } const creator = ET.user({ userInfo: messageInfo.creator }); return ET`${creator} ${text}`; }, shimUnsupportedMessageInfo( rawMessageInfo: RawCreateSidebarMessageInfo, platformDetails: ?PlatformDetails, ): RawCreateSidebarMessageInfo | RawUnsupportedMessageInfo { if (hasMinCodeVersion(platformDetails, 75)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'created a thread', unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo( unwrapped: RawCreateSidebarMessageInfo, ): RawCreateSidebarMessageInfo { return unwrapped; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, params: NotificationTextsParams, ): Promise { const createSidebarMessageInfo = messageInfos[0]; invariant( createSidebarMessageInfo.type === messageTypes.CREATE_SIDEBAR, 'first MessageInfo should be messageTypes.CREATE_SIDEBAR!', ); let sidebarSourceMessageInfo; const secondMessageInfo = messageInfos[1]; if ( secondMessageInfo && secondMessageInfo.type === messageTypes.SIDEBAR_SOURCE ) { sidebarSourceMessageInfo = secondMessageInfo; } return notifTextsForSidebarCreation({ createSidebarMessageInfo, sidebarSourceMessageInfo, threadInfo, params, }); }, notificationCollapseKey(rawMessageInfo: RawCreateSidebarMessageInfo): string { return joinResult(messageTypes.CREATE_SIDEBAR, rawMessageInfo.threadID); }, generatesNotifs: async () => pushTypes.NOTIF, userIDs(rawMessageInfo: RawCreateSidebarMessageInfo): $ReadOnlyArray { return rawMessageInfo.initialThreadState.memberIDs; }, threadIDs( rawMessageInfo: RawCreateSidebarMessageInfo, ): $ReadOnlyArray { const { parentThreadID } = rawMessageInfo.initialThreadState; return [parentThreadID]; }, + + validator: rawCreateSidebarMessageInfoValidator, }); diff --git a/lib/shared/messages/create-sub-thread-message-spec.js b/lib/shared/messages/create-sub-thread-message-spec.js index 5bf4eef27..536b43d5b 100644 --- a/lib/shared/messages/create-sub-thread-message-spec.js +++ b/lib/shared/messages/create-sub-thread-message-spec.js @@ -1,163 +1,166 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type GeneratesNotifsParams, } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import { permissionLookup } from '../../permissions/thread-permissions.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - CreateSubthreadMessageData, - CreateSubthreadMessageInfo, - RawCreateSubthreadMessageInfo, +import { + type CreateSubthreadMessageData, + type CreateSubthreadMessageInfo, + type RawCreateSubthreadMessageInfo, + rawCreateSubthreadMessageInfoValidator, } from '../../types/messages/create-subthread.js'; import type { NotifTexts } from '../../types/notif-types.js'; import { threadPermissions, threadTypes } from '../../types/thread-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { notifTextsForSubthreadCreation } from '../notif-utils.js'; export const createSubThreadMessageSpec: MessageSpec< CreateSubthreadMessageData, RawCreateSubthreadMessageInfo, CreateSubthreadMessageInfo, > = Object.freeze({ messageContentForServerDB( data: CreateSubthreadMessageData | RawCreateSubthreadMessageInfo, ): string { return data.childThreadID; }, messageContentForClientDB(data: RawCreateSubthreadMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): ?RawCreateSubthreadMessageInfo { const subthreadPermissions = row.subthread_permissions; if (!permissionLookup(subthreadPermissions, threadPermissions.KNOW_OF)) { return null; } return { type: messageTypes.CREATE_SUB_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), childThreadID: row.content, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawCreateSubthreadMessageInfo { const content = clientDBMessageInfo.content; invariant( content !== undefined && content !== null, 'content must be defined for CreateSubThread', ); const rawCreateSubthreadMessageInfo: RawCreateSubthreadMessageInfo = { type: messageTypes.CREATE_SUB_THREAD, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, childThreadID: content, }; return rawCreateSubthreadMessageInfo; }, createMessageInfo( rawMessageInfo: RawCreateSubthreadMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?CreateSubthreadMessageInfo { const { threadInfos } = params; const childThreadInfo = threadInfos[rawMessageInfo.childThreadID]; if (!childThreadInfo) { return null; } return { type: messageTypes.CREATE_SUB_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, childThreadInfo, }; }, rawMessageInfoFromMessageData( messageData: CreateSubthreadMessageData, id: ?string, ): RawCreateSubthreadMessageInfo { invariant(id, 'RawCreateSubthreadMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: CreateSubthreadMessageInfo): EntityText { const threadEntity = ET.thread({ display: 'shortName', threadInfo: messageInfo.childThreadInfo, subchannel: true, }); let text; if (messageInfo.childThreadInfo.name) { const childNoun = messageInfo.childThreadInfo.type === threadTypes.SIDEBAR ? 'thread' : 'subchannel'; text = ET`created a ${childNoun} named "${threadEntity}"`; } else { text = ET`created a ${threadEntity}`; } const creator = ET.user({ userInfo: messageInfo.creator }); return ET`${creator} ${text}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.CREATE_SUB_THREAD, 'messageInfo should be messageTypes.CREATE_SUB_THREAD!', ); return notifTextsForSubthreadCreation({ creator: messageInfo.creator, threadType: messageInfo.childThreadInfo.type, parentThreadInfo: threadInfo, childThreadName: messageInfo.childThreadInfo.name, childThreadUIName: messageInfo.childThreadInfo.uiName, }); }, generatesNotifs: async ( rawMessageInfo: RawCreateSubthreadMessageInfo, messageData: CreateSubthreadMessageData, params: GeneratesNotifsParams, ) => { const { userNotMemberOfSubthreads } = params; return userNotMemberOfSubthreads.has(rawMessageInfo.childThreadID) ? pushTypes.NOTIF : undefined; }, threadIDs( rawMessageInfo: RawCreateSubthreadMessageInfo, ): $ReadOnlyArray { return [rawMessageInfo.childThreadID]; }, + + validator: rawCreateSubthreadMessageInfoValidator, }); diff --git a/lib/shared/messages/create-thread-message-spec.js b/lib/shared/messages/create-thread-message-spec.js index 596c51e30..2e73b635c 100644 --- a/lib/shared/messages/create-thread-message-spec.js +++ b/lib/shared/messages/create-thread-message-spec.js @@ -1,203 +1,206 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type RobotextParams, } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - CreateThreadMessageData, - CreateThreadMessageInfo, - RawCreateThreadMessageInfo, +import { + type CreateThreadMessageData, + type CreateThreadMessageInfo, + type RawCreateThreadMessageInfo, + rawCreateThreadMessageInfoValidator, } from '../../types/messages/create-thread.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { notifTextsForSubthreadCreation } from '../notif-utils.js'; import { threadNoun } from '../thread-utils.js'; export const createThreadMessageSpec: MessageSpec< CreateThreadMessageData, RawCreateThreadMessageInfo, CreateThreadMessageInfo, > = Object.freeze({ messageContentForServerDB( data: CreateThreadMessageData | RawCreateThreadMessageInfo, ): string { return JSON.stringify(data.initialThreadState); }, messageContentForClientDB(data: RawCreateThreadMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawCreateThreadMessageInfo { return { type: messageTypes.CREATE_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), initialThreadState: JSON.parse(row.content), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawCreateThreadMessageInfo { const content = clientDBMessageInfo.content; invariant( content !== undefined && content !== null, 'content must be defined for CreateThread', ); const rawCreateThreadMessageInfo: RawCreateThreadMessageInfo = { type: messageTypes.CREATE_THREAD, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, initialThreadState: JSON.parse(content), }; return rawCreateThreadMessageInfo; }, createMessageInfo( rawMessageInfo: RawCreateThreadMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): CreateThreadMessageInfo { const initialParentThreadID = rawMessageInfo.initialThreadState.parentThreadID; const parentThreadInfo = initialParentThreadID ? params.threadInfos[initialParentThreadID] : null; return { type: messageTypes.CREATE_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, initialThreadState: { name: rawMessageInfo.initialThreadState.name, parentThreadInfo, type: rawMessageInfo.initialThreadState.type, color: rawMessageInfo.initialThreadState.color, otherMembers: params.createRelativeUserInfos( rawMessageInfo.initialThreadState.memberIDs.filter( (userID: string) => userID !== rawMessageInfo.creatorID, ), ), }, }; }, rawMessageInfoFromMessageData( messageData: CreateThreadMessageData, id: ?string, ): RawCreateThreadMessageInfo { invariant(id, 'RawCreateThreadMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: CreateThreadMessageInfo, params: RobotextParams, ): EntityText { let text = ET`created ${ET.thread({ display: 'alwaysDisplayShortName', threadID: messageInfo.threadID, threadType: params.threadInfo?.type, parentThreadID: params.threadInfo?.parentThreadID, })}`; const parentThread = messageInfo.initialThreadState.parentThreadInfo; if (parentThread) { text = ET`${text} as a child of ${ET.thread({ display: 'uiName', threadInfo: parentThread, })}`; } if (messageInfo.initialThreadState.name) { text = ET`${text} with the name "${messageInfo.initialThreadState.name}"`; } const users = messageInfo.initialThreadState.otherMembers; if (users.length !== 0) { const initialUsers = pluralizeEntityText( users.map(user => ET`${ET.user({ userInfo: user })}`), ); text = ET`${text} and added ${initialUsers}`; } const creator = ET.user({ userInfo: messageInfo.creator }); return ET`${creator} ${text}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.CREATE_THREAD, 'messageInfo should be messageTypes.CREATE_THREAD!', ); const threadType = messageInfo.initialThreadState.type; const parentThreadInfo = messageInfo.initialThreadState.parentThreadInfo; const threadName = messageInfo.initialThreadState.name; if (parentThreadInfo) { return notifTextsForSubthreadCreation({ creator: messageInfo.creator, threadType, parentThreadInfo, childThreadName: threadName, childThreadUIName: threadInfo.uiName, }); } const creator = ET.user({ userInfo: messageInfo.creator }); const prefix = ET`${creator}`; const body = `created a new ${threadNoun(threadType)}`; let merged = ET`${prefix} ${body}`; if (threadName) { merged = ET`${merged} called "${threadName}"`; } return { merged, body, title: threadInfo.uiName, prefix, }; }, generatesNotifs: async () => pushTypes.NOTIF, userIDs(rawMessageInfo: RawCreateThreadMessageInfo): $ReadOnlyArray { return rawMessageInfo.initialThreadState.memberIDs; }, startsThread: true, threadIDs( rawMessageInfo: RawCreateThreadMessageInfo, ): $ReadOnlyArray { const { parentThreadID } = rawMessageInfo.initialThreadState; return parentThreadID ? [parentThreadID] : []; }, + + validator: rawCreateThreadMessageInfoValidator, }); diff --git a/lib/shared/messages/delete-entry-message-spec.js b/lib/shared/messages/delete-entry-message-spec.js index 0873c40e4..4c17b7446 100644 --- a/lib/shared/messages/delete-entry-message-spec.js +++ b/lib/shared/messages/delete-entry-message-spec.js @@ -1,137 +1,140 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - DeleteEntryMessageData, - DeleteEntryMessageInfo, - RawDeleteEntryMessageInfo, +import { + type DeleteEntryMessageData, + type DeleteEntryMessageInfo, + type RawDeleteEntryMessageInfo, + rawDeleteEntryMessageInfoValidator, } from '../../types/messages/delete-entry.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { prettyDate } from '../../utils/date-utils.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; export const deleteEntryMessageSpec: MessageSpec< DeleteEntryMessageData, RawDeleteEntryMessageInfo, DeleteEntryMessageInfo, > = Object.freeze({ messageContentForServerDB( data: DeleteEntryMessageData | RawDeleteEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawDeleteEntryMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawDeleteEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.DELETE_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawDeleteEntryMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for DeleteEntry', ); const content = JSON.parse(clientDBMessageInfo.content); const rawDeleteEntryMessageInfo: RawDeleteEntryMessageInfo = { type: messageTypes.DELETE_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawDeleteEntryMessageInfo; }, createMessageInfo( rawMessageInfo: RawDeleteEntryMessageInfo, creator: RelativeUserInfo, ): DeleteEntryMessageInfo { return { type: messageTypes.DELETE_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: DeleteEntryMessageData, id: ?string, ): RawDeleteEntryMessageInfo { invariant(id, 'RawDeleteEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: DeleteEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} deleted an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.DELETE_ENTRY, 'messageInfo should be messageTypes.DELETE_ENTRY!', ); const thread = ET.thread({ display: 'shortName', threadInfo }); const creator = ET.user({ userInfo: messageInfo.creator }); const date = prettyDate(messageInfo.date); const prefix = ET`${creator}`; let body = ET`deleted an event in ${thread}`; body = ET`${body} scheduled for ${date}: "${messageInfo.text}"`; const merged = ET`${prefix} ${body}`; return { merged, title: threadInfo.uiName, body, prefix, }; }, generatesNotifs: async () => pushTypes.NOTIF, + + validator: rawDeleteEntryMessageInfoValidator, }); diff --git a/lib/shared/messages/edit-entry-message-spec.js b/lib/shared/messages/edit-entry-message-spec.js index d7dca655f..5d34d8de5 100644 --- a/lib/shared/messages/edit-entry-message-spec.js +++ b/lib/shared/messages/edit-entry-message-spec.js @@ -1,122 +1,125 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - EditEntryMessageData, - EditEntryMessageInfo, - RawEditEntryMessageInfo, +import { + type EditEntryMessageData, + type EditEntryMessageInfo, + type RawEditEntryMessageInfo, + rawEditEntryMessageInfoValidator, } from '../../types/messages/edit-entry.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { prettyDate } from '../../utils/date-utils.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { notifTextsForEntryCreationOrEdit } from '../notif-utils.js'; export const editEntryMessageSpec: MessageSpec< EditEntryMessageData, RawEditEntryMessageInfo, EditEntryMessageInfo, > = Object.freeze({ messageContentForServerDB( data: EditEntryMessageData | RawEditEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawEditEntryMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawEditEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.EDIT_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawEditEntryMessageInfo { invariant( clientDBMessageInfo.content !== null && clientDBMessageInfo.content !== undefined, 'content must be defined for EditEntry', ); const content = JSON.parse(clientDBMessageInfo.content); const rawEditEntryMessageInfo: RawEditEntryMessageInfo = { type: messageTypes.EDIT_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawEditEntryMessageInfo; }, createMessageInfo( rawMessageInfo: RawEditEntryMessageInfo, creator: RelativeUserInfo, ): EditEntryMessageInfo { return { type: messageTypes.EDIT_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: EditEntryMessageData, id: ?string, ): RawEditEntryMessageInfo { invariant(id, 'RawEditEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: EditEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} updated the text of an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { return notifTextsForEntryCreationOrEdit(messageInfos, threadInfo); }, notificationCollapseKey(rawMessageInfo: RawEditEntryMessageInfo): string { return joinResult(rawMessageInfo.creatorID, rawMessageInfo.entryID); }, generatesNotifs: async () => pushTypes.NOTIF, + + validator: rawEditEntryMessageInfoValidator, }); diff --git a/lib/shared/messages/edit-message-spec.js b/lib/shared/messages/edit-message-spec.js index 52397505c..3e7768356 100644 --- a/lib/shared/messages/edit-message-spec.js +++ b/lib/shared/messages/edit-message-spec.js @@ -1,143 +1,146 @@ // @flow import invariant from 'invariant'; import { type MessageSpec, type MessageTitleParam } from './message-spec.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { assertMessageType, messageTypes, } from '../../types/message-types-enum.js'; import { type ClientDBMessageInfo } from '../../types/message-types.js'; -import type { - EditMessageData, - RawEditMessageInfo, - EditMessageInfo, +import { + type EditMessageData, + type RawEditMessageInfo, + type EditMessageInfo, + rawEditMessageInfoValidator, } from '../../types/messages/edit.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET } from '../../utils/entity-text.js'; import { hasMinCodeVersion } from '../version-utils.js'; export const editMessageSpec: MessageSpec< EditMessageData, RawEditMessageInfo, EditMessageInfo, > = Object.freeze({ messageContentForServerDB( data: EditMessageData | RawEditMessageInfo, ): string { return JSON.stringify({ text: data.text, }); }, messageContentForClientDB(data: RawEditMessageInfo): string { return JSON.stringify({ targetMessageID: data.targetMessageID, text: data.text, }); }, messageTitle({ messageInfo }: MessageTitleParam) { const creator = ET.user({ userInfo: messageInfo.creator }); return ET`${creator} edited a message`; }, rawMessageInfoFromServerDBRow(row: Object): RawEditMessageInfo { invariant( row.targetMessageID !== null && row.targetMessageID !== undefined, 'targetMessageID should be set', ); const content = JSON.parse(row.content); return { type: messageTypes.EDIT_MESSAGE, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), targetMessageID: row.targetMessageID.toString(), text: content.text, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawEditMessageInfo { const messageType = assertMessageType(parseInt(clientDBMessageInfo.type)); invariant( messageType === messageTypes.EDIT_MESSAGE, 'message must be of type EDIT_MESSAGE', ); invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for EditMessage', ); const content = JSON.parse(clientDBMessageInfo.content); const rawEditMessageInfo: RawEditMessageInfo = { type: messageTypes.EDIT_MESSAGE, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, targetMessageID: content.targetMessageID, text: content.text, }; return rawEditMessageInfo; }, createMessageInfo( rawMessageInfo: RawEditMessageInfo, creator: RelativeUserInfo, ): EditMessageInfo { return { type: messageTypes.EDIT_MESSAGE, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, targetMessageID: rawMessageInfo.targetMessageID, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: EditMessageData, id: ?string, ): RawEditMessageInfo { invariant(id, 'RawEditMessageInfo needs id'); return { ...messageData, id }; }, shimUnsupportedMessageInfo( rawMessageInfo: RawEditMessageInfo, platformDetails: ?PlatformDetails, ): RawEditMessageInfo | RawUnsupportedMessageInfo { if (hasMinCodeVersion(platformDetails, 215)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'edited a message', unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo(unwrapped: RawEditMessageInfo): RawEditMessageInfo { return unwrapped; }, generatesNotifs: async () => undefined, + + validator: rawEditMessageInfoValidator, }); diff --git a/lib/shared/messages/join-thread-message-spec.js b/lib/shared/messages/join-thread-message-spec.js index 9fe9a67ad..f6e0f2602 100644 --- a/lib/shared/messages/join-thread-message-spec.js +++ b/lib/shared/messages/join-thread-message-spec.js @@ -1,122 +1,125 @@ // @flow import invariant from 'invariant'; import type { MessageSpec, RobotextParams } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - JoinThreadMessageData, - JoinThreadMessageInfo, - RawJoinThreadMessageInfo, +import { + type JoinThreadMessageData, + type JoinThreadMessageInfo, + type RawJoinThreadMessageInfo, + rawJoinThreadMessageInfoValidator, } from '../../types/messages/join-thread.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; export const joinThreadMessageSpec: MessageSpec< JoinThreadMessageData, RawJoinThreadMessageInfo, JoinThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromServerDBRow(row: Object): RawJoinThreadMessageInfo { return { type: messageTypes.JOIN_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawJoinThreadMessageInfo { const rawJoinThreadMessageInfo: RawJoinThreadMessageInfo = { type: messageTypes.JOIN_THREAD, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, }; return rawJoinThreadMessageInfo; }, createMessageInfo( rawMessageInfo: RawJoinThreadMessageInfo, creator: RelativeUserInfo, ): JoinThreadMessageInfo { return { type: messageTypes.JOIN_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData( messageData: JoinThreadMessageData, id: ?string, ): RawJoinThreadMessageInfo { invariant(id, 'RawJoinThreadMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: JoinThreadMessageInfo, params: RobotextParams, ): EntityText { const creator = ET.user({ userInfo: messageInfo.creator }); return ET`${creator} joined ${ET.thread({ display: 'alwaysDisplayShortName', threadID: messageInfo.threadID, threadType: params.threadInfo?.type, parentThreadID: params.threadInfo?.parentThreadID, })}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const joinerArray = {}; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.JOIN_THREAD, 'messageInfo should be messageTypes.JOIN_THREAD!', ); joinerArray[messageInfo.creator.id] = messageInfo.creator; } const joiners = values(joinerArray); const joiningUsers = pluralizeEntityText( joiners.map(user => ET`${ET.user({ userInfo: user })}`), ); const body = ET`${joiningUsers} joined`; const thread = ET.thread({ display: 'shortName', threadInfo }); const merged = ET`${body} ${thread}`; return { merged, title: threadInfo.uiName, body, }; }, notificationCollapseKey(rawMessageInfo: RawJoinThreadMessageInfo): string { return joinResult(rawMessageInfo.type, rawMessageInfo.threadID); }, generatesNotifs: async () => undefined, + + validator: rawJoinThreadMessageInfoValidator, }); diff --git a/lib/shared/messages/leave-thread-message-spec.js b/lib/shared/messages/leave-thread-message-spec.js index d505b360a..874edfa7e 100644 --- a/lib/shared/messages/leave-thread-message-spec.js +++ b/lib/shared/messages/leave-thread-message-spec.js @@ -1,122 +1,125 @@ // @flow import invariant from 'invariant'; import type { MessageSpec, RobotextParams } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - LeaveThreadMessageData, - LeaveThreadMessageInfo, - RawLeaveThreadMessageInfo, +import { + type LeaveThreadMessageData, + type LeaveThreadMessageInfo, + type RawLeaveThreadMessageInfo, + rawLeaveThreadMessageInfoValidator, } from '../../types/messages/leave-thread.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; export const leaveThreadMessageSpec: MessageSpec< LeaveThreadMessageData, RawLeaveThreadMessageInfo, LeaveThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromServerDBRow(row: Object): RawLeaveThreadMessageInfo { return { type: messageTypes.LEAVE_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawLeaveThreadMessageInfo { const rawLeaveThreadMessageInfo: RawLeaveThreadMessageInfo = { type: messageTypes.LEAVE_THREAD, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, }; return rawLeaveThreadMessageInfo; }, createMessageInfo( rawMessageInfo: RawLeaveThreadMessageInfo, creator: RelativeUserInfo, ): LeaveThreadMessageInfo { return { type: messageTypes.LEAVE_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData( messageData: LeaveThreadMessageData, id: ?string, ): RawLeaveThreadMessageInfo { invariant(id, 'RawLeaveThreadMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: LeaveThreadMessageInfo, params: RobotextParams, ): EntityText { const creator = ET.user({ userInfo: messageInfo.creator }); return ET`${creator} left ${ET.thread({ display: 'alwaysDisplayShortName', threadID: messageInfo.threadID, threadType: params.threadInfo?.type, parentThreadID: params.threadInfo?.parentThreadID, })}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const leaverBeavers = {}; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.LEAVE_THREAD, 'messageInfo should be messageTypes.LEAVE_THREAD!', ); leaverBeavers[messageInfo.creator.id] = messageInfo.creator; } const leavers = values(leaverBeavers); const leavingUsers = pluralizeEntityText( leavers.map(user => ET`${ET.user({ userInfo: user })}`), ); const body = ET`${leavingUsers} left`; const thread = ET.thread({ display: 'shortName', threadInfo }); const merged = ET`${body} ${thread}`; return { merged, title: threadInfo.uiName, body, }; }, notificationCollapseKey(rawMessageInfo: RawLeaveThreadMessageInfo): string { return joinResult(rawMessageInfo.type, rawMessageInfo.threadID); }, generatesNotifs: async () => undefined, + + validator: rawLeaveThreadMessageInfoValidator, }); diff --git a/lib/shared/messages/message-spec.js b/lib/shared/messages/message-spec.js index 0373dbc0b..7b0a461cb 100644 --- a/lib/shared/messages/message-spec.js +++ b/lib/shared/messages/message-spec.js @@ -1,117 +1,120 @@ // @flow +import type { TType } from 'tcomb'; + import type { PlatformDetails } from '../../types/device-types.js'; import type { Media } from '../../types/media-types.js'; import type { MessageInfo, ClientDBMessageInfo, RawComposableMessageInfo, RawMessageInfo, RawRobotextMessageInfo, } from '../../types/message-types.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo, UserInfo } from '../../types/user-types.js'; import type { EntityText } from '../../utils/entity-text.js'; import { type ParserRules } from '../markdown.js'; export type MessageTitleParam = { +messageInfo: Info, +threadInfo: ThreadInfo, +markdownRules: ParserRules, }; export type RawMessageInfoFromServerDBRowParams = { +localID: ?string, +media?: $ReadOnlyArray, +derivedMessages: $ReadOnlyMap< string, RawComposableMessageInfo | RawRobotextMessageInfo, >, }; export type CreateMessageInfoParams = { +threadInfos: { +[id: string]: ThreadInfo }, +createMessageInfoFromRaw: (rawInfo: RawMessageInfo) => ?MessageInfo, +createRelativeUserInfos: ( userIDs: $ReadOnlyArray, ) => RelativeUserInfo[], }; export type RobotextParams = { +threadInfo: ?ThreadInfo, }; export type NotificationTextsParams = { +notificationTexts: ( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ) => Promise, +notifTargetUserInfo: UserInfo, }; export type GeneratesNotifsParams = { +notifTargetUserID: string, +userNotMemberOfSubthreads: Set, +fetchMessageInfoByID: (messageID: string) => Promise, }; export const pushTypes = Object.freeze({ NOTIF: 'notif', RESCIND: 'rescind', }); export type PushType = $Values; export type CreationSideEffectsFunc = ( messageInfo: RawInfo, threadInfo: ThreadInfo, parentThreadInfo: ?ThreadInfo, ) => Promise; export type MessageSpec = { +messageContentForServerDB?: (data: Data | RawInfo) => string, +messageContentForClientDB?: (data: RawInfo) => string, +messageTitle?: (param: MessageTitleParam) => EntityText, +rawMessageInfoFromServerDBRow?: ( row: Object, params: RawMessageInfoFromServerDBRowParams, ) => ?RawInfo, +rawMessageInfoFromClientDB: ( clientDBMessageInfo: ClientDBMessageInfo, ) => RawInfo, +createMessageInfo: ( rawMessageInfo: RawInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ) => ?Info, +rawMessageInfoFromMessageData?: (messageData: Data, id: ?string) => RawInfo, +robotext?: (messageInfo: Info, params: RobotextParams) => EntityText, +shimUnsupportedMessageInfo?: ( rawMessageInfo: RawInfo, platformDetails: ?PlatformDetails, ) => RawInfo | RawUnsupportedMessageInfo, +unshimMessageInfo?: ( unwrapped: RawInfo, messageInfo: RawMessageInfo, ) => ?RawMessageInfo, +notificationTexts?: ( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, params: NotificationTextsParams, ) => Promise, +notificationCollapseKey?: ( rawMessageInfo: RawInfo, messageData: Data, ) => ?string, +generatesNotifs: ( rawMessageInfo: RawInfo, messageData: Data, params: GeneratesNotifsParams, ) => Promise, +userIDs?: (rawMessageInfo: RawInfo) => $ReadOnlyArray, +startsThread?: boolean, +threadIDs?: (rawMessageInfo: RawInfo) => $ReadOnlyArray, +includedInRepliesCount?: boolean, +useCreationSideEffectsFunc?: () => CreationSideEffectsFunc, + +validator: TType, }; diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js index 899a21408..492a35a2e 100644 --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -1,335 +1,340 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec, type MessageTitleParam, type RawMessageInfoFromServerDBRowParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import { contentStringForMediaArray, multimediaMessagePreview, } from '../../media/media-utils.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { assertMessageType, messageTypes, } from '../../types/message-types-enum.js'; -import { isMediaMessageType } from '../../types/message-types.js'; +import { + isMediaMessageType, + rawMultimediaMessageInfoValidator, +} from '../../types/message-types.js'; import type { MessageInfo, RawMessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import type { ImagesMessageData, RawImagesMessageInfo, ImagesMessageInfo, } from '../../types/messages/images.js'; import type { MediaMessageData, MediaMessageInfo, RawMediaMessageInfo, } from '../../types/messages/media.js'; import { getMediaMessageServerDBContentsFromMedia } from '../../types/messages/media.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET } from '../../utils/entity-text.js'; import { translateClientDBMediaInfosToMedia, translateClientDBMediaInfoToImage, } from '../../utils/message-ops-utils.js'; import { createMediaMessageInfo } from '../message-utils.js'; import { threadIsGroupChat } from '../thread-utils.js'; import { hasMinCodeVersion } from '../version-utils.js'; export const multimediaMessageSpec: MessageSpec< MediaMessageData | ImagesMessageData, RawMediaMessageInfo | RawImagesMessageInfo, MediaMessageInfo | ImagesMessageInfo, > = Object.freeze({ messageContentForServerDB( data: | MediaMessageData | ImagesMessageData | RawMediaMessageInfo | RawImagesMessageInfo, ): string { if (data.type === messageTypes.MULTIMEDIA) { return JSON.stringify( getMediaMessageServerDBContentsFromMedia(data.media), ); } const mediaIDs = data.media.map(media => parseInt(media.id, 10)); return JSON.stringify(mediaIDs); }, messageContentForClientDB( data: RawMediaMessageInfo | RawImagesMessageInfo, ): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawImagesMessageInfo | RawMediaMessageInfo { const messageType = assertMessageType(parseInt(clientDBMessageInfo.type)); invariant( isMediaMessageType(messageType), 'message must be of type IMAGES or MULTIMEDIA', ); invariant( clientDBMessageInfo.media_infos !== null && clientDBMessageInfo.media_infos !== undefined, `media_infos must be defined`, ); let rawMessageInfo: RawImagesMessageInfo | RawMediaMessageInfo = messageType === messageTypes.IMAGES ? { type: messageTypes.IMAGES, threadID: clientDBMessageInfo.thread, creatorID: clientDBMessageInfo.user, time: parseInt(clientDBMessageInfo.time), media: clientDBMessageInfo.media_infos?.map( translateClientDBMediaInfoToImage, ) ?? [], } : { type: messageTypes.MULTIMEDIA, threadID: clientDBMessageInfo.thread, creatorID: clientDBMessageInfo.user, time: parseInt(clientDBMessageInfo.time), media: translateClientDBMediaInfosToMedia(clientDBMessageInfo), }; if (clientDBMessageInfo.local_id) { rawMessageInfo = { ...rawMessageInfo, localID: clientDBMessageInfo.local_id, }; } if (clientDBMessageInfo.id !== clientDBMessageInfo.local_id) { rawMessageInfo = { ...rawMessageInfo, id: clientDBMessageInfo.id, }; } return rawMessageInfo; }, messageTitle({ messageInfo, }: MessageTitleParam) { const creator = ET.user({ userInfo: messageInfo.creator }); const preview = multimediaMessagePreview(messageInfo); return ET`${creator} ${preview}`; }, rawMessageInfoFromServerDBRow( row: Object, params: RawMessageInfoFromServerDBRowParams, ): RawMediaMessageInfo | RawImagesMessageInfo { const { localID, media } = params; invariant(media, 'Media should be provided'); return createMediaMessageInfo({ threadID: row.threadID.toString(), creatorID: row.creatorID.toString(), media, id: row.id.toString(), localID, time: row.time, }); }, createMessageInfo( rawMessageInfo: RawMediaMessageInfo | RawImagesMessageInfo, creator: RelativeUserInfo, ): ?(MediaMessageInfo | ImagesMessageInfo) { if (rawMessageInfo.type === messageTypes.IMAGES) { let messageInfo: ImagesMessageInfo = { type: messageTypes.IMAGES, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, media: rawMessageInfo.media, }; if (rawMessageInfo.id) { messageInfo = { ...messageInfo, id: rawMessageInfo.id }; } if (rawMessageInfo.localID) { messageInfo = { ...messageInfo, localID: rawMessageInfo.localID }; } return messageInfo; } else if (rawMessageInfo.type === messageTypes.MULTIMEDIA) { let messageInfo: MediaMessageInfo = { type: messageTypes.MULTIMEDIA, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, media: rawMessageInfo.media, }; if (rawMessageInfo.id) { messageInfo = { ...messageInfo, id: rawMessageInfo.id }; } if (rawMessageInfo.localID) { messageInfo = { ...messageInfo, localID: rawMessageInfo.localID }; } return messageInfo; } return undefined; }, rawMessageInfoFromMessageData( messageData: MediaMessageData | ImagesMessageData, id: ?string, ): RawMediaMessageInfo | RawImagesMessageInfo { const { sidebarCreation, ...rest } = messageData; if (rest.type === messageTypes.IMAGES && id) { return ({ ...rest, id }: RawImagesMessageInfo); } else if (rest.type === messageTypes.IMAGES) { return ({ ...rest }: RawImagesMessageInfo); } else if (id) { return ({ ...rest, id }: RawMediaMessageInfo); } else { return ({ ...rest }: RawMediaMessageInfo); } }, shimUnsupportedMessageInfo( rawMessageInfo: RawMediaMessageInfo | RawImagesMessageInfo, platformDetails: ?PlatformDetails, ): RawMediaMessageInfo | RawImagesMessageInfo | RawUnsupportedMessageInfo { if (rawMessageInfo.type === messageTypes.IMAGES) { return rawMessageInfo; } const containsEncryptedMedia = rawMessageInfo.media.some( media => media.type === 'encrypted_photo' || media.type === 'encrypted_video', ); if (!containsEncryptedMedia && hasMinCodeVersion(platformDetails, 158)) { return rawMessageInfo; } if (hasMinCodeVersion(platformDetails, 205)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: multimediaMessagePreview(rawMessageInfo), unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo( unwrapped: RawMediaMessageInfo | RawImagesMessageInfo, messageInfo: RawMessageInfo, ): ?RawMessageInfo { if (unwrapped.type === messageTypes.IMAGES) { return { ...unwrapped, media: unwrapped.media.map(media => { if (media.dimensions) { return media; } const dimensions = preDimensionUploads[media.id]; invariant( dimensions, 'only four photos were uploaded before dimensions were calculated, ' + `and ${media.id} was not one of them`, ); return { ...media, dimensions }; }), }; } else if (unwrapped.type === messageTypes.MULTIMEDIA) { for (const { type } of unwrapped.media) { if ( type !== 'photo' && type !== 'video' && type !== 'encrypted_photo' && type !== 'encrypted_video' ) { return messageInfo; } } } return undefined; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const media = []; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.IMAGES || messageInfo.type === messageTypes.MULTIMEDIA, 'messageInfo should be multimedia type!', ); for (const singleMedia of messageInfo.media) { media.push(singleMedia); } } const contentString = contentStringForMediaArray(media); const creator = ET.user({ userInfo: messageInfos[0].creator }); let body, merged; if (!threadInfo.name && !threadIsGroupChat(threadInfo)) { body = `sent you ${contentString}`; merged = body; } else { body = `sent ${contentString}`; const thread = ET.thread({ display: 'shortName', threadInfo }); merged = ET`${body} to ${thread}`; } merged = ET`${creator} ${merged}`; return { merged, body, title: threadInfo.uiName, prefix: ET`${creator}`, }; }, notificationCollapseKey( rawMessageInfo: RawMediaMessageInfo | RawImagesMessageInfo, ): string { // We use the legacy constant here to collapse both types into one return joinResult( messageTypes.IMAGES, rawMessageInfo.threadID, rawMessageInfo.creatorID, ); }, generatesNotifs: async ( rawMessageInfo: RawMediaMessageInfo | RawImagesMessageInfo, messageData: MediaMessageData | ImagesMessageData, ) => (messageData.sidebarCreation ? undefined : pushTypes.NOTIF), includedInRepliesCount: true, + + validator: rawMultimediaMessageInfoValidator, }); // Four photos were uploaded before dimensions were calculated server-side, // and delivered to clients without dimensions in the MultimediaMessageInfo. const preDimensionUploads = { '156642': { width: 1440, height: 1080 }, '156649': { width: 720, height: 803 }, '156794': { width: 720, height: 803 }, '156877': { width: 574, height: 454 }, }; diff --git a/lib/shared/messages/reaction-message-spec.js b/lib/shared/messages/reaction-message-spec.js index e8dc1f3d9..ec5d7931d 100644 --- a/lib/shared/messages/reaction-message-spec.js +++ b/lib/shared/messages/reaction-message-spec.js @@ -1,219 +1,222 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec, type MessageTitleParam, type GeneratesNotifsParams, } from './message-spec.js'; import { assertSingleMessageInfo, joinResult } from './utils.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { assertMessageType, messageTypes, } from '../../types/message-types-enum.js'; import { type MessageInfo, type ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - ReactionMessageData, - RawReactionMessageInfo, - ReactionMessageInfo, +import { + type ReactionMessageData, + type RawReactionMessageInfo, + type ReactionMessageInfo, + rawReactionMessageInfoValidator, } from '../../types/messages/reaction.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET } from '../../utils/entity-text.js'; import { threadIsGroupChat } from '../thread-utils.js'; import { hasMinCodeVersion } from '../version-utils.js'; export const reactionMessageSpec: MessageSpec< ReactionMessageData, RawReactionMessageInfo, ReactionMessageInfo, > = Object.freeze({ messageContentForServerDB( data: ReactionMessageData | RawReactionMessageInfo, ): string { return JSON.stringify({ reaction: data.reaction, action: data.action, }); }, messageContentForClientDB(data: RawReactionMessageInfo): string { return JSON.stringify({ targetMessageID: data.targetMessageID, reaction: data.reaction, action: data.action, }); }, messageTitle({ messageInfo }: MessageTitleParam) { const creator = ET.user({ userInfo: messageInfo.creator }); const preview = messageInfo.action === 'add_reaction' ? `reacted ${messageInfo.reaction} to a message` : 'unreacted to a message'; return ET`${creator} ${preview}`; }, rawMessageInfoFromServerDBRow(row: Object): RawReactionMessageInfo { invariant( row.targetMessageID !== null && row.targetMessageID !== undefined, 'targetMessageID should be set', ); const content = JSON.parse(row.content); return { type: messageTypes.REACTION, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), targetMessageID: row.targetMessageID.toString(), reaction: content.reaction, action: content.action, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawReactionMessageInfo { const messageType = assertMessageType(parseInt(clientDBMessageInfo.type)); invariant( messageType === messageTypes.REACTION, 'message must be of type REACTION', ); invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for Reaction', ); const content = JSON.parse(clientDBMessageInfo.content); const rawReactionMessageInfo: RawReactionMessageInfo = { type: messageTypes.REACTION, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, targetMessageID: content.targetMessageID, reaction: content.reaction, action: content.action, }; return rawReactionMessageInfo; }, createMessageInfo( rawMessageInfo: RawReactionMessageInfo, creator: RelativeUserInfo, ): ReactionMessageInfo { return { type: messageTypes.REACTION, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, targetMessageID: rawMessageInfo.targetMessageID, reaction: rawMessageInfo.reaction, action: rawMessageInfo.action, }; }, rawMessageInfoFromMessageData( messageData: ReactionMessageData, id: ?string, ): RawReactionMessageInfo { invariant(id, 'RawReactionMessageInfo needs id'); return { ...messageData, id }; }, shimUnsupportedMessageInfo( rawMessageInfo: RawReactionMessageInfo, platformDetails: ?PlatformDetails, ): RawReactionMessageInfo | RawUnsupportedMessageInfo { if (hasMinCodeVersion(platformDetails, 167)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'reacted to a message', unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo(unwrapped: RawReactionMessageInfo): RawReactionMessageInfo { return unwrapped; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.REACTION, 'messageInfo should be reaction type', ); const creator = ET.user({ userInfo: messageInfo.creator }); const body = messageInfo.action === 'add_reaction' ? `reacted ${messageInfo.reaction} to your message` : 'unreacted to your message'; let merged = ET`${creator} ${body}`; if (threadInfo.name || threadIsGroupChat(threadInfo)) { const thread = ET.thread({ display: 'shortName', threadInfo }); merged = ET`${merged} in ${thread}`; } return { merged, body, title: threadInfo.uiName, prefix: ET`${creator}`, }; }, notificationCollapseKey(rawMessageInfo: RawReactionMessageInfo): string { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, rawMessageInfo.targetMessageID, ); }, generatesNotifs: async ( rawMessageInfo: RawReactionMessageInfo, messageData: ReactionMessageData, params: GeneratesNotifsParams, ) => { const { action } = rawMessageInfo; const { notifTargetUserID, fetchMessageInfoByID } = params; const targetMessageInfo = await fetchMessageInfoByID( rawMessageInfo.targetMessageID, ); if (targetMessageInfo?.creatorID !== notifTargetUserID) { return undefined; } return action === 'add_reaction' ? pushTypes.NOTIF : pushTypes.RESCIND; }, + + validator: rawReactionMessageInfoValidator, }); diff --git a/lib/shared/messages/remove-members-message-spec.js b/lib/shared/messages/remove-members-message-spec.js index 30603a3a0..20f8e6071 100644 --- a/lib/shared/messages/remove-members-message-spec.js +++ b/lib/shared/messages/remove-members-message-spec.js @@ -1,159 +1,162 @@ // @flow import invariant from 'invariant'; import type { CreateMessageInfoParams, MessageSpec } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - RawRemoveMembersMessageInfo, - RemoveMembersMessageData, - RemoveMembersMessageInfo, +import { + type RawRemoveMembersMessageInfo, + type RemoveMembersMessageData, + type RemoveMembersMessageInfo, + rawRemoveMembersMessageInfoValidator, } from '../../types/messages/remove-members.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; export const removeMembersMessageSpec: MessageSpec< RemoveMembersMessageData, RawRemoveMembersMessageInfo, RemoveMembersMessageInfo, > = Object.freeze({ messageContentForServerDB( data: RemoveMembersMessageData | RawRemoveMembersMessageInfo, ): string { return JSON.stringify(data.removedUserIDs); }, messageContentForClientDB(data: RawRemoveMembersMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawRemoveMembersMessageInfo { return { type: messageTypes.REMOVE_MEMBERS, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), removedUserIDs: JSON.parse(row.content), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawRemoveMembersMessageInfo { const content = clientDBMessageInfo.content; invariant( content !== undefined && content !== null, 'content must be defined for RemoveMembers', ); const rawRemoveMembersMessageInfo: RawRemoveMembersMessageInfo = { type: messageTypes.REMOVE_MEMBERS, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, removedUserIDs: JSON.parse(content), }; return rawRemoveMembersMessageInfo; }, createMessageInfo( rawMessageInfo: RawRemoveMembersMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): RemoveMembersMessageInfo { const removedMembers = params.createRelativeUserInfos( rawMessageInfo.removedUserIDs, ); return { type: messageTypes.REMOVE_MEMBERS, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, removedMembers, }; }, rawMessageInfoFromMessageData( messageData: RemoveMembersMessageData, id: ?string, ): RawRemoveMembersMessageInfo { invariant(id, 'RawRemoveMembersMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: RemoveMembersMessageInfo): EntityText { const users = messageInfo.removedMembers; invariant(users.length !== 0, 'added who??'); const creator = ET.user({ userInfo: messageInfo.creator }); const removedUsers = pluralizeEntityText( users.map(user => ET`${ET.user({ userInfo: user })}`), ); return ET`${creator} removed ${removedUsers}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const removedMembersObject = {}; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.REMOVE_MEMBERS, 'messageInfo should be messageTypes.REMOVE_MEMBERS!', ); for (const member of messageInfo.removedMembers) { removedMembersObject[member.id] = member; } } const removedMembers = values(removedMembersObject); const mostRecentMessageInfo = messageInfos[0]; invariant( mostRecentMessageInfo.type === messageTypes.REMOVE_MEMBERS, 'messageInfo should be messageTypes.REMOVE_MEMBERS!', ); const mergedMessageInfo = { ...mostRecentMessageInfo, removedMembers }; const robotext = notifRobotextForMessageInfo(mergedMessageInfo, threadInfo); const merged = ET`${robotext} from ${ET.thread({ display: 'shortName', threadInfo, })}`; return { merged, title: threadInfo.uiName, body: robotext, }; }, notificationCollapseKey(rawMessageInfo: RawRemoveMembersMessageInfo): string { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, ); }, generatesNotifs: async () => undefined, userIDs(rawMessageInfo: RawRemoveMembersMessageInfo): $ReadOnlyArray { return rawMessageInfo.removedUserIDs; }, + + validator: rawRemoveMembersMessageInfoValidator, }); diff --git a/lib/shared/messages/restore-entry-message-spec.js b/lib/shared/messages/restore-entry-message-spec.js index 00c7f4bb4..38b6f45bd 100644 --- a/lib/shared/messages/restore-entry-message-spec.js +++ b/lib/shared/messages/restore-entry-message-spec.js @@ -1,137 +1,140 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - RawRestoreEntryMessageInfo, - RestoreEntryMessageData, - RestoreEntryMessageInfo, +import { + type RawRestoreEntryMessageInfo, + type RestoreEntryMessageData, + type RestoreEntryMessageInfo, + rawRestoreEntryMessageInfoValidator, } from '../../types/messages/restore-entry.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { prettyDate } from '../../utils/date-utils.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; export const restoreEntryMessageSpec: MessageSpec< RestoreEntryMessageData, RawRestoreEntryMessageInfo, RestoreEntryMessageInfo, > = Object.freeze({ messageContentForServerDB( data: RestoreEntryMessageData | RawRestoreEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawRestoreEntryMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawRestoreEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.RESTORE_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawRestoreEntryMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for RestoreEntry', ); const content = JSON.parse(clientDBMessageInfo.content); const rawRestoreEntryMessageInfo: RawRestoreEntryMessageInfo = { type: messageTypes.RESTORE_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawRestoreEntryMessageInfo; }, createMessageInfo( rawMessageInfo: RawRestoreEntryMessageInfo, creator: RelativeUserInfo, ): RestoreEntryMessageInfo { return { type: messageTypes.RESTORE_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: RestoreEntryMessageData, id: ?string, ): RawRestoreEntryMessageInfo { invariant(id, 'RawRestoreEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: RestoreEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} restored an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.RESTORE_ENTRY, 'messageInfo should be messageTypes.RESTORE_ENTRY!', ); const thread = ET.thread({ display: 'shortName', threadInfo }); const creator = ET.user({ userInfo: messageInfo.creator }); const date = prettyDate(messageInfo.date); const prefix = ET`${creator}`; let body = ET`restored an event in ${thread}`; body = ET`${body} scheduled for ${date}: "${messageInfo.text}"`; const merged = ET`${prefix} ${body}`; return { merged, title: threadInfo.uiName, body, prefix, }; }, generatesNotifs: async () => pushTypes.NOTIF, + + validator: rawRestoreEntryMessageInfoValidator, }); diff --git a/lib/shared/messages/sidebar-source-message-spec.js b/lib/shared/messages/sidebar-source-message-spec.js index 08bd5d6c0..abe54ec70 100644 --- a/lib/shared/messages/sidebar-source-message-spec.js +++ b/lib/shared/messages/sidebar-source-message-spec.js @@ -1,176 +1,179 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type RawMessageInfoFromServerDBRowParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { type RawSidebarSourceMessageInfo, type SidebarSourceMessageData, type SidebarSourceMessageInfo, type ClientDBMessageInfo, isMessageSidebarSourceReactionOrEdit, + rawSidebarSourceMessageInfoValidator, } from '../../types/message-types.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { hasMinCodeVersion } from '../version-utils.js'; export const sidebarSourceMessageSpec: MessageSpec< SidebarSourceMessageData, RawSidebarSourceMessageInfo, SidebarSourceMessageInfo, > = Object.freeze({ messageContentForServerDB( data: SidebarSourceMessageData | RawSidebarSourceMessageInfo, ): string { const sourceMessageID = data.sourceMessage?.id; invariant(sourceMessageID, 'Source message id should be set'); return JSON.stringify({ sourceMessageID, }); }, messageContentForClientDB(data: RawSidebarSourceMessageInfo): string { invariant( data.sourceMessage && data.sourceMessage.id, 'sourceMessage and sourceMessage.id should be defined', ); return JSON.stringify(data.sourceMessage); }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawSidebarSourceMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for SidebarSource', ); const sourceMessage = JSON.parse(clientDBMessageInfo.content); const rawSidebarSourceMessageInfo: RawSidebarSourceMessageInfo = { type: messageTypes.SIDEBAR_SOURCE, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, sourceMessage, }; return rawSidebarSourceMessageInfo; }, messageTitle() { invariant(false, 'Cannot call messageTitle on sidebarSourceMessageSpec'); }, rawMessageInfoFromServerDBRow( row: Object, params: RawMessageInfoFromServerDBRowParams, ): RawSidebarSourceMessageInfo { const { derivedMessages } = params; invariant(derivedMessages, 'Derived messages should be provided'); const content = JSON.parse(row.content); const sourceMessage = derivedMessages.get(content.sourceMessageID); if (!sourceMessage) { console.warn( `Message with id ${row.id} has a derived message ` + `${content.sourceMessageID} which is not present in the database`, ); } return { type: messageTypes.SIDEBAR_SOURCE, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), sourceMessage, }; }, createMessageInfo( rawMessageInfo: RawSidebarSourceMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?SidebarSourceMessageInfo { if (!rawMessageInfo.sourceMessage) { return null; } const sourceMessage = params.createMessageInfoFromRaw( rawMessageInfo.sourceMessage, ); invariant( sourceMessage && !isMessageSidebarSourceReactionOrEdit(sourceMessage), 'Sidebars can not be created from SIDEBAR SOURCE, REACTION or EDIT MESSAGE', ); return { type: messageTypes.SIDEBAR_SOURCE, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, sourceMessage, }; }, rawMessageInfoFromMessageData( messageData: SidebarSourceMessageData, id: ?string, ): RawSidebarSourceMessageInfo { invariant(id, 'RawSidebarSourceMessageInfo needs id'); return { ...messageData, id }; }, shimUnsupportedMessageInfo( rawMessageInfo: RawSidebarSourceMessageInfo, platformDetails: ?PlatformDetails, ): RawSidebarSourceMessageInfo | RawUnsupportedMessageInfo { if ( hasMinCodeVersion(platformDetails, 75) && rawMessageInfo.sourceMessage ) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'first message in thread', dontPrefixCreator: true, unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo( unwrapped: RawSidebarSourceMessageInfo, ): RawSidebarSourceMessageInfo { return unwrapped; }, notificationTexts(): Promise { invariant( false, 'SIDEBAR_SOURCE notificationTexts should never be called directly!', ); }, notificationCollapseKey(rawMessageInfo: RawSidebarSourceMessageInfo): string { return joinResult(messageTypes.CREATE_SIDEBAR, rawMessageInfo.threadID); }, generatesNotifs: async () => pushTypes.NOTIF, startsThread: true, + + validator: rawSidebarSourceMessageInfoValidator, }); diff --git a/lib/shared/messages/text-message-spec.js b/lib/shared/messages/text-message-spec.js index 711668c97..971226272 100644 --- a/lib/shared/messages/text-message-spec.js +++ b/lib/shared/messages/text-message-spec.js @@ -1,315 +1,318 @@ // @flow import invariant from 'invariant'; import * as SimpleMarkdown from 'simple-markdown'; import { pushTypes, type MessageSpec, type RawMessageInfoFromServerDBRowParams, type NotificationTextsParams, } from './message-spec.js'; import { assertSingleMessageInfo, joinResult } from './utils.js'; import { changeThreadSettingsActionTypes, changeThreadSettings, } from '../../actions/thread-actions.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; -import type { - RawTextMessageInfo, - TextMessageData, - TextMessageInfo, +import { + type RawTextMessageInfo, + type TextMessageData, + type TextMessageInfo, + rawTextMessageInfoValidator, } from '../../types/messages/text.js'; import type { NotifTexts } from '../../types/notif-types.js'; import { threadTypes } from '../../types/thread-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { useDispatchActionPromise, useServerCall, } from '../../utils/action-utils.js'; import { ET } from '../../utils/entity-text.js'; import { type ASTNode, type SingleASTNode, stripSpoilersFromNotifications, stripSpoilersFromMarkdownAST, } from '../markdown.js'; import { isMentioned } from '../mention-utils.js'; import { notifTextsForSidebarCreation } from '../notif-utils.js'; import { threadIsGroupChat, extractNewMentionedParentMembers, } from '../thread-utils.js'; /** * most of the markdown leaves contain `content` field * (it is an array or a string) apart from lists, * which have `items` field (that holds an array) */ const rawTextFromMarkdownAST = (node: ASTNode): string => { if (Array.isArray(node)) { return node.map(rawTextFromMarkdownAST).join(''); } const { content, items } = node; if (content && typeof content === 'string') { return content; } else if (items) { return rawTextFromMarkdownAST(items); } else if (content) { return rawTextFromMarkdownAST(content); } return ''; }; const getFirstNonQuotedRawLine = ( nodes: $ReadOnlyArray, ): string => { let result = 'message'; for (const node of nodes) { if (node.type === 'blockQuote') { result = 'quoted message'; } else { const rawText = rawTextFromMarkdownAST(node); if (!rawText || !rawText.replace(/\s/g, '')) { // handles the case of an empty(or containing only white spaces) // new line that usually occurs between a quote and the rest // of the message(we don't want it as a title, thus continue) continue; } return rawText; } } return result; }; export const textMessageSpec: MessageSpec< TextMessageData, RawTextMessageInfo, TextMessageInfo, > = Object.freeze({ messageContentForServerDB( data: TextMessageData | RawTextMessageInfo, ): string { return data.text; }, messageContentForClientDB(data: RawTextMessageInfo): string { return this.messageContentForServerDB(data); }, messageTitle({ messageInfo, markdownRules }) { const { text } = messageInfo; const parser = SimpleMarkdown.parserFor(markdownRules); const ast = stripSpoilersFromMarkdownAST( parser(text, { disableAutoBlockNewlines: true }), ); return ET`${getFirstNonQuotedRawLine(ast).trim()}`; }, rawMessageInfoFromServerDBRow( row: Object, params: RawMessageInfoFromServerDBRowParams, ): RawTextMessageInfo { let rawTextMessageInfo: RawTextMessageInfo = { type: messageTypes.TEXT, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), text: row.content, }; if (params.localID) { rawTextMessageInfo = { ...rawTextMessageInfo, localID: params.localID }; } return rawTextMessageInfo; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawTextMessageInfo { let rawTextMessageInfo: RawTextMessageInfo = { type: messageTypes.TEXT, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, text: clientDBMessageInfo.content ?? '', }; if (clientDBMessageInfo.local_id) { rawTextMessageInfo = { ...rawTextMessageInfo, localID: clientDBMessageInfo.local_id, }; } if (clientDBMessageInfo.id !== clientDBMessageInfo.local_id) { rawTextMessageInfo = { ...rawTextMessageInfo, id: clientDBMessageInfo.id, }; } return rawTextMessageInfo; }, createMessageInfo( rawMessageInfo: RawTextMessageInfo, creator: RelativeUserInfo, ): TextMessageInfo { let messageInfo: TextMessageInfo = { type: messageTypes.TEXT, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, text: rawMessageInfo.text, }; if (rawMessageInfo.id) { messageInfo = { ...messageInfo, id: rawMessageInfo.id }; } if (rawMessageInfo.localID) { messageInfo = { ...messageInfo, localID: rawMessageInfo.localID }; } return messageInfo; }, rawMessageInfoFromMessageData( messageData: TextMessageData, id: ?string, ): RawTextMessageInfo { const { sidebarCreation, ...rest } = messageData; if (id) { return { ...rest, id }; } else { return { ...rest }; } }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, params: NotificationTextsParams, ): Promise { // We special-case sidebarCreations. Usually we don't send any notifs in // that case to avoid a double-notif, but we need to update the original // notif if somebody was @-tagged in this message if ( messageInfos.length === 3 && messageInfos[2].type === messageTypes.SIDEBAR_SOURCE && messageInfos[1].type === messageTypes.CREATE_SIDEBAR ) { const sidebarSourceMessageInfo = messageInfos[2]; const createSidebarMessageInfo = messageInfos[1]; const sourceMessage = messageInfos[2].sourceMessage; const { username } = params.notifTargetUserInfo; if (!username) { // If we couldn't fetch the username for some reason, we won't be able // to extract @-mentions anyways, so we'll give up on updating the notif return null; } if ( sourceMessage.type === messageTypes.TEXT && isMentioned(username, sourceMessage.text) ) { // If the notif target was already mentioned in the source message, // there's no need to update the notif return null; } const messageInfo = messageInfos[0]; invariant( messageInfo.type === messageTypes.TEXT, 'messageInfo should be messageTypes.TEXT!', ); if (!isMentioned(username, messageInfo.text)) { // We only need to update the notif if the notif target is mentioned return null; } return notifTextsForSidebarCreation({ createSidebarMessageInfo, sidebarSourceMessageInfo, firstSidebarMessageInfo: messageInfo, threadInfo, params, }); } const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.TEXT, 'messageInfo should be messageTypes.TEXT!', ); const notificationTextWithoutSpoilers = stripSpoilersFromNotifications( messageInfo.text, ); if (!threadInfo.name && !threadIsGroupChat(threadInfo)) { const thread = ET.thread({ display: 'uiName', threadInfo }); return { merged: ET`${thread}: ${notificationTextWithoutSpoilers}`, body: notificationTextWithoutSpoilers, title: threadInfo.uiName, }; } else { const creator = ET.user({ userInfo: messageInfo.creator }); const thread = ET.thread({ display: 'shortName', threadInfo }); return { merged: ET`${creator} to ${thread}: ${notificationTextWithoutSpoilers}`, body: notificationTextWithoutSpoilers, title: threadInfo.uiName, prefix: ET`${creator}:`, }; } }, notificationCollapseKey( rawMessageInfo: RawTextMessageInfo, messageData: TextMessageData, ): ?string { if (!messageData.sidebarCreation) { return null; } return joinResult(messageTypes.CREATE_SIDEBAR, rawMessageInfo.threadID); }, generatesNotifs: async () => pushTypes.NOTIF, includedInRepliesCount: true, useCreationSideEffectsFunc: () => { const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useServerCall(changeThreadSettings); return async ( messageInfo: RawTextMessageInfo, threadInfo: ThreadInfo, parentThreadInfo: ?ThreadInfo, ) => { if (threadInfo.type !== threadTypes.SIDEBAR) { return; } invariant(parentThreadInfo, 'all sidebars should have a parent thread'); const mentionedNewMembers = extractNewMentionedParentMembers( messageInfo.text, threadInfo, parentThreadInfo, ); if (mentionedNewMembers.length === 0) { return; } const newMemberIDs = mentionedNewMembers.map(({ id }) => id); const addMembersPromise = callChangeThreadSettings({ threadID: threadInfo.id, changes: { newMemberIDs }, }); dispatchActionPromise(changeThreadSettingsActionTypes, addMembersPromise); await addMembersPromise; }; }, + + validator: rawTextMessageInfoValidator, }); diff --git a/lib/shared/messages/toggle-pin-message-spec.js b/lib/shared/messages/toggle-pin-message-spec.js index fbc3dc937..30d49e6ef 100644 --- a/lib/shared/messages/toggle-pin-message-spec.js +++ b/lib/shared/messages/toggle-pin-message-spec.js @@ -1,158 +1,161 @@ // @flow import invariant from 'invariant'; import type { MessageSpec, RobotextParams, RawMessageInfoFromServerDBRowParams, } from './message-spec.js'; import type { PlatformDetails } from '../../types/device-types'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientDBMessageInfo } from '../../types/message-types.js'; -import type { - TogglePinMessageData, - TogglePinMessageInfo, - RawTogglePinMessageInfo, +import { + type TogglePinMessageData, + type TogglePinMessageInfo, + type RawTogglePinMessageInfo, + rawTogglePinMessageInfoValidator, } from '../../types/messages/toggle-pin.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { getPinnedContentFromClientDBMessageInfo } from '../../utils/message-ops-utils.js'; import { getPinnedContentFromMessage } from '../message-utils.js'; import { hasMinCodeVersion } from '../version-utils.js'; export const togglePinMessageSpec: MessageSpec< TogglePinMessageData, RawTogglePinMessageInfo, TogglePinMessageInfo, > = Object.freeze({ messageContentForServerDB( data: TogglePinMessageData | RawTogglePinMessageInfo, ): string { return JSON.stringify({ action: data.action, threadID: data.threadID, targetMessageID: data.targetMessageID, }); }, messageContentForClientDB(data: RawTogglePinMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow( row: Object, params: RawMessageInfoFromServerDBRowParams, ): RawTogglePinMessageInfo { const content = JSON.parse(row.content); const { derivedMessages } = params; const targetMessage = derivedMessages.get(content.targetMessageID); invariant(targetMessage, 'targetMessage should be defined'); return { type: messageTypes.TOGGLE_PIN, id: row.id.toString(), threadID: row.threadID.toString(), targetMessageID: content.targetMessageID.toString(), action: content.action, pinnedContent: getPinnedContentFromMessage(targetMessage), time: row.time, creatorID: row.creatorID.toString(), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawTogglePinMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for TogglePin', ); const content = JSON.parse(clientDBMessageInfo.content); const pinnedContent = getPinnedContentFromClientDBMessageInfo(clientDBMessageInfo); const rawTogglePinMessageInfo: RawTogglePinMessageInfo = { type: messageTypes.TOGGLE_PIN, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, targetMessageID: content.targetMessageID, action: content.action, pinnedContent, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, }; return rawTogglePinMessageInfo; }, createMessageInfo( rawMessageInfo: RawTogglePinMessageInfo, creator: RelativeUserInfo, ): TogglePinMessageInfo { return { type: messageTypes.TOGGLE_PIN, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, targetMessageID: rawMessageInfo.targetMessageID, action: rawMessageInfo.action, pinnedContent: rawMessageInfo.pinnedContent, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData( messageData: TogglePinMessageData, id: ?string, ): RawTogglePinMessageInfo { invariant(id, 'RawTogglePinMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: TogglePinMessageInfo, params: RobotextParams, ): EntityText { const creator = ET.user({ userInfo: messageInfo.creator }); const action = messageInfo.action === 'pin' ? 'pinned' : 'unpinned'; const pinnedContent = messageInfo.pinnedContent; const preposition = messageInfo.action === 'pin' ? 'in' : 'from'; return ET`${creator} ${action} ${pinnedContent} ${preposition} ${ET.thread({ display: 'alwaysDisplayShortName', threadID: messageInfo.threadID, threadType: params.threadInfo?.type, parentThreadID: params.threadInfo?.parentThreadID, })}`; }, shimUnsupportedMessageInfo( rawMessageInfo: RawTogglePinMessageInfo, platformDetails: ?PlatformDetails, ): RawTogglePinMessageInfo | RawUnsupportedMessageInfo { if (hasMinCodeVersion(platformDetails, 209)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'toggled a message pin', unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo( unwrapped: RawTogglePinMessageInfo, ): RawTogglePinMessageInfo { return unwrapped; }, generatesNotifs: async () => undefined, + + validator: rawTogglePinMessageInfoValidator, }); diff --git a/lib/shared/messages/unsupported-message-spec.js b/lib/shared/messages/unsupported-message-spec.js index 8bd3df273..011223399 100644 --- a/lib/shared/messages/unsupported-message-spec.js +++ b/lib/shared/messages/unsupported-message-spec.js @@ -1,75 +1,78 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec } from './message-spec.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { type ClientDBMessageInfo } from '../../types/message-types.js'; -import type { - RawUnsupportedMessageInfo, - UnsupportedMessageInfo, +import { + type RawUnsupportedMessageInfo, + type UnsupportedMessageInfo, + rawUnsupportedMessageInfoValidator, } from '../../types/messages/unsupported.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; export const unsupportedMessageSpec: MessageSpec< null, RawUnsupportedMessageInfo, UnsupportedMessageInfo, > = Object.freeze({ messageContentForClientDB(data: RawUnsupportedMessageInfo): string { return JSON.stringify({ robotext: data.robotext, dontPrefixCreator: data.dontPrefixCreator, unsupportedMessageInfo: data.unsupportedMessageInfo, }); }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawUnsupportedMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for Unsupported', ); const content = JSON.parse(clientDBMessageInfo.content); const rawUnsupportedMessageInfo: RawUnsupportedMessageInfo = { type: messageTypes.UNSUPPORTED, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, robotext: content.robotext, dontPrefixCreator: content.dontPrefixCreator, unsupportedMessageInfo: content.unsupportedMessageInfo, }; return rawUnsupportedMessageInfo; }, createMessageInfo( rawMessageInfo: RawUnsupportedMessageInfo, creator: RelativeUserInfo, ): UnsupportedMessageInfo { return { type: messageTypes.UNSUPPORTED, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, robotext: rawMessageInfo.robotext, dontPrefixCreator: rawMessageInfo.dontPrefixCreator, unsupportedMessageInfo: rawMessageInfo.unsupportedMessageInfo, }; }, robotext(messageInfo: UnsupportedMessageInfo): EntityText { if (messageInfo.dontPrefixCreator) { return ET`${messageInfo.robotext}`; } const creator = ET.user({ userInfo: messageInfo.creator }); return ET`${creator} ${messageInfo.robotext}`; }, generatesNotifs: async () => pushTypes.NOTIF, + + validator: rawUnsupportedMessageInfoValidator, }); diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js index 45e73e7d2..7dda38747 100644 --- a/lib/shared/messages/update-relationship-message-spec.js +++ b/lib/shared/messages/update-relationship-message-spec.js @@ -1,178 +1,181 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported.js'; -import type { - RawUpdateRelationshipMessageInfo, - UpdateRelationshipMessageData, - UpdateRelationshipMessageInfo, +import { + type RawUpdateRelationshipMessageInfo, + type UpdateRelationshipMessageData, + type UpdateRelationshipMessageInfo, + rawUpdateRelationshipMessageInfoValidator, } from '../../types/messages/update-relationship.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { hasMinCodeVersion } from '../version-utils.js'; export const updateRelationshipMessageSpec: MessageSpec< UpdateRelationshipMessageData, RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageInfo, > = Object.freeze({ messageContentForServerDB( data: UpdateRelationshipMessageData | RawUpdateRelationshipMessageInfo, ): string { return JSON.stringify({ operation: data.operation, targetID: data.targetID, }); }, messageContentForClientDB(data: RawUpdateRelationshipMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawUpdateRelationshipMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.UPDATE_RELATIONSHIP, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), targetID: content.targetID, operation: content.operation, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawUpdateRelationshipMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for UpdateRelationship', ); const content = JSON.parse(clientDBMessageInfo.content); const rawUpdateRelationshipMessageInfo: RawUpdateRelationshipMessageInfo = { type: messageTypes.UPDATE_RELATIONSHIP, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, targetID: content.targetID, operation: content.operation, }; return rawUpdateRelationshipMessageInfo; }, createMessageInfo( rawMessageInfo: RawUpdateRelationshipMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?UpdateRelationshipMessageInfo { const target = params.createRelativeUserInfos([rawMessageInfo.targetID])[0]; if (!target) { return null; } return { type: messageTypes.UPDATE_RELATIONSHIP, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, target, time: rawMessageInfo.time, operation: rawMessageInfo.operation, }; }, rawMessageInfoFromMessageData( messageData: UpdateRelationshipMessageData, id: ?string, ): RawUpdateRelationshipMessageInfo { invariant(id, 'RawUpdateRelationshipMessageInfo needs id'); return { ...messageData, id }; }, // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return robotext(messageInfo: UpdateRelationshipMessageInfo): EntityText { const creator = ET.user({ userInfo: messageInfo.creator }); if (messageInfo.operation === 'request_sent') { const target = ET.user({ userInfo: messageInfo.target }); return ET`${creator} sent ${target} a friend request`; } else if (messageInfo.operation === 'request_accepted') { const targetPossessive = ET.user({ userInfo: messageInfo.target, possessive: true, }); return ET`${creator} accepted ${targetPossessive} friend request`; } invariant( false, `Invalid operation ${messageInfo.operation} ` + `of message with type ${messageInfo.type}`, ); }, shimUnsupportedMessageInfo( rawMessageInfo: RawUpdateRelationshipMessageInfo, platformDetails: ?PlatformDetails, ): RawUpdateRelationshipMessageInfo | RawUnsupportedMessageInfo { if (hasMinCodeVersion(platformDetails, 71)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'performed a relationship action', unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo( unwrapped: RawUpdateRelationshipMessageInfo, ): RawUpdateRelationshipMessageInfo { return unwrapped; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); const creator = ET.user({ userInfo: messageInfo.creator }); const prefix = ET`${creator}`; const title = threadInfo.uiName; const body = messageInfo.operation === 'request_sent' ? 'sent you a friend request' : 'accepted your friend request'; const merged = ET`${prefix} ${body}`; return { merged, body, title, prefix, }; }, generatesNotifs: async () => pushTypes.NOTIF, + + validator: rawUpdateRelationshipMessageInfoValidator, }); diff --git a/lib/types/message-types.js b/lib/types/message-types.js index d57bbd495..1b573bce5 100644 --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -1,678 +1,676 @@ // @flow import invariant from 'invariant'; import t, { type TUnion, type TInterface } from 'tcomb'; import { type ClientDBMediaInfo } from './media-types.js'; import { messageTypes, type MessageType } from './message-types-enum.js'; import { type AddMembersMessageData, type AddMembersMessageInfo, type RawAddMembersMessageInfo, rawAddMembersMessageInfoValidator, } from './messages/add-members.js'; import { type ChangeRoleMessageData, type ChangeRoleMessageInfo, type RawChangeRoleMessageInfo, rawChangeRoleMessageInfoValidator, } from './messages/change-role.js'; import { type ChangeSettingsMessageData, type ChangeSettingsMessageInfo, type RawChangeSettingsMessageInfo, rawChangeSettingsMessageInfoValidator, } from './messages/change-settings.js'; import { type CreateEntryMessageData, type CreateEntryMessageInfo, type RawCreateEntryMessageInfo, rawCreateEntryMessageInfoValidator, } from './messages/create-entry.js'; import { type CreateSidebarMessageData, type CreateSidebarMessageInfo, type RawCreateSidebarMessageInfo, rawCreateSidebarMessageInfoValidator, } from './messages/create-sidebar.js'; import { type CreateSubthreadMessageData, type CreateSubthreadMessageInfo, type RawCreateSubthreadMessageInfo, rawCreateSubthreadMessageInfoValidator, } from './messages/create-subthread.js'; import { type CreateThreadMessageData, type CreateThreadMessageInfo, type RawCreateThreadMessageInfo, rawCreateThreadMessageInfoValidator, } from './messages/create-thread.js'; import { type DeleteEntryMessageData, type DeleteEntryMessageInfo, type RawDeleteEntryMessageInfo, rawDeleteEntryMessageInfoValidator, } from './messages/delete-entry.js'; import { type EditEntryMessageData, type EditEntryMessageInfo, type RawEditEntryMessageInfo, rawEditEntryMessageInfoValidator, } from './messages/edit-entry.js'; import { type RawEditMessageInfo, rawEditMessageInfoValidator, type EditMessageData, type EditMessageInfo, } from './messages/edit.js'; import { type ImagesMessageData, type ImagesMessageInfo, type RawImagesMessageInfo, rawImagesMessageInfoValidator, } from './messages/images.js'; import { type JoinThreadMessageData, type JoinThreadMessageInfo, type RawJoinThreadMessageInfo, rawJoinThreadMessageInfoValidator, } from './messages/join-thread.js'; import { type LeaveThreadMessageData, type LeaveThreadMessageInfo, type RawLeaveThreadMessageInfo, rawLeaveThreadMessageInfoValidator, } from './messages/leave-thread.js'; import { type MediaMessageData, type MediaMessageInfo, type MediaMessageServerDBContent, type RawMediaMessageInfo, rawMediaMessageInfoValidator, } from './messages/media.js'; import { type ReactionMessageData, type RawReactionMessageInfo, rawReactionMessageInfoValidator, type ReactionMessageInfo, } from './messages/reaction.js'; import { type RawRemoveMembersMessageInfo, rawRemoveMembersMessageInfoValidator, type RemoveMembersMessageData, type RemoveMembersMessageInfo, } from './messages/remove-members.js'; import { type RawRestoreEntryMessageInfo, rawRestoreEntryMessageInfoValidator, type RestoreEntryMessageData, type RestoreEntryMessageInfo, } from './messages/restore-entry.js'; import { type RawTextMessageInfo, rawTextMessageInfoValidator, type TextMessageData, type TextMessageInfo, } from './messages/text.js'; import { type TogglePinMessageData, type TogglePinMessageInfo, type RawTogglePinMessageInfo, rawTogglePinMessageInfoValidator, } from './messages/toggle-pin.js'; import { type RawUnsupportedMessageInfo, rawUnsupportedMessageInfoValidator, type UnsupportedMessageInfo, } from './messages/unsupported.js'; import { type RawUpdateRelationshipMessageInfo, rawUpdateRelationshipMessageInfoValidator, type UpdateRelationshipMessageData, type UpdateRelationshipMessageInfo, } from './messages/update-relationship.js'; import { type RelativeUserInfo, type UserInfos } from './user-types.js'; import type { CallServerEndpointResultInfoInterface } from '../utils/call-server-endpoint.js'; import { tNumber, tShape, tID } from '../utils/validation-utils.js'; const composableMessageTypes = new Set([ messageTypes.TEXT, messageTypes.IMAGES, messageTypes.MULTIMEDIA, ]); export function isComposableMessageType(ourMessageType: MessageType): boolean { return composableMessageTypes.has(ourMessageType); } export function assertComposableMessageType( ourMessageType: MessageType, ): MessageType { invariant( isComposableMessageType(ourMessageType), 'MessageType is not composed', ); return ourMessageType; } export function assertComposableRawMessage( message: RawMessageInfo, ): RawComposableMessageInfo { invariant( message.type === messageTypes.TEXT || message.type === messageTypes.IMAGES || message.type === messageTypes.MULTIMEDIA, 'Message is not composable', ); return message; } export function messageDataLocalID(messageData: MessageData): ?string { if ( messageData.type !== messageTypes.TEXT && messageData.type !== messageTypes.IMAGES && messageData.type !== messageTypes.MULTIMEDIA && messageData.type !== messageTypes.REACTION ) { return null; } return messageData.localID; } export function isMessageSidebarSourceReactionOrEdit( message: RawMessageInfo | MessageInfo, ): boolean %checks { return ( message.type === messageTypes.SIDEBAR_SOURCE || message.type === messageTypes.REACTION || message.type === messageTypes.EDIT_MESSAGE ); } const mediaMessageTypes = new Set([ messageTypes.IMAGES, messageTypes.MULTIMEDIA, ]); export function isMediaMessageType(ourMessageType: MessageType): boolean { return mediaMessageTypes.has(ourMessageType); } export function assertMediaMessageType( ourMessageType: MessageType, ): MessageType { invariant(isMediaMessageType(ourMessageType), 'MessageType is not media'); return ourMessageType; } // *MessageData = passed to createMessages function to insert into database // Raw*MessageInfo = used by server, and contained in client's local store // *MessageInfo = used by client in UI code export type SidebarSourceMessageData = { +type: 17, +threadID: string, +creatorID: string, +time: number, +sourceMessage?: RawComposableMessageInfo | RawRobotextMessageInfo, }; export type MessageData = | TextMessageData | CreateThreadMessageData | AddMembersMessageData | CreateSubthreadMessageData | ChangeSettingsMessageData | RemoveMembersMessageData | ChangeRoleMessageData | LeaveThreadMessageData | JoinThreadMessageData | CreateEntryMessageData | EditEntryMessageData | DeleteEntryMessageData | RestoreEntryMessageData | ImagesMessageData | MediaMessageData | UpdateRelationshipMessageData | SidebarSourceMessageData | CreateSidebarMessageData | ReactionMessageData | EditMessageData | TogglePinMessageData; export type MultimediaMessageData = ImagesMessageData | MediaMessageData; export type RawMultimediaMessageInfo = | RawImagesMessageInfo | RawMediaMessageInfo; -const rawMultimediaMessageInfoValidator = t.union([ - rawImagesMessageInfoValidator, - rawMediaMessageInfoValidator, -]); +export const rawMultimediaMessageInfoValidator: TUnion = + t.union([rawImagesMessageInfoValidator, rawMediaMessageInfoValidator]); export type RawComposableMessageInfo = | RawTextMessageInfo | RawMultimediaMessageInfo; const rawComposableMessageInfoValidator = t.union([ rawTextMessageInfoValidator, rawMultimediaMessageInfoValidator, ]); export type RawRobotextMessageInfo = | RawCreateThreadMessageInfo | RawAddMembersMessageInfo | RawCreateSubthreadMessageInfo | RawChangeSettingsMessageInfo | RawRemoveMembersMessageInfo | RawChangeRoleMessageInfo | RawLeaveThreadMessageInfo | RawJoinThreadMessageInfo | RawCreateEntryMessageInfo | RawEditEntryMessageInfo | RawDeleteEntryMessageInfo | RawRestoreEntryMessageInfo | RawUpdateRelationshipMessageInfo | RawCreateSidebarMessageInfo | RawUnsupportedMessageInfo | RawTogglePinMessageInfo; const rawRobotextMessageInfoValidator = t.union([ rawCreateThreadMessageInfoValidator, rawAddMembersMessageInfoValidator, rawCreateSubthreadMessageInfoValidator, rawChangeSettingsMessageInfoValidator, rawRemoveMembersMessageInfoValidator, rawChangeRoleMessageInfoValidator, rawLeaveThreadMessageInfoValidator, rawJoinThreadMessageInfoValidator, rawCreateEntryMessageInfoValidator, rawEditEntryMessageInfoValidator, rawDeleteEntryMessageInfoValidator, rawRestoreEntryMessageInfoValidator, rawUpdateRelationshipMessageInfoValidator, rawCreateSidebarMessageInfoValidator, rawUnsupportedMessageInfoValidator, rawTogglePinMessageInfoValidator, ]); export type RawSidebarSourceMessageInfo = { ...SidebarSourceMessageData, id: string, }; export const rawSidebarSourceMessageInfoValidator: TInterface = tShape({ type: tNumber(messageTypes.SIDEBAR_SOURCE), threadID: tID, creatorID: t.String, time: t.Number, sourceMessage: t.maybe( t.union([ rawComposableMessageInfoValidator, rawRobotextMessageInfoValidator, ]), ), id: tID, }); export type RawMessageInfo = | RawComposableMessageInfo | RawRobotextMessageInfo | RawSidebarSourceMessageInfo | RawReactionMessageInfo | RawEditMessageInfo; export const rawMessageInfoValidator: TUnion = t.union([ rawComposableMessageInfoValidator, rawRobotextMessageInfoValidator, rawSidebarSourceMessageInfoValidator, rawReactionMessageInfoValidator, rawEditMessageInfoValidator, ]); export type LocallyComposedMessageInfo = | ({ ...RawImagesMessageInfo, +localID: string, } & RawImagesMessageInfo) | ({ ...RawMediaMessageInfo, +localID: string, } & RawMediaMessageInfo) | ({ ...RawTextMessageInfo, +localID: string, } & RawTextMessageInfo) | ({ ...RawReactionMessageInfo, +localID: string, } & RawReactionMessageInfo); export type MultimediaMessageInfo = ImagesMessageInfo | MediaMessageInfo; export type ComposableMessageInfo = TextMessageInfo | MultimediaMessageInfo; export type RobotextMessageInfo = | CreateThreadMessageInfo | AddMembersMessageInfo | CreateSubthreadMessageInfo | ChangeSettingsMessageInfo | RemoveMembersMessageInfo | ChangeRoleMessageInfo | LeaveThreadMessageInfo | JoinThreadMessageInfo | CreateEntryMessageInfo | EditEntryMessageInfo | DeleteEntryMessageInfo | RestoreEntryMessageInfo | UnsupportedMessageInfo | UpdateRelationshipMessageInfo | CreateSidebarMessageInfo | TogglePinMessageInfo; export type PreviewableMessageInfo = | RobotextMessageInfo | MultimediaMessageInfo | ReactionMessageInfo; export type SidebarSourceMessageInfo = { +type: 17, +id: string, +threadID: string, +creator: RelativeUserInfo, +time: number, +sourceMessage: ComposableMessageInfo | RobotextMessageInfo, }; export type MessageInfo = | ComposableMessageInfo | RobotextMessageInfo | SidebarSourceMessageInfo | ReactionMessageInfo | EditMessageInfo; export type ThreadMessageInfo = { messageIDs: string[], startReached: boolean, lastNavigatedTo: number, // millisecond timestamp lastPruned: number, // millisecond timestamp }; // Tracks client-local information about a message that hasn't been assigned an // ID by the server yet. As soon as the client gets an ack from the server for // this message, it will clear the LocalMessageInfo. export type LocalMessageInfo = { +sendFailed?: boolean, }; export type MessageStoreThreads = { +[threadID: string]: ThreadMessageInfo, }; export type MessageStore = { +messages: { +[id: string]: RawMessageInfo }, +threads: MessageStoreThreads, +local: { +[id: string]: LocalMessageInfo }, +currentAsOf: number, }; // MessageStore messages ops export type RemoveMessageOperation = { +type: 'remove', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveMessagesForThreadsOperation = { +type: 'remove_messages_for_threads', +payload: { +threadIDs: $ReadOnlyArray }, }; export type ReplaceMessageOperation = { +type: 'replace', +payload: { +id: string, +messageInfo: RawMessageInfo }, }; export type RekeyMessageOperation = { +type: 'rekey', +payload: { +from: string, +to: string }, }; export type RemoveAllMessagesOperation = { +type: 'remove_all', }; // MessageStore threads ops export type ReplaceMessageStoreThreadsOperation = { +type: 'replace_threads', +payload: { +threads: MessageStoreThreads }, }; export type RemoveMessageStoreThreadsOperation = { +type: 'remove_threads', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveMessageStoreAllThreadsOperation = { +type: 'remove_all_threads', }; // We were initially using `number`s` for `thread`, `type`, `future_type`, etc. // However, we ended up changing `thread` to `string` to account for thread IDs // including information about the keyserver (eg 'GENESIS|123') in the future. // // At that point we discussed whether we should switch the remaining `number` // fields to `string`s for consistency and flexibility. We researched whether // there was any performance cost to using `string`s instead of `number`s and // found the differences to be negligible. We also concluded using `string`s // may be safer after considering `jsi::Number` and the various C++ number // representations on the CommCoreModule side. export type ClientDBMessageInfo = { +id: string, +local_id: ?string, +thread: string, +user: string, +type: string, +future_type: ?string, +content: ?string, +time: string, +media_infos: ?$ReadOnlyArray, }; export type ClientDBReplaceMessageOperation = { +type: 'replace', +payload: ClientDBMessageInfo, }; export type ClientDBThreadMessageInfo = { +id: string, +start_reached: string, +last_navigated_to: string, +last_pruned: string, }; export type ClientDBReplaceThreadsOperation = { +type: 'replace_threads', +payload: { +threads: $ReadOnlyArray }, }; export type MessageStoreOperation = | RemoveMessageOperation | ReplaceMessageOperation | RekeyMessageOperation | RemoveMessagesForThreadsOperation | RemoveAllMessagesOperation | ReplaceMessageStoreThreadsOperation | RemoveMessageStoreThreadsOperation | RemoveMessageStoreAllThreadsOperation; export type ClientDBMessageStoreOperation = | RemoveMessageOperation | ClientDBReplaceMessageOperation | RekeyMessageOperation | RemoveMessagesForThreadsOperation | RemoveAllMessagesOperation | ClientDBReplaceThreadsOperation | RemoveMessageStoreThreadsOperation | RemoveMessageStoreAllThreadsOperation; export const messageTruncationStatus = Object.freeze({ // EXHAUSTIVE means we've reached the start of the thread. Either the result // set includes the very first message for that thread, or there is nothing // behind the cursor you queried for. Given that the client only ever issues // ranged queries whose range, when unioned with what is in state, represent // the set of all messages for a given thread, we can guarantee that getting // EXHAUSTIVE means the start has been reached. EXHAUSTIVE: 'exhaustive', // TRUNCATED is rare, and means that the server can't guarantee that the // result set for a given thread is contiguous with what the client has in its // state. If the client can't verify the contiguousness itself, it needs to // replace its Redux store's contents with what it is in this payload. // 1) getMessageInfosSince: Result set for thread is equal to max, and the // truncation status isn't EXHAUSTIVE (ie. doesn't include the very first // message). // 2) getMessageInfos: MessageSelectionCriteria does not specify cursors, the // result set for thread is equal to max, and the truncation status isn't // EXHAUSTIVE. If cursors are specified, we never return truncated, since // the cursor given us guarantees the contiguousness of the result set. // Note that in the reducer, we can guarantee contiguousness if there is any // intersection between messageIDs in the result set and the set currently in // the Redux store. TRUNCATED: 'truncated', // UNCHANGED means the result set is guaranteed to be contiguous with what the // client has in its state, but is not EXHAUSTIVE. Basically, it's anything // that isn't either EXHAUSTIVE or TRUNCATED. UNCHANGED: 'unchanged', }); export type MessageTruncationStatus = $Values; export function assertMessageTruncationStatus( ourMessageTruncationStatus: string, ): MessageTruncationStatus { invariant( ourMessageTruncationStatus === 'truncated' || ourMessageTruncationStatus === 'unchanged' || ourMessageTruncationStatus === 'exhaustive', 'string is not ourMessageTruncationStatus enum', ); return ourMessageTruncationStatus; } export type MessageTruncationStatuses = { [threadID: string]: MessageTruncationStatus, }; export type ThreadCursors = { +[threadID: string]: ?string }; export type MessageSelectionCriteria = { +threadCursors?: ?ThreadCursors, +joinedThreads?: ?boolean, +newerThan?: ?number, }; export type FetchMessageInfosRequest = { +cursors: ThreadCursors, +numberPerThread?: ?number, }; export type FetchMessageInfosResponse = { +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: UserInfos, }; export type FetchMessageInfosResult = { +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, }; export type FetchMessageInfosPayload = { +threadID: string, +rawMessageInfos: $ReadOnlyArray, +truncationStatus: MessageTruncationStatus, }; export type MessagesResponse = { +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +currentAsOf: number, }; export type SimpleMessagesPayload = { +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, }; export const defaultNumberPerThread = 20; export const defaultMaxMessageAge = 14 * 24 * 60 * 60 * 1000; // 2 weeks export type SendMessageResponse = { +newMessageInfo: RawMessageInfo, }; export type SendMessageResult = { +id: string, +time: number, +interface: CallServerEndpointResultInfoInterface, }; export type SendMessagePayload = { +localID: string, +serverID: string, +threadID: string, +time: number, +interface: CallServerEndpointResultInfoInterface, }; export type SendTextMessageRequest = { +threadID: string, +localID?: string, +text: string, +sidebarCreation?: boolean, }; export type SendMultimediaMessageRequest = // This option is only used for messageTypes.IMAGES | { +threadID: string, +localID: string, +sidebarCreation?: boolean, +mediaIDs: $ReadOnlyArray, } | { +threadID: string, +localID: string, +sidebarCreation?: boolean, +mediaMessageContents: $ReadOnlyArray, }; export type SendReactionMessageRequest = { +threadID: string, +localID?: string, +targetMessageID: string, +reaction: string, +action: 'add_reaction' | 'remove_reaction', }; export type SendEditMessageRequest = { +targetMessageID: string, +text: string, }; export type SendEditMessageResponse = { +newMessageInfos: $ReadOnlyArray, }; export type EditMessagePayload = SendEditMessageResponse; export type SendEditMessageResult = SendEditMessageResponse; export type EditMessageContent = { +text: string, }; // Used for the message info included in log-in type actions export type GenericMessagesResult = { +messageInfos: RawMessageInfo[], +truncationStatus: MessageTruncationStatuses, +watchedIDsAtRequestTime: $ReadOnlyArray, +currentAsOf: number, }; export type SaveMessagesPayload = { +rawMessageInfos: $ReadOnlyArray, +updatesCurrentAsOf: number, }; export type NewMessagesPayload = { +messagesResult: MessagesResponse, }; export type MessageStorePrunePayload = { +threadIDs: $ReadOnlyArray, }; export type FetchPinnedMessagesRequest = { +threadID: string, }; export type FetchPinnedMessagesResult = { +pinnedMessages: $ReadOnlyArray, }; diff --git a/lib/types/validation.test.js b/lib/types/validation.test.js index b16b27203..13714556a 100644 --- a/lib/types/validation.test.js +++ b/lib/types/validation.test.js @@ -1,362 +1,321 @@ // @flow import _findKey from 'lodash/fp/findKey.js'; import { imageValidator, videoValidator, mediaValidator, } from './media-types.js'; import { messageTypes } from './message-types-enum.js'; -import { rawSidebarSourceMessageInfoValidator } from './message-types.js'; -import { rawAddMembersMessageInfoValidator } from './messages/add-members.js'; -import { rawChangeRoleMessageInfoValidator } from './messages/change-role.js'; -import { rawChangeSettingsMessageInfoValidator } from './messages/change-settings.js'; -import { rawCreateEntryMessageInfoValidator } from './messages/create-entry.js'; -import { rawCreateSidebarMessageInfoValidator } from './messages/create-sidebar.js'; -import { rawCreateSubthreadMessageInfoValidator } from './messages/create-subthread.js'; -import { rawCreateThreadMessageInfoValidator } from './messages/create-thread.js'; -import { rawDeleteEntryMessageInfoValidator } from './messages/delete-entry.js'; -import { rawEditEntryMessageInfoValidator } from './messages/edit-entry.js'; -import { rawEditMessageInfoValidator } from './messages/edit.js'; -import { rawImagesMessageInfoValidator } from './messages/images.js'; -import { rawJoinThreadMessageInfoValidator } from './messages/join-thread.js'; -import { rawLeaveThreadMessageInfoValidator } from './messages/leave-thread.js'; -import { rawMediaMessageInfoValidator } from './messages/media.js'; -import { rawReactionMessageInfoValidator } from './messages/reaction.js'; -import { rawRemoveMembersMessageInfoValidator } from './messages/remove-members.js'; -import { rawRestoreEntryMessageInfoValidator } from './messages/restore-entry.js'; -import { rawTextMessageInfoValidator } from './messages/text.js'; -import { rawTogglePinMessageInfoValidator } from './messages/toggle-pin.js'; -import { rawUnsupportedMessageInfoValidator } from './messages/unsupported.js'; -import { rawUpdateRelationshipMessageInfoValidator } from './messages/update-relationship.js'; +import { messageSpecs } from '../shared/messages/message-specs.js'; describe('media validation', () => { const photo = { id: '92696', type: 'photo', uri: 'http://0.0.0.0:3000/comm/upload/92696/0fb272bd1c75d976', dimensions: { width: 340, height: 288, }, }; const video = { type: 'video', id: '92769', uri: 'http://0.0.0.0:3000/comm/upload/92769/4bcc6987b25b2f66', dimensions: { width: 480, height: 270, }, thumbnailID: '92770', thumbnailURI: 'http://0.0.0.0:3000/comm/upload/92770/d56466051dcef1db', }; it('should validate correct media', () => { expect(mediaValidator.is(photo)).toBe(true); expect(imageValidator.is(photo)).toBe(true); expect(mediaValidator.is(video)).toBe(true); expect(videoValidator.is(video)).toBe(true); }); it('should not validate incorrect media', () => { expect(imageValidator.is(video)).toBe(false); expect(videoValidator.is(photo)).toBe(false); expect(mediaValidator.is({ ...photo, type: undefined })).toBe(false); expect(mediaValidator.is({ ...video, dimensions: undefined })).toBe(false); }); }); describe('message validation', () => { const messages = [ { type: messageTypes.TEXT, threadID: '83859', creatorID: '83853', time: 1682077048858, text: 'text', localID: 'local1', id: '92837', }, { type: messageTypes.CREATE_THREAD, id: '83876', threadID: '83859', time: 1673561105839, creatorID: '83853', initialThreadState: { type: 6, name: null, parentThreadID: '1', color: '57697f', memberIDs: ['256', '83853'], }, }, { type: messageTypes.ADD_MEMBERS, id: '4754380', threadID: '4746046', time: 1680179819346, creatorID: '256', addedUserIDs: ['518252', '1329299', '1559042'], }, { type: messageTypes.CREATE_SUB_THREAD, threadID: '87111', creatorID: '83928', time: 1682083573756, childThreadID: '92993', id: '93000', }, { type: messageTypes.CHANGE_SETTINGS, threadID: '83859', creatorID: '83853', time: 1682082984605, field: 'color', value: 'b8753d', id: '92880', }, { type: messageTypes.REMOVE_MEMBERS, threadID: '92993', creatorID: '83928', time: 1682083613415, removedUserIDs: ['83890'], id: '93012', }, { type: messageTypes.CHANGE_ROLE, threadID: '85027', creatorID: '256', time: 1632393331694, userIDs: ['85081'], newRole: 'role', id: '85431', }, { type: messageTypes.LEAVE_THREAD, id: '93027', threadID: '92993', time: 1682083651037, creatorID: '83928', }, { type: messageTypes.JOIN_THREAD, threadID: '92993', creatorID: '83928', time: 1682083678595, id: '93035', }, { type: messageTypes.CREATE_ENTRY, threadID: '84695', creatorID: '83928', time: 1682083217395, entryID: '92917', date: '2023-04-02', text: 'text', id: '92920', }, { type: messageTypes.EDIT_ENTRY, threadID: '84695', creatorID: '83928', time: 1682083374471, entryID: '92917', date: '2023-04-02', text: 'text', id: '92950', }, { type: messageTypes.DELETE_ENTRY, threadID: '86033', creatorID: '83928', time: 1682083220296, entryID: '92904', date: '2023-04-02', text: 'text', id: '92932', }, { type: messageTypes.RESTORE_ENTRY, id: '92962', threadID: '86033', time: 1682083414244, creatorID: '83928', entryID: '92904', date: '2023-04-02', text: 'text', }, { type: messageTypes.UNSUPPORTED, threadID: '87080', creatorID: '256', time: 1640733462322, robotext: 'unsupported message', unsupportedMessageInfo: { type: 105, threadID: '97489', creatorID: '256', time: 1640773011289, id: '97672', }, id: '97730', }, { type: messageTypes.IMAGES, threadID: '92796', creatorID: '83928', time: 1682083469079, media: [ { id: '92974', uri: 'http://0.0.0.0:3000/comm/upload/92974/ff3d02ded71e2762', type: 'photo', dimensions: { width: 220, height: 220, }, }, ], localID: 'local0', id: '92976', }, { type: messageTypes.MULTIMEDIA, threadID: '89644', creatorID: '83853', time: 1682076177257, media: [ { type: 'video', id: '92769', uri: 'http://0.0.0.0:3000/comm/upload/92769/4bcc6987b25b2f66', dimensions: { width: 480, height: 270, }, thumbnailID: '92770', thumbnailURI: 'http://0.0.0.0:3000/comm/upload/92770/d56466051dcef1db', }, ], id: '92771', }, { type: messageTypes.UPDATE_RELATIONSHIP, threadID: '92796', creatorID: '83928', targetID: '83853', time: 1682083716312, operation: 'request_sent', id: '93039', }, { type: messageTypes.SIDEBAR_SOURCE, threadID: '93044', creatorID: '83928', time: 1682083756831, sourceMessage: { type: 0, id: '92816', threadID: '92796', time: 1682076737518, creatorID: '83928', text: 'text', }, id: '93049', }, { type: messageTypes.CREATE_SIDEBAR, threadID: '93044', creatorID: '83928', time: 1682083756831, sourceMessageAuthorID: '83928', initialThreadState: { name: 'text', parentThreadID: '92796', color: 'aa4b4b', memberIDs: ['83853', '83928'], }, id: '93050', }, { type: messageTypes.REACTION, threadID: '86033', localID: 'local8', creatorID: '83928', time: 1682083295820, targetMessageID: '91607', reaction: '😂', action: 'add_reaction', id: '92943', }, { type: messageTypes.EDIT_MESSAGE, threadID: '86033', creatorID: '83928', time: 1682083295820, targetMessageID: '91607', text: 'text', id: '92943', }, { type: messageTypes.TOGGLE_PIN, threadID: '86033', targetMessageID: '91607', action: 'pin', pinnedContent: 'text', creatorID: '83928', time: 1682083295820, id: '92943', }, ]; - const validatorByMessageType = { - '0': rawTextMessageInfoValidator, - '1': rawCreateThreadMessageInfoValidator, - '2': rawAddMembersMessageInfoValidator, - '3': rawCreateSubthreadMessageInfoValidator, - '4': rawChangeSettingsMessageInfoValidator, - '5': rawRemoveMembersMessageInfoValidator, - '6': rawChangeRoleMessageInfoValidator, - '7': rawLeaveThreadMessageInfoValidator, - '8': rawJoinThreadMessageInfoValidator, - '9': rawCreateEntryMessageInfoValidator, - '10': rawEditEntryMessageInfoValidator, - '11': rawDeleteEntryMessageInfoValidator, - '12': rawRestoreEntryMessageInfoValidator, - '13': rawUnsupportedMessageInfoValidator, - '14': rawImagesMessageInfoValidator, - '15': rawMediaMessageInfoValidator, - '16': rawUpdateRelationshipMessageInfoValidator, - '17': rawSidebarSourceMessageInfoValidator, - '18': rawCreateSidebarMessageInfoValidator, - '19': rawReactionMessageInfoValidator, - '20': rawEditMessageInfoValidator, - '21': rawTogglePinMessageInfoValidator, - }; - - for (const validatorMessageType in validatorByMessageType) { - const validator = validatorByMessageType[validatorMessageType]; - const validatorMessageTypeName = _findKey( - e => e === Number(validatorMessageType), - )(messageTypes); + for (const validatorMessageTypeName in messageTypes) { + const validatorMessageType = messageTypes[validatorMessageTypeName]; + const validator = messageSpecs[validatorMessageType].validator; for (const message of messages) { const messageTypeName = _findKey(e => e === message.type)(messageTypes); - if (Number(validatorMessageType) === message.type) { + if (validatorMessageType === message.type) { it(`${validatorMessageTypeName} should validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(true); }); - } else { + } else if ( + !( + (validatorMessageType === messageTypes.IMAGES && + message.type === messageTypes.MULTIMEDIA) || + (validatorMessageType === messageTypes.MULTIMEDIA && + message.type === messageTypes.IMAGES) + ) + ) { it(`${validatorMessageTypeName} shouldn't validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(false); }); } } } });