Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
74fb8cb
feat: add comprehensive Telegram Bot API provider
KumarVandit Apr 18, 2026
8c53775
fix: skip bot.start() polling when webhook mode is active
KumarVandit Apr 18, 2026
74c4547
Remove webhook support, defer to future integration
KumarVandit Apr 18, 2026
63d115b
Add webhook support for Telegram provider
KumarVandit Apr 18, 2026
1ed749a
Add Inline Mode and HTML5 Games support
KumarVandit Apr 18, 2026
d6e8e57
Remove webhook server, refactor event handling with EventSink pattern
KumarVandit Apr 18, 2026
9c6f2bf
Address CodeRabbit review findings
KumarVandit Apr 18, 2026
0a5c4b4
Fix HttpError retry and chatMember userId mapping
KumarVandit Apr 18, 2026
2557f09
Update README with Telegram provider and new platform actions
KumarVandit Apr 18, 2026
daba827
Address CodeRabbit README and type assertion feedback
KumarVandit Apr 18, 2026
0fd8252
Capture game callbacks and document by-design decisions
KumarVandit Apr 19, 2026
911ca45
feat(spectrum-ts): add contact content type with vCard support
underthestars-zhy Apr 19, 2026
12a3280
refactor(examples): simplify basic example to use terminal provider
underthestars-zhy Apr 19, 2026
5299fc8
Update README to document contact content type
underthestars-zhy Apr 19, 2026
2858b28
Merge contact content type from ryan/contact-card
KumarVandit Apr 19, 2026
70f618e
Wire contact content type through Telegram provider
KumarVandit Apr 19, 2026
101b273
Add example bots: AI moderator (AI SDK) and inline knowledge agent (M…
KumarVandit Apr 20, 2026
4932d4f
Add voice message content type with ffmpeg transcoding support
underthestars-zhy Apr 20, 2026
5cd1392
Merge voice content type from ryan/feature-content-voice
KumarVandit Apr 20, 2026
71c988c
Wire voice content type through Telegram provider
KumarVandit Apr 20, 2026
69b474f
Merge latest main to resolve conflicts
KumarVandit Apr 20, 2026
ea15b2f
Remove example bots from this PR
KumarVandit Apr 20, 2026
715fcea
Merge remote-tracking branch 'origin/main' into telegram-support
KumarVandit Apr 21, 2026
2038041
Align Telegram provider with OutboundMessage + SendResult APIs
KumarVandit Apr 21, 2026
4ab2df1
Expose platform client on PlatformInstance
KumarVandit Apr 21, 2026
e47f080
Strip Telegram provider to universal Spectrum APIs only
KumarVandit Apr 22, 2026
81b1dd4
Rewrite Telegram provider against Bot API directly
KumarVandit Apr 23, 2026
b00ac6a
Address CodeRabbit review findings on Telegram provider
KumarVandit Apr 23, 2026
4c0dd56
Merge remote-tracking branch 'origin/main' into telegram-support
KumarVandit Apr 23, 2026
5600288
Add richlink content type support to Telegram provider
KumarVandit Apr 23, 2026
9b81545
Use full LinkPreviewOptions API for Telegram richlinks
KumarVandit Apr 23, 2026
898b6e4
Merge remote-tracking branch 'origin/main' into telegram-support
KumarVandit Apr 23, 2026
2db84cd
Add reaction support to Telegram provider
KumarVandit Apr 24, 2026
dd8d9e7
Address CodeRabbit review: retry/client/events hardening + Bot API 9.…
KumarVandit Apr 24, 2026
72e7f83
Address CodeRabbit re-review: transport unification + leak/channel-po…
KumarVandit Apr 24, 2026
535de51
Fix CodeRabbit: decouple lazy media reads + defer signal cleanup
KumarVandit Apr 24, 2026
6a1f08f
Address CodeRabbit nitpicks: input validation + error context + gener…
KumarVandit Apr 24, 2026
a585da1
Merge remote-tracking branch 'origin/main' into telegram-support
KumarVandit Apr 25, 2026
ffa8367
feat(provider/telegram): sync with PR #33/#35 + ship cache-free addit…
KumarVandit Apr 26, 2026
cd51deb
docs(provider/telegram): document richlink customization drop
KumarVandit Apr 27, 2026
44ba062
fix(provider/telegram): nested-blob multipart corruption + retry_afte…
KumarVandit Apr 27, 2026
a4b90c4
refactor(provider/telegram): split events.ts into events/{inbound,rea…
KumarVandit Apr 27, 2026
e48b73d
feat(provider/telegram): in-process LRU cache for getMessage, reactio…
KumarVandit Apr 28, 2026
b21a618
Merge remote-tracking branch 'origin/main' into telegram-support
KumarVandit Apr 28, 2026
023b57e
fix(provider/telegram): align with upstream PR #38 + #39 contracts
KumarVandit Apr 28, 2026
b75af6d
fix(provider/telegram): address CodeRabbit review findings on PR #16
KumarVandit Apr 28, 2026
74f5288
fix(provider/telegram): address CodeRabbit follow-up review on PR #16
KumarVandit Apr 28, 2026
4fe808d
fix(provider/telegram): address third CodeRabbit review on PR #16
KumarVandit Apr 28, 2026
68e4933
fix(provider/telegram): address fourth CodeRabbit review on PR #16
KumarVandit Apr 28, 2026
dac6b91
chore(provider/telegram): address fifth CodeRabbit nitpick pass on PR…
KumarVandit Apr 28, 2026
91a172f
fix(provider/telegram): address sixth CodeRabbit review on PR #16
KumarVandit Apr 28, 2026
5df6691
fix(provider/telegram): address seventh CodeRabbit review on PR #16
KumarVandit Apr 28, 2026
90455e2
fix(provider/telegram): address eighth CodeRabbit review on PR #16
KumarVandit Apr 28, 2026
153d0e3
fix(provider/telegram): pre-validate group children + share identity …
KumarVandit Apr 28, 2026
232a4f0
fix(provider/telegram): dedupe TelegramSpaceShape in identity.ts
KumarVandit Apr 28, 2026
c60d6d3
fix(provider/telegram): remove allow_adding_options + fix quiz JSDoc …
KumarVandit Apr 28, 2026
6b701a5
revert(root): drop package.json gen drift check
KumarVandit Apr 28, 2026
08d3a73
refactor(provider/telegram): scope bot-api-spec under telegram provider
KumarVandit Apr 28, 2026
7cdea4e
Merge remote-tracking branch 'origin/main' into telegram-support
KumarVandit May 11, 2026
1f7ff75
refactor(provider/telegram): align with collapsed messages+send surfa…
KumarVandit May 11, 2026
4e2e7e9
fix(provider/telegram): clamp polling timeout to Telegram's 50s serve…
KumarVandit May 11, 2026
511e946
fix(provider/telegram): truncate fractional polling timeouts in sanit…
KumarVandit May 11, 2026
6ed91a2
fix(provider/telegram): cache coalesced album wrapper under its emit id
KumarVandit May 11, 2026
51683a7
fix(provider/telegram): validate requestTimeoutMs in TelegramClient ctor
KumarVandit May 11, 2026
ac55058
chore(provider/telegram): clean up comments and dead schema fields
KumarVandit May 11, 2026
03c4825
chore(provider/telegram): drop redundant validation and narrative com…
KumarVandit May 11, 2026
b77e8d1
refactor(provider/telegram): adopt quick-lru in place of handwritten …
KumarVandit May 11, 2026
d244dbf
fix(provider/telegram): tighten TelegramClient timeout handling
KumarVandit May 11, 2026
aca471e
feat(provider/telegram): add sendPoll allow_adding_options (Bot API 9.6)
KumarVandit May 11, 2026
7a9843a
refactor(provider/telegram): reuse chatToSpace in space.resolve
KumarVandit May 11, 2026
39200c1
fix(provider/telegram): sync cached poll options on poll updates
KumarVandit May 11, 2026
8f8f631
fix(provider/telegram): harden sendGroupContent and sendContact paths
KumarVandit May 11, 2026
1fa5616
fix(provider/telegram): reject nested Blob params in TelegramClient
KumarVandit May 11, 2026
2f098ea
chore(provider/telegram): simplify extractEmoji ternary
KumarVandit May 11, 2026
8938920
fix(provider/telegram): keep prose alongside link previews
KumarVandit May 11, 2026
0c2733d
fix(provider/telegram): strict-parse Telegram message_id
KumarVandit May 11, 2026
67b0b90
fix(provider/telegram): normalize baseUrl and propagate timeout aborts
KumarVandit May 11, 2026
42efc61
fix(provider/telegram): replace stale album members in message cache
KumarVandit May 11, 2026
9ffd439
feat(provider/telegram): surface readable title for private chats
KumarVandit May 11, 2026
82ab8c2
fix(provider/telegram): reject zero/negative Telegram message_id
KumarVandit May 11, 2026
a1b998b
fix(provider/telegram): require non-empty token in TelegramClient
KumarVandit May 11, 2026
761285a
fix(provider/telegram): drop undefined entries when merging retry policy
KumarVandit May 11, 2026
8f69c12
refactor(provider/telegram): use native ErrorOptions.cause in network…
KumarVandit May 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# Finder (MacOS) folder config
.DS_Store

# test results
test/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
19 changes: 18 additions & 1 deletion bun.lock

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

2 changes: 2 additions & 0 deletions examples/basic/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Spectrum } from "spectrum-ts";
import { imessage } from "spectrum-ts/providers/imessage";

// import { telegram } from "spectrum-ts/providers/telegram";
// import { terminal } from "spectrum-ts/providers/terminal";

const app = await Spectrum({
projectId: "",
projectSecret: "",
providers: [
imessage.config(),
// telegram.config({ token: "YOUR_BOT_TOKEN" }),
// terminal.config({}),
],
});
Expand Down
3 changes: 2 additions & 1 deletion packages/spectrum-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@
},
"dependencies": {
"@photon-ai/advanced-imessage": "^0.4.3",
"@photon-ai/whatsapp-business": "^0.1.1",
"@photon-ai/imessage-kit": "^3.0.0-rc.2",
"@photon-ai/whatsapp-business": "^0.1.1",
"@repeaterjs/repeater": "^3.0.6",
"better-grpc": "^0.3.2",
"grammy": "^1.42.0",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
"mime-types": "^3.0.1",
"type-fest": "^5.4.1",
"zod": "^4.2.1"
Expand Down
63 changes: 63 additions & 0 deletions packages/spectrum-ts/src/platform/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,69 @@ function createPlatformInstance<
stopTyping: async () => {
await def.actions.stopTyping?.(typingCtx);
},
editMessage: async (
messageId: string,
...content: [ContentInput, ...ContentInput[]]
) => {
if (!def.actions.editMessage) {
return;
}
const built = await resolveContents(content);
for (const item of built) {
await def.actions.editMessage({
...typingCtx,
messageId,
content: item,
});
}
},
deleteMessage: async (messageId: string) => {
if (!def.actions.deleteMessage) {
return;
}
await def.actions.deleteMessage({
...typingCtx,
messageId,
});
},
forwardMessage: async (messageId: string, toSpaceId: string) => {
if (!def.actions.forwardMessage) {
return;
}
await def.actions.forwardMessage({
...typingCtx,
messageId,
toSpaceId,
});
},
copyMessage: async (messageId: string, toSpaceId: string) => {
if (!def.actions.copyMessage) {
return;
}
await def.actions.copyMessage({
...typingCtx,
messageId,
toSpaceId,
});
},
pinMessage: async (messageId: string) => {
if (!def.actions.pinMessage) {
return;
}
await def.actions.pinMessage({
...typingCtx,
messageId,
});
},
unpinMessage: async (messageId: string) => {
if (!def.actions.unpinMessage) {
return;
}
await def.actions.unpinMessage({
...typingCtx,
messageId,
});
},
responding: async <T>(fn: () => T | Promise<T>): Promise<T> => {
await def.actions.startTyping?.(typingCtx);
try {
Expand Down
51 changes: 51 additions & 0 deletions packages/spectrum-ts/src/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,45 @@ export interface PlatformDef<
client: _Client;
config: z.infer<_ConfigSchema>;
}) => Promise<void>;
editMessage?: (_: {
space: _ResolvedSpace & SpaceRef;
messageId: string;
content: Content;
client: _Client;
config: z.infer<_ConfigSchema>;
}) => Promise<void>;
deleteMessage?: (_: {
space: _ResolvedSpace & SpaceRef;
messageId: string;
client: _Client;
config: z.infer<_ConfigSchema>;
}) => Promise<void>;
forwardMessage?: (_: {
space: _ResolvedSpace & SpaceRef;
messageId: string;
toSpaceId: string;
client: _Client;
config: z.infer<_ConfigSchema>;
}) => Promise<void>;
copyMessage?: (_: {
space: _ResolvedSpace & SpaceRef;
messageId: string;
toSpaceId: string;
client: _Client;
config: z.infer<_ConfigSchema>;
}) => Promise<void>;
pinMessage?: (_: {
space: _ResolvedSpace & SpaceRef;
messageId: string;
client: _Client;
config: z.infer<_ConfigSchema>;
}) => Promise<void>;
unpinMessage?: (_: {
space: _ResolvedSpace & SpaceRef;
messageId: string;
client: _Client;
config: z.infer<_ConfigSchema>;
}) => Promise<void>;
};

config: _ConfigSchema;
Expand Down Expand Up @@ -192,6 +231,18 @@ export interface AnyPlatformDef {
reactToMessage?: (_: any) => Promise<void>;
// biome-ignore lint/suspicious/noExplicitAny: wildcard action
replyToMessage?: (_: any) => Promise<void>;
// biome-ignore lint/suspicious/noExplicitAny: wildcard action
editMessage?: (_: any) => Promise<void>;
// biome-ignore lint/suspicious/noExplicitAny: wildcard action
deleteMessage?: (_: any) => Promise<void>;
// biome-ignore lint/suspicious/noExplicitAny: wildcard action
forwardMessage?: (_: any) => Promise<void>;
// biome-ignore lint/suspicious/noExplicitAny: wildcard action
copyMessage?: (_: any) => Promise<void>;
// biome-ignore lint/suspicious/noExplicitAny: wildcard action
pinMessage?: (_: any) => Promise<void>;
// biome-ignore lint/suspicious/noExplicitAny: wildcard action
unpinMessage?: (_: any) => Promise<void>;
};
config: z.ZodType<object>;
events: {
Expand Down
122 changes: 122 additions & 0 deletions packages/spectrum-ts/src/providers/telegram/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { GrammyError, HttpError } from "grammy";

export type LogLevel = "silent" | "error" | "warn" | "info" | "debug";

const LOG_LEVELS: Record<LogLevel, number> = {
silent: 0,
error: 1,
warn: 2,
info: 3,
debug: 4,
};

export interface TelegramLogger {
debug(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
}

export const createLogger = (level: LogLevel = "error"): TelegramLogger => {
const threshold = LOG_LEVELS[level];
const noop = () => {};

return {
debug:
threshold >= LOG_LEVELS.debug
? (msg, ...args) => console.debug(`[Telegram] ${msg}`, ...args)
: noop,
info:
threshold >= LOG_LEVELS.info
? (msg, ...args) => console.info(`[Telegram] ${msg}`, ...args)
: noop,
warn:
threshold >= LOG_LEVELS.warn
? (msg, ...args) => console.warn(`[Telegram] ${msg}`, ...args)
: noop,
error:
threshold >= LOG_LEVELS.error
? (msg, ...args) => console.error(`[Telegram] ${msg}`, ...args)
: noop,
};
};

export class TelegramError extends Error {
readonly errorCode: number;
readonly description: string;
readonly retryAfter?: number;

constructor(errorCode: number, description: string, retryAfter?: number) {
super(`Telegram API error ${errorCode}: ${description}`);
this.name = "TelegramError";
this.errorCode = errorCode;
this.description = description;
this.retryAfter = retryAfter;
}

static fromGrammyError(err: GrammyError): TelegramError {
const retryAfter =
err.error_code === 429
? (err.parameters?.retry_after ?? undefined)
: undefined;
return new TelegramError(err.error_code, err.description, retryAfter);
}

static fromUnknown(err: unknown): TelegramError | Error {
if (err instanceof TelegramError) {
return err;
}
if (err instanceof GrammyError) {
return TelegramError.fromGrammyError(err);
}
if (err instanceof HttpError) {
return new TelegramError(0, `Network error: ${err.message}`);
}
if (err instanceof Error) {
return err;
}
return new Error(String(err));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));

export const withRetry = async <T>(
fn: () => Promise<T>,
logger: TelegramLogger,
maxRetries = 3
): Promise<T> => {
let lastError: unknown;

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
const wrapped = TelegramError.fromUnknown(err);

if (wrapped instanceof TelegramError && wrapped.retryAfter) {
const waitMs = wrapped.retryAfter * 1000;
logger.warn(
`Rate limited, retrying in ${wrapped.retryAfter}s (attempt ${attempt + 1}/${maxRetries + 1})`
);
await sleep(waitMs);
continue;
}

if (wrapped instanceof TelegramError && wrapped.errorCode >= 500) {
const backoff = Math.min(1000 * 2 ** attempt, 30_000);
logger.warn(
`Server error ${wrapped.errorCode}, retrying in ${backoff}ms (attempt ${attempt + 1}/${maxRetries + 1})`
);
await sleep(backoff);
continue;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

throw wrapped;
}
}

throw TelegramError.fromUnknown(lastError);
};
Loading