Skip to content

Commit

Permalink
feat: Implement custom instance support
Browse files Browse the repository at this point in the history
  • Loading branch information
otomir23 committed Jan 10, 2025
1 parent eea7ea8 commit e179592
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 7 deletions.
4 changes: 4 additions & 0 deletions locales/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,9 @@ setting-lang-ru = русский
setting-lang-uk-UA = українська
setting-lang-unset = same as telegram
setting-instance = custom instance
setting-instance-unset = disabled
setting-instance-custom = override
stats-personal = i helped you with downloading { $count } times! (˶ᵔ ᵕ ᵔ˶)
stats-global = i helped with downloading { $count } times! (˶ᵔ ᵕ ᵔ˶)
4 changes: 4 additions & 0 deletions locales/ru.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,9 @@ setting-attribution-1 = давай
setting-lang = язык
setting-lang-unset = как в тг
setting-instance = кастомный инстанс
setting-instance-unset = выключен
setting-instance-custom = настроить
stats-personal = я помог тебе с загрузкой { $count } раз! (˶ᵔ ᵕ ᵔ˶)
stats-global = я помог с загрузкой { $count } раз! (˶ᵔ ᵕ ᵔ˶)
1 change: 1 addition & 0 deletions migrations/0001_eminent_ultron.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE settings ADD `instance` text;
129 changes: 129 additions & 0 deletions migrations/meta/0001_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
{
"version": "5",
"dialect": "sqlite",
"id": "8f524a1e-29ad-4518-88f2-435c6a3114f0",
"prevId": "a3959bd4-7853-4642-b401-7ed157ba3283",
"tables": {
"requests": {
"name": "requests",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"author_id": {
"name": "author_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"settings": {
"name": "settings",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"output": {
"name": "output",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"attribution": {
"name": "attribution",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"instance": {
"name": "instance",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"settings_id_unique": {
"name": "settings_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"downloads": {
"name": "downloads",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_id_unique": {
"name": "users_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
7 changes: 7 additions & 0 deletions migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
"when": 1720203773408,
"tag": "0000_supreme_ben_urich",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1736519894021,
"tag": "0001_eminent_ultron",
"breakpoints": true
}
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"better-sqlite3": "^11.5.0",
"dotenv": "^16.3.1",
"drizzle-orm": "^0.29.3",
"ipaddr.js": "^2.2.0",
"mediainfo.js": "^0.3.2",
"zod": "^3.22.4"
}
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/core/data/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export const settings = sqliteTable("settings", {
preferredOutput: text("output"),
preferredAttribution: int("attribution").notNull().default(0),
languageOverride: text("language"),
instanceOverride: text("instance"),
})
29 changes: 29 additions & 0 deletions src/core/data/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@ import type { Result } from "@/core/utils/result"
import { error, ok } from "@/core/utils/result"
import type { CompoundText, Text } from "@/core/utils/text"
import { compound, literal, translatable } from "@/core/utils/text"
import { safeUrlSchema } from "@/core/utils/url"

export const apiServerSchema = z.object({
name: z.string().optional(),
url: z.string().url(),
auth: z.string().optional(),
youtubeHls: z.boolean().optional(),
unsafe: z.boolean().optional(),
}).or(
z.string().url().transform(data => ({
name: undefined,
url: data,
auth: undefined,
youtubeHls: undefined,
unsafe: undefined,
})),
).transform(data => ({
...data,
Expand Down Expand Up @@ -116,6 +119,16 @@ async function tryDownload(outputType: string, request: MediaRequest, apiPool: A
if (!currentApi)
return error(compound(...fails))

if (currentApi.unsafe && !(await safeUrlSchema.safeParseAsync(currentApi.url)).success) {
return tryDownload(
outputType,
request,
apiPool.slice(1),
lang,
[...fails, compound(literal(`\n${currentApi.name}: `), translatable("error-invalid-custom-instance"))],
)
}

const res = await fetchMedia({
url: request.url,
downloadMode: outputType,
Expand All @@ -135,5 +148,21 @@ async function tryDownload(outputType: string, request: MediaRequest, apiPool: A
)
}

if (currentApi.unsafe) {
if (
(res.result.status === "picker" && !(await safeUrlSchema.safeParseAsync(res.result.audio)).success)
|| (res.result.status === "tunnel" && !(await safeUrlSchema.safeParseAsync(res.result.url)).success)
|| (res.result.status === "redirect" && !(await safeUrlSchema.safeParseAsync(res.result.url)).success)
) {
return tryDownload(
outputType,
request,
apiPool.slice(1),
lang,
[...fails, literal(`\n${currentApi.name}: unsafe api response`)],
)
}
}

return res
}
3 changes: 3 additions & 0 deletions src/core/data/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const defaultSettings: Settings = {
preferredOutput: null,
preferredAttribution: 0,
languageOverride: null,
instanceOverride: null,
}

export const settingOptions: {
Expand All @@ -21,6 +22,7 @@ export const settingOptions: {
preferredOutput: [null, ...outputOptions],
preferredAttribution: [0, 1],
languageOverride: [null, ...locales],
instanceOverride: [null, customValue],
}

export const settingI18n: {
Expand All @@ -29,6 +31,7 @@ export const settingI18n: {
preferredOutput: { key: "output", mode: "translatable" },
preferredAttribution: { key: "attribution", mode: "translatable" },
languageOverride: { key: "lang", mode: "translatable" },
instanceOverride: { key: "instance", mode: "literal" },
}

export async function getSettings(id: number): Promise<Settings> {
Expand Down
20 changes: 20 additions & 0 deletions src/core/utils/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as dns from "node:dns/promises"

import ipaddr from "ipaddr.js"
import { z } from "zod"

export const safeUrlSchema = z
.string()
.url()
.refine(async (u) => {
if (!URL.canParse(u))
return false
const url = new URL(u)
if (ipaddr.isValid(url.hostname) && ipaddr.parse(url.hostname).range() !== "unicast")
return false
const res = await dns.lookup(url.hostname, { all: true }).catch(() => null)
if (!res || !res.every(i => ipaddr.parse(i.address).range() === "unicast"))
return false
return url.protocol === "https:"
})
.nullable()
8 changes: 6 additions & 2 deletions src/telegram/helpers/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import type { GeneralTrack, ImageTrack, VideoTrack } from "mediainfo.js"
import { CallbackDataBuilder } from "@mtcute/dispatcher"
import mediaInfoFactory from "mediainfo.js"

import type { MediaRequest } from "@/core/data/request"
import type { ApiServer, MediaRequest } from "@/core/data/request"
import { finishRequest, outputOptions } from "@/core/data/request"
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 { env } from "@/telegram/helpers/env"
import { getPeerLocale } from "@/telegram/helpers/i18n"
import { getPeerSettings } from "@/telegram/helpers/settings"

export const OutputButton = new CallbackDataBuilder("dl", "output", "request")
export const getOutputSelectionMessage = (requestId: string) => ({
Expand Down Expand Up @@ -77,7 +78,10 @@ async function analyze(buffer: ArrayBuffer): Promise<AnalysisResult> {
export async function handleMediaDownload(outputType: string, request: MediaRequest | undefined, peer: Peer): Promise<Result<InputMediaLike, Text>> {
if (!request)
return error(translatable("error-request-not-found"))
const res = await finishRequest(outputType, request, env.API_ENDPOINTS, await getPeerLocale(peer))
const settings = await getPeerSettings(peer)
const locale = settings.languageOverride ?? getPeerLocale(peer)
const endpoints: ApiServer[] = settings.instanceOverride ? [{ name: "custom", url: settings.instanceOverride, unsafe: true }] : env.API_ENDPOINTS
const res = await finishRequest(outputType, request, endpoints, locale)
if (!res.success)
return res

Expand Down
10 changes: 5 additions & 5 deletions src/telegram/helpers/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import type { TranslationParams } from "@/core/utils/i18n"
import { fallbackLocale, translate } from "@/core/utils/i18n"
import { getPeerSettings } from "@/telegram/helpers/settings"

export async function getPeerLocale(peer: Peer) {
const { languageOverride } = await getPeerSettings(peer)
return languageOverride ?? ("language" in peer ? peer.language : null) ?? fallbackLocale
export function getPeerLocale(peer: Peer) {
return ("language" in peer ? peer.language : null) ?? fallbackLocale
}

export type Translator = Awaited<ReturnType<typeof translatorFor>>
export async function translatorFor(peer: Peer) {
const locale = await getPeerLocale(peer)
return (key: string, params?: TranslationParams) => translate(locale, key, params)
const { languageOverride } = await getPeerSettings(peer)
const locale = getPeerLocale(peer)
return (key: string, params?: TranslationParams) => translate(languageOverride ?? locale, key, params)
}

0 comments on commit e179592

Please sign in to comment.