feat: add Telegram sticker import functionality

- Implemented TelegramImport component for importing sticker packs from Telegram using a bot token.
- Added utility functions for animated sticker conversion (TGS and WEBM to GIF).
- Created telegramStickerImport module to handle fetching and processing of Telegram sticker sets.
- Introduced GIF.js type definitions for TypeScript support.
This commit is contained in:
2026-02-19 21:57:25 +11:00
parent eb3768ae30
commit 21d4bc6ad9
9 changed files with 968 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { GlobalPacks } from './GlobalPacks';
import { UserPack } from './UserPack';
import { TelegramImport } from './TelegramImport';
import { ImagePack } from '../../../plugins/custom-emoji';
import { ImagePackView } from '../../../components/image-pack-view';
@@ -42,6 +43,7 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) {
<Box direction="Column" gap="700">
<UserPack onViewPack={setImagePack} />
<GlobalPacks onViewPack={setImagePack} />
<TelegramImport onViewPack={setImagePack} />
</Box>
</PageContent>
</Scroll>

View File

@@ -0,0 +1,167 @@
import React, { useState, useCallback } from 'react';
import {
Box,
Text,
Button,
Input,
Spinner,
color,
toRem,
} from 'folds';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import {
extractPackName,
importTelegramStickerPack,
} from '../../../utils/telegramStickerImport';
import { getUserImagePack, ImagePack } from '../../../plugins/custom-emoji';
const TELEGRAM_BOT_TOKEN_KEY = 'cinny_telegram_bot_token';
type TelegramImportProps = {
onViewPack?: (pack: ImagePack) => void;
};
/**
* Component for importing Telegram sticker packs
*/
export function TelegramImport({ onViewPack }: TelegramImportProps) {
const mx = useMatrixClient();
const [botToken, setBotToken] = useState(() =>
localStorage.getItem(TELEGRAM_BOT_TOKEN_KEY) || ''
);
const [packUrl, setPackUrl] = useState('');
const [importing, setImporting] = useState(false);
const [progress, setProgress] = useState<string>('');
const [error, setError] = useState<string>('');
const [success, setSuccess] = useState<string>('');
const handleBotTokenChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const token = e.target.value;
setBotToken(token);
localStorage.setItem(TELEGRAM_BOT_TOKEN_KEY, token);
}, []);
const handlePackUrlChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setPackUrl(e.target.value);
setError('');
setSuccess('');
}, []);
const handleImport = useCallback(async () => {
if (!botToken.trim()) {
setError('Please enter your Telegram Bot token');
return;
}
const packName = extractPackName(packUrl);
if (!packName) {
setError('Invalid Telegram sticker pack URL. Use format: t.me/addstickers/PackName');
return;
}
setImporting(true);
setError('');
setSuccess('');
setProgress('Starting import...');
try {
const result = await importTelegramStickerPack(
mx,
botToken.trim(),
packUrl.trim(),
(current, total, status) => {
setProgress(status);
}
);
const count = Object.keys(result.content.images || {}).length;
setSuccess(`Successfully imported ${count} stickers!`);
setPackUrl('');
// Show the user's pack with the newly imported stickers
const userPack = getUserImagePack(mx);
if (userPack && onViewPack) {
onViewPack(userPack);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to import sticker pack');
} finally {
setImporting(false);
setProgress('');
}
}, [mx, botToken, packUrl, onViewPack]);
return (
<Box direction="Column" gap="100">
<Text size="L400">Import from Telegram</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Telegram Bot Token"
description="Create a bot via @BotFather on Telegram to get a token. Your token is stored locally."
>
<Input
type="password"
value={botToken}
onChange={handleBotTokenChange}
placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
style={{ width: '100%', marginTop: toRem(8) }}
disabled={importing}
/>
</SettingTile>
<SettingTile
title="Sticker Pack URL"
description="Paste a Telegram sticker pack link (e.g., t.me/addstickers/PackName)"
>
<Input
value={packUrl}
onChange={handlePackUrlChange}
placeholder="https://t.me/addstickers/YourPackName"
style={{ width: '100%', marginTop: toRem(8) }}
disabled={importing}
/>
</SettingTile>
{error && (
<Text size="T200" style={{ color: color.Critical.Main }}>
{error}
</Text>
)}
{success && (
<Text size="T200" style={{ color: color.Success.Main }}>
{success}
</Text>
)}
{importing && progress && (
<Box gap="200" alignItems="Center">
<Spinner size="200" variant="Secondary" />
<Text size="T200" priority="300">{progress}</Text>
</Box>
)}
<Box justifyContent="End">
<Button
variant="Secondary"
fill="Soft"
size="300"
radii="300"
onClick={handleImport}
disabled={importing || !packUrl.trim()}
>
<Text size="B300">{importing ? 'Importing...' : 'Import Pack'}</Text>
</Button>
</Box>
</SequenceCard>
</Box>
);
}