Skip to content
Merged
9 changes: 7 additions & 2 deletions locales/en.ftl
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
start =
hii! just send me a link and i'll download it. (ᵔᵕᵔ)◜
details: /info
join =
hii! (ᵔᵕᵔ)◜
i will download all links i'll find in this chat.
details: /info

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 @@ -73,7 +78,7 @@ stats-global = i helped with downloading { $count } times! (˶ᵔ ᵕ ᵔ˶)
info-open = about me
info =
i can download videos, photos and music from any service supported by cobalt!
you can use me right here in pms or in any chat via inline mode.
you can use me right here in pms & in any chat via inline mode or add me to your favorite chat or channel.

running { $name }@{ $version }
sources: { $repository }
Expand Down
9 changes: 7 additions & 2 deletions locales/ru.ftl
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
start =
привет! просто отправь мне ссылку и я её скачаю. (ᵔᵕᵔ)◜
подробнее: /info
join =
привет! (ᵔᵕᵔ)◜
я буду скачивать все ссылки, которые я найду в этом чате.
подробнее: /info

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 @@ -59,7 +64,7 @@ stats-global = я помог с загрузкой { $count } раз! (˶ᵔ ᵕ
info-open = обо мне
info =
я умею скачивать видео, фото и музыку из любих сервисов, которые доступны в cobalt!
меня можно использовать прямо тут в лс или в любом чате через инлайн.
меня можно использовать прямо тут в лс и в любом чате через инлайн, или добавить в любимую группу или канал.

выполняется { $name }@{ $version }
сурсы: { $repository }
Expand Down
7 changes: 4 additions & 3 deletions src/core/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ export function tryParseUrl(url: string) {
if (originalParsed.success)
return originalParsed.data

const domain = url.split("/")[0]
if (!domain.includes(".") || domain.includes(" ") || domain.includes(":"))
const protocollessUrl = url.startsWith("//") ? url.slice(2) : url
const domain = protocollessUrl.split("/")[0]
if (!domain.includes(".") || domain.includes(" ") || domain.endsWith(".") || domain.startsWith("."))
return null

const withHttpsParsed = mediaUrlSchema.safeParse(`https://${url}`)
const withHttpsParsed = mediaUrlSchema.safeParse(`https://${protocollessUrl}`)
if (withHttpsParsed.success)
return withHttpsParsed.data

Expand Down
133 changes: 92 additions & 41 deletions src/telegram/bot/download.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,102 @@
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,
handleMediaDownload,
OutputButton,
} from "@/telegram/helpers/handler"
import { deferredReply, replyText } from "@/telegram/helpers/sent"
import { getPeerSettings } from "@/telegram/helpers/settings"
import type { Evaluators } from "@/telegram/helpers/text"
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) => {
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 isGroupChat = msg.chat.type === "chat"
const isChannel = isGroupChat && msg.chat.chatType === "channel"

if (!req.success) {
await msg.replyText(t("error", { message: e(req.error) }))
return
}
const settings = await getPeerSettings(msg.chat)
const { e, t } = await evaluatorsFor(msg.chat)

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,
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 = isGroupChat ? extractedUrls : (extractedUrls.length ? extractedUrls : [msg.text])

if (isChannel) {
const [url] = urls
if (!url || msg.media)
return

const req = await createRequest(url, msg.sender.id)
if (!req.success)
return

const originalText = msg.text
const res = await onOutputSelected(
settings.preferredOutput || "auto",
req.result,
args => msg.client.editMessage({ ...args, message: reply }),
args => msg.client.editMessage({ ...args, message: msg }),
{ e, t },
msg.sender,
!!settings.preferredAttribution,
settings,
({ medias }) => msg.replyMediaGroup(medias),
msg.sender,
)

if (!res)
msg.client.editMessage({ text: originalText, message: msg })

return
}

for (const url of urls) {
const req = await createRequest(url, msg.sender.id)

if (!req.success) {
if (!isGroupChat)
await msg.replyText(t("error", { message: e(req.error) }))
return
}

const selectMsg = getOutputSelectionMessage(req.result.id)
const reply = await (isGroupChat ? deferredReply : replyText)(msg, e(selectMsg.caption), {
replyMarkup: BotKeyboard.inline([
selectMsg.options.map(o => BotKeyboard.callback(
e(o.name),
o.key,
)),
]),
})

if (settings.preferredOutput || isGroupChat) {
const res = await onOutputSelected(
settings.preferredOutput || "auto",
req.result,
args => reply.edit(args),
{ e, t },
settings,
({ medias }) => msg.replyMediaGroup(medias),
msg.sender,
)
if (res && isGroupChat)
await reply.flush()
}
}
})

Expand Down Expand Up @@ -107,8 +148,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 @@ -118,15 +164,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 @@ -141,9 +189,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 @@ -153,16 +201,16 @@ 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) {
const errorMessage = t("error", { message: e(res.error) })
await editMessage({ text: leaveSourceLink ? `${errorMessage}\n\n${request?.url}` : errorMessage })
return
await editMessage({ text: settings.preferredAttribution ? `${errorMessage}\n\n${request?.url}` : errorMessage })
return false
}

await editMessage({ text: t("uploading-title") })
Expand All @@ -174,10 +222,13 @@ async function onOutputSelected(
await sendGroup({ medias: chunk })
}
} else {
// FIXME: Merge two edit calls
await editMessage({ media: res.result[0] })
await editMessage({ text: (leaveSourceLink && request?.url) || "" })
await editMessage({ text: (!!settings.preferredAttribution && request?.url) || "" })
}

incrementDownloadCount(peer.id)
incrementDownloadCount(sender.id)
.catch(() => { /* noop */ })

return res.result.length === 1
}
2 changes: 1 addition & 1 deletion src/telegram/bot/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const infoDp = Dispatcher.child()
infoDp.onNewMessage(
filters.or(filters.command("info"), filters.deeplink(["info"])),
async (msg) => {
const { t } = await evaluatorsFor(msg.sender)
const { t } = await evaluatorsFor(msg.chat)
const infoText = t("info", { bugs, name, repository, version, homepage })
await msg.replyText(`${infoText}\n\n${env.ADDITIONAL_INFO}`)
},
Expand Down
Loading
Loading