1060 lines
31 KiB
TypeScript
1060 lines
31 KiB
TypeScript
import { Api as GramJs } from '../../../lib/gramjs';
|
|
import {
|
|
ApiMessage,
|
|
ApiMessageForwardInfo,
|
|
ApiPhoto,
|
|
ApiSticker,
|
|
ApiVideo,
|
|
ApiVoice,
|
|
ApiAudio,
|
|
ApiDocument,
|
|
ApiAction,
|
|
ApiContact,
|
|
ApiAttachment,
|
|
ApiPoll,
|
|
ApiNewPoll,
|
|
ApiWebPage,
|
|
ApiMessageEntity,
|
|
ApiFormattedText,
|
|
ApiReplyKeyboard,
|
|
ApiKeyboardButton,
|
|
ApiChat,
|
|
ApiThreadInfo,
|
|
ApiInvoice,
|
|
ApiGroupCall,
|
|
ApiUser,
|
|
ApiSponsoredMessage,
|
|
} from '../../types';
|
|
|
|
import {
|
|
DELETED_COMMENTS_CHANNEL_ID,
|
|
LOCAL_MESSAGE_ID_BASE,
|
|
SERVICE_NOTIFICATIONS_USER_ID,
|
|
SPONSORED_MESSAGE_CACHE_MS,
|
|
SUPPORTED_IMAGE_CONTENT_TYPES,
|
|
SUPPORTED_VIDEO_CONTENT_TYPES,
|
|
VIDEO_MOV_TYPE,
|
|
} from '../../../config';
|
|
import { pick } from '../../../util/iteratees';
|
|
import { buildStickerFromDocument } from './symbols';
|
|
import { buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromStripped } from './common';
|
|
import { interpolateArray } from '../../../util/waveform';
|
|
import { buildPeer } from '../gramjsBuilders';
|
|
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
|
|
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
|
|
|
|
const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp';
|
|
const INPUT_WAVEFORM_LENGTH = 63;
|
|
|
|
let localMessageCounter = LOCAL_MESSAGE_ID_BASE;
|
|
let currentUserId!: string;
|
|
|
|
export function setMessageBuilderCurrentUserId(_currentUserId: string) {
|
|
currentUserId = _currentUserId;
|
|
}
|
|
|
|
export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): ApiSponsoredMessage | undefined {
|
|
const {
|
|
fromId, message, entities, startParam, channelPost, chatInvite, chatInviteHash, randomId,
|
|
} = mtpMessage;
|
|
const chatId = fromId ? getApiChatIdFromMtpPeer(fromId) : undefined;
|
|
const chatInviteTitle = chatInvite
|
|
? (chatInvite instanceof GramJs.ChatInvite
|
|
? chatInvite.title
|
|
: !(chatInvite.chat instanceof GramJs.ChatEmpty) ? chatInvite.chat.title : undefined)
|
|
: undefined;
|
|
|
|
return {
|
|
randomId: serializeBytes(randomId),
|
|
isBot: fromId ? isPeerUser(fromId) : false,
|
|
text: buildMessageTextContent(message, entities),
|
|
expiresAt: Math.round(Date.now() / 1000) + SPONSORED_MESSAGE_CACHE_MS,
|
|
...(chatId && { chatId }),
|
|
...(chatInviteHash && { chatInviteHash }),
|
|
...(chatInvite && { chatInviteTitle }),
|
|
...(startParam && { startParam }),
|
|
...(channelPost && { channelPostId: channelPost }),
|
|
};
|
|
}
|
|
|
|
export function buildApiMessage(mtpMessage: GramJs.TypeMessage): ApiMessage | undefined {
|
|
const chatId = resolveMessageApiChatId(mtpMessage);
|
|
if (
|
|
!chatId
|
|
|| !(mtpMessage instanceof GramJs.Message || mtpMessage instanceof GramJs.MessageService)) {
|
|
return undefined;
|
|
}
|
|
|
|
return buildApiMessageWithChatId(chatId, mtpMessage);
|
|
}
|
|
|
|
export function buildApiMessageFromShort(mtpMessage: GramJs.UpdateShortMessage): ApiMessage {
|
|
const chatId = buildApiPeerId(mtpMessage.userId, 'user');
|
|
|
|
return buildApiMessageWithChatId(chatId, {
|
|
...mtpMessage,
|
|
fromId: buildPeer(mtpMessage.out ? currentUserId : buildApiPeerId(mtpMessage.userId, 'user')),
|
|
});
|
|
}
|
|
|
|
export function buildApiMessageFromShortChat(mtpMessage: GramJs.UpdateShortChatMessage): ApiMessage {
|
|
const chatId = buildApiPeerId(mtpMessage.chatId, 'chat');
|
|
|
|
return buildApiMessageWithChatId(chatId, {
|
|
...mtpMessage,
|
|
fromId: buildPeer(buildApiPeerId(mtpMessage.fromId, 'user')),
|
|
});
|
|
}
|
|
|
|
export function buildApiMessageFromNotification(
|
|
notification: GramJs.UpdateServiceNotification,
|
|
currentDate: number,
|
|
): ApiMessage {
|
|
const localId = localMessageCounter++;
|
|
let content: ApiMessage['content'] = {};
|
|
|
|
if (notification.media) {
|
|
content = {
|
|
...buildMessageMediaContent(notification.media),
|
|
};
|
|
}
|
|
|
|
if (notification.message && !content.sticker && !content.poll && !content.contact) {
|
|
content = {
|
|
...content,
|
|
text: buildMessageTextContent(notification.message, notification.entities),
|
|
};
|
|
}
|
|
|
|
return {
|
|
id: localId,
|
|
chatId: SERVICE_NOTIFICATIONS_USER_ID,
|
|
date: notification.inboxDate || currentDate,
|
|
content,
|
|
isOutgoing: false,
|
|
};
|
|
}
|
|
|
|
type UniversalMessage = (
|
|
Pick<GramJs.Message & GramJs.MessageService, ('id' | 'date')>
|
|
& Pick<Partial<GramJs.Message & GramJs.MessageService>, (
|
|
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
|
|
'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' |
|
|
'replies' | 'fromScheduled' | 'postAuthor'
|
|
)>
|
|
);
|
|
|
|
export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalMessage): ApiMessage {
|
|
const fromId = mtpMessage.fromId ? getApiChatIdFromMtpPeer(mtpMessage.fromId) : undefined;
|
|
const peerId = mtpMessage.peerId ? getApiChatIdFromMtpPeer(mtpMessage.peerId) : undefined;
|
|
const isChatWithSelf = !fromId && chatId === currentUserId;
|
|
const isOutgoing = (mtpMessage.out && !mtpMessage.post) || (isChatWithSelf && !mtpMessage.fwdFrom);
|
|
|
|
let content: ApiMessage['content'] = {};
|
|
|
|
if (mtpMessage.media) {
|
|
content = {
|
|
...buildMessageMediaContent(mtpMessage.media),
|
|
};
|
|
}
|
|
|
|
if (mtpMessage.message && !content.sticker && !content.poll && !content.contact) {
|
|
content = {
|
|
...content,
|
|
text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities),
|
|
};
|
|
}
|
|
|
|
const action = mtpMessage.action
|
|
&& buildAction(mtpMessage.action, fromId, peerId, Boolean(mtpMessage.post), isOutgoing);
|
|
if (action) {
|
|
content.action = action;
|
|
}
|
|
|
|
const { replyToMsgId, replyToTopId, replyToPeerId } = mtpMessage.replyTo || {};
|
|
const isEdited = mtpMessage.editDate && !mtpMessage.editHide;
|
|
const {
|
|
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse,
|
|
} = buildReplyButtons(mtpMessage) || {};
|
|
const forwardInfo = mtpMessage.fwdFrom && buildApiMessageForwardInfo(mtpMessage.fwdFrom, isChatWithSelf);
|
|
const { replies, mediaUnread: isMediaUnread, postAuthor } = mtpMessage;
|
|
const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId);
|
|
const isInAlbum = Boolean(groupedId) && !(content.document || content.audio);
|
|
const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide;
|
|
|
|
return {
|
|
id: mtpMessage.id,
|
|
chatId,
|
|
isOutgoing,
|
|
content,
|
|
date: mtpMessage.date,
|
|
senderId: fromId || (mtpMessage.out && mtpMessage.post && currentUserId) || chatId,
|
|
views: mtpMessage.views,
|
|
isFromScheduled: mtpMessage.fromScheduled,
|
|
...(replyToMsgId && { replyToMessageId: replyToMsgId }),
|
|
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
|
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
|
...(forwardInfo && { forwardInfo }),
|
|
...(isEdited && { isEdited, editDate: mtpMessage.editDate }),
|
|
...(isMediaUnread && { isMediaUnread }),
|
|
...(mtpMessage.mentioned && isMediaUnread && { hasUnreadMention: true }),
|
|
...(mtpMessage.mentioned && { isMentioned: true }),
|
|
...(groupedId && {
|
|
groupedId,
|
|
isInAlbum,
|
|
}),
|
|
inlineButtons,
|
|
...(keyboardButtons && { keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse }),
|
|
...(shouldHideKeyboardButtons && { shouldHideKeyboardButtons }),
|
|
...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }),
|
|
...(replies?.comments && { threadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }),
|
|
...(postAuthor && { adminTitle: postAuthor }),
|
|
};
|
|
}
|
|
|
|
export function buildMessageTextContent(
|
|
message: string,
|
|
entities?: GramJs.TypeMessageEntity[],
|
|
): ApiFormattedText {
|
|
return {
|
|
text: message,
|
|
...(entities && { entities: entities.map(buildApiMessageEntity) }),
|
|
};
|
|
}
|
|
|
|
export function buildMessageDraft(draft: GramJs.TypeDraftMessage) {
|
|
if (draft instanceof GramJs.DraftMessageEmpty) {
|
|
return undefined;
|
|
}
|
|
|
|
const {
|
|
message, entities, replyToMsgId, date,
|
|
} = draft;
|
|
|
|
return {
|
|
formattedText: message ? buildMessageTextContent(message, entities) : undefined,
|
|
replyingToId: replyToMsgId,
|
|
date,
|
|
};
|
|
}
|
|
|
|
export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMessage['content'] | undefined {
|
|
if ('ttlSeconds' in media && media.ttlSeconds) {
|
|
return undefined;
|
|
}
|
|
|
|
const sticker = buildSticker(media);
|
|
if (sticker) return { sticker };
|
|
|
|
const photo = buildPhoto(media);
|
|
if (photo) return { photo };
|
|
|
|
const video = buildVideo(media);
|
|
if (video) return { video };
|
|
|
|
const audio = buildAudio(media);
|
|
if (audio) return { audio };
|
|
|
|
const voice = buildVoice(media);
|
|
if (voice) return { voice };
|
|
|
|
const document = buildDocumentFromMedia(media);
|
|
if (document) return { document };
|
|
|
|
const contact = buildContact(media);
|
|
if (contact) return { contact };
|
|
|
|
const poll = buildPollFromMedia(media);
|
|
if (poll) return { poll };
|
|
|
|
const webPage = buildWebPage(media);
|
|
if (webPage) return { webPage };
|
|
|
|
const invoice = buildInvoiceFromMedia(media);
|
|
if (invoice) return { invoice };
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function buildApiMessageForwardInfo(fwdFrom: GramJs.MessageFwdHeader, isChatWithSelf = false): ApiMessageForwardInfo {
|
|
const savedFromPeerId = fwdFrom.savedFromPeer && getApiChatIdFromMtpPeer(fwdFrom.savedFromPeer);
|
|
const fromId = fwdFrom.fromId && getApiChatIdFromMtpPeer(fwdFrom.fromId);
|
|
|
|
return {
|
|
date: fwdFrom.date,
|
|
isChannelPost: Boolean(fwdFrom.channelPost),
|
|
channelPostId: fwdFrom.channelPost,
|
|
isLinkedChannelPost: Boolean(fwdFrom.channelPost && savedFromPeerId && !isChatWithSelf),
|
|
fromChatId: savedFromPeerId || fromId,
|
|
fromMessageId: fwdFrom.channelPost || fwdFrom.savedFromMsgId,
|
|
senderUserId: fromId,
|
|
hiddenUserName: fwdFrom.fromName,
|
|
adminTitle: fwdFrom.postAuthor,
|
|
};
|
|
}
|
|
|
|
function buildSticker(media: GramJs.TypeMessageMedia): ApiSticker | undefined {
|
|
if (
|
|
!(media instanceof GramJs.MessageMediaDocument)
|
|
|| !media.document
|
|
|| !(media.document instanceof GramJs.Document)
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return buildStickerFromDocument(media.document);
|
|
}
|
|
|
|
function buildPhoto(media: GramJs.TypeMessageMedia): ApiPhoto | undefined {
|
|
if (!(media instanceof GramJs.MessageMediaPhoto) || !media.photo || !(media.photo instanceof GramJs.Photo)) {
|
|
return undefined;
|
|
}
|
|
|
|
return buildApiPhoto(media.photo);
|
|
}
|
|
|
|
export function buildVideoFromDocument(document: GramJs.Document): ApiVideo | undefined {
|
|
if (document instanceof GramJs.DocumentEmpty) {
|
|
return undefined;
|
|
}
|
|
|
|
const {
|
|
id, mimeType, thumbs, size, attributes,
|
|
} = document;
|
|
|
|
// eslint-disable-next-line no-restricted-globals
|
|
if (mimeType === VIDEO_MOV_TYPE && !(self as any).isMovSupported) {
|
|
return undefined;
|
|
}
|
|
|
|
const videoAttr = attributes
|
|
.find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo);
|
|
|
|
if (!videoAttr) {
|
|
return undefined;
|
|
}
|
|
|
|
const gifAttr = attributes
|
|
.find((a: any): a is GramJs.DocumentAttributeAnimated => a instanceof GramJs.DocumentAttributeAnimated);
|
|
|
|
const {
|
|
duration,
|
|
w: width,
|
|
h: height,
|
|
supportsStreaming = false,
|
|
roundMessage: isRound = false,
|
|
} = videoAttr;
|
|
|
|
return {
|
|
id: String(id),
|
|
mimeType,
|
|
duration,
|
|
fileName: getFilenameFromDocument(document, 'video'),
|
|
width,
|
|
height,
|
|
supportsStreaming,
|
|
isRound,
|
|
isGif: Boolean(gifAttr),
|
|
thumbnail: buildApiThumbnailFromStripped(thumbs),
|
|
size,
|
|
};
|
|
}
|
|
|
|
function buildVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined {
|
|
if (
|
|
!(media instanceof GramJs.MessageMediaDocument)
|
|
|| !(media.document instanceof GramJs.Document)
|
|
|| !media.document.mimeType.startsWith('video')
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return buildVideoFromDocument(media.document);
|
|
}
|
|
|
|
function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined {
|
|
if (
|
|
!(media instanceof GramJs.MessageMediaDocument)
|
|
|| !media.document
|
|
|| !(media.document instanceof GramJs.Document)
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
const audioAttribute = media.document.attributes
|
|
.find((attr: any): attr is GramJs.DocumentAttributeAudio => (
|
|
attr instanceof GramJs.DocumentAttributeAudio
|
|
));
|
|
|
|
if (!audioAttribute || audioAttribute.voice) {
|
|
return undefined;
|
|
}
|
|
|
|
const thumbnailSizes = media.document.thumbs && media.document.thumbs
|
|
.filter((thumb): thumb is GramJs.PhotoSize => thumb instanceof GramJs.PhotoSize)
|
|
.map((thumb) => buildApiPhotoSize(thumb));
|
|
|
|
return {
|
|
fileName: getFilenameFromDocument(media.document, 'audio'),
|
|
thumbnailSizes,
|
|
...pick(media.document, ['size', 'mimeType']),
|
|
...pick(audioAttribute, ['duration', 'performer', 'title']),
|
|
};
|
|
}
|
|
|
|
function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined {
|
|
if (
|
|
!(media instanceof GramJs.MessageMediaDocument)
|
|
|| !media.document
|
|
|| !(media.document instanceof GramJs.Document)
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
const audioAttribute = media.document.attributes
|
|
.find((attr: any): attr is GramJs.DocumentAttributeAudio => (
|
|
attr instanceof GramJs.DocumentAttributeAudio
|
|
));
|
|
|
|
if (!audioAttribute || !audioAttribute.voice) {
|
|
return undefined;
|
|
}
|
|
|
|
const { duration, waveform } = audioAttribute;
|
|
|
|
return {
|
|
duration,
|
|
waveform: waveform ? Array.from(waveform) : undefined,
|
|
};
|
|
}
|
|
|
|
function buildDocumentFromMedia(media: GramJs.TypeMessageMedia) {
|
|
if (!(media instanceof GramJs.MessageMediaDocument) || !media.document) {
|
|
return undefined;
|
|
}
|
|
|
|
return buildApiDocument(media.document);
|
|
}
|
|
|
|
export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | undefined {
|
|
if (!(document instanceof GramJs.Document)) {
|
|
return undefined;
|
|
}
|
|
|
|
const {
|
|
id, size, mimeType, date, thumbs, attributes,
|
|
} = document;
|
|
|
|
const thumbnail = thumbs && buildApiThumbnailFromStripped(thumbs);
|
|
|
|
let mediaType: ApiDocument['mediaType'] | undefined;
|
|
let mediaSize: ApiDocument['mediaSize'] | undefined;
|
|
const photoSize = thumbs && thumbs.find((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize);
|
|
if (photoSize) {
|
|
mediaSize = {
|
|
width: photoSize.w,
|
|
height: photoSize.h,
|
|
};
|
|
|
|
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) {
|
|
mediaType = 'photo';
|
|
|
|
const imageAttribute = attributes
|
|
.find((a: any): a is GramJs.DocumentAttributeImageSize => a instanceof GramJs.DocumentAttributeImageSize);
|
|
|
|
if (imageAttribute) {
|
|
const { w: width, h: height } = imageAttribute;
|
|
mediaSize = {
|
|
width,
|
|
height,
|
|
};
|
|
}
|
|
} else if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) {
|
|
mediaType = 'video';
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: String(id),
|
|
size,
|
|
mimeType,
|
|
timestamp: date,
|
|
fileName: getFilenameFromDocument(document),
|
|
thumbnail,
|
|
mediaType,
|
|
mediaSize,
|
|
};
|
|
}
|
|
|
|
function buildContact(media: GramJs.TypeMessageMedia): ApiContact | undefined {
|
|
if (!(media instanceof GramJs.MessageMediaContact)) {
|
|
return undefined;
|
|
}
|
|
|
|
const {
|
|
firstName, lastName, phoneNumber, userId,
|
|
} = media;
|
|
|
|
return {
|
|
firstName, lastName, phoneNumber, userId: buildApiPeerId(userId, 'user'),
|
|
};
|
|
}
|
|
|
|
function buildPollFromMedia(media: GramJs.TypeMessageMedia): ApiPoll | undefined {
|
|
if (!(media instanceof GramJs.MessageMediaPoll)) {
|
|
return undefined;
|
|
}
|
|
|
|
return buildPoll(media.poll, media.results);
|
|
}
|
|
|
|
function buildInvoiceFromMedia(media: GramJs.TypeMessageMedia): ApiInvoice | undefined {
|
|
if (!(media instanceof GramJs.MessageMediaInvoice)) {
|
|
return undefined;
|
|
}
|
|
|
|
return buildInvoice(media);
|
|
}
|
|
|
|
export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll {
|
|
const { id, answers: rawAnswers } = poll;
|
|
const answers = rawAnswers.map((answer) => ({
|
|
text: answer.text,
|
|
option: serializeBytes(answer.option),
|
|
}));
|
|
|
|
return {
|
|
id: String(id),
|
|
summary: {
|
|
isPublic: poll.publicVoters,
|
|
...pick(poll, [
|
|
'closed',
|
|
'multipleChoice',
|
|
'quiz',
|
|
'question',
|
|
'closePeriod',
|
|
'closeDate',
|
|
]),
|
|
answers,
|
|
},
|
|
results: buildPollResults(pollResults),
|
|
};
|
|
}
|
|
|
|
export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
|
|
const {
|
|
description: text, title, photo, test, totalAmount, currency, receiptMsgId,
|
|
} = media;
|
|
|
|
return {
|
|
text,
|
|
title,
|
|
photoUrl: photo?.url,
|
|
receiptMsgId,
|
|
amount: Number(totalAmount),
|
|
currency,
|
|
isTest: test,
|
|
};
|
|
}
|
|
|
|
export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['results'] {
|
|
const {
|
|
results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities,
|
|
} = pollResults;
|
|
const results = rawResults && rawResults.map(({
|
|
option, chosen, correct, voters,
|
|
}) => ({
|
|
isChosen: chosen,
|
|
isCorrect: correct,
|
|
option: serializeBytes(option),
|
|
votersCount: voters,
|
|
}));
|
|
|
|
return {
|
|
totalVoters,
|
|
recentVoterIds: recentVoters?.map((id) => buildApiPeerId(id, 'user')),
|
|
results,
|
|
solution,
|
|
...(entities && { solutionEntities: entities.map(buildApiMessageEntity) }),
|
|
};
|
|
}
|
|
|
|
export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undefined {
|
|
if (
|
|
!(media instanceof GramJs.MessageMediaWebPage)
|
|
|| !(media.webpage instanceof GramJs.WebPage)
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
const { id, photo, document } = media.webpage;
|
|
|
|
let video;
|
|
if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) {
|
|
video = buildVideoFromDocument(document);
|
|
}
|
|
|
|
return {
|
|
id: Number(id),
|
|
...pick(media.webpage, [
|
|
'url',
|
|
'displayUrl',
|
|
'type',
|
|
'siteName',
|
|
'title',
|
|
'description',
|
|
'duration',
|
|
]),
|
|
photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined,
|
|
document: !video && document ? buildApiDocument(document) : undefined,
|
|
video,
|
|
};
|
|
}
|
|
|
|
function buildAction(
|
|
action: GramJs.TypeMessageAction,
|
|
senderId: string | undefined,
|
|
targetPeerId: string | undefined,
|
|
isChannelPost: boolean,
|
|
isOutgoing: boolean,
|
|
): ApiAction | undefined {
|
|
if (action instanceof GramJs.MessageActionEmpty) {
|
|
return undefined;
|
|
}
|
|
|
|
let call: Partial<ApiGroupCall> | undefined;
|
|
let amount: number | undefined;
|
|
let currency: string | undefined;
|
|
let text: string;
|
|
const translationValues = [];
|
|
let type: ApiAction['type'] = 'other';
|
|
let photo: ApiPhoto | undefined;
|
|
|
|
const targetUserIds = 'users' in action
|
|
? action.users && action.users.map((id) => buildApiPeerId(id, 'user'))
|
|
: ('userId' in action && [buildApiPeerId(action.userId, 'user')]) || [];
|
|
let targetChatId: string | undefined;
|
|
|
|
if (action instanceof GramJs.MessageActionChatCreate) {
|
|
text = 'Notification.CreatedChatWithTitle';
|
|
translationValues.push('%action_origin%', action.title);
|
|
type = 'chatCreate';
|
|
} else if (action instanceof GramJs.MessageActionChatEditTitle) {
|
|
if (isChannelPost) {
|
|
text = 'Channel.MessageTitleUpdated';
|
|
translationValues.push(action.title);
|
|
} else {
|
|
text = 'Notification.ChangedGroupName';
|
|
translationValues.push('%action_origin%', action.title);
|
|
}
|
|
} else if (action instanceof GramJs.MessageActionChatEditPhoto) {
|
|
if (isChannelPost) {
|
|
text = 'Channel.MessagePhotoUpdated';
|
|
} else {
|
|
text = 'Notification.ChangedGroupPhoto';
|
|
translationValues.push('%action_origin%');
|
|
}
|
|
} else if (action instanceof GramJs.MessageActionChatDeletePhoto) {
|
|
if (isChannelPost) {
|
|
text = 'Channel.MessagePhotoRemoved';
|
|
} else {
|
|
text = 'Group.MessagePhotoRemoved';
|
|
}
|
|
} else if (action instanceof GramJs.MessageActionChatAddUser) {
|
|
if (!senderId || targetUserIds.includes(senderId)) {
|
|
text = 'Notification.JoinedChat';
|
|
translationValues.push('%target_user%');
|
|
} else {
|
|
text = 'Notification.Invited';
|
|
translationValues.push('%action_origin%', '%target_user%');
|
|
}
|
|
} else if (action instanceof GramJs.MessageActionChatDeleteUser) {
|
|
if (!senderId || targetUserIds.includes(senderId)) {
|
|
text = 'Notification.LeftChat';
|
|
translationValues.push('%target_user%');
|
|
} else {
|
|
text = 'Notification.Kicked';
|
|
translationValues.push('%action_origin%', '%target_user%');
|
|
}
|
|
} else if (action instanceof GramJs.MessageActionChatJoinedByLink) {
|
|
text = 'Notification.JoinedGroupByLink';
|
|
translationValues.push('%action_origin%');
|
|
} else if (action instanceof GramJs.MessageActionChannelCreate) {
|
|
text = 'Notification.CreatedChannel';
|
|
} else if (action instanceof GramJs.MessageActionChatMigrateTo) {
|
|
targetChatId = getApiChatIdFromMtpPeer(action);
|
|
text = 'Migrated to %target_chat%';
|
|
translationValues.push('%target_chat%');
|
|
} else if (action instanceof GramJs.MessageActionChannelMigrateFrom) {
|
|
targetChatId = getApiChatIdFromMtpPeer(action);
|
|
text = 'Migrated from %target_chat%';
|
|
translationValues.push('%target_chat%');
|
|
} else if (action instanceof GramJs.MessageActionPinMessage) {
|
|
text = 'Chat.Service.Group.UpdatedPinnedMessage1';
|
|
translationValues.push('%action_origin%', '%message%');
|
|
} else if (action instanceof GramJs.MessageActionHistoryClear) {
|
|
text = 'HistoryCleared';
|
|
type = 'historyClear';
|
|
} else if (action instanceof GramJs.MessageActionPhoneCall) {
|
|
const withDuration = Boolean(action.duration);
|
|
text = [
|
|
withDuration ? 'ChatList.Service' : 'Chat',
|
|
action.video ? 'VideoCall' : 'Call',
|
|
isOutgoing ? (withDuration ? 'outgoing' : 'Outgoing') : (withDuration ? 'incoming' : 'Incoming'),
|
|
].join('.');
|
|
|
|
if (withDuration) {
|
|
const mins = Math.max(Math.round(action.duration! / 60), 1);
|
|
translationValues.push(`${mins} min${mins > 1 ? 's' : ''}`);
|
|
}
|
|
} else if (action instanceof GramJs.MessageActionInviteToGroupCall) {
|
|
text = 'Notification.VoiceChatInvitation';
|
|
call = {
|
|
id: action.call.id.toString(),
|
|
accessHash: action.call.accessHash.toString(),
|
|
};
|
|
translationValues.push('%action_origin%', '%target_user%');
|
|
} else if (action instanceof GramJs.MessageActionContactSignUp) {
|
|
text = 'Notification.Joined';
|
|
translationValues.push('%action_origin%');
|
|
type = 'contactSignUp';
|
|
} else if (action instanceof GramJs.MessageActionPaymentSent) {
|
|
amount = Number(action.totalAmount);
|
|
currency = action.currency;
|
|
text = 'PaymentSuccessfullyPaid';
|
|
if (targetPeerId) {
|
|
targetUserIds.push(targetPeerId);
|
|
}
|
|
translationValues.push('%payment_amount%', '%target_user%', '%product%');
|
|
} else if (action instanceof GramJs.MessageActionGroupCall) {
|
|
if (action.duration) {
|
|
const mins = Math.max(Math.round(action.duration / 60), 1);
|
|
text = 'Notification.VoiceChatEnded';
|
|
translationValues.push(`${mins} min${mins > 1 ? 's' : ''}`);
|
|
} else {
|
|
text = 'Notification.VoiceChatStartedChannel';
|
|
call = {
|
|
id: action.call.id.toString(),
|
|
accessHash: action.call.accessHash.toString(),
|
|
};
|
|
}
|
|
} else if (action instanceof GramJs.MessageActionBotAllowed) {
|
|
text = 'Chat.Service.BotPermissionAllowed';
|
|
translationValues.push(action.domain);
|
|
} else {
|
|
text = 'ChatList.UnsupportedMessage';
|
|
}
|
|
|
|
if ('photo' in action && action.photo instanceof GramJs.Photo) {
|
|
addPhotoToLocalDb(action.photo);
|
|
photo = buildApiPhoto(action.photo);
|
|
}
|
|
|
|
return {
|
|
text,
|
|
type,
|
|
targetUserIds,
|
|
targetChatId,
|
|
photo, // TODO Only used internally now, will be used for the UI in future
|
|
amount,
|
|
currency,
|
|
translationValues,
|
|
call,
|
|
};
|
|
}
|
|
|
|
function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefined {
|
|
const { id: messageId, replyMarkup, media } = message;
|
|
|
|
if (!replyMarkup) {
|
|
if (media instanceof GramJs.MessageMediaWebPage && media.webpage instanceof GramJs.WebPage) {
|
|
if (media.webpage.type === 'telegram_message') {
|
|
return {
|
|
inlineButtons: [[{
|
|
type: 'url' as const,
|
|
text: 'Show Message',
|
|
messageId,
|
|
value: media.webpage.url,
|
|
}]],
|
|
};
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// TODO
|
|
if (!(replyMarkup instanceof GramJs.ReplyKeyboardMarkup || replyMarkup instanceof GramJs.ReplyInlineMarkup)) {
|
|
return undefined;
|
|
}
|
|
|
|
const markup = replyMarkup.rows.map(({ buttons }) => {
|
|
return buttons.map((button) => {
|
|
let { text } = button;
|
|
|
|
let type;
|
|
let value;
|
|
if (button instanceof GramJs.KeyboardButton) {
|
|
type = 'command';
|
|
value = text;
|
|
} else if (button instanceof GramJs.KeyboardButtonUrl) {
|
|
type = 'url';
|
|
value = button.url;
|
|
} else if (button instanceof GramJs.KeyboardButtonCallback) {
|
|
type = 'callback';
|
|
value = serializeBytes(button.data);
|
|
} else if (button instanceof GramJs.KeyboardButtonRequestPoll) {
|
|
type = 'requestPoll';
|
|
} else if (button instanceof GramJs.KeyboardButtonBuy) {
|
|
if (media instanceof GramJs.MessageMediaInvoice && media.receiptMsgId) {
|
|
text = 'PaymentReceipt';
|
|
value = media.receiptMsgId;
|
|
}
|
|
type = 'buy';
|
|
} else {
|
|
type = 'NOT_SUPPORTED';
|
|
}
|
|
|
|
return {
|
|
type,
|
|
text,
|
|
messageId,
|
|
value,
|
|
} as ApiKeyboardButton;
|
|
});
|
|
});
|
|
|
|
return {
|
|
[replyMarkup instanceof GramJs.ReplyKeyboardMarkup ? 'keyboardButtons' : 'inlineButtons']: markup,
|
|
...(replyMarkup instanceof GramJs.ReplyKeyboardMarkup && {
|
|
keyboardPlaceholder: replyMarkup.placeholder,
|
|
isKeyboardSingleUse: replyMarkup.singleUse,
|
|
}),
|
|
};
|
|
}
|
|
|
|
function getFilenameFromDocument(document: GramJs.Document, defaultBase = 'file') {
|
|
const { mimeType, attributes } = document;
|
|
const filenameAttribute = attributes
|
|
.find((a: any): a is GramJs.DocumentAttributeFilename => a instanceof GramJs.DocumentAttributeFilename);
|
|
|
|
if (filenameAttribute) {
|
|
return filenameAttribute.fileName;
|
|
}
|
|
|
|
const extension = mimeType.split('/')[1];
|
|
|
|
return `${defaultBase}${String(document.id)}.${extension}`;
|
|
}
|
|
|
|
export function buildLocalMessage(
|
|
chat: ApiChat,
|
|
text?: string,
|
|
entities?: ApiMessageEntity[],
|
|
replyingTo?: number,
|
|
attachment?: ApiAttachment,
|
|
sticker?: ApiSticker,
|
|
gif?: ApiVideo,
|
|
poll?: ApiNewPoll,
|
|
groupedId?: string,
|
|
scheduledAt?: number,
|
|
sendAs?: ApiChat | ApiUser,
|
|
serverTimeOffset = 0,
|
|
): ApiMessage {
|
|
const localId = localMessageCounter++;
|
|
const media = attachment && buildUploadingMedia(attachment);
|
|
const isChannel = chat.type === 'chatTypeChannel';
|
|
|
|
return {
|
|
id: localId,
|
|
chatId: chat.id,
|
|
content: {
|
|
...(text && {
|
|
text: {
|
|
text,
|
|
entities,
|
|
},
|
|
}),
|
|
...media,
|
|
...(sticker && { sticker }),
|
|
...(gif && { video: gif }),
|
|
...(poll && buildNewPoll(poll, localId)),
|
|
},
|
|
date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset,
|
|
isOutgoing: !isChannel,
|
|
senderId: sendAs?.id || currentUserId,
|
|
...(replyingTo && { replyToMessageId: replyingTo }),
|
|
...(groupedId && {
|
|
groupedId,
|
|
...(media && (media.photo || media.video) && { isInAlbum: true }),
|
|
}),
|
|
...(scheduledAt && { isScheduled: true }),
|
|
};
|
|
}
|
|
|
|
export function buildLocalForwardedMessage(
|
|
toChat: ApiChat,
|
|
message: ApiMessage,
|
|
serverTimeOffset: number,
|
|
scheduledAt?: number,
|
|
): ApiMessage {
|
|
const localId = localMessageCounter++;
|
|
const {
|
|
content,
|
|
chatId: fromChatId,
|
|
id: fromMessageId,
|
|
senderId,
|
|
groupedId,
|
|
isInAlbum,
|
|
} = message;
|
|
|
|
const isAudio = content.audio;
|
|
const asIncomingInChatWithSelf = (
|
|
toChat.id === currentUserId && (fromChatId !== toChat.id || message.forwardInfo) && !isAudio
|
|
);
|
|
|
|
return {
|
|
id: localId,
|
|
chatId: toChat.id,
|
|
content,
|
|
date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset,
|
|
isOutgoing: !asIncomingInChatWithSelf && toChat.type !== 'chatTypeChannel',
|
|
senderId: currentUserId,
|
|
sendingState: 'messageSendingStatePending',
|
|
groupedId,
|
|
isInAlbum,
|
|
// Forward info doesn't get added when users forwards his own messages, also when forwarding audio
|
|
...(senderId !== currentUserId && !isAudio && {
|
|
forwardInfo: {
|
|
date: message.date,
|
|
isChannelPost: false,
|
|
fromChatId,
|
|
fromMessageId,
|
|
senderUserId: senderId,
|
|
},
|
|
}),
|
|
...(scheduledAt && { isScheduled: true }),
|
|
};
|
|
}
|
|
|
|
function buildUploadingMedia(
|
|
attachment: ApiAttachment,
|
|
): ApiMessage['content'] {
|
|
const {
|
|
filename: fileName,
|
|
blobUrl,
|
|
previewBlobUrl,
|
|
mimeType,
|
|
size,
|
|
} = attachment;
|
|
|
|
if (attachment.quick) {
|
|
const { width, height, duration } = attachment.quick;
|
|
|
|
if (mimeType.startsWith('image/')) {
|
|
return {
|
|
photo: {
|
|
id: LOCAL_MEDIA_UPLOADING_TEMP_ID,
|
|
sizes: [],
|
|
thumbnail: { width, height, dataUri: '' }, // Used only for dimensions
|
|
blobUrl,
|
|
},
|
|
};
|
|
} else {
|
|
return {
|
|
video: {
|
|
id: LOCAL_MEDIA_UPLOADING_TEMP_ID,
|
|
mimeType,
|
|
duration: duration || 0,
|
|
fileName,
|
|
width,
|
|
height,
|
|
blobUrl,
|
|
...(previewBlobUrl && { thumbnail: { width, height, dataUri: previewBlobUrl } }),
|
|
size,
|
|
},
|
|
};
|
|
}
|
|
} else if (attachment.voice) {
|
|
const { duration, waveform } = attachment.voice;
|
|
const { data: inputWaveform } = interpolateArray(waveform, INPUT_WAVEFORM_LENGTH);
|
|
return {
|
|
voice: {
|
|
duration,
|
|
waveform: inputWaveform,
|
|
},
|
|
};
|
|
} else if (mimeType.startsWith('audio/')) {
|
|
return {
|
|
audio: {
|
|
mimeType,
|
|
fileName,
|
|
size,
|
|
duration: 200, // Arbitrary
|
|
},
|
|
};
|
|
} else {
|
|
return {
|
|
document: {
|
|
mimeType,
|
|
fileName,
|
|
size,
|
|
...(previewBlobUrl && { previewBlobUrl }),
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
function buildNewPoll(poll: ApiNewPoll, localId: number) {
|
|
return {
|
|
poll: {
|
|
id: String(localId),
|
|
summary: pick(poll.summary, ['question', 'answers']),
|
|
results: {},
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMessageEntity {
|
|
const { className: type, offset, length } = entity;
|
|
return {
|
|
type,
|
|
offset,
|
|
length,
|
|
...('userId' in entity && typeof entity.userId === 'number' && { userId: entity.userId }),
|
|
...('url' in entity && { url: entity.url }),
|
|
};
|
|
}
|
|
|
|
function buildThreadInfo(
|
|
messageReplies: GramJs.TypeMessageReplies, messageId: number, chatId: string,
|
|
): ApiThreadInfo | undefined {
|
|
const {
|
|
channelId, replies, maxId, readMaxId, recentRepliers,
|
|
} = messageReplies;
|
|
if (!channelId) {
|
|
return undefined;
|
|
}
|
|
|
|
const apiChannelId = buildApiPeerId(channelId, 'channel');
|
|
if (apiChannelId === DELETED_COMMENTS_CHANNEL_ID) {
|
|
return undefined;
|
|
}
|
|
|
|
const isPostThread = chatId !== apiChannelId;
|
|
|
|
return {
|
|
threadId: messageId,
|
|
...(isPostThread ? {
|
|
chatId: apiChannelId,
|
|
originChannelId: chatId,
|
|
} : {
|
|
chatId,
|
|
}),
|
|
messagesCount: replies,
|
|
lastMessageId: maxId,
|
|
lastReadInboxMessageId: readMaxId,
|
|
...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }),
|
|
};
|
|
}
|