Update telegram.provider.ts

- Updated the logic in 'authenticate' to handle both public and private Telegram groups/channels.
- Updated the logic in 'getBotId' to handle both public and private Telegram groups/channels.
- Updated the logic in 'post' to apply sending local files, include contentType, media handling (captions/groups), link generation (to fix undefined links)
This commit is contained in:
Zerocool 2025-02-05 03:06:25 +02:00 committed by GitHub
parent 13585a1472
commit 57e3d4ddb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 126 additions and 50 deletions

View File

@ -7,10 +7,15 @@ import {
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import dayjs from 'dayjs';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
//@ts-ignore
import mime from 'mime';
import TelegramBot from 'node-telegram-bot-api';
import { Integration } from '@prisma/client';
const telegramBot = new TelegramBot(process.env.TELEGRAM_TOKEN!);
// Added to support local storage posting
const frontendURL = process.env.FRONTEND_URL || 'http://localhost:5000';
const mediaStorage = process.env.STORAGE_PROVIDER || 'local';
export class TelegramProvider extends SocialAbstract implements SocialProvider {
identifier = 'telegram';
@ -47,7 +52,7 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
}) {
const chat = await telegramBot.getChat(params.code);
console.log(JSON.stringify(chat))
console.log(JSON.stringify(chat));
if (!chat?.id) {
return 'No chat found';
}
@ -55,8 +60,10 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
const photo = !chat?.photo?.big_file_id
? ''
: await telegramBot.getFileLink(chat.photo.big_file_id);
// Modified id to work with chat.username (public groups/channels) or chat.id (private groups/channels) when chat.username is not available
return {
id: String(chat.username),
id: String(chat.username ? chat.username : chat.id),
name: chat.title!,
accessToken: String(chat.id),
refreshToken: '',
@ -67,21 +74,29 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
}
async getBotId(query: { id?: number; word: string }) {
// Added allowed_updates Ensure only necessary updates are fetched
const res = await telegramBot.getUpdates({
...(query.id ? { offset: query.id } : {}),
allowed_updates: ['message', 'channel_post'],
});
const chatId = res?.find(
(p) => p?.message?.text === `/connect ${query.word}`
)?.message?.chat?.id;
//message.text is for groups, channel_post.text is for channels
const match = res.find(
(p) =>
(p?.message?.text === `/connect ${query.word}` &&
p?.message?.chat?.id) ||
(p?.channel_post?.text === `/connect ${query.word}` &&
p?.channel_post?.chat?.id)
);
// get correct chatId based on the channel type
const chatId = match?.message?.chat?.id || match?.channel_post?.chat?.id;
// modified lastChatId to work with any type of channel (private/public groups/channels)
return chatId
? {
chatId,
}
? { chatId }
: res.length > 0
? {
lastChatId: res?.[res.length - 1]?.message?.chat?.id,
lastChatId:
res?.[res.length - 1]?.message?.chat?.id ||
res?.[res.length - 1]?.channel_post?.chat?.id,
}
: {};
}
@ -92,50 +107,111 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
postDetails: PostDetails[]
): Promise<PostResponse[]> {
const ids: PostResponse[] = [];
for (const message of postDetails) {
if (
(message?.media?.length || 0) === 1
) {
const [{ message_id }] = await telegramBot.sendMediaGroup(
accessToken,
message?.media?.map((m) => ({
type: m.url.indexOf('mp4') > -1 ? 'video' : 'photo',
caption: message.message,
media: m.url,
})) || []
);
ids.push({
id: message.id,
postId: String(message_id),
releaseURL: `https://t.me/${id}/${message_id}`,
status: 'completed',
});
} else {
const { message_id } = await telegramBot.sendMessage(
accessToken,
message.message
);
ids.push({
id: message.id,
postId: String(message_id),
releaseURL: `https://t.me/${id}/${message_id}`,
status: 'completed',
});
if ((message?.media?.length || 0) > 0) {
await telegramBot.sendMediaGroup(
accessToken,
message?.media?.map((m) => ({
type: m.url.indexOf('mp4') > -1 ? 'video' : 'photo',
media: m.url,
})) || []
);
let messageId: number | null = null;
const mediaFiles = message.media || [];
const text = message.message || '';
// check if media is local to modify url
const processedMedia = mediaFiles.map((media) => {
let mediaUrl = media.url;
if (mediaStorage === 'local' && mediaUrl.startsWith(frontendURL)) {
mediaUrl = mediaUrl.replace(frontendURL, '');
}
//get mime type to pass contentType to telegram api.
//some photos and videos might not pass telegram api restrictions, so they are sent as documents instead of returning errors
const mimeType = mime.getType(mediaUrl); // Detect MIME type
let mediaType: 'photo' | 'video' | 'document';
if (mimeType?.startsWith('image/')) {
mediaType = 'photo';
} else if (mimeType?.startsWith('video/')) {
mediaType = 'video';
} else {
mediaType = 'document';
}
return {
type: mediaType,
media: mediaUrl,
fileOptions: {
filename: media.url.split('/').pop(),
contentType: mimeType || 'application/octet-stream',
},
};
});
// if there's no media, bot sends a text message only
if (processedMedia.length === 0) {
const response = await telegramBot.sendMessage(accessToken, text);
messageId = response.message_id;
}
// if there's only one media, bot sends the media with the text message as caption
else if (processedMedia.length === 1) {
const media = processedMedia[0];
const response =
media.type === 'video'
? await telegramBot.sendVideo(
accessToken,
media.media,
{ caption: text },
media.fileOptions
)
: media.type === 'photo'
? await telegramBot.sendPhoto(
accessToken,
media.media,
{ caption: text },
media.fileOptions
)
: await telegramBot.sendDocument(
accessToken,
media.media,
{ caption: text },
media.fileOptions
);
messageId = response.message_id;
}
// if there are multiple media, bot sends them as a media group - max 10 media per group - with the text as a caption (if there are more than 1 group, the caption will only be sent with the first group)
else {
const mediaGroups = this.chunkMedia(processedMedia, 10);
for (let i = 0; i < mediaGroups.length; i++) {
const mediaGroup = mediaGroups[i].map((m, index) => ({
type: m.type === 'document' ? 'document' : m.type, // Documents are not allowed in media groups
media: m.media,
caption: i === 0 && index === 0 ? text : undefined,
}));
const response = await telegramBot.sendMediaGroup(
accessToken,
mediaGroup
);
if (i === 0) {
messageId = response[0].message_id;
}
}
}
// for private groups/channels message.id is undefined so the link generated by Postiz will be unusable "https://t.me/c/undefined/16"
// to avoid that, we use accessToken instead of message.id and we generate the link manually removing the -100 from the start.
if (messageId) {
ids.push({
id: message.id,
postId: String(messageId),
releaseURL: `https://t.me/${
id !== 'undefined' ? id : `c/${accessToken.replace('-100', '')}`
}/${messageId}`,
status: 'completed',
});
}
}
return ids;
}
// chunkMedia is used to split media into groups of "size". 10 is used here because telegram api allows a maximum of 10 media per group
private chunkMedia(media: { type: string; media: string }[], size: number) {
const result = [];
for (let i = 0; i < media.length; i += size) {
result.push(media.slice(i, i + size));
}
return result;
}
}