Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions locales/en.ftl
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
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...
error-invalid-url = looks like i dont recognise the link you sent... maybe the service isn't supported or you pasted it wrong
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 (。 · ᎑ ·。)
Expand Down Expand Up @@ -71,4 +76,4 @@ stats-global = i helped with downloading { $count } times! (˶ᵔ ᵕ ᵔ˶)
info =
running { $name }@{ $version }
sources: { $repository }
report bugs: { $bugs }
report bugs: { $bugs }
9 changes: 7 additions & 2 deletions locales/ru.ftl
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
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 = не удалось подключиться к серверу, наверное он лежит...
error-invalid-url = кажется у меня не получается распознать отправленную сылку... возможно она неправильно вставлена или этот сервис не поддерживается
error-media-unavailable = я нашёл нужный файл, но не смог его скачать. возможно на нём ограничения по региону, возрасту или приватности.
error-unknown = ой, произошла внутреняя ошибка. я сообщил об этом моим разработчикам, чтобы они исправили!

note-picker = по твоей ссылке я нашёл несколько медиа файлов, поэтому я отправил тебе их в лс
note-picker = по твоей ссылке я нашёл несколько медиа файлов, поэтому я отправил тебе отдельно или их в лс

download-title = скачать по ссылке
type-select-title = выбери тип загрузки (。 · ᎑ ·。)
Expand Down Expand Up @@ -57,4 +62,4 @@ stats-global = я помог с загрузкой { $count } раз! (˶ᵔ ᵕ
info =
выполняется { $name }@{ $version }
сурсы: { $repository }
репорт багов: { $bugs }
репорт багов: { $bugs }
108 changes: 63 additions & 45 deletions src/telegram/bot/download.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)
}
}
})

Expand Down Expand Up @@ -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)
Expand All @@ -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) => {
Expand All @@ -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,
)
}
})
Expand All @@ -148,15 +165,15 @@ async function onOutputSelected(
request: MediaRequest | undefined,
editMessage: (edit: { text?: string, media?: InputMediaLike }) => Promise<unknown>,
{ t, e }: Evaluators,
peer: Peer,
leaveSourceLink: boolean,
settings: Settings,
sendGroup: (send: { medias: InputMediaLike[] }) => Promise<unknown>,
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") })
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion src/telegram/bot/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
})
47 changes: 35 additions & 12 deletions src/telegram/bot/settings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Peer, TelegramClient, User } from "@mtcute/node"
import { Dispatcher, filters, PropagationAction } from "@mtcute/dispatcher"
import { BotKeyboard } from "@mtcute/node"

Expand Down Expand Up @@ -34,6 +35,13 @@ function settingsMessage(e: TextEvaluator, settings: Settings) {
}
}

async function isAdmin(client: Pick<TelegramClient, "getChatMember">, 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 {
Expand All @@ -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
Expand All @@ -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()
Expand Down
7 changes: 6 additions & 1 deletion src/telegram/bot/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
})
4 changes: 2 additions & 2 deletions src/telegram/bot/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
Expand Down
Loading