feat: add OpenAI OAuth, strict model overrides, and Gmail pull mode

This commit is contained in:
William Valentin
2026-02-13 14:55:40 -08:00
parent 8f644d5e25
commit 955b9e28e0
50 changed files with 5955 additions and 160 deletions
+29
View File
@@ -228,6 +228,35 @@ describe('TelegramAdapter', () => {
expect(msg.metadata).toEqual({ isCommand: true, command: 'reset' });
});
it('/model command strips @bot suffix in groups', async () => {
const handler = vi.fn();
adapter.onMessage(handler);
await adapter.connect();
// Find the /model command handler
const modelCall = mockCommand.mock.calls.find((call) => call[0] === 'model');
expect(modelCall).toBeDefined();
const modelHandler = modelCall![1];
const ctx = {
message: { message_id: 123, text: '/model@flynn_bot default github/gpt-5-mini' },
chat: { id: 100 },
from: { first_name: 'Will' },
};
await modelHandler(ctx);
expect(handler).toHaveBeenCalledTimes(1);
const msg: InboundMessage = handler.mock.calls[0][0];
expect(msg.text).toBe('/model default github/gpt-5-mini');
expect(msg.metadata).toEqual({
isCommand: true,
command: 'model',
commandArgs: 'default github/gpt-5-mini',
});
});
// ── Auth middleware ───────────────────────────────────────────
it('auth middleware blocks unauthorized chat IDs', async () => {
+39 -4
View File
@@ -166,7 +166,9 @@ export class TelegramAdapter implements ChannelAdapter {
this.bot.command('model', async (ctx) => {
if (!this.messageHandler) {return;}
const args = ctx.message?.text?.replace(/^\/model\s*/, '').trim() ?? '';
// Telegram can deliver group commands in the form: /model@bot_username ...
// Strip the optional @mention so args parsing is consistent across DMs/groups.
const args = ctx.message?.text?.replace(/^\/model(?:@\S+)?\s*/i, '').trim() ?? '';
this.messageHandler({
id: String(ctx.message?.message_id ?? Date.now()),
@@ -439,15 +441,48 @@ export class TelegramAdapter implements ChannelAdapter {
if (!this.bot) {throw new Error('Telegram adapter not connected');}
const chatId = Number(peerId);
const text = message.text;
const text = message.text ?? '';
// Telegram rejects empty text messages.
// If there is no text, skip straight to attachments.
if (!text.trim()) {
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
await this.sendAttachment(chatId, attachment);
}
}
return;
}
const sendChunk = async (chunk: string): Promise<void> => {
// We default to Markdown for nicer formatting, but Telegram's Markdown parsing
// is strict and can fail on unescaped characters. If Telegram rejects the
// message, retry once without parse_mode so users still get the content.
try {
await this.bot!.api.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
} catch (error) {
const description = error && typeof error === 'object' && 'description' in error
? String((error as { description?: unknown }).description)
: '';
const isParseError = description.includes("can't parse entities")
|| description.includes('message text is empty');
if (!isParseError) {
throw error;
}
await this.bot!.api.sendMessage(chatId, chunk);
}
};
// Telegram enforces a 4096-character limit per message
if (text.length <= 4096) {
await this.bot.api.sendMessage(chatId, text, { parse_mode: 'Markdown' });
await sendChunk(text);
} else {
const chunks = splitMessage(text, 4096);
for (const chunk of chunks) {
await this.bot.api.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
await sendChunk(chunk);
}
}