diff --git a/locales/en.ftl b/locales/en.ftl index da52fbe..4895e26 100644 --- a/locales/en.ftl +++ b/locales/en.ftl @@ -1,10 +1,15 @@ start = hii! just send me a link and i'll download it. (ᵔᵕᵔ)◜ +join = + hii! (ᵔᵕᵔ)◜ + i will download all links i'll find in this chat. + all error messages will be deleted within 30 seconds to not annoy you. error-title = error error = { error-title }: { $message } { $error-emoticon } error-not-url = i couldn't find url in your message error-request-not-found = looks like i forgot your link, try sending it again error-not-button-owner = looks like this button is not yours (¬_¬") +error-admin-button = only admins can touch this button!! error-too-large = sorry, but this file is too big - telegram doesn't allow me to upload it error-invalid-response = server response is invalid, maybe it's down or encountered an internal error error-unresponsive = couldn't connect to this server, maybe it's down... @@ -12,7 +17,7 @@ error-invalid-url = looks like i dont recognise the link you sent... maybe the s error-media-unavailable = i found your media, but couldn't download it. maybe its private, age restricted or region locked. error-unknown = oops, an internal error happened. i reported it to my developer, so they'll fix it! -note-picker = your link contained multiple media files, so i sent them to you via pms +note-picker = your link contained multiple media files, so i sent them to you seperately or via pms download-title = download from provided url type-select-title = select download type (。 · ᎑ ·。) @@ -71,4 +76,4 @@ stats-global = i helped with downloading { $count } times! (˶ᵔ ᵕ ᵔ˶) info = running { $name }@{ $version } sources: { $repository } - report bugs: { $bugs } \ No newline at end of file + report bugs: { $bugs } diff --git a/locales/ru.ftl b/locales/ru.ftl index 04d865b..91f0f98 100644 --- a/locales/ru.ftl +++ b/locales/ru.ftl @@ -1,10 +1,15 @@ start = привет! просто отправь мне ссылку и я её скачаю. (ᵔᵕᵔ)◜ +join = + привет! (ᵔᵕᵔ)◜ + я буду скачивать все ссылки, которые я найду в этом чате. + все сообщения об ошибках будут удаляться в течении 30 секунд, чтобы не мешать вам. error-title = ошибка error = { error-title }: { $message } { $error-emoticon } error-not-url = я не нашёл ссылки в твоём сообщении error-request-not-found = похоже я потерял твою ссылку, можешь отправить её снова? error-not-button-owner = похоже что эта кнопка не твоя (¬_¬") +error-admin-button = только админам можно тыкать эту кнопку!! error-too-large = этот файл слишком большой, к сожалению тг не даёт его загрузить error-invalid-response = сервер некорректно ответил, возможно он столкнулся с внутренней ошибкой или лежит error-unresponsive = не удалось подключиться к серверу, наверное он лежит... @@ -12,7 +17,7 @@ error-invalid-url = кажется у меня не получается рас error-media-unavailable = я нашёл нужный файл, но не смог его скачать. возможно на нём ограничения по региону, возрасту или приватности. error-unknown = ой, произошла внутреняя ошибка. я сообщил об этом моим разработчикам, чтобы они исправили! -note-picker = по твоей ссылке я нашёл несколько медиа файлов, поэтому я отправил тебе их в лс +note-picker = по твоей ссылке я нашёл несколько медиа файлов, поэтому я отправил тебе отдельно или их в лс download-title = скачать по ссылке type-select-title = выбери тип загрузки (。 · ᎑ ·。) @@ -57,4 +62,4 @@ stats-global = я помог с загрузкой { $count } раз! (˶ᵔ ᵕ info = выполняется { $name }@{ $version } сурсы: { $repository } - репорт багов: { $bugs } \ No newline at end of file + репорт багов: { $bugs } diff --git a/src/telegram/bot/download.ts b/src/telegram/bot/download.ts index 25f5a78..4501867 100644 --- a/src/telegram/bot/download.ts +++ b/src/telegram/bot/download.ts @@ -1,11 +1,13 @@ +import type { BusinessCallbackQueryContext, CallbackQueryContext, InlineCallbackQueryContext } from "@mtcute/dispatcher" import type { InputMediaLike, Peer } from "@mtcute/node" import { randomUUID } from "node:crypto" -import { Dispatcher, filters } from "@mtcute/dispatcher" +import { Dispatcher } from "@mtcute/dispatcher" import { BotInline, BotKeyboard } from "@mtcute/node" import type { MediaRequest } from "@/core/data/request" import { createRequest, getRequest } from "@/core/data/request" +import type { Settings } from "@/core/data/settings" import { incrementDownloadCount } from "@/core/data/stats" import { getOutputSelectionMessage, @@ -18,44 +20,52 @@ import { evaluatorsFor } from "@/telegram/helpers/text" export const downloadDp = Dispatcher.child() -downloadDp.onNewMessage(filters.chat("user"), async (msg) => { - const { e, t } = await evaluatorsFor(msg.sender) +const errorDeleteDelay = 30 * 1000 + +downloadDp.onNewMessage(async (msg) => { + const { e, t } = await evaluatorsFor(msg.chat) if (msg.text === "meow") { await msg.replyText("meow :з") return } - const urlEntity = msg.entities.find(e => e.is("text_link") || e.is("url")) - const extractedUrl = urlEntity && (urlEntity.is("text_link") ? urlEntity.params.url : urlEntity.text) - const req = await createRequest(extractedUrl || msg.text, msg.sender.id) + const urlEntities = msg.entities.filter(e => e.is("text_link") || e.is("url")) + const extractedUrls = urlEntities.map(e => (e.is("text_link") ? e.params.url : e.text)) + const urls = extractedUrls.length ? extractedUrls : [msg.text] + for (const url of urls) { + const req = await createRequest(url, msg.sender.id) - if (!req.success) { - await msg.replyText(t("error", { message: e(req.error) })) - return - } + if (!req.success) { + if (msg.chat.type === "user") + await msg.replyText(t("error", { message: e(req.error) })) + return + } - const selectMsg = getOutputSelectionMessage(req.result.id) - const reply = await msg.replyText(e(selectMsg.caption), { - replyMarkup: BotKeyboard.inline([ - selectMsg.options.map(o => BotKeyboard.callback( - e(o.name), - o.key, - )), - ]), - }) - - const settings = await getPeerSettings(msg.sender) - if (settings.preferredOutput) { - await onOutputSelected( - settings.preferredOutput, - req.result, - args => msg.client.editMessage({ ...args, message: reply }), - { e, t }, - msg.sender, - !!settings.preferredAttribution, - ({ medias }) => msg.replyMediaGroup(medias), - ) + const selectMsg = getOutputSelectionMessage(req.result.id) + const reply = await msg.replyText(e(selectMsg.caption), { + replyMarkup: BotKeyboard.inline([ + selectMsg.options.map(o => BotKeyboard.callback( + e(o.name), + o.key, + )), + ]), + }) + + const settings = await getPeerSettings(msg.chat) + if (settings.preferredOutput || msg.chat.type !== "user") { + const res = await onOutputSelected( + settings.preferredOutput || "auto", + req.result, + args => msg.client.editMessage({ ...args, message: reply }), + { e, t }, + settings, + ({ medias }) => msg.replyMediaGroup(medias), + msg.sender, + ) + if (!res && msg.chat.type !== "user") + setTimeout(() => msg.client.deleteMessages([reply]), errorDeleteDelay) + } } }) @@ -102,8 +112,13 @@ downloadDp.onInlineQuery(async (ctx) => { }) downloadDp.onAnyCallbackQuery(OutputButton.filter(), async (upd) => { - const settings = await getPeerSettings(upd.user) - const { t, e } = await evaluatorsFor(upd.user) + // When passing a filter to onAnyCallbackQuery it applies a modification to the update object, which makes it lose its enum-like properties. + // To access the original update object, we need to cast it to the original type. + const rawUpd = upd as unknown as (CallbackQueryContext | InlineCallbackQueryContext | BusinessCallbackQueryContext) + + const peer = rawUpd._name === "callback_query" ? rawUpd.chat : upd.user + const settings = await getPeerSettings(peer) + const { t, e } = await evaluatorsFor(peer) const { output: outputType, request: requestId } = upd.match const request = await getRequest(requestId) @@ -113,15 +128,17 @@ downloadDp.onAnyCallbackQuery(OutputButton.filter(), async (upd) => { }) } - await onOutputSelected( + const res = await onOutputSelected( outputType, request, args => upd.editMessage(args), { t, e }, + settings, + ({ medias }) => upd.client.sendMediaGroup(peer.id, medias), upd.user, - !!settings.preferredAttribution, - ({ medias }) => upd.client.sendMediaGroup(upd.user.id, medias), ) + if (!res && rawUpd._name === "callback_query" && rawUpd.chat.type !== "user") + setTimeout(() => upd.client.deleteMessagesById(rawUpd.chat.id, [rawUpd.messageId]), errorDeleteDelay) }) downloadDp.onChosenInlineResult(async (upd) => { @@ -136,9 +153,9 @@ downloadDp.onChosenInlineResult(async (upd) => { request, args => upd.editMessage({ ...args, messageId }), await evaluatorsFor(upd.user), - upd.user, - !!settings.preferredAttribution, + settings, ({ medias }) => upd.client.sendMediaGroup(upd.user.id, medias), + upd.user, ) } }) @@ -148,15 +165,15 @@ async function onOutputSelected( request: MediaRequest | undefined, editMessage: (edit: { text?: string, media?: InputMediaLike }) => Promise, { t, e }: Evaluators, - peer: Peer, - leaveSourceLink: boolean, + settings: Settings, sendGroup: (send: { medias: InputMediaLike[] }) => Promise, + sender: Peer, ) { await editMessage({ text: t("downloading-title") }) - const res = await handleMediaDownload(outputType, request, peer) + const res = await handleMediaDownload(outputType, request, settings) if (!res.success) { await editMessage({ text: t("error", { message: e(res.error) }) }) - return + return false } await editMessage({ text: t("uploading-title") }) @@ -168,10 +185,11 @@ async function onOutputSelected( await sendGroup({ medias: chunk }) } } else { - await editMessage({ media: res.result[0] }) - await editMessage({ text: (leaveSourceLink && request?.url) || "" }) + await editMessage({ media: res.result[0], text: (!!settings.preferredAttribution && request?.url) || "" }) } - incrementDownloadCount(peer.id) + incrementDownloadCount(sender.id) .catch(() => { /* noop */ }) + + return true } diff --git a/src/telegram/bot/info.ts b/src/telegram/bot/info.ts index df5930c..8302083 100644 --- a/src/telegram/bot/info.ts +++ b/src/telegram/bot/info.ts @@ -6,6 +6,6 @@ import { evaluatorsFor } from "@/telegram/helpers/text" export const infoDp = Dispatcher.child() settingsDp.onNewMessage(filters.command("info"), async (msg) => { - const { t } = await evaluatorsFor(msg.sender) + const { t } = await evaluatorsFor(msg.chat) await msg.replyText(t("info", { bugs, name, repository, version })) }) diff --git a/src/telegram/bot/settings.ts b/src/telegram/bot/settings.ts index 6f436d2..602158f 100644 --- a/src/telegram/bot/settings.ts +++ b/src/telegram/bot/settings.ts @@ -1,3 +1,4 @@ +import type { Peer, TelegramClient, User } from "@mtcute/node" import { Dispatcher, filters, PropagationAction } from "@mtcute/dispatcher" import { BotKeyboard } from "@mtcute/node" @@ -34,6 +35,13 @@ function settingsMessage(e: TextEvaluator, settings: Settings) { } } +async function isAdmin(client: Pick, chat: Peer, user: User) { + if (chat.type !== "chat") + return true + const member = await client.getChatMember({ chatId: chat, userId: user }) + return member?.status === "admin" || member?.status === "creator" +} + function settingEditMessage(e: TextEvaluator, settings: Settings, setting: keyof Settings) { const menu = getSettingMenu(settings, setting) return { @@ -49,16 +57,21 @@ function settingEditMessage(e: TextEvaluator, settings: Settings, setting: keyof settingsDp.onNewMessage( filters.or(filters.command("settings"), filters.deeplink(["settings"])), async (msg) => { - const { e } = await evaluatorsFor(msg.sender) - const settings = await getPeerSettings(msg.sender) + const { e } = await evaluatorsFor(msg.chat) + const settings = await getPeerSettings(msg.chat) const { text, ...props } = settingsMessage(e, settings) await msg.replyText(text, props) }, ) -settingsDp.onAnyCallbackQuery(SettingButton.filter(), async (upd) => { - const { e } = await evaluatorsFor(upd.user) - const settings = await getPeerSettings(upd.user) +settingsDp.onCallbackQuery(SettingButton.filter(), async (upd) => { + const { e, t } = await evaluatorsFor(upd.chat) + if (!await isAdmin(upd.client, upd.chat, upd.user)) { + return await upd.answer({ + text: t("error-admin-button"), + }) + } + const settings = await getPeerSettings(upd.chat) if (upd.match.setting === "back") { await upd.editMessage(settingsMessage(e, settings)) return @@ -69,36 +82,46 @@ settingsDp.onAnyCallbackQuery(SettingButton.filter(), async (upd) => { await upd.editMessage(settingEditMessage(e, settings, upd.match.setting)) }) -settingsDp.onAnyCallbackQuery(SettingUpdateButton.filter(), async (upd, state) => { +settingsDp.onCallbackQuery(SettingUpdateButton.filter(), async (upd, state) => { if (!isValidSettingKey(upd.match.setting)) return // Invalid key + if (!await isAdmin(upd.client, upd.chat, upd.user)) { + const { t } = await evaluatorsFor(upd.chat) + return await upd.answer({ + text: t("error-admin-button"), + }) + } - const settings = await getPeerSettings(upd.user) + const settings = await getPeerSettings(upd.chat) const valueIndex = +upd.match.value const value = getSettingValues(upd.match.setting)[valueIndex] if (value === customValue) { - const { e, t } = await evaluatorsFor(upd.user) + const { e, t } = await evaluatorsFor(upd.chat) const { text: _, ...props } = settingEditMessage(e, settings, upd.match.setting) await upd.editMessage({ text: t("setting-custom"), ...props }) await state.enter(settingInputScene, { with: { setting: upd.match.setting } }) return } - const newSettings = await updateSetting(upd.match.setting, value, upd.user.id) + const newSettings = await updateSetting(upd.match.setting, value, upd.chat.id) // We're getting evaluator AFTER the possible locale update - const { e } = await evaluatorsFor(upd.user) + const { e } = await evaluatorsFor(upd.chat) await upd.editMessage(settingEditMessage(e, newSettings ?? settings, upd.match.setting)) }) settingInputScene.onNewMessage(async (upd, state) => { + if (upd.sender.type !== "user" || !await isAdmin(upd.client, upd.chat, upd.sender)) { + return + } + const stateData = await state.get() if (!stateData) { await state.exit() return } - const { t } = await evaluatorsFor(upd.sender) - await updateSetting(stateData.setting, upd.text, upd.sender.id) + const { t } = await evaluatorsFor(upd.chat) + await updateSetting(stateData.setting, upd.text, upd.chat.id) await upd.replyText(t("setting-saved")) await state.exit() diff --git a/src/telegram/bot/start.ts b/src/telegram/bot/start.ts index 7f5e387..9013685 100644 --- a/src/telegram/bot/start.ts +++ b/src/telegram/bot/start.ts @@ -3,6 +3,11 @@ import { translatorFor } from "@/telegram/helpers/i18n" export const startDp = Dispatcher.child() startDp.onNewMessage(filters.start, async (msg) => { - const t = await translatorFor(msg.sender) + const t = await translatorFor(msg.chat) await msg.replyText(t("start")) }) + +startDp.onChatMemberUpdate(filters.and(filters.chatMemberSelf, filters.chatMember("added")), async (upd) => { + const t = await translatorFor(upd.chat) + await upd.client.sendText(upd.chat, t("join")) +}) diff --git a/src/telegram/bot/stats.ts b/src/telegram/bot/stats.ts index 211b5dc..015fe59 100644 --- a/src/telegram/bot/stats.ts +++ b/src/telegram/bot/stats.ts @@ -6,13 +6,13 @@ import { evaluatorsFor } from "@/telegram/helpers/text" export const statsDp = Dispatcher.child() statsDp.onNewMessage(filters.command("stats"), async (msg) => { - const { t } = await evaluatorsFor(msg.sender) + const { t } = await evaluatorsFor(msg.chat) const count = await getDownloadStats() await msg.replyText(t("stats-global", { count })) }) statsDp.onNewMessage(filters.command("mystats"), async (msg) => { - const { t } = await evaluatorsFor(msg.sender) + const { t } = await evaluatorsFor(msg.chat) const id = msg.sender.id const count = await getDownloadStats(id) await msg.replyText(t("stats-personal", { count })) diff --git a/src/telegram/helpers/handler.ts b/src/telegram/helpers/handler.ts index eaf94b9..4d3cadf 100644 --- a/src/telegram/helpers/handler.ts +++ b/src/telegram/helpers/handler.ts @@ -1,4 +1,4 @@ -import type { InputMediaLike, Peer } from "@mtcute/node" +import type { InputMediaLike } from "@mtcute/node" import type { GeneralTrack, ImageTrack, VideoTrack } from "mediainfo.js" import { CallbackDataBuilder } from "@mtcute/dispatcher" @@ -8,13 +8,13 @@ import type { ApiServer, CobaltDownloadParams } from "@/core/data/cobalt" import type { DownloadedMediaContent } from "@/core/data/cobalt/tunnel" import type { MediaRequest } from "@/core/data/request" import { finishRequest, outputOptions } from "@/core/data/request" +import type { Settings } from "@/core/data/settings" import type { Result } from "@/core/utils/result" import { error, ok } from "@/core/utils/result" import type { Text } from "@/core/utils/text" import { translatable } from "@/core/utils/text" import { urlWithAuthSchema } from "@/core/utils/url" import { env } from "@/telegram/helpers/env" -import { getPeerSettings } from "@/telegram/helpers/settings" export const OutputButton = new CallbackDataBuilder("dl", "output", "request") export const getOutputSelectionMessage = (requestId: string) => ({ @@ -108,10 +108,9 @@ function getApiEndpoints(override: string | null): Result { ) } -export async function handleMediaDownload(outputType: string, request: MediaRequest | undefined, peer: Peer): Promise> { +export async function handleMediaDownload(outputType: string, request: MediaRequest | undefined, settings: Settings): Promise> { if (!request) return error(translatable("error-request-not-found")) - const settings = await getPeerSettings(peer) const endpoints = getApiEndpoints(settings.instanceOverride) if (!endpoints.success) return endpoints