feat: add Telegram Topics (forum mode) support
Some checks failed
Bump version / bump-version (push) Has been cancelled
Merge-forward skill branches / merge-forward (push) Has been cancelled
Update token count / update-tokens (push) Has been cancelled

- buildJid() constructs tg:{chatId}:{threadId} for topic messages
- parseJid() extracts chatId + threadId from JID for outbound routing
- /chatid command shows thread ID in forum topics
- sendMessage and setTyping pass message_thread_id when present
- All message handlers (text, photo, voice, media) use thread-aware JIDs

Allows each forum topic to be registered as an independent Nanoclaw group.
This commit is contained in:
Andy
2026-03-14 12:04:06 +00:00
parent c0902877fa
commit 8e24a31bd4
26 changed files with 5190 additions and 717 deletions

View File

@@ -2,14 +2,14 @@ import { describe, it, expect, beforeEach } from 'vitest';
import {
_initTestDatabase,
closeDatabase,
createTask,
deleteTask,
getAllChats,
getAllRegisteredGroups,
getMessagesSince,
getNewMessages,
getTaskById,
setRegisteredGroup,
logTaskRun,
storeChatMetadata,
storeMessage,
updateTask,
@@ -391,94 +391,37 @@ describe('task CRUD', () => {
});
});
// --- LIMIT behavior ---
describe('message query LIMIT', () => {
beforeEach(() => {
storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
for (let i = 1; i <= 10; i++) {
store({
id: `lim-${i}`,
chat_jid: 'group@g.us',
sender: 'user@s.whatsapp.net',
sender_name: 'User',
content: `message ${i}`,
timestamp: `2024-01-01T00:00:${String(i).padStart(2, '0')}.000Z`,
});
}
});
it('getNewMessages caps to limit and returns most recent in chronological order', () => {
const { messages, newTimestamp } = getNewMessages(
['group@g.us'],
'2024-01-01T00:00:00.000Z',
'Andy',
3,
);
expect(messages).toHaveLength(3);
expect(messages[0].content).toBe('message 8');
expect(messages[2].content).toBe('message 10');
// Chronological order preserved
expect(messages[1].timestamp > messages[0].timestamp).toBe(true);
// newTimestamp reflects latest returned row
expect(newTimestamp).toBe('2024-01-01T00:00:10.000Z');
});
it('getMessagesSince caps to limit and returns most recent in chronological order', () => {
const messages = getMessagesSince(
'group@g.us',
'2024-01-01T00:00:00.000Z',
'Andy',
3,
);
expect(messages).toHaveLength(3);
expect(messages[0].content).toBe('message 8');
expect(messages[2].content).toBe('message 10');
expect(messages[1].timestamp > messages[0].timestamp).toBe(true);
});
it('returns all messages when count is under the limit', () => {
const { messages } = getNewMessages(
['group@g.us'],
'2024-01-01T00:00:00.000Z',
'Andy',
50,
);
expect(messages).toHaveLength(10);
describe('closeDatabase', () => {
it('can be called without throwing', () => {
expect(() => closeDatabase()).not.toThrow();
});
});
// --- RegisteredGroup isMain round-trip ---
describe('registered group isMain', () => {
it('persists isMain=true through set/get round-trip', () => {
setRegisteredGroup('main@s.whatsapp.net', {
name: 'Main Chat',
folder: 'whatsapp_main',
trigger: '@Andy',
added_at: '2024-01-01T00:00:00.000Z',
isMain: true,
describe('deleteTask atomicity', () => {
it('deletes task and its logs', () => {
createTask({
id: 'task-del-1',
group_folder: 'main',
chat_jid: 'jid@g.us',
prompt: 'test',
schedule_type: 'once',
schedule_value: '2026-01-01T00:00:00Z',
context_mode: 'isolated',
next_run: '2026-01-01T00:00:00Z',
status: 'active',
created_at: new Date().toISOString(),
});
logTaskRun({
task_id: 'task-del-1',
run_at: new Date().toISOString(),
duration_ms: 100,
status: 'success',
result: 'ok',
error: null,
});
const groups = getAllRegisteredGroups();
const group = groups['main@s.whatsapp.net'];
expect(group).toBeDefined();
expect(group.isMain).toBe(true);
expect(group.folder).toBe('whatsapp_main');
});
deleteTask('task-del-1');
it('omits isMain for non-main groups', () => {
setRegisteredGroup('group@g.us', {
name: 'Family Chat',
folder: 'whatsapp_family-chat',
trigger: '@Andy',
added_at: '2024-01-01T00:00:00.000Z',
});
const groups = getAllRegisteredGroups();
const group = groups['group@g.us'];
expect(group).toBeDefined();
expect(group.isMain).toBeUndefined();
expect(getTaskById('task-del-1')).toBeUndefined();
});
});