Page MenuHomePhabricator

No OneTemporary

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

Mime Type
text/x-diff
Expires
Sat, Nov 23, 2:20 AM (1 d, 14 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2559370
Default Alt Text
(26 KB)

Event Timeline