From cd85d05692cd290d6f5c5b973c899af13f67ed7c Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Tue, 5 Mar 2024 18:10:13 +0100 Subject: [PATCH 1/9] refactor telegram/tglib for Hono --- lib/config.ts | 4 + lib/init-routes.ts | 2 + lib/middleware/cache.ts | 4 + lib/middleware/template.ts | 1 + lib/routes/telegram/router.ts | 10 +- lib/routes/telegram/tglib/channel-media.ts | 225 ++++++++++++++++++ lib/routes/telegram/tglib/channel.ts | 132 ++-------- lib/routes/telegram/tglib/client.ts | 162 +------------ website/docs/install/config.md | 2 +- website/docs/routes/social-media.mdx | 2 +- .../current/install/config.md | 2 +- 11 files changed, 283 insertions(+), 263 deletions(-) create mode 100644 lib/routes/telegram/tglib/channel-media.ts diff --git a/lib/config.ts b/lib/config.ts index d02536fb294be9..e30bb4611a29d1 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -240,6 +240,10 @@ export type Config = { }; telegram: { token?: string; + session?: string; + apiId?: string; + apiHash?: string; + maxConcurrentDownloads?: string; }; tophub: { cookie?: string; diff --git a/lib/init-routes.ts b/lib/init-routes.ts index d59ccdb2d4b343..fe2057ffe2447d 100644 --- a/lib/init-routes.ts +++ b/lib/init-routes.ts @@ -12,6 +12,7 @@ const __dirname = getCurrentPath(import.meta.url); type Root = { get: (routePath: string, filePath: string) => void; + app: Hono }; const routes: Record void> = {}; @@ -48,6 +49,7 @@ export default function (app: Hono) { }; subApp.get(routePath, wrapedHandler); }, + app: subApp }); } diff --git a/lib/middleware/cache.ts b/lib/middleware/cache.ts index a9e8c8a8f48368..3bf033a4162c10 100644 --- a/lib/middleware/cache.ts +++ b/lib/middleware/cache.ts @@ -38,6 +38,10 @@ const middleware: MiddlewareHandler = async (ctx, next) => { // Doesn't hit the cache? We need to let others know! await cacheModule.globalCache.set(controlKey, '1', config.cache.requestTimeout); + // let routers control cache + ctx.set('cacheKey', key); + ctx.set('cacheControlKey', controlKey); + try { await next(); } catch (error) { diff --git a/lib/middleware/template.ts b/lib/middleware/template.ts index 294d6e4df5af98..84cc7378f7ddeb 100644 --- a/lib/middleware/template.ts +++ b/lib/middleware/template.ts @@ -10,6 +10,7 @@ const __dirname = getCurrentPath(import.meta.url); const middleware: MiddlewareHandler = async (ctx, next) => { await next(); + if (ctx.get('no-template')) { return; } const data: Data = ctx.get('data'); const outputType = ctx.req.query('format') || 'rss'; diff --git a/lib/routes/telegram/router.ts b/lib/routes/telegram/router.ts index ffe122a1feb545..5ef39fc396aca0 100644 --- a/lib/routes/telegram/router.ts +++ b/lib/routes/telegram/router.ts @@ -1,5 +1,13 @@ +import { config } from '@/config'; + export default (router) => { - router.get('/channel/:username/:routeParams?', './channel'); + if (config.telegram.session) { + router.get('/channel/:username', './tglib/channel'); + import('./tglib/channel-media').then((channelMedia) => + router.app.get('/channel/:username/:media', channelMedia.default)); + } else { + router.get('/channel/:username/:routeParams?', './channel'); + } router.get('/stickerpack/:name', './stickerpack'); router.get('/blog', './blog'); }; diff --git a/lib/routes/telegram/tglib/channel-media.ts b/lib/routes/telegram/tglib/channel-media.ts new file mode 100644 index 00000000000000..e777170cdcf54b --- /dev/null +++ b/lib/routes/telegram/tglib/channel-media.ts @@ -0,0 +1,225 @@ +import { config } from '@/config'; +import cacheModule from '@/utils/cache/index'; +import { stream } from 'hono/streaming'; +import { getAppropriatedPartSize } from 'telegram/Utils'; +import { Api } from 'telegram'; +import { returnBigInt as bigInt } from 'telegram/Helpers'; +import { client, getFilename } from './client'; +import { StreamingApi } from 'hono/utils/stream'; +/** + * https://core.telegram.org/api/files#stripped-thumbnails + * @param bytes Buffer + * @returns Buffer jpeg + */ +function ExpandInlineBytes(bytes) { + if (bytes.length < 3 || bytes[0] !== 0x1) { + return []; + } + const header = Buffer.from([ + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x28, 0x1c, 0x1e, 0x23, 0x1e, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2d, 0x2b, + 0x28, 0x30, 0x3c, 0x64, 0x41, 0x3c, 0x37, 0x37, 0x3c, 0x7b, 0x58, 0x5d, 0x49, 0x64, 0x91, 0x80, 0x99, 0x96, 0x8f, 0x80, 0x8c, 0x8a, 0xa0, 0xb4, 0xe6, 0xc3, 0xa0, 0xaa, 0xda, 0xad, 0x8a, 0x8c, 0xc8, 0xff, 0xcb, 0xda, 0xee, + 0xf5, 0xff, 0xff, 0xff, 0x9b, 0xc1, 0xff, 0xff, 0xff, 0xfa, 0xff, 0xe6, 0xfd, 0xff, 0xf8, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x2b, 0x2d, 0x2d, 0x3c, 0x35, 0x3c, 0x76, 0x41, 0x41, 0x76, 0xf8, 0xa5, 0x8c, 0xa5, 0xf8, 0xf8, 0xf8, + 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, + 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, + 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, + 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, + 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, + 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, + 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, + 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, + ]); + const footer = Buffer.from([0xff, 0xd9]); + const real = Buffer.alloc(header.length + bytes.length + footer.length); + header.copy(real); + bytes.copy(real, header.length, 3); + bytes.copy(real, 164, 1, 2); + bytes.copy(real, 166, 2, 3); + footer.copy(real, header.length + bytes.length, 0); + return real; +} + +function sortThumb(thumb) { + if (thumb instanceof Api.PhotoStrippedSize) { + return thumb.bytes.length; + } + if (thumb instanceof Api.PhotoCachedSize) { + return thumb.bytes.length; + } + if (thumb instanceof Api.PhotoSize) { + return thumb.size; + } + if (thumb instanceof Api.PhotoSizeProgressive) { + return Math.max(...thumb.sizes); + } + if (thumb instanceof Api.VideoSize) { + return thumb.size; + } + return 0; +} + +function chooseLargestThumb(thumbs) { + thumbs = [...thumbs].sort((a, b) => sortThumb(a) - sortThumb(b)); + return thumbs.pop(); +} + +export function streamThumbnail(doc: Api.Document) { + if (doc.thumbs?.length ?? 0 > 0) { + const size = chooseLargestThumb(doc.thumbs); + if (size instanceof Api.PhotoCachedSize || size instanceof Api.PhotoStrippedSize) { + return (function* () { + yield ExpandInlineBytes(size.bytes); + })(); + } + return streamDocument(doc, size && 'type' in size ? size.type : ''); + } + throw new Error('not supported'); +} + +export async function decodeMedia(channelName: string, x: string, retry = false) { + const [channel, msg] = x.split('_'); + + try { + const msgs = await client.getMessages(channel, { + ids: [Number(msg)], + }); + return msgs[0]?.media; + } catch (error) { + if (!retry) { + // channel likely not seen before, we need to resolve ID and retry + await client.getInputEntity(channelName); + return decodeMedia(channelName, x, true); + } + throw error; + } +} + +export function streamDocument(obj: Api.Document, thumbSize = '', offset?, limit?) { + const chunkSize = (obj.size ? getAppropriatedPartSize(obj.size) : 64) * 1024; + const iterFileParams = { + file: new Api.InputDocumentFileLocation({ + id: obj.id, + accessHash: obj.accessHash, + fileReference: obj.fileReference, + thumbSize, + }), + chunkSize, + requestSize: 512 * 1024, // MAX_CHUNK_SIZE + dcId: obj.dcId, + offset: undefined, + limit: undefined + }; + if (offset) { + iterFileParams.offset = offset; + } + if (limit) { + iterFileParams.limit = limit; + } + return client.iterDownload(iterFileParams); +} + +function parseRange(range, length) { + if (!range) { + return []; + } + const [typ, segstr] = range.split('='); + if (typ !== 'bytes') { + throw new Error(`unsupported range: ${typ}`); + } + const segs = segstr.split(',').map((s) => s.trim()); + const parsedSegs: Array[] = []; // actually BigInt but ts is unhappy + for (const seg of segs) { + const range = seg + .split('-', 2) + .filter((v) => !!v) + .map(bigInt); + if (range.length < 2) { + if (seg.startsWith('-')) { + range.unshift(0); + } else { + range.push(length); + } + } + parsedSegs.push(range); + } + return parsedSegs; +} + +async function configureMiddlewares(ctx) { + // media is too heavy to cache in memory or redis, and lock-up is not needed + await cacheModule.set(ctx.get('cacheControlKey'), '0', config.cache.requestTimeout); + ctx.req.raw.headers.delete('Accept-Encoding'); // avoid hono compress() middleware detecting Accept-Encoding on req + ctx.set('no-template', true); // skip RSSHub template middleware +} + +function streamResponse(c, bodyIter, cb?: (e: Error|undefined, s: StreamingApi) => Promise) { + return stream(c, async (stream) => { + let aborted = false; + stream.onAbort(() => { + // console.log(`stream aborted`); + aborted = true; + }); + for await (const chunk of bodyIter) { + if (aborted) { break; } + // console.log(`writing ${chunk.length / 1024}kB`); + await stream.write(chunk); + } + if (cb) { + await cb(undefined, stream); + } + }, cb); +} + +export default async function handler(ctx) { + await configureMiddlewares(ctx); + + const media = await decodeMedia(ctx.req.param('username'), ctx.req.param('media')); + if (!media) { + return ctx.text('Unknown media', 404); + } + + if (media instanceof Api.MessageMediaPhoto) { + const buf = await client.downloadMedia(media); + return new Response(buf, {headers: {'Content-Type': 'image/jpeg'}}); + } + + if (media instanceof Api.MessageMediaDocument) { + const doc = media.document as Api.Document; + if ('thumb' in ctx.req.query()) { + ctx.header('Content-Type', 'image/jpeg'); + return streamResponse(ctx, streamThumbnail(doc)); + } + ctx.header('Content-Type', doc.mimeType); + ctx.header('Accept-Ranges', 'bytes'); + + const rangeHeader = ctx.req.header('Range'); + const range = parseRange(rangeHeader, doc.size.valueOf() - 1); + if (range.length > 1) { + return ctx.text('Not Satisfiable', 416); + } + + let stream; + if (range.length === 0) { + ctx.header('Content-Length', doc.size); + if (doc.mimeType.startsWith('application/')) { + ctx.header('Content-Disposition', `attachment; filename="${encodeURIComponent(getFilename(media))}"`); + } + stream = streamDocument(doc); + } else { + const [offset, limit] = range[0]; + // console.log(`${ctx.method} ${ctx.req.url} Range: ${rangeHeader}`); + ctx.status(206); // partial content + ctx.header('Content-Length', (limit - offset + 1).toString()); + ctx.header('Content-Range', `bytes ${offset}-${limit}/${doc.size}`); + stream = streamDocument(doc, '', offset, limit); + } + return streamResponse(ctx, stream, () => stream.close()); + } + + return ctx.text(media.className, 415); +} diff --git a/lib/routes/telegram/tglib/channel.ts b/lib/routes/telegram/tglib/channel.ts index c16f1230ccc882..ff8c4cb42e61a0 100644 --- a/lib/routes/telegram/tglib/channel.ts +++ b/lib/routes/telegram/tglib/channel.ts @@ -1,118 +1,40 @@ // @ts-nocheck import wait from '@/utils/wait'; import { config } from '@/config'; -const { client, decodeMedia, getFilename, getMediaLink, streamDocument, streamThumbnail } = require('./client'); -const bigInt = require('telegram/Helpers').returnBigInt; -const HTMLParser = require('telegram/extensions/html').HTMLParser; +import { Api } from 'telegram'; +import { HTMLParser } from 'telegram/extensions/html'; +import { client, getFilename } from './client'; -function parseRange(range, length) { - if (!range) { - return []; +function getMediaLink(ctx, channel, channelName, message) { + const base = `${new URL(ctx.req.url).origin}/telegram/channel/${channelName}/`; + const src = base + `${channel.channelId}_${message.id}`; + + const x = message.media; + if (x instanceof Api.MessageMediaPhoto || (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('image/'))) { + return ``; } - const [typ, segstr] = range.split('='); - if (typ !== 'bytes') { - throw `unsupported range: ${typ}`; + if (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('video/')) { + const vid = x.document.attributes.find((t) => t.className === 'DocumentAttributeVideo') ?? { w: 1080, h: 720 }; + return ``; } - const segs = segstr.split(',').map((s) => s.trim()); - const parsedSegs = []; - for (const seg of segs) { - const range = seg - .split('-', 2) - .filter((v) => !!v) - .map(bigInt); - if (range.length < 2) { - if (seg.startsWith('-')) { - range.unshift(0); - } else { - range.push(length); - } - } - parsedSegs.push(range); + if (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('audio/')) { + return ``; } - return parsedSegs; -} -async function getMedia(ctx) { - const media = await decodeMedia(ctx.req.param('username'), ctx.req.param('media')); - if (!media) { - ctx.status = 500; - return ctx.res.end(); - } - if (ctx.res.closed) { - // console.log(`prematurely closed ${ctx.req.param('media')}`); - return; + let linkText = getFilename(x); + if (x instanceof Api.MessageMediaDocument) { + linkText += ` (${humanFileSize(x.document.size)})`; + return `
${linkText}
`; } + return; +} - if (media.document) { - ctx.status = 200; - let stream; - if ('thumb' in ctx.req.query()) { - try { - stream = streamThumbnail(media); - ctx.set('Content-Type', 'image/jpeg'); - } catch { - ctx.status = 404; - return ctx.res.end(); - } - } else { - ctx.set('Content-Type', media.document.mimeType); - - ctx.set('Accept-Ranges', 'bytes'); - const range = parseRange(ctx.get('Range'), media.document.size - 1); - if (range.length > 1) { - ctx.status = 416; // range not satisfiable - return ctx.res.end(); - } - if (range.length === 1) { - // console.log(`${ctx.method} ${ctx.req.url} Range: ${ctx.get('Range')}`); - ctx.status = 206; // partial content - const [offset, limit] = range[0]; - ctx.set('Content-Length', limit - offset + 1); - ctx.set('Content-Range', `bytes ${offset}-${limit}/${media.document.size}`); - - const stream = streamDocument(media.document, '', offset, limit); - for await (const chunk of stream) { - ctx.res.write(chunk); - if (ctx.res.closed) { - break; - } - } - return ctx.res.end(); - } - - ctx.set('Content-Length', media.document.size); - if (media.document.mimeType.startsWith('application/')) { - ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(getFilename(media))}"`); - } - stream = streamDocument(media.document); - } - // const addr = JSON.stringify(ctx.res.socket.address()); - // console.log(`streaming ${ctx.req.param('media')} to ${addr}`); - - for await (const chunk of stream) { - if (ctx.res.closed) { - // console.log(`closed ${addr}`); - break; - } - // console.log(`writing ${chunk.length / 1024} to ${addr}`); - ctx.res.write(chunk); - } - if ('close' in stream) { - stream.close(); - } - } else if (media.photo) { - ctx.status = 200; - ctx.set('Content-Type', 'image/jpeg'); - const buf = await client.downloadMedia(media); - ctx.res.write(buf); - } else { - ctx.status = 415; - ctx.write(media.className); - } - return ctx.res.end(); +function humanFileSize(size) { + const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; } -export default async (ctx) => { +export default async function handler(ctx) { if (!config.telegram.session) { return []; } @@ -161,6 +83,4 @@ export default async (ctx) => { allowEmpty: ctx.req.param('id') === 'allow_empty', description: `@${channelInfo.username} on Telegram`, }); -}; - -module.exports.getMedia = getMedia; +} diff --git a/lib/routes/telegram/tglib/client.ts b/lib/routes/telegram/tglib/client.ts index a99814fd78a212..ad50e337cc1313 100644 --- a/lib/routes/telegram/tglib/client.ts +++ b/lib/routes/telegram/tglib/client.ts @@ -1,16 +1,16 @@ // @ts-nocheck -const readline = require('node:readline/promises'); -const { Api, TelegramClient } = require('telegram'); -const { StringSession } = require('telegram/sessions'); -const { getAppropriatedPartSize } = require('telegram/Utils'); - import { config } from '@/config'; +import * as readline from 'node:readline/promises'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { Api, TelegramClient } from 'telegram'; +import { StringSession } from 'telegram/sessions'; const apiId = Number(config.telegram.apiId ?? 4); const apiHash = config.telegram.apiHash ?? '014b35b6184100b085b0d0572f9b5103'; const stringSession = new StringSession(config.telegram.session); -const client = new TelegramClient(stringSession, apiId, apiHash, { +export const client = new TelegramClient(stringSession, apiId, apiHash, { connectionRetries: Infinity, autoReconnect: true, retryDelay: 3000, @@ -25,73 +25,7 @@ if (config.telegram.session) { }); } -function humanFileSize(size) { - const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); - return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; -} - -/** - * https://core.telegram.org/api/files#stripped-thumbnails - * @param bytes Buffer - * @returns Buffer jpeg - */ -function ExpandInlineBytes(bytes) { - if (bytes.length < 3 || bytes[0] !== 0x1) { - return []; - } - const header = Buffer.from([ - 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x28, 0x1c, 0x1e, 0x23, 0x1e, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2d, 0x2b, - 0x28, 0x30, 0x3c, 0x64, 0x41, 0x3c, 0x37, 0x37, 0x3c, 0x7b, 0x58, 0x5d, 0x49, 0x64, 0x91, 0x80, 0x99, 0x96, 0x8f, 0x80, 0x8c, 0x8a, 0xa0, 0xb4, 0xe6, 0xc3, 0xa0, 0xaa, 0xda, 0xad, 0x8a, 0x8c, 0xc8, 0xff, 0xcb, 0xda, 0xee, - 0xf5, 0xff, 0xff, 0xff, 0x9b, 0xc1, 0xff, 0xff, 0xff, 0xfa, 0xff, 0xe6, 0xfd, 0xff, 0xf8, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x2b, 0x2d, 0x2d, 0x3c, 0x35, 0x3c, 0x76, 0x41, 0x41, 0x76, 0xf8, 0xa5, 0x8c, 0xa5, 0xf8, 0xf8, 0xf8, - 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, - 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, - 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, - 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, - 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, - 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, - 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, - 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, - 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, - 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, - 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, - 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, - ]); - const footer = Buffer.from([0xff, 0xd9]); - const real = Buffer.alloc(header.length + bytes.length + footer.length); - header.copy(real); - bytes.copy(real, header.length, 3); - bytes.copy(real, 164, 1, 2); - bytes.copy(real, 166, 2, 3); - footer.copy(real, header.length + bytes.length, 0); - return real; -} - -function getMediaLink(ctx, channel, channelName, message) { - const base = `${ctx.protocol}://${ctx.host}/telegram/channel/${channelName}`; - const src = base + `${channel.channelId}_${message.id}`; - - const x = message.media; - if (x instanceof Api.MessageMediaPhoto || (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('image/'))) { - return ``; - } - if (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('video/')) { - const vid = x.document.attributes.find((t) => t.className === 'DocumentAttributeVideo') ?? { w: 1080, h: 720 }; - return ``; - } - if (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('audio/')) { - return ``; - } - - let linkText = getFilename(x); - if (x instanceof Api.MessageMediaDocument) { - linkText += ` (${humanFileSize(x.document.size)})`; - return `
${linkText}
`; - } - return; -} -function getFilename(x) { +export function getFilename(x) { if (x instanceof Api.MessageMediaDocument) { const docFilename = x.document.attributes.find((a) => a.className === 'DocumentAttributeFilename'); if (docFilename) { @@ -101,85 +35,7 @@ function getFilename(x) { return x.className; } -function sortThumb(thumb) { - if (thumb instanceof Api.PhotoStrippedSize) { - return thumb.bytes.length; - } - if (thumb instanceof Api.PhotoCachedSize) { - return thumb.bytes.length; - } - if (thumb instanceof Api.PhotoSize) { - return thumb.size; - } - if (thumb instanceof Api.PhotoSizeProgressive) { - return Math.max(...thumb.sizes); - } - if (thumb instanceof Api.VideoSize) { - return thumb.size; - } - return 0; -} - -function chooseLargestThumb(thumbs) { - thumbs = [...thumbs].sort((a, b) => sortThumb(a) - sortThumb(b)); - return thumbs.pop(); -} - -function streamThumbnail(x) { - if (x instanceof Api.MessageMediaDocument && x.document.thumbs.length > 0) { - const size = chooseLargestThumb(x.document.thumbs); - if (size instanceof Api.PhotoCachedSize || size instanceof Api.PhotoStrippedSize) { - return (function* () { - yield ExpandInlineBytes(size.bytes); - })(); - } - return streamDocument(x.document, size && 'type' in size ? size.type : ''); - } - throw 'not supported'; -} - -async function decodeMedia(channelName, x, retry = false) { - const [channel, msg] = x.split('_'); - - try { - const msgs = await client.getMessages(channel, { - ids: [Number(msg)], - }); - return msgs[0]?.media; - } catch (error) { - if (!retry) { - // channel likely not seen before, we need to resolve ID and retry - await client.getInputEntity(channelName); - return decodeMedia(channelName, x, true); - } - throw error; - } -} - -function streamDocument(obj, thumbSize = '', offset, limit) { - const chunkSize = (obj.size ? getAppropriatedPartSize(obj.size) : 64) * 1024; - const iterFileParams = { - file: new Api.InputDocumentFileLocation({ - id: obj.id, - accessHash: obj.accessHash, - fileReference: obj.fileReference, - thumbSize, - }), - chunkSize, - dcId: obj.dcId, - }; - if (offset) { - iterFileParams.offset = offset; - } - if (limit) { - iterFileParams.limit = limit; - } - return client.iterDownload(iterFileParams); -} - -module.exports = { client, getMediaLink, decodeMedia, getFilename, streamDocument, streamThumbnail }; - -if (require.main === module) { +if (process.argv[1] === fileURLToPath(import.meta.url)) { Promise.resolve().then(async () => { client.session = new StringSession(''); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); @@ -189,7 +45,7 @@ if (require.main === module) { phoneCode: () => rl.question('Please enter the code you received: '), onError: (err) => process.stderr.write(err), }); - process.stdout.write(`TG_SESSION=${client.session.save()}\n`); + process.stdout.write(`TELEGRAM_SESSION=${client.session.save()}\n`); process.exit(0); }); } diff --git a/website/docs/install/config.md b/website/docs/install/config.md index d041eb80b6316a..905c09c2c89879 100644 --- a/website/docs/install/config.md +++ b/website/docs/install/config.md @@ -468,7 +468,7 @@ Remember to check `user-top-read` and `user-library-read` in the scope for `Pers [Bot application](https://telegram.org/blog/bot-revolution) - `TELEGRAM_TOKEN`: Telegram bot token for stickerpack feeds -- `TELEGRAM_SESSION`: for video and file streaming, can be acquired by running `node lib/routes/telegram/tglib/client.js` +- `TELEGRAM_SESSION`: for video and file streaming, can be acquired by running `npx tsx lib/routes/telegram/tglib/client.ts` ### Twitter diff --git a/website/docs/routes/social-media.mdx b/website/docs/routes/social-media.mdx index 50fef929325db0..4c98eb8fb8fe2a 100644 --- a/website/docs/routes/social-media.mdx +++ b/website/docs/routes/social-media.mdx @@ -784,7 +784,7 @@ If the instance address is not `mastodon.social` or `pawoo.net`, then the route :::warning -This route requires user-based `TELEGRAM_SESSION`. +This route requires user-based `TELEGRAM_SESSION`. It can be acquired by running `npx tsx lib/routes/telegram/tglib/client.ts` ::: ### Sticker Pack {#telegram-sticker-pack} diff --git a/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md b/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md index a68ae55eadf1de..b0cbeb2851ae56 100644 --- a/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md +++ b/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md @@ -445,7 +445,7 @@ RSSHub 支持使用访问密钥 / 码进行访问控制。开启将会激活全 贴纸包路由:[Telegram 机器人](https://telegram.org/blog/bot-revolution) - `TELEGRAM_TOKEN`: Telegram 机器人 token -- `TELEGRAM_SESSION`: 可通过运行 `node lib/routes/telegram/tglib/client.js` +- `TELEGRAM_SESSION`: 可通过运行 `npx tsx lib/routes/telegram/tglib/client.ts` ### Twitter From 94a78a70bf72463460c3ff17d7b2872f9236981f Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Tue, 5 Mar 2024 18:21:12 +0100 Subject: [PATCH 2/9] linter fix --- lib/routes/telegram/tglib/channel-media.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes/telegram/tglib/channel-media.ts b/lib/routes/telegram/tglib/channel-media.ts index e777170cdcf54b..8ba00de09bf51f 100644 --- a/lib/routes/telegram/tglib/channel-media.ts +++ b/lib/routes/telegram/tglib/channel-media.ts @@ -157,7 +157,7 @@ async function configureMiddlewares(ctx) { ctx.set('no-template', true); // skip RSSHub template middleware } -function streamResponse(c, bodyIter, cb?: (e: Error|undefined, s: StreamingApi) => Promise) { +function streamResponse(c, bodyIter, cb?: (e: Error | undefined, s: StreamingApi) => Promise) { return stream(c, async (stream) => { let aborted = false; stream.onAbort(() => { From 992157562d215bef60e1619abbeb9228e1aa4113 Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Fri, 8 Mar 2024 13:53:57 +0100 Subject: [PATCH 3/9] add CSP for channel media --- lib/routes/telegram/tglib/channel-media.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/routes/telegram/tglib/channel-media.ts b/lib/routes/telegram/tglib/channel-media.ts index 8ba00de09bf51f..b624732868aaa2 100644 --- a/lib/routes/telegram/tglib/channel-media.ts +++ b/lib/routes/telegram/tglib/channel-media.ts @@ -196,6 +196,7 @@ export default async function handler(ctx) { } ctx.header('Content-Type', doc.mimeType); ctx.header('Accept-Ranges', 'bytes'); + ctx.header('Content-Security-Policy', "default-src 'self'"); const rangeHeader = ctx.req.header('Range'); const range = parseRange(rangeHeader, doc.size.valueOf() - 1); @@ -206,7 +207,9 @@ export default async function handler(ctx) { let stream; if (range.length === 0) { ctx.header('Content-Length', doc.size); - if (doc.mimeType.startsWith('application/')) { + if (!doc.mimeType.startsWith('video/') && + !doc.mimeType.startsWith('audio/') && + !doc.mimeType.startsWith('image/')) { ctx.header('Content-Disposition', `attachment; filename="${encodeURIComponent(getFilename(media))}"`); } stream = streamDocument(doc); From 2914b06e6b3a10d9a752ee6568d98d16a72e83ed Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Fri, 8 Mar 2024 13:54:43 +0100 Subject: [PATCH 4/9] don't cut feed item title prematurely --- lib/routes/telegram/tglib/channel.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/routes/telegram/tglib/channel.ts b/lib/routes/telegram/tglib/channel.ts index ce2aa9750d9119..f0caa88bc35091 100644 --- a/lib/routes/telegram/tglib/channel.ts +++ b/lib/routes/telegram/tglib/channel.ts @@ -41,29 +41,29 @@ export default async function handler(ctx) { await wait(1000); } - const item = []; + const item: object[] = []; const chat = await client.getInputEntity(ctx.req.param('username')); - const channelInfo = await client.getEntity(chat); + const channelInfo = await client.getEntity(chat) as Api.Channel; - let attachments = []; + let attachments: string[] = []; const messages = await client.getMessages(chat, { limit: 50 }); + let i = 0; for (const message of messages) { if (message.media) { // messages that have no text are shown as if they're one post // because in TG only 1 attachment per message is possible attachments.push(getMediaLink(ctx, chat, ctx.req.param('username'), message)); } - if (message.text !== '') { - let description = attachments.join('\n'); + if (message.text !== '' || ++i === messages.length) { + let description = attachments.join('
\n'); attachments = []; // emitting these, buffer other ones if (message.text) { description += `

${HTMLParser.unparse(message.message, message.entities).replaceAll('\n', '
')}

`; } - const title = message.text ? message.text.substring(0, 80) + (message.text.length > 80 ? '...' : '') : new Date(message.date * 1000).toUTCString(); - + const title = message.text ?? new Date(message.date * 1000).toLocaleString(); item.push({ title, description, From 71c6f5cc79bb62c901782b50acd86f95db25fb0d Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Fri, 8 Mar 2024 13:55:27 +0100 Subject: [PATCH 5/9] implement shared Geo position attachment --- lib/routes/telegram/tglib/channel.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/routes/telegram/tglib/channel.ts b/lib/routes/telegram/tglib/channel.ts index f0caa88bc35091..4db20d76993046 100644 --- a/lib/routes/telegram/tglib/channel.ts +++ b/lib/routes/telegram/tglib/channel.ts @@ -19,13 +19,15 @@ function getMediaLink(ctx, channel, channelName, message) { if (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('audio/')) { return ``; } - + if (x instanceof Api.MessageMediaGeo) { + return `Geo LatLon: ${x.geo.lat}, ${x.geo.long}`; + } let linkText = getFilename(x); if (x instanceof Api.MessageMediaDocument) { linkText += ` (${humanFileSize(x.document.size)})`; return `
${linkText}
`; } - return; + return x.className; } function humanFileSize(size) { From bec5b6e0d6f8ac8e5783812ca935b57d3b4991a8 Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Fri, 8 Mar 2024 13:56:13 +0100 Subject: [PATCH 6/9] don't start TG client unless routes are requested --- lib/routes/telegram/router.ts | 4 +- lib/routes/telegram/tglib/channel-media.ts | 18 ++++----- lib/routes/telegram/tglib/channel.ts | 10 +---- lib/routes/telegram/tglib/client.ts | 45 ++++++++++++---------- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/lib/routes/telegram/router.ts b/lib/routes/telegram/router.ts index 5ef39fc396aca0..c91370ef26ec40 100644 --- a/lib/routes/telegram/router.ts +++ b/lib/routes/telegram/router.ts @@ -1,10 +1,10 @@ import { config } from '@/config'; +import channelMedia from './tglib/channel-media'; export default (router) => { if (config.telegram.session) { router.get('/channel/:username', './tglib/channel'); - import('./tglib/channel-media').then((channelMedia) => - router.app.get('/channel/:username/:media', channelMedia.default)); + router.app.get('/channel/:username/:media', channelMedia); } else { router.get('/channel/:username/:routeParams?', './channel'); } diff --git a/lib/routes/telegram/tglib/channel-media.ts b/lib/routes/telegram/tglib/channel-media.ts index b624732868aaa2..4c3abe8d2a1b74 100644 --- a/lib/routes/telegram/tglib/channel-media.ts +++ b/lib/routes/telegram/tglib/channel-media.ts @@ -2,9 +2,9 @@ import { config } from '@/config'; import cacheModule from '@/utils/cache/index'; import { stream } from 'hono/streaming'; import { getAppropriatedPartSize } from 'telegram/Utils'; -import { Api } from 'telegram'; +import { Api, TelegramClient } from 'telegram'; import { returnBigInt as bigInt } from 'telegram/Helpers'; -import { client, getFilename } from './client'; +import { getClient, getFilename } from './client'; import { StreamingApi } from 'hono/utils/stream'; /** * https://core.telegram.org/api/files#stripped-thumbnails @@ -81,7 +81,7 @@ export function streamThumbnail(doc: Api.Document) { throw new Error('not supported'); } -export async function decodeMedia(channelName: string, x: string, retry = false) { +export async function decodeMedia(client: TelegramClient, channelName: string, x: string, retry = false) { const [channel, msg] = x.split('_'); try { @@ -93,13 +93,13 @@ export async function decodeMedia(channelName: string, x: string, retry = false) if (!retry) { // channel likely not seen before, we need to resolve ID and retry await client.getInputEntity(channelName); - return decodeMedia(channelName, x, true); + return decodeMedia(client, channelName, x, true); } throw error; } } -export function streamDocument(obj: Api.Document, thumbSize = '', offset?, limit?) { +export function streamDocument(client: TelegramClient, obj: Api.Document, thumbSize = '', offset?, limit?) { const chunkSize = (obj.size ? getAppropriatedPartSize(obj.size) : 64) * 1024; const iterFileParams = { file: new Api.InputDocumentFileLocation({ @@ -177,8 +177,8 @@ function streamResponse(c, bodyIter, cb?: (e: Error | undefined, s: StreamingApi export default async function handler(ctx) { await configureMiddlewares(ctx); - - const media = await decodeMedia(ctx.req.param('username'), ctx.req.param('media')); + const client = await getClient(); + const media = await decodeMedia(client, ctx.req.param('username'), ctx.req.param('media')); if (!media) { return ctx.text('Unknown media', 404); } @@ -212,14 +212,14 @@ export default async function handler(ctx) { !doc.mimeType.startsWith('image/')) { ctx.header('Content-Disposition', `attachment; filename="${encodeURIComponent(getFilename(media))}"`); } - stream = streamDocument(doc); + stream = streamDocument(client, doc); } else { const [offset, limit] = range[0]; // console.log(`${ctx.method} ${ctx.req.url} Range: ${rangeHeader}`); ctx.status(206); // partial content ctx.header('Content-Length', (limit - offset + 1).toString()); ctx.header('Content-Range', `bytes ${offset}-${limit}/${doc.size}`); - stream = streamDocument(doc, '', offset, limit); + stream = streamDocument(client, doc, '', offset, limit); } return streamResponse(ctx, stream, () => stream.close()); } diff --git a/lib/routes/telegram/tglib/channel.ts b/lib/routes/telegram/tglib/channel.ts index 4db20d76993046..1066ec6cfd53fb 100644 --- a/lib/routes/telegram/tglib/channel.ts +++ b/lib/routes/telegram/tglib/channel.ts @@ -1,8 +1,7 @@ -import wait from '@/utils/wait'; import { config } from '@/config'; import { Api } from 'telegram'; import { HTMLParser } from 'telegram/extensions/html'; -import { client, getFilename } from './client'; +import { getClient, getFilename } from './client'; function getMediaLink(ctx, channel, channelName, message) { const base = `${new URL(ctx.req.url).origin}/telegram/channel/${channelName}/`; @@ -36,12 +35,7 @@ function humanFileSize(size) { } export default async function handler(ctx) { - if (!config.telegram.session) { - return []; - } - if (!client.connected) { - await wait(1000); - } + const client = await getClient(); const item: object[] = []; const chat = await client.getInputEntity(ctx.req.param('username')); diff --git a/lib/routes/telegram/tglib/client.ts b/lib/routes/telegram/tglib/client.ts index bc3adca82a5787..946174289fd240 100644 --- a/lib/routes/telegram/tglib/client.ts +++ b/lib/routes/telegram/tglib/client.ts @@ -3,25 +3,31 @@ import * as readline from 'node:readline/promises'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; import { Api, TelegramClient } from 'telegram'; +import { UserAuthParams } from 'telegram/client/auth'; import { StringSession } from 'telegram/sessions'; -const apiId = Number(config.telegram.apiId ?? 4); -const apiHash = config.telegram.apiHash ?? '014b35b6184100b085b0d0572f9b5103'; - -const stringSession = new StringSession(config.telegram.session); -export const client = new TelegramClient(stringSession, apiId, apiHash, { - connectionRetries: Infinity, - autoReconnect: true, - retryDelay: 3000, - maxConcurrentDownloads: Number(config.telegram.maxConcurrentDownloads ?? 10), -}); +let client: TelegramClient | undefined; +export async function getClient(authParams?: UserAuthParams, session?: string) { + if (!config.telegram.session && session === undefined) { + throw new Error('TELEGRAM_SESSION is not configured'); + } + if (client) { + return client; + } + const apiId = Number(config.telegram.apiId ?? 4); + const apiHash = config.telegram.apiHash ?? '014b35b6184100b085b0d0572f9b5103'; -if (config.telegram.session) { - client.start({ - onError: (err) => { - throw 'Cannot start TG: ' + err; - }, + const stringSession = new StringSession(session ?? config.telegram.session); + client = new TelegramClient(stringSession, apiId, apiHash, { + connectionRetries: Infinity, + autoReconnect: true, + retryDelay: 3000, + maxConcurrentDownloads: Number(config.telegram.maxConcurrentDownloads ?? 10), }); + await client.start(Object.assign(authParams ?? {}, { + onError: (err) => { throw new Error('Cannot start TG: ' + err); }, + }) as any); + return client; } export function getFilename(x) { @@ -36,14 +42,13 @@ export function getFilename(x) { if (process.argv[1] === fileURLToPath(import.meta.url)) { Promise.resolve().then(async () => { - client.session = new StringSession(''); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - await client.start({ - phoneNumber: () => rl.question('Please enter your number: '), + const client = await getClient({ + phoneNumber: () => rl.question('Please enter your phone number: '), password: () => rl.question('Please enter your password: '), phoneCode: () => rl.question('Please enter the code you received: '), - onError: (err) => process.stderr.write(err), - }); + onError: (err) => process.stderr.write(err.toString()), + }, ''); process.stdout.write(`TELEGRAM_SESSION=${client.session.save()}\n`); process.exit(0); }); From e45372a1341f1b6334c918c2553c5bf4c1518f81 Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Fri, 8 Mar 2024 17:59:00 +0100 Subject: [PATCH 7/9] resolve typescript; fix private chats and groups; fix stickers and no-thumbnail document attachments; fix media-by-link attachments --- lib/routes/telegram/tglib/channel-media.ts | 88 +++++++++++----------- lib/routes/telegram/tglib/channel.ts | 81 ++++++++++++-------- lib/routes/telegram/tglib/client.ts | 25 ++++-- 3 files changed, 108 insertions(+), 86 deletions(-) diff --git a/lib/routes/telegram/tglib/channel-media.ts b/lib/routes/telegram/tglib/channel-media.ts index 4c3abe8d2a1b74..414025e6e5819a 100644 --- a/lib/routes/telegram/tglib/channel-media.ts +++ b/lib/routes/telegram/tglib/channel-media.ts @@ -1,19 +1,21 @@ -import { config } from '@/config'; -import cacheModule from '@/utils/cache/index'; +import { Context } from 'hono'; import { stream } from 'hono/streaming'; -import { getAppropriatedPartSize } from 'telegram/Utils'; import { Api, TelegramClient } from 'telegram'; +import { IterDownloadFunction } from 'telegram/client/downloads'; +import { getAppropriatedPartSize } from 'telegram/Utils'; +import { config } from '@/config'; +import cacheModule from '@/utils/cache/index'; +import { getClient, getDocument, getFilename } from './client'; import { returnBigInt as bigInt } from 'telegram/Helpers'; -import { getClient, getFilename } from './client'; -import { StreamingApi } from 'hono/utils/stream'; + /** * https://core.telegram.org/api/files#stripped-thumbnails * @param bytes Buffer * @returns Buffer jpeg */ -function ExpandInlineBytes(bytes) { +function ExpandInlineBytes(bytes: Buffer) { if (bytes.length < 3 || bytes[0] !== 0x1) { - return []; + throw new Error('cannot inflate a stripped jpeg'); } const header = Buffer.from([ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x28, 0x1c, 0x1e, 0x23, 0x1e, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2d, 0x2b, @@ -44,7 +46,7 @@ function ExpandInlineBytes(bytes) { return real; } -function sortThumb(thumb) { +function sortThumb(thumb: Api.TypePhotoSize) { if (thumb instanceof Api.PhotoStrippedSize) { return thumb.bytes.length; } @@ -57,28 +59,25 @@ function sortThumb(thumb) { if (thumb instanceof Api.PhotoSizeProgressive) { return Math.max(...thumb.sizes); } - if (thumb instanceof Api.VideoSize) { - return thumb.size; - } return 0; } -function chooseLargestThumb(thumbs) { +function chooseLargestThumb(thumbs: Api.TypePhotoSize[]) { thumbs = [...thumbs].sort((a, b) => sortThumb(a) - sortThumb(b)); return thumbs.pop(); } -export function streamThumbnail(doc: Api.Document) { +export async function* streamThumbnail(client: TelegramClient, doc: Api.Document) { if (doc.thumbs?.length ?? 0 > 0) { - const size = chooseLargestThumb(doc.thumbs); + const size = chooseLargestThumb(doc.thumbs!); if (size instanceof Api.PhotoCachedSize || size instanceof Api.PhotoStrippedSize) { - return (function* () { - yield ExpandInlineBytes(size.bytes); - })(); + yield ExpandInlineBytes(size.bytes); + } else { + yield* streamDocument(client, doc, size && 'type' in size ? size.type : ''); } - return streamDocument(doc, size && 'type' in size ? size.type : ''); + return; } - throw new Error('not supported'); + throw new Error('no thumbnails available'); } export async function decodeMedia(client: TelegramClient, channelName: string, x: string, retry = false) { @@ -99,9 +98,9 @@ export async function decodeMedia(client: TelegramClient, channelName: string, x } } -export function streamDocument(client: TelegramClient, obj: Api.Document, thumbSize = '', offset?, limit?) { +export async function* streamDocument(client: TelegramClient, obj: Api.Document, thumbSize = '', offset?: bigInt.BigInteger, limit?: bigInt.BigInteger) { const chunkSize = (obj.size ? getAppropriatedPartSize(obj.size) : 64) * 1024; - const iterFileParams = { + const iterFileParams: IterDownloadFunction = { file: new Api.InputDocumentFileLocation({ id: obj.id, accessHash: obj.accessHash, @@ -118,12 +117,14 @@ export function streamDocument(client: TelegramClient, obj: Api.Document, thumbS iterFileParams.offset = offset; } if (limit) { - iterFileParams.limit = limit; + iterFileParams.limit = limit.valueOf(); } - return client.iterDownload(iterFileParams); + const stream = client.iterDownload(iterFileParams); + yield* stream; + await stream.close(); } -function parseRange(range, length) { +function parseRange(range: string, length: bigInt.BigInteger) { if (!range) { return []; } @@ -132,7 +133,7 @@ function parseRange(range, length) { throw new Error(`unsupported range: ${typ}`); } const segs = segstr.split(',').map((s) => s.trim()); - const parsedSegs: Array[] = []; // actually BigInt but ts is unhappy + const parsedSegs: bigInt.BigInteger[][] = []; for (const seg of segs) { const range = seg .split('-', 2) @@ -140,9 +141,9 @@ function parseRange(range, length) { .map(bigInt); if (range.length < 2) { if (seg.startsWith('-')) { - range.unshift(0); + range.unshift(bigInt(0)); } else { - range.push(length); + range.push(length.subtract(bigInt(1))); } } parsedSegs.push(range); @@ -150,14 +151,14 @@ function parseRange(range, length) { return parsedSegs; } -async function configureMiddlewares(ctx) { +async function configureMiddlewares(ctx: Context) { // media is too heavy to cache in memory or redis, and lock-up is not needed await cacheModule.set(ctx.get('cacheControlKey'), '0', config.cache.requestTimeout); ctx.req.raw.headers.delete('Accept-Encoding'); // avoid hono compress() middleware detecting Accept-Encoding on req ctx.set('no-template', true); // skip RSSHub template middleware } -function streamResponse(c, bodyIter, cb?: (e: Error | undefined, s: StreamingApi) => Promise) { +function streamResponse(c: Context, bodyIter: AsyncGenerator) { return stream(c, async (stream) => { let aborted = false; stream.onAbort(() => { @@ -169,13 +170,10 @@ function streamResponse(c, bodyIter, cb?: (e: Error | undefined, s: StreamingApi // console.log(`writing ${chunk.length / 1024}kB`); await stream.write(chunk); } - if (cb) { - await cb(undefined, stream); - } - }, cb); + }); } -export default async function handler(ctx) { +export default async function handler(ctx: Context) { await configureMiddlewares(ctx); const client = await getClient(); const media = await decodeMedia(client, ctx.req.param('username'), ctx.req.param('media')); @@ -188,40 +186,38 @@ export default async function handler(ctx) { return new Response(buf, {headers: {'Content-Type': 'image/jpeg'}}); } - if (media instanceof Api.MessageMediaDocument) { - const doc = media.document as Api.Document; + const doc = getDocument(media); + if (doc) { if ('thumb' in ctx.req.query()) { ctx.header('Content-Type', 'image/jpeg'); - return streamResponse(ctx, streamThumbnail(doc)); + return streamResponse(ctx, streamThumbnail(client, doc)); } ctx.header('Content-Type', doc.mimeType); ctx.header('Accept-Ranges', 'bytes'); ctx.header('Content-Security-Policy', "default-src 'self'"); - const rangeHeader = ctx.req.header('Range'); - const range = parseRange(rangeHeader, doc.size.valueOf() - 1); + const rangeHeader = ctx.req.header('Range') ?? ''; + const range = parseRange(rangeHeader, doc.size); if (range.length > 1) { return ctx.text('Not Satisfiable', 416); } - let stream; if (range.length === 0) { - ctx.header('Content-Length', doc.size); + ctx.header('Content-Length', doc.size.toString()); if (!doc.mimeType.startsWith('video/') && !doc.mimeType.startsWith('audio/') && !doc.mimeType.startsWith('image/')) { ctx.header('Content-Disposition', `attachment; filename="${encodeURIComponent(getFilename(media))}"`); } - stream = streamDocument(client, doc); + return streamResponse(ctx, streamDocument(client, doc)); } else { const [offset, limit] = range[0]; - // console.log(`${ctx.method} ${ctx.req.url} Range: ${rangeHeader}`); + // console.log(`Range: ${rangeHeader}`); ctx.status(206); // partial content - ctx.header('Content-Length', (limit - offset + 1).toString()); + ctx.header('Content-Length', (limit.subtract(offset).add(1)).toString()); ctx.header('Content-Range', `bytes ${offset}-${limit}/${doc.size}`); - stream = streamDocument(client, doc, '', offset, limit); + return streamResponse(ctx, streamDocument(client, doc, '', offset, limit)); } - return streamResponse(ctx, stream, () => stream.close()); } return ctx.text(media.className, 415); diff --git a/lib/routes/telegram/tglib/channel.ts b/lib/routes/telegram/tglib/channel.ts index 1066ec6cfd53fb..bf935002928491 100644 --- a/lib/routes/telegram/tglib/channel.ts +++ b/lib/routes/telegram/tglib/channel.ts @@ -1,55 +1,72 @@ -import { config } from '@/config'; +import { Context } from 'hono'; import { Api } from 'telegram'; import { HTMLParser } from 'telegram/extensions/html'; -import { getClient, getFilename } from './client'; +import { getClient, getDocument, getFilename } from './client'; +import { getDisplayName } from 'telegram/Utils'; -function getMediaLink(ctx, channel, channelName, message) { - const base = `${new URL(ctx.req.url).origin}/telegram/channel/${channelName}/`; - const src = base + `${channel.channelId}_${message.id}`; +function getPeerId(p: Api.TypePeer) { + return p instanceof Api.PeerChannel ? p.channelId : + p instanceof Api.PeerUser ? p.userId : + /* groups are negative */ p.chatId.multiply(-1); +} + +function getMediaLink(src: string, m: Api.TypeMessageMedia) { + const doc = getDocument(m); + const mime = doc ? doc.mimeType : ''; - const x = message.media; - if (x instanceof Api.MessageMediaPhoto || (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('image/'))) { + if (m instanceof Api.MessageMediaPhoto || mime.startsWith('image/')) { return ``; } - if (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('video/')) { - const vid = x.document.attributes.find((t) => t.className === 'DocumentAttributeVideo') ?? { w: 1080, h: 720 }; - return ``; + if (doc && mime.startsWith('video/')) { + const vid = (doc.attributes.find((t) => t instanceof Api.DocumentAttributeVideo) ?? { w: 1080, h: 720 }) as {w: number, h: number}; + return ``; } - if (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('audio/')) { + if (mime.startsWith('audio/')) { return ``; } - if (x instanceof Api.MessageMediaGeo) { - return `Geo LatLon: ${x.geo.lat}, ${x.geo.long}`; + + if (doc && mime.startsWith('application/')) { + let linkText = `${getFilename(m)} (${humanFileSize(doc.size.valueOf())})`; + if (mime.endsWith('x-tgsticker')) { + linkText = ''; // remove filename, it's only an animated sticker + } + if ((doc.thumbs?.length ?? 0) > 0) { + linkText = `
${linkText}`; + } + return `${linkText}`; + } + if (m instanceof Api.MessageMediaGeo && m.geo instanceof Api.GeoPoint) { + return `Geo LatLon: ${m.geo.lat}, ${m.geo.long}`; } - let linkText = getFilename(x); - if (x instanceof Api.MessageMediaDocument) { - linkText += ` (${humanFileSize(x.document.size)})`; - return `
${linkText}
`; + if (m instanceof Api.MessageMediaWebPage) { + return ''; // a link without a document attach, usually is in the message text, so we can skip here } - return x.className; + + return m.className; } -function humanFileSize(size) { +function humanFileSize(size: number) { const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); - return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; + return (size / Math.pow(1024, i)).toFixed(2) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; } -export default async function handler(ctx) { +export default async function handler(ctx: Context) { const client = await getClient(); - - const item: object[] = []; - const chat = await client.getInputEntity(ctx.req.param('username')); - const channelInfo = await client.getEntity(chat) as Api.Channel; + const username = ctx.req.param('username'); + const peer = await client.getInputEntity(username); + const entity = await client.getEntity(peer); let attachments: string[] = []; - const messages = await client.getMessages(chat, { limit: 50 }); + const messages = await client.getMessages(peer, { limit: 50 }); let i = 0; + const item: object[] = []; for (const message of messages) { if (message.media) { // messages that have no text are shown as if they're one post // because in TG only 1 attachment per message is possible - attachments.push(getMediaLink(ctx, chat, ctx.req.param('username'), message)); + const src = `${new URL(ctx.req.url).origin}/telegram/channel/${username}/${getPeerId(message.peerId)}_${message.id}`; + attachments.push(getMediaLink(src, message.media)); } if (message.text !== '' || ++i === messages.length) { let description = attachments.join('
\n'); @@ -64,18 +81,18 @@ export default async function handler(ctx) { title, description, pubDate: new Date(message.date * 1000).toUTCString(), - link: `https://t.me/s/${channelInfo.username}/${message.id}`, - author: `${channelInfo.title} (@${channelInfo.username})`, + link: `https://t.me/s/${username}/${message.id}`, + author: getDisplayName(message.sender ?? entity), }); } } ctx.set('data', { - title: channelInfo.title, + title: getDisplayName(entity), language: null, - link: `https://t.me/${channelInfo.username}`, + link: `https://t.me/${username}`, item, allowEmpty: ctx.req.param('id') === 'allow_empty', - description: `@${channelInfo.username} on Telegram`, + description: `@${username} on Telegram`, }); } diff --git a/lib/routes/telegram/tglib/client.ts b/lib/routes/telegram/tglib/client.ts index 946174289fd240..525f7bcd2ce275 100644 --- a/lib/routes/telegram/tglib/client.ts +++ b/lib/routes/telegram/tglib/client.ts @@ -7,7 +7,7 @@ import { UserAuthParams } from 'telegram/client/auth'; import { StringSession } from 'telegram/sessions'; let client: TelegramClient | undefined; -export async function getClient(authParams?: UserAuthParams, session?: string) { +export async function getClient(authParams?: Partial, session?: string) { if (!config.telegram.session && session === undefined) { throw new Error('TELEGRAM_SESSION is not configured'); } @@ -25,29 +25,38 @@ export async function getClient(authParams?: UserAuthParams, session?: string) { maxConcurrentDownloads: Number(config.telegram.maxConcurrentDownloads ?? 10), }); await client.start(Object.assign(authParams ?? {}, { - onError: (err) => { throw new Error('Cannot start TG: ' + err); }, + onError: (err: Error) => { throw new Error('Cannot start TG: ' + err); }, }) as any); return client; } -export function getFilename(x) { +export function getFilename(x: Api.TypeMessageMedia) { if (x instanceof Api.MessageMediaDocument) { - const docFilename = x.document.attributes.find((a) => a.className === 'DocumentAttributeFilename'); - if (docFilename) { - return docFilename.fileName; + for (const a of (x.document as Api.Document).attributes) { + if (a instanceof Api.DocumentAttributeFilename) { + return a.fileName; + } } } return x.className; } +export function getDocument(m: Api.TypeMessageMedia) { + if (m instanceof Api.MessageMediaDocument && m.document && !(m.document instanceof Api.DocumentEmpty)) { + return m.document; + } + if (m instanceof Api.MessageMediaWebPage && m.webpage instanceof Api.WebPage && m.webpage.document instanceof Api.Document) { + return m.webpage.document; + } +} + if (process.argv[1] === fileURLToPath(import.meta.url)) { Promise.resolve().then(async () => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const client = await getClient({ phoneNumber: () => rl.question('Please enter your phone number: '), password: () => rl.question('Please enter your password: '), - phoneCode: () => rl.question('Please enter the code you received: '), - onError: (err) => process.stderr.write(err.toString()), + phoneCode: () => rl.question('Please enter the code you received: ') }, ''); process.stdout.write(`TELEGRAM_SESSION=${client.session.save()}\n`); process.exit(0); From 1a966cdcd323c71bbf88bbf862ec2d5fae7af874 Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Fri, 8 Mar 2024 21:00:01 +0100 Subject: [PATCH 8/9] fix empty text and message grouping; add GeoLive media type; add Poll/Quiz media type --- lib/routes/telegram/tglib/channel.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/routes/telegram/tglib/channel.ts b/lib/routes/telegram/tglib/channel.ts index bf935002928491..c8f4f5b3ab4b84 100644 --- a/lib/routes/telegram/tglib/channel.ts +++ b/lib/routes/telegram/tglib/channel.ts @@ -35,9 +35,13 @@ function getMediaLink(src: string, m: Api.TypeMessageMedia) { } return `${linkText}`; } - if (m instanceof Api.MessageMediaGeo && m.geo instanceof Api.GeoPoint) { + if ((m instanceof Api.MessageMediaGeo || m instanceof Api.MessageMediaGeoLive) && m.geo instanceof Api.GeoPoint) { return `Geo LatLon: ${m.geo.lat}, ${m.geo.long}`; } + if (m instanceof Api.MessageMediaPoll) { + return `

${m.poll.quiz ? 'Quiz' : 'Poll'}: ${m.poll.question}


+
    ${m.poll.answers.map((a) => `
  • ${a.text}
  • `).join('')}
`; + } if (m instanceof Api.MessageMediaWebPage) { return ''; // a link without a document attach, usually is in the message text, so we can skip here } @@ -62,13 +66,18 @@ export default async function handler(ctx: Context) { let i = 0; const item: object[] = []; for (const message of messages) { + if (message.fwdFrom?.fromId !== undefined) { + // eslint-disable-next-line no-await-in-loop + const fwdFrom = await client.getEntity(message.fwdFrom.fromId); + attachments.push(`Forwarded from: ${getDisplayName(fwdFrom)}:`); + } if (message.media) { // messages that have no text are shown as if they're one post // because in TG only 1 attachment per message is possible const src = `${new URL(ctx.req.url).origin}/telegram/channel/${username}/${getPeerId(message.peerId)}_${message.id}`; attachments.push(getMediaLink(src, message.media)); } - if (message.text !== '' || ++i === messages.length) { + if (message.text !== '' || ++i === messages.length - 1) { let description = attachments.join('
\n'); attachments = []; // emitting these, buffer other ones @@ -76,7 +85,7 @@ export default async function handler(ctx: Context) { description += `

${HTMLParser.unparse(message.message, message.entities).replaceAll('\n', '
')}

`; } - const title = message.text ?? new Date(message.date * 1000).toLocaleString(); + const title = message.text ? message.text : new Date(message.date * 1000).toLocaleString(); item.push({ title, description, From feb3f9003d6ec1caaa2dce1aa1e1d8b3a6e3e031 Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Sat, 9 Mar 2024 11:00:04 +0100 Subject: [PATCH 9/9] add script-src none to CSP Co-authored-by: Tony --- lib/routes/telegram/tglib/channel-media.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes/telegram/tglib/channel-media.ts b/lib/routes/telegram/tglib/channel-media.ts index 414025e6e5819a..ceadd704ad7667 100644 --- a/lib/routes/telegram/tglib/channel-media.ts +++ b/lib/routes/telegram/tglib/channel-media.ts @@ -194,7 +194,7 @@ export default async function handler(ctx: Context) { } ctx.header('Content-Type', doc.mimeType); ctx.header('Accept-Ranges', 'bytes'); - ctx.header('Content-Security-Policy', "default-src 'self'"); + ctx.header('Content-Security-Policy', "default-src 'self'; script-src 'none'"); const rangeHeader = ctx.req.header('Range') ?? ''; const range = parseRange(rangeHeader, doc.size);