Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3332849
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
26 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/keyserver/src/fetchers/upload-fetchers.js b/keyserver/src/fetchers/upload-fetchers.js
index fd8e964ee..3a6491a35 100644
--- a/keyserver/src/fetchers/upload-fetchers.js
+++ b/keyserver/src/fetchers/upload-fetchers.js
@@ -1,408 +1,419 @@
// @flow
import ip from 'internal-ip';
import _keyBy from 'lodash/fp/keyBy.js';
import type { Media, Image, EncryptedImage } from 'lib/types/media-types.js';
import { messageTypes } from 'lib/types/message-types-enum.js';
import type { MediaMessageServerDBContent } from 'lib/types/messages/media.js';
import { getUploadIDsFromMediaMessageServerDBContents } from 'lib/types/messages/media.js';
import { threadPermissions } from 'lib/types/thread-permission-types.js';
import type {
ThreadFetchMediaResult,
ThreadFetchMediaRequest,
} from 'lib/types/thread-types.js';
import { makeBlobServiceURI } from 'lib/utils/blob-service.js';
import { isDev } from 'lib/utils/dev-utils.js';
import { ServerError } from 'lib/utils/errors.js';
import { dbQuery, SQL } from '../database/database.js';
import type { Viewer } from '../session/viewer.js';
import { getAndAssertKeyserverURLFacts } from '../utils/urls.js';
type UploadInfo = {
content: Buffer,
mime: string,
};
async function fetchUpload(
viewer: Viewer,
id: string,
secret: string,
): Promise<UploadInfo> {
const query = SQL`
SELECT content, mime, extra
FROM uploads
WHERE id = ${id} AND secret = ${secret}
`;
const [result] = await dbQuery(query);
if (result.length === 0) {
throw new ServerError('invalid_parameters');
}
const [row] = result;
const { content, mime, extra } = row;
const { blobHash } = JSON.parse(extra);
if (blobHash) {
throw new ServerError('resource_unavailable');
}
return { content, mime };
}
async function fetchUploadChunk(
id: string,
secret: string,
pos: number,
len: number,
): Promise<UploadInfo> {
// We use pos + 1 because SQL is 1-indexed whereas js is 0-indexed
const query = SQL`
SELECT SUBSTRING(content, ${pos + 1}, ${len}) AS content, mime, extra
FROM uploads
WHERE id = ${id} AND secret = ${secret}
`;
const [result] = await dbQuery(query);
if (result.length === 0) {
throw new ServerError('invalid_parameters');
}
const [row] = result;
const { content, mime, extra } = row;
if (extra) {
const { blobHash } = JSON.parse(extra);
if (blobHash) {
throw new ServerError('resource_unavailable');
}
}
return {
content,
mime,
};
}
// Returns total size in bytes.
async function getUploadSize(id: string, secret: string): Promise<number> {
const query = SQL`
SELECT LENGTH(content) AS length, extra
FROM uploads
WHERE id = ${id} AND secret = ${secret}
`;
const [result] = await dbQuery(query);
if (result.length === 0) {
throw new ServerError('invalid_parameters');
}
const [row] = result;
const { length, extra } = row;
if (extra) {
const { blobHash } = JSON.parse(extra);
if (blobHash) {
throw new ServerError('resource_unavailable');
}
}
return length;
}
function getUploadURL(id: string, secret: string): string {
const { baseDomain, basePath } = getAndAssertKeyserverURLFacts();
const uploadPath = `${basePath}upload/${id}/${secret}`;
if (isDev) {
const ipV4 = ip.v4.sync() || 'localhost';
const port = parseInt(process.env.PORT, 10) || 3000;
return `http://${ipV4}:${port}${uploadPath}`;
}
return `${baseDomain}${uploadPath}`;
}
function makeUploadURI(blobHash: ?string, id: string, secret: string): string {
if (blobHash) {
return makeBlobServiceURI(blobHash);
}
return getUploadURL(id, secret);
}
function imagesFromRow(row: Object): Image | EncryptedImage {
const uploadExtra = JSON.parse(row.uploadExtra);
const { width, height, blobHash, thumbHash } = uploadExtra;
const { uploadType: type, uploadSecret: secret } = row;
const id = row.uploadID.toString();
const dimensions = { width, height };
const uri = makeUploadURI(blobHash, id, secret);
const isEncrypted = !!uploadExtra.encryptionKey;
if (type !== 'photo') {
throw new ServerError('invalid_parameters');
}
if (!isEncrypted) {
return { id, type: 'photo', uri, dimensions, thumbHash };
}
return {
id,
type: 'encrypted_photo',
blobURI: uri,
dimensions,
thumbHash,
encryptionKey: uploadExtra.encryptionKey,
};
}
-async function fetchImages(
+// This function technically fetches all kinds of unassigned media,
+// but it's only called by legacy clients that only support images
+async function fetchUnassignedImages(
viewer: Viewer,
mediaIDs: $ReadOnlyArray<string>,
): Promise<$ReadOnlyArray<Media>> {
const query = SQL`
SELECT id AS uploadID, secret AS uploadSecret,
type AS uploadType, extra AS uploadExtra
FROM uploads
- WHERE id IN (${mediaIDs}) AND uploader = ${viewer.id} AND container IS NULL
+ WHERE id IN (${mediaIDs})
+ AND uploader = ${viewer.id}
+ AND container IS NULL
+ AND user_container IS NULL
`;
const [result] = await dbQuery(query);
return result.map(imagesFromRow);
}
async function fetchMediaForThread(
viewer: Viewer,
request: ThreadFetchMediaRequest,
): Promise<ThreadFetchMediaResult> {
const visibleExtractString = `$.${threadPermissions.VISIBLE}.value`;
const query = SQL`
SELECT content.photo AS uploadID,
u.secret AS uploadSecret,
u.type AS uploadType, u.extra AS uploadExtra,
u.container, u.creation_time,
NULL AS thumbnailID,
NULL AS thumbnailUploadSecret,
NULL AS thumbnailUploadExtra
FROM messages m
LEFT JOIN JSON_TABLE(
m.content,
"$[*]" COLUMNS(photo INT PATH "$")
) content ON 1
LEFT JOIN uploads u ON u.id = content.photo
LEFT JOIN memberships mm ON mm.thread = ${request.threadID}
AND mm.user = ${viewer.id}
WHERE m.thread = ${request.threadID} AND m.type = ${messageTypes.IMAGES}
AND JSON_EXTRACT(mm.permissions, ${visibleExtractString}) IS TRUE
UNION SELECT content.media AS uploadID,
uv.secret AS uploadSecret,
uv.type AS uploadType, uv.extra AS uploadExtra,
uv.container, uv.creation_time,
content.thumbnail AS thumbnailID,
ut.secret AS thumbnailUploadSecret,
ut.extra AS thumbnailUploadExtra
FROM messages m
LEFT JOIN JSON_TABLE(
m.content,
"$[*]" COLUMNS(
media INT PATH "$.uploadID",
thumbnail INT PATH "$.thumbnailUploadID"
)
) content ON 1
LEFT JOIN uploads uv ON uv.id = content.media
LEFT JOIN uploads ut ON ut.id = content.thumbnail
LEFT JOIN memberships mm ON mm.thread = ${request.threadID}
AND mm.user = ${viewer.id}
WHERE m.thread = ${request.threadID}
AND m.type = ${messageTypes.MULTIMEDIA}
AND JSON_EXTRACT(mm.permissions, ${visibleExtractString}) IS TRUE
ORDER BY creation_time DESC
LIMIT ${request.limit} OFFSET ${request.offset}
`;
const [uploads] = await dbQuery(query);
const media = uploads.map(upload => {
const { uploadID, uploadType, uploadSecret, uploadExtra } = upload;
const { width, height, encryptionKey, blobHash, thumbHash } =
JSON.parse(uploadExtra);
const dimensions = { width, height };
const uri = makeUploadURI(blobHash, uploadID, uploadSecret);
if (uploadType === 'photo') {
if (encryptionKey) {
return {
type: 'encrypted_photo',
id: uploadID.toString(),
blobURI: uri,
encryptionKey,
dimensions,
thumbHash,
};
}
return {
type: 'photo',
id: uploadID.toString(),
uri,
dimensions,
thumbHash,
};
}
const { thumbnailUploadSecret, thumbnailUploadExtra } = upload;
const thumbnailID = upload.thumbnailID?.toString();
const {
encryptionKey: thumbnailEncryptionKey,
blobHash: thumbnailBlobHash,
thumbHash: thumbnailThumbHash,
} = JSON.parse(thumbnailUploadExtra);
const thumbnailURI = makeUploadURI(
thumbnailBlobHash,
thumbnailID,
thumbnailUploadSecret,
);
if (encryptionKey) {
return {
type: 'encrypted_video',
id: uploadID.toString(),
blobURI: uri,
encryptionKey,
dimensions,
thumbnailID,
thumbnailBlobURI: thumbnailURI,
thumbnailEncryptionKey,
thumbnailThumbHash,
};
}
return {
type: 'video',
id: uploadID.toString(),
uri,
dimensions,
thumbnailID,
thumbnailURI,
thumbnailThumbHash,
};
});
return { media };
}
-async function fetchUploadsForMessage(
+async function fetchUnassignedUploadsForMessage(
viewer: Viewer,
mediaMessageContents: $ReadOnlyArray<MediaMessageServerDBContent>,
): Promise<$ReadOnlyArray<Object>> {
const uploadIDs =
getUploadIDsFromMediaMessageServerDBContents(mediaMessageContents);
const query = SQL`
SELECT id AS uploadID, secret AS uploadSecret,
type AS uploadType, extra AS uploadExtra
FROM uploads
- WHERE id IN (${uploadIDs}) AND uploader = ${viewer.id} AND container IS NULL
+ WHERE id IN (${uploadIDs})
+ AND uploader = ${viewer.id}
+ AND container IS NULL
+ AND user_container IS NULL
`;
const [uploads] = await dbQuery(query);
return uploads;
}
-async function fetchMediaFromMediaMessageContent(
+async function fetchUnassignedMediaFromMediaMessageContent(
viewer: Viewer,
mediaMessageContents: $ReadOnlyArray<MediaMessageServerDBContent>,
): Promise<$ReadOnlyArray<Media>> {
- const uploads = await fetchUploadsForMessage(viewer, mediaMessageContents);
+ const uploads = await fetchUnassignedUploadsForMessage(
+ viewer,
+ mediaMessageContents,
+ );
return constructMediaFromMediaMessageContentsAndUploadRows(
mediaMessageContents,
uploads,
);
}
function constructMediaFromMediaMessageContentsAndUploadRows(
mediaMessageContents: $ReadOnlyArray<MediaMessageServerDBContent>,
uploadRows: $ReadOnlyArray<Object>,
): $ReadOnlyArray<Media> {
const uploadMap = _keyBy('uploadID')(uploadRows);
const media: Media[] = [];
for (const mediaMessageContent of mediaMessageContents) {
const primaryUploadID = mediaMessageContent.uploadID;
const primaryUpload = uploadMap[primaryUploadID];
const uploadExtra = JSON.parse(primaryUpload.uploadExtra);
const { width, height, loop, blobHash, encryptionKey, thumbHash } =
uploadExtra;
const dimensions = { width, height };
const primaryUploadURI = makeUploadURI(
blobHash,
primaryUploadID,
primaryUpload.uploadSecret,
);
if (mediaMessageContent.type === 'photo') {
if (encryptionKey) {
media.push({
type: 'encrypted_photo',
id: primaryUploadID,
blobURI: primaryUploadURI,
encryptionKey,
dimensions,
thumbHash,
});
} else {
media.push({
type: 'photo',
id: primaryUploadID,
uri: primaryUploadURI,
dimensions,
thumbHash,
});
}
continue;
}
const thumbnailUploadID = mediaMessageContent.thumbnailUploadID;
const thumbnailUpload = uploadMap[thumbnailUploadID];
const thumbnailUploadExtra = JSON.parse(thumbnailUpload.uploadExtra);
const { blobHash: thumbnailBlobHash, thumbHash: thumbnailThumbHash } =
thumbnailUploadExtra;
const thumbnailUploadURI = makeUploadURI(
thumbnailBlobHash,
thumbnailUploadID,
thumbnailUpload.uploadSecret,
);
if (encryptionKey) {
const video = {
type: 'encrypted_video',
id: primaryUploadID,
blobURI: primaryUploadURI,
encryptionKey,
dimensions,
thumbnailID: thumbnailUploadID,
thumbnailBlobURI: thumbnailUploadURI,
thumbnailEncryptionKey: thumbnailUploadExtra.encryptionKey,
thumbnailThumbHash,
};
media.push(loop ? { ...video, loop } : video);
} else {
const video = {
type: 'video',
id: primaryUploadID,
uri: primaryUploadURI,
dimensions,
thumbnailID: thumbnailUploadID,
thumbnailURI: thumbnailUploadURI,
thumbnailThumbHash,
};
media.push(loop ? { ...video, loop } : video);
}
}
return media;
}
export {
fetchUpload,
fetchUploadChunk,
getUploadSize,
getUploadURL,
makeUploadURI,
imagesFromRow,
- fetchImages,
+ fetchUnassignedImages,
fetchMediaForThread,
- fetchMediaFromMediaMessageContent,
+ fetchUnassignedMediaFromMediaMessageContent,
constructMediaFromMediaMessageContentsAndUploadRows,
};
diff --git a/keyserver/src/responders/message-responders.js b/keyserver/src/responders/message-responders.js
index bafa54cb1..ef78d8d5f 100644
--- a/keyserver/src/responders/message-responders.js
+++ b/keyserver/src/responders/message-responders.js
@@ -1,423 +1,426 @@
// @flow
import invariant from 'invariant';
import t, { type TInterface, type TUnion } from 'tcomb';
import { onlyOneEmojiRegex } from 'lib/shared/emojis.js';
import {
createMediaMessageData,
trimMessage,
} from 'lib/shared/message-utils.js';
import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils.js';
import type { Media } from 'lib/types/media-types.js';
import { messageTypes } from 'lib/types/message-types-enum.js';
import {
type SendTextMessageRequest,
type SendMultimediaMessageRequest,
type SendReactionMessageRequest,
type SendEditMessageRequest,
type FetchMessageInfosResponse,
type FetchMessageInfosRequest,
defaultNumberPerThread,
type SendMessageResponse,
type SendEditMessageResponse,
type FetchPinnedMessagesRequest,
type FetchPinnedMessagesResult,
type SearchMessagesResponse,
type SearchMessagesRequest,
} from 'lib/types/message-types.js';
import type { EditMessageData } from 'lib/types/messages/edit.js';
import type { ReactionMessageData } from 'lib/types/messages/reaction.js';
import type { TextMessageData } from 'lib/types/messages/text.js';
import { threadPermissions } from 'lib/types/thread-permission-types.js';
import { ServerError } from 'lib/utils/errors.js';
import { values } from 'lib/utils/objects.js';
import {
tRegex,
tShape,
tMediaMessageMedia,
tID,
} from 'lib/utils/validation-utils.js';
import createMessages from '../creators/message-creator.js';
import {
fetchMessageInfos,
fetchMessageInfoForLocalID,
fetchMessageInfoByID,
fetchThreadMessagesCount,
fetchPinnedMessageInfos,
searchMessagesInSingleChat,
} from '../fetchers/message-fetchers.js';
import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js';
import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js';
import {
- fetchImages,
- fetchMediaFromMediaMessageContent,
+ fetchUnassignedImages,
+ fetchUnassignedMediaFromMediaMessageContent,
} from '../fetchers/upload-fetchers.js';
import { fetchKnownUserInfos } from '../fetchers/user-fetchers.js';
import type { Viewer } from '../session/viewer.js';
import {
assignImages,
assignMessageContainerToMedia,
} from '../updaters/upload-updaters.js';
export const sendTextMessageRequestInputValidator: TInterface<SendTextMessageRequest> =
tShape<SendTextMessageRequest>({
threadID: tID,
localID: t.maybe(t.String),
text: t.String,
sidebarCreation: t.maybe(t.Boolean),
});
async function textMessageCreationResponder(
viewer: Viewer,
request: SendTextMessageRequest,
): Promise<SendMessageResponse> {
const { threadID, localID, text: rawText, sidebarCreation } = request;
const text = trimMessage(rawText);
if (!text) {
throw new ServerError('invalid_parameters');
}
const hasPermission = await checkThreadPermission(
viewer,
threadID,
threadPermissions.VOICED,
);
if (!hasPermission) {
throw new ServerError('invalid_parameters');
}
let messageData: TextMessageData = {
type: messageTypes.TEXT,
threadID,
creatorID: viewer.id,
time: Date.now(),
text,
};
if (localID) {
messageData = { ...messageData, localID };
}
if (sidebarCreation) {
const numMessages = await fetchThreadMessagesCount(threadID);
if (numMessages === 2) {
// sidebarCreation is set below to prevent double notifs from a sidebar
// creation. We expect precisely two messages to appear before a
// sidebarCreation: a SIDEBAR_SOURCE and a CREATE_SIDEBAR. If two users
// attempt to create a sidebar at the same time, then both clients will
// attempt to set sidebarCreation here, but we only want to suppress
// notifs for the client that won the race.
messageData = { ...messageData, sidebarCreation };
}
}
const rawMessageInfos = await createMessages(viewer, [messageData]);
return { newMessageInfo: rawMessageInfos[0] };
}
export const fetchMessageInfosRequestInputValidator: TInterface<FetchMessageInfosRequest> =
tShape<FetchMessageInfosRequest>({
cursors: t.dict(tID, t.maybe(tID)),
numberPerThread: t.maybe(t.Number),
});
async function messageFetchResponder(
viewer: Viewer,
request: FetchMessageInfosRequest,
): Promise<FetchMessageInfosResponse> {
const response = await fetchMessageInfos(
viewer,
{ threadCursors: request.cursors },
request.numberPerThread ? request.numberPerThread : defaultNumberPerThread,
);
return {
...response,
userInfos: {},
};
}
export const sendMultimediaMessageRequestInputValidator: TUnion<SendMultimediaMessageRequest> =
t.union<SendMultimediaMessageRequest>([
// This option is only used for messageTypes.IMAGES
tShape({
threadID: tID,
localID: t.String,
sidebarCreation: t.maybe(t.Boolean),
mediaIDs: t.list(tID),
}),
tShape({
threadID: tID,
localID: t.String,
sidebarCreation: t.maybe(t.Boolean),
mediaMessageContents: t.list(tMediaMessageMedia),
}),
]);
async function multimediaMessageCreationResponder(
viewer: Viewer,
request: SendMultimediaMessageRequest,
): Promise<SendMessageResponse> {
if (
(request.mediaIDs && request.mediaIDs.length === 0) ||
(request.mediaMessageContents && request.mediaMessageContents.length === 0)
) {
throw new ServerError('invalid_parameters');
}
const { threadID, localID, sidebarCreation } = request;
const hasPermission = await checkThreadPermission(
viewer,
threadID,
threadPermissions.VOICED,
);
if (!hasPermission) {
throw new ServerError('invalid_parameters');
}
const existingMessageInfoPromise = fetchMessageInfoForLocalID(
viewer,
localID,
);
const mediaPromise: Promise<$ReadOnlyArray<Media>> = request.mediaIDs
- ? fetchImages(viewer, request.mediaIDs)
- : fetchMediaFromMediaMessageContent(viewer, request.mediaMessageContents);
+ ? fetchUnassignedImages(viewer, request.mediaIDs)
+ : fetchUnassignedMediaFromMediaMessageContent(
+ viewer,
+ request.mediaMessageContents,
+ );
const [existingMessageInfo, media] = await Promise.all([
existingMessageInfoPromise,
mediaPromise,
]);
if (media.length === 0 && !existingMessageInfo) {
throw new ServerError('invalid_parameters');
}
// We use the MULTIMEDIA type for encrypted photos
const containsEncryptedMedia = media.some(
m => m.type === 'encrypted_photo' || m.type === 'encrypted_video',
);
const messageData = createMediaMessageData(
{
localID,
threadID,
creatorID: viewer.id,
media,
sidebarCreation,
},
{ forceMultimediaMessageType: containsEncryptedMedia },
);
const [newMessageInfo] = await createMessages(viewer, [messageData]);
const { id } = newMessageInfo;
invariant(
id !== null && id !== undefined,
'serverID should be set in createMessages result',
);
if (request.mediaIDs) {
await assignImages(viewer, request.mediaIDs, id, threadID);
} else {
await assignMessageContainerToMedia(
viewer,
request.mediaMessageContents,
id,
threadID,
);
}
return { newMessageInfo };
}
export const sendReactionMessageRequestInputValidator: TInterface<SendReactionMessageRequest> =
tShape<SendReactionMessageRequest>({
threadID: tID,
localID: t.maybe(t.String),
targetMessageID: tID,
reaction: tRegex(onlyOneEmojiRegex),
action: t.enums.of(['add_reaction', 'remove_reaction']),
});
async function reactionMessageCreationResponder(
viewer: Viewer,
request: SendReactionMessageRequest,
): Promise<SendMessageResponse> {
const { threadID, localID, targetMessageID, reaction, action } = request;
if (!targetMessageID || !reaction) {
throw new ServerError('invalid_parameters');
}
const targetMessageInfo = await fetchMessageInfoByID(viewer, targetMessageID);
if (!targetMessageInfo || !targetMessageInfo.id) {
throw new ServerError('invalid_parameters');
}
const [serverThreadInfos, hasPermission, targetMessageUserInfos] =
await Promise.all([
fetchServerThreadInfos({ threadID }),
checkThreadPermission(
viewer,
threadID,
threadPermissions.REACT_TO_MESSAGE,
),
fetchKnownUserInfos(viewer, [targetMessageInfo.creatorID]),
]);
const targetMessageThreadInfo = serverThreadInfos.threadInfos[threadID];
if (targetMessageThreadInfo.sourceMessageID === targetMessageID) {
throw new ServerError('invalid_parameters');
}
const targetMessageCreator =
targetMessageUserInfos[targetMessageInfo.creatorID];
const targetMessageCreatorRelationship =
targetMessageCreator?.relationshipStatus;
const creatorRelationshipHasBlock =
targetMessageCreatorRelationship &&
relationshipBlockedInEitherDirection(targetMessageCreatorRelationship);
if (!hasPermission || creatorRelationshipHasBlock) {
throw new ServerError('invalid_parameters');
}
let messageData: ReactionMessageData = {
type: messageTypes.REACTION,
threadID,
creatorID: viewer.id,
time: Date.now(),
targetMessageID,
reaction,
action,
};
if (localID) {
messageData = { ...messageData, localID };
}
const rawMessageInfos = await createMessages(viewer, [messageData]);
return { newMessageInfo: rawMessageInfos[0] };
}
export const editMessageRequestInputValidator: TInterface<SendEditMessageRequest> =
tShape<SendEditMessageRequest>({
targetMessageID: tID,
text: t.String,
});
async function editMessageCreationResponder(
viewer: Viewer,
request: SendEditMessageRequest,
): Promise<SendEditMessageResponse> {
const { targetMessageID, text: rawText } = request;
const text = trimMessage(rawText);
if (!targetMessageID || !text) {
throw new ServerError('invalid_parameters');
}
const targetMessageInfo = await fetchMessageInfoByID(viewer, targetMessageID);
if (!targetMessageInfo || !targetMessageInfo.id) {
throw new ServerError('invalid_parameters');
}
if (targetMessageInfo.type !== messageTypes.TEXT) {
throw new ServerError('invalid_parameters');
}
const { threadID } = targetMessageInfo;
const [serverThreadInfos, hasPermission, rawSidebarThreadInfos] =
await Promise.all([
fetchServerThreadInfos({ threadID }),
checkThreadPermission(viewer, threadID, threadPermissions.EDIT_MESSAGE),
fetchServerThreadInfos({
parentThreadID: threadID,
sourceMessageID: targetMessageID,
}),
]);
const targetMessageThreadInfo = serverThreadInfos.threadInfos[threadID];
if (targetMessageThreadInfo.sourceMessageID === targetMessageID) {
// We are editing first message of the sidebar
// If client wants to do that it sends id of the sourceMessage instead
throw new ServerError('invalid_parameters');
}
if (!hasPermission) {
throw new ServerError('invalid_parameters');
}
if (targetMessageInfo.creatorID !== viewer.id) {
throw new ServerError('invalid_parameters');
}
const time = Date.now();
const messagesData = [];
let messageData: EditMessageData = {
type: messageTypes.EDIT_MESSAGE,
threadID,
creatorID: viewer.id,
time,
targetMessageID,
text,
};
messagesData.push(messageData);
const sidebarThreadValues = values(rawSidebarThreadInfos.threadInfos);
for (const sidebarThreadValue of sidebarThreadValues) {
if (sidebarThreadValue && sidebarThreadValue.id) {
messageData = {
type: messageTypes.EDIT_MESSAGE,
threadID: sidebarThreadValue.id,
creatorID: viewer.id,
time,
targetMessageID,
text: text,
};
messagesData.push(messageData);
}
}
const newMessageInfos = await createMessages(viewer, messagesData);
return { newMessageInfos };
}
export const fetchPinnedMessagesResponderInputValidator: TInterface<FetchPinnedMessagesRequest> =
tShape<FetchPinnedMessagesRequest>({
threadID: tID,
});
async function fetchPinnedMessagesResponder(
viewer: Viewer,
request: FetchPinnedMessagesRequest,
): Promise<FetchPinnedMessagesResult> {
return await fetchPinnedMessageInfos(viewer, request);
}
export const searchMessagesResponderInputValidator: TInterface<SearchMessagesRequest> =
tShape<SearchMessagesRequest>({
query: t.String,
threadID: tID,
cursor: t.maybe(tID),
});
async function searchMessagesResponder(
viewer: Viewer,
request: SearchMessagesRequest,
): Promise<SearchMessagesResponse> {
return await searchMessagesInSingleChat(
request.query,
request.threadID,
viewer,
request.cursor,
);
}
export {
textMessageCreationResponder,
messageFetchResponder,
multimediaMessageCreationResponder,
reactionMessageCreationResponder,
editMessageCreationResponder,
fetchPinnedMessagesResponder,
searchMessagesResponder,
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 2:20 AM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2559370
Default Alt Text
(26 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment