release: v1.4.3
GitHub Release / release (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s
Add a Telegram channel or group storage backend, with proxied image URLs and an optional API proxy.
This commit is contained in:
1 parent
4ed4692f5e
commit
cc19dc6435
32 files changed
+1527
-143
No files matched your search
@@ -0,0 +1,297 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
||||
import {
|
||||
parseTelegramConfig,
|
||||
parseTelegramSecrets,
|
||||
validateApiProxyUrl,
|
||||
validateTelegramConfig,
|
||||
TelegramStorageBackend,
|
||||
TELEGRAM_MAX_FILE_BYTES
|
||||
} from './telegram'
|
||||
import {
|
||||
deleteStorageObjectRef,
|
||||
getStorageObjectRef
|
||||
} from './storage-object-refs'
|
||||
import {
|
||||
ensureDefaultBackends,
|
||||
ensureStorageSchema,
|
||||
getStorageBackendRow,
|
||||
insertTelegramBackend
|
||||
} from '../storage-backends'
|
||||
|
||||
describe('telegram config', () => {
|
||||
it('parses chat id and optional proxy', () => {
|
||||
const config = parseTelegramConfig(JSON.stringify({
|
||||
chatId: '-1001',
|
||||
apiProxyUrl: 'http://127.0.0.1:7890'
|
||||
}))
|
||||
expect(config).toEqual({
|
||||
chatId: '-1001',
|
||||
apiProxyUrl: 'http://127.0.0.1:7890'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects invalid proxy protocol', () => {
|
||||
expect(validateApiProxyUrl('socks5://127.0.0.1:1080')).toMatch(/http/)
|
||||
})
|
||||
|
||||
it('accepts http proxy', () => {
|
||||
expect(validateApiProxyUrl('http://127.0.0.1:7890')).toBeNull()
|
||||
})
|
||||
|
||||
it('validates required chat id', () => {
|
||||
expect(validateTelegramConfig({ chatId: '' })).toBeTruthy()
|
||||
expect(validateTelegramConfig({ chatId: '-100' })).toBeNull()
|
||||
})
|
||||
|
||||
it('parses bot token secret', () => {
|
||||
expect(parseTelegramSecrets(JSON.stringify({ botToken: '123:abc' }))).toEqual({
|
||||
botToken: '123:abc'
|
||||
})
|
||||
})
|
||||
|
||||
it('exposes 20MB limit constant', () => {
|
||||
expect(TELEGRAM_MAX_FILE_BYTES).toBe(20 * 1024 * 1024)
|
||||
})
|
||||
})
|
||||
|
||||
describe('TelegramStorageBackend', () => {
|
||||
const backendId = 'tg-test'
|
||||
const key = 'images/2026/01/test.webp'
|
||||
|
||||
beforeEach(() => {
|
||||
ensureStorageSchema()
|
||||
ensureDefaultBackends()
|
||||
if (!getStorageBackendRow(backendId)) {
|
||||
insertTelegramBackend({
|
||||
id: backendId,
|
||||
name: 'TG test',
|
||||
config: { chatId: '-100' },
|
||||
secrets: { botToken: 'token' },
|
||||
quotaBytes: 1024 * 1024 * 1024,
|
||||
enabled: false
|
||||
})
|
||||
}
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
deleteStorageObjectRef(backendId, key)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('retries sendDocument after supergroup chat id migration', async () => {
|
||||
const fetchMock = vi.mocked(fetch)
|
||||
fetchMock
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
ok: false,
|
||||
description: 'Bad Request: group chat was upgraded to a supergroup chat',
|
||||
parameters: { migrate_to_chat_id: -100999 }
|
||||
})
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 99,
|
||||
document: { file_id: 'MIG', file_unique_id: 'UQ2' }
|
||||
}
|
||||
})
|
||||
} as Response)
|
||||
|
||||
const backend = new TelegramStorageBackend(
|
||||
backendId,
|
||||
{ chatId: '-123' },
|
||||
{ botToken: 'token' }
|
||||
)
|
||||
|
||||
await backend.put(key, new Uint8Array([4, 5]), {
|
||||
originalName: 'test.webp',
|
||||
uploadedAt: new Date().toISOString(),
|
||||
contentType: 'image/webp',
|
||||
size: 2
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
const ref = getStorageObjectRef(backendId, key)
|
||||
expect(ref?.fileId).toBe('MIG')
|
||||
})
|
||||
|
||||
it('put accepts a single photo object and sticker file id', async () => {
|
||||
const fetchMock = vi.mocked(fetch)
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 8,
|
||||
sticker: { file_id: 'stk', file_unique_id: 'us' }
|
||||
}
|
||||
})
|
||||
} as Response)
|
||||
|
||||
const backend = new TelegramStorageBackend(
|
||||
backendId,
|
||||
{ chatId: '-100' },
|
||||
{ botToken: 'token' }
|
||||
)
|
||||
|
||||
await backend.put(key, new Uint8Array([1]), {
|
||||
originalName: 'test.webp',
|
||||
uploadedAt: new Date().toISOString(),
|
||||
contentType: 'image/webp',
|
||||
size: 1
|
||||
})
|
||||
|
||||
expect(getStorageObjectRef(backendId, key)?.fileId).toBe('stk')
|
||||
})
|
||||
|
||||
it('put accepts photo array when document is absent', async () => {
|
||||
const fetchMock = vi.mocked(fetch)
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 7,
|
||||
photo: [
|
||||
{ file_id: 'small', file_unique_id: 'u1', width: 90, height: 90 },
|
||||
{ file_id: 'large', file_unique_id: 'u2', width: 800, height: 600 }
|
||||
]
|
||||
}
|
||||
})
|
||||
} as Response)
|
||||
|
||||
const backend = new TelegramStorageBackend(
|
||||
backendId,
|
||||
{ chatId: '-100' },
|
||||
{ botToken: 'token' }
|
||||
)
|
||||
|
||||
await backend.put(key, new Uint8Array([1]), {
|
||||
originalName: 'test.png',
|
||||
uploadedAt: new Date().toISOString(),
|
||||
contentType: 'image/png',
|
||||
size: 1
|
||||
})
|
||||
|
||||
const ref = getStorageObjectRef(backendId, key)
|
||||
expect(ref?.fileId).toBe('large')
|
||||
})
|
||||
|
||||
it('put stores file ref after sendDocument', async () => {
|
||||
const fetchMock = vi.mocked(fetch)
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 42,
|
||||
document: {
|
||||
file_id: 'AAA',
|
||||
file_unique_id: 'UQ'
|
||||
}
|
||||
}
|
||||
})
|
||||
} as Response)
|
||||
|
||||
const backend = new TelegramStorageBackend(
|
||||
backendId,
|
||||
{ chatId: '-100' },
|
||||
{ botToken: 'token' }
|
||||
)
|
||||
|
||||
await backend.put(key, new Uint8Array([1, 2, 3]), {
|
||||
originalName: 'test.webp',
|
||||
uploadedAt: new Date().toISOString(),
|
||||
contentType: 'image/webp',
|
||||
size: 3
|
||||
})
|
||||
|
||||
const ref = getStorageObjectRef(backendId, key)
|
||||
expect(ref?.fileId).toBe('AAA')
|
||||
expect(ref?.messageId).toBe(42)
|
||||
})
|
||||
|
||||
it('testConnection reports telegram errors', async () => {
|
||||
const fetchMock = vi.mocked(fetch)
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ ok: false, description: 'Unauthorized' })
|
||||
} as Response)
|
||||
|
||||
const backend = new TelegramStorageBackend(
|
||||
backendId,
|
||||
{ chatId: '-100' },
|
||||
{ botToken: 'bad' }
|
||||
)
|
||||
|
||||
const result = await backend.testConnection()
|
||||
expect(result.ok).toBe(false)
|
||||
expect(result.message).toContain('Unauthorized')
|
||||
})
|
||||
|
||||
it('createStream downloads file bytes', async () => {
|
||||
const fetchMock = vi.mocked(fetch)
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 1,
|
||||
document: { file_id: 'FILE1', file_unique_id: 'U1' }
|
||||
}
|
||||
})
|
||||
} as Response)
|
||||
|
||||
const putBackend = new TelegramStorageBackend(
|
||||
backendId,
|
||||
{ chatId: '-100' },
|
||||
{ botToken: 'token' }
|
||||
)
|
||||
await putBackend.put(key, new Uint8Array([1]), {
|
||||
originalName: 'test.webp',
|
||||
uploadedAt: new Date().toISOString(),
|
||||
contentType: 'image/webp',
|
||||
size: 1
|
||||
})
|
||||
|
||||
fetchMock.mockReset()
|
||||
fetchMock
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
result: { file_id: 'FILE1', file_path: 'photos/file.jpg' }
|
||||
})
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
arrayBuffer: async () => new Uint8Array([9, 8, 7]).buffer
|
||||
} as Response)
|
||||
|
||||
const backend = new TelegramStorageBackend(
|
||||
backendId,
|
||||
{ chatId: '-100' },
|
||||
{ botToken: 'token' }
|
||||
)
|
||||
|
||||
const stream = await backend.createStream(key)
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
}
|
||||
expect(Buffer.concat(chunks)).toEqual(Buffer.from([9, 8, 7]))
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user