import { AbortSignal } from 'abort-controller'; import { getEmojiDataFromNative } from 'emoji-mart'; import { Data } from '../../../../data/data'; import { ConversationModel } from '../../../../models/conversation'; import { Action, FixedBaseEmoji, OpenGroupReactionResponse, Reaction, } from '../../../../types/Reaction'; import { Reactions } from '../../../../util/reactions'; import { OnionSending } from '../../../onions/onionSend'; import { UserUtils } from '../../../utils'; import { OpenGroupPollingUtils } from '../opengroupV2/OpenGroupPollingUtils'; import { getUsBlindedInThatServer } from './knownBlindedkeys'; import { batchGlobalIsSuccess, parseBatchGlobalStatusCode } from './sogsV3BatchPoll'; import { addToMutationCache, ChangeType, SogsV3Mutation, updateMutationCache, } from './sogsV3MutationCache'; export const hasReactionSupport = async ( serverId: number ): Promise<{ supported: boolean; conversation: ConversationModel | null }> => { const found = await Data.getMessageByServerId(serverId); if (!found) { window.log.warn(`Open Group Message ${serverId} not found in db`); return { supported: false, conversation: null }; } const conversationModel = found?.getConversation(); if (!conversationModel) { window.log.warn(`Conversation for ${serverId} not found in db`); return { supported: false, conversation: null }; } if (!conversationModel.hasReactions()) { window.log.warn("This open group doesn't have reaction support. Server Message ID", serverId); return { supported: false, conversation: null }; } return { supported: true, conversation: conversationModel }; }; export const sendSogsReactionOnionV4 = async ( serverUrl: string, room: string, // this is the roomId abortSignal: AbortSignal, reaction: Reaction, blinded: boolean ): Promise => { const allValidRoomInfos = OpenGroupPollingUtils.getAllValidRoomInfos(serverUrl, new Set([room])); if (!allValidRoomInfos?.length) { window?.log?.info('getSendReactionRequest: no valid roominfos got.'); throw new Error(`Could not find sogs pubkey of url:${serverUrl}`); } const { supported, conversation } = await hasReactionSupport(reaction.id); if (!supported) { return false; } if (Reactions.hitRateLimit()) { return false; } if (!conversation) { window.log.warn(`Conversation for ${reaction.id} not found in db`); return false; } // The SOGS endpoint supports any text input so we need to make sure we are sending a valid unicode emoji // for an invalid input we use https://emojipedia.org/frame-with-an-x/ as a replacement since it cannot rendered as an emoji but is valid unicode // NOTE emoji-mart v5.2.2 types for getEmojiDataFromNative are broken // @ts-ignore const emoji = (getEmojiDataFromNative(reaction.emoji) as FixedBaseEmoji) ? reaction.emoji : '🖾'; const endpoint = `/room/${room}/reaction/${reaction.id}/${emoji}`; const method = reaction.action === Action.REACT ? 'PUT' : 'DELETE'; const serverPubkey = allValidRoomInfos[0].serverPublicKey; const cacheEntry: SogsV3Mutation = { server: serverUrl, room: room, changeType: ChangeType.REACTIONS, seqno: null, metadata: { messageId: reaction.id, emoji, action: reaction.action === Action.REACT ? 'ADD' : 'REMOVE', }, }; addToMutationCache(cacheEntry); // Since responses can take a long time we immediately update the sender's UI and if there is a problem it is overwritten by handleOpenGroupMessageReactions later. const me = UserUtils.getOurPubKeyStrFromCache(); await Reactions.handleMessageReaction({ reaction, sender: blinded ? getUsBlindedInThatServer(conversation) || me : me, you: true, isOpenGroup: true, }); // reaction endpoint requires an empty dict {} const stringifiedBody = null; const result = await OnionSending.sendJsonViaOnionV4ToSogs({ serverUrl, endpoint, serverPubkey, method, abortSignal, blinded, stringifiedBody, headers: null, throwErrors: true, }); if (!batchGlobalIsSuccess(result)) { window?.log?.warn('sendSogsReactionWithOnionV4 Got unknown status code; res:', result); throw new Error( `sendSogsReactionOnionV4: invalid status code: ${parseBatchGlobalStatusCode(result)}` ); } if (!result) { throw new Error('Could not putReaction, res is invalid'); } const rawMessage = result.body as OpenGroupReactionResponse; if (!rawMessage) { throw new Error('putReaction parsing failed'); } const success = Boolean(reaction.action === Action.REACT ? rawMessage.added : rawMessage.removed); if (success) { updateMutationCache(cacheEntry, rawMessage.seqno); } return success; };