@stage-labs/metro 0.1.0-beta.21 → 0.1.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cli.js +18 -12
  2. package/dist/control.js +68 -12
  3. package/dist/prompt.js +51 -0
  4. package/package.json +1 -1
  5. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/package.json +1 -1
  6. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/accounts.ts +10 -10
  7. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/actions.ts +1 -1
  8. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/attachments.ts +1 -1
  9. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/format.ts +7 -7
  10. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group-actions.ts +1 -1
  11. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/index.ts +10 -10
  12. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/station.ts +2 -2
  13. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-transcribe.ts +6 -6
  14. package/runtime/node_modules/@metro-labs/mcp/package.json +2 -2
  15. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +14 -8
  16. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +2 -4
  17. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +49 -27
  18. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -2
  19. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +6 -6
  20. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +6 -6
  21. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +6 -6
  22. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +2 -2
  23. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +4 -4
  24. package/runtime/node_modules/@metro-labs/telegram/package.json +5 -2
  25. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +47 -86
  26. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +210 -251
  27. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +66 -107
  28. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/client.ts +4 -4
  29. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +110 -141
  30. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/group.ts +1 -1
  31. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/history.ts +1 -1
  32. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/inbound.ts +6 -6
  33. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +35 -92
  34. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/login.ts +20 -20
  35. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +65 -151
  36. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +60 -32
  37. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/reactions.ts +1 -1
  38. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +8 -1
  39. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -47
  40. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -3
  41. package/runtime/node_modules/@metro-labs/{telegram-user → telegram-bot}/package.json +3 -6
  42. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  43. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  44. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  45. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  46. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  47. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  48. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  49. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  50. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  51. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  52. package/runtime/runtime.json +1 -1
  53. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +0 -81
  54. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +0 -254
  55. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +0 -87
  56. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +0 -141
  57. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +0 -46
  58. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +0 -80
  59. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +0 -75
  60. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +0 -24
  61. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +0 -7
  62. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +0 -1
  63. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/api-base.ts +0 -0
  64. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group.ts +0 -0
  65. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/members.ts +0 -0
  66. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/send-files.ts +0 -0
  67. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/verify.ts +0 -0
  68. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-speak.ts +0 -0
  69. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice.ts +0 -0
  70. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/wire.ts +0 -0
  71. /package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/normalize.ts +0 -0
  72. /package/runtime/node_modules/@metro-labs/{telegram → telegram-bot}/src/verify.ts +0 -0
@@ -1,128 +1,87 @@
1
+ import type { Message } from '@mtcute/bun';
2
+ import { FileLocation } from '@mtcute/bun';
1
3
  import {
2
4
  saveBufferToCache,
3
- assertContentLength,
5
+ type SavedAttachment,
4
6
  } from '@metro-labs/mcp/stations/attachments';
5
- import type { SavedAttachment } from '@metro-labs/mcp/stations/attachments';
6
- import { tg, accounts } from './accounts.js';
7
- import type { TgMsg } from './types.js';
7
+ import type { UserClient } from './client.js';
8
8
 
9
- export type { SavedAttachment };
9
+ type Media = NonNullable<Message['media']>;
10
+ export type DownloadableMedia = Media & FileLocation;
10
11
 
11
- function isOggRef(mime: string | undefined, ref: string | undefined): boolean {
12
- const ext = ref?.split('?')[0]?.split('.').pop()?.toLowerCase();
13
- return mime === 'audio/ogg' || ext === 'ogg' || ext === 'oga';
12
+ export function isDownloadable(media: Media): media is DownloadableMedia {
13
+ return media instanceof FileLocation;
14
14
  }
15
15
 
16
- function isVoiceRef(
17
- kind: string | undefined,
18
- mime: string | undefined,
19
- ref: string | undefined,
20
- ): boolean {
21
- const isOgg = isOggRef(mime, ref);
22
- if (kind === 'voice') return true;
23
- if (kind === 'audio') return isOgg;
24
- return !kind && isOgg;
16
+ interface MediaMeta {
17
+ mime?: string;
18
+ name?: string;
25
19
  }
26
20
 
27
- export function mediaKindOf(
28
- kind: string | undefined,
29
- mime: string | undefined,
30
- ref: string | undefined,
31
- ): 'image' | 'voice' | 'audio' | 'video' | 'document' {
32
- const m = mime ?? '';
33
- if (kind === 'image' || m.startsWith('image/')) return 'image';
34
- if (isVoiceRef(kind, mime, ref)) return 'voice';
35
- if (kind === 'video' || m.startsWith('video/')) return 'video';
36
- if (kind === 'audio' || m.startsWith('audio/')) return 'audio';
37
- return 'document';
21
+ function fallbackMime(media: DownloadableMedia): string | undefined {
22
+ const type = (media as { type?: unknown }).type;
23
+ if (type === 'photo') return 'image/jpeg';
24
+ if (type === 'sticker') return 'image/webp';
25
+ return undefined;
38
26
  }
39
27
 
40
- export interface TgMediaRef {
41
- fileId: string;
28
+ function metaOf(media: DownloadableMedia): MediaMeta {
29
+ const withMime = media as { mimeType?: unknown };
30
+ const withName = media as { fileName?: unknown };
31
+ const mime =
32
+ (typeof withMime.mimeType === 'string' ? withMime.mimeType : undefined) ??
33
+ fallbackMime(media);
34
+ const name =
35
+ typeof withName.fileName === 'string' ? withName.fileName : undefined;
36
+ return {
37
+ ...(mime ? { mime } : {}),
38
+ ...(name ? { name } : {}),
39
+ };
40
+ }
41
+
42
+ export interface PendingDescriptor {
43
+ kind: string;
42
44
  name?: string;
43
- mime?: string;
44
45
  }
45
46
 
46
- const MEDIA_EXTRACTORS: ((m: TgMsg) => TgMediaRef | null)[] = [
47
- (m) => {
48
- const largest = m.photo?.[m.photo.length - 1];
49
- return largest ? { fileId: largest.file_id, mime: 'image/jpeg' } : null;
50
- },
51
- (m) =>
52
- m.document?.file_id
53
- ? {
54
- fileId: m.document.file_id,
55
- name: m.document.file_name,
56
- mime: m.document.mime_type,
57
- }
58
- : null,
59
- (m) =>
60
- m.video?.file_id
61
- ? {
62
- fileId: m.video.file_id,
63
- name: m.video.file_name,
64
- mime: m.video.mime_type ?? 'video/mp4',
65
- }
66
- : null,
67
- (m) =>
68
- m.animation?.file_id
69
- ? {
70
- fileId: m.animation.file_id,
71
- name: m.animation.file_name,
72
- mime: m.animation.mime_type,
73
- }
74
- : null,
75
- (m) =>
76
- m.audio?.file_id
77
- ? {
78
- fileId: m.audio.file_id,
79
- name: m.audio.file_name,
80
- mime: m.audio.mime_type,
81
- }
82
- : null,
83
- (m) =>
84
- m.voice?.file_id
85
- ? { fileId: m.voice.file_id, mime: m.voice.mime_type ?? 'audio/ogg' }
86
- : null,
87
- (m) =>
88
- m.sticker?.file_id
89
- ? { fileId: m.sticker.file_id, mime: 'image/webp' }
90
- : null,
91
- ];
47
+ const KIND_BY_TYPE: Record<string, string> = {
48
+ photo: 'image',
49
+ sticker: 'image',
50
+ voice: 'audio',
51
+ audio: 'audio',
52
+ video: 'video',
53
+ video_note: 'video',
54
+ animation: 'video',
55
+ };
56
+
57
+ function mediaType(media: DownloadableMedia): string {
58
+ const type = (media as { type?: unknown }).type;
59
+ return typeof type === 'string' ? type : '';
60
+ }
61
+
62
+ function kindOf(media: DownloadableMedia, mime: string | undefined): string {
63
+ const byType = KIND_BY_TYPE[mediaType(media)];
64
+ if (byType) return byType;
65
+ if (mime?.startsWith('image/')) return 'image';
66
+ if (mime?.startsWith('audio/')) return 'audio';
67
+ if (mime?.startsWith('video/')) return 'video';
68
+ return 'file';
69
+ }
92
70
 
93
- export function mediaRefOf(m: TgMsg): TgMediaRef | null {
94
- for (const extract of MEDIA_EXTRACTORS) {
95
- const ref = extract(m);
96
- if (ref) return ref;
97
- }
98
- return null;
71
+ export function pendingDescriptorOf(media: DownloadableMedia): PendingDescriptor {
72
+ const meta = metaOf(media);
73
+ return {
74
+ kind: kindOf(media, meta.mime),
75
+ ...(meta.name ? { name: meta.name } : {}),
76
+ };
99
77
  }
100
78
 
101
- export async function saveTelegramMedia(
102
- accountId: string,
103
- ref: TgMediaRef,
79
+ export async function downloadMedia(
80
+ client: UserClient,
81
+ media: DownloadableMedia,
104
82
  messageId: string,
105
83
  index = 0,
106
84
  ): Promise<SavedAttachment> {
107
- const file = await tg<{ file_path?: string }>(accountId, 'getFile', {
108
- file_id: ref.fileId,
109
- });
110
- if (!file.file_path)
111
- throw new Error(`telegram getFile returned no file_path for ${ref.fileId}`);
112
- const acct = accounts.get(accountId);
113
- if (!acct) throw new Error(`unknown account '${accountId}'`);
114
- const res = await fetch(`${acct.fileApi}/${file.file_path}`, {
115
- signal: AbortSignal.timeout(60_000),
116
- });
117
- if (!res.ok)
118
- throw new Error(
119
- `telegram file download ${res.status} for ${file.file_path}`,
120
- );
121
- assertContentLength(res.headers.get('content-length'));
122
- const data = new Uint8Array(await res.arrayBuffer());
123
- const saved = await saveBufferToCache(data, messageId, index, {
124
- mime: ref.mime,
125
- name: ref.name ?? file.file_path,
126
- });
127
- return { ...saved, name: ref.name };
85
+ const buf = await client.tg.downloadAsBuffer(media);
86
+ return saveBufferToCache(buf, messageId, index, metaOf(media));
128
87
  }
@@ -24,7 +24,7 @@ export interface UserClient {
24
24
  }
25
25
 
26
26
  function storagePath(accountId: string): string {
27
- const dir = join(STATE_DIR, 'telegram-user');
27
+ const dir = join(STATE_DIR, 'telegram');
28
28
  mkdirSync(dir, { recursive: true });
29
29
  return join(dir, `${accountId}.session`);
30
30
  }
@@ -34,7 +34,7 @@ export function createClient(account: UserAccount): UserClient {
34
34
  const { apiHash } = account;
35
35
  if (!Number.isInteger(apiId) || apiId <= 0 || !apiHash)
36
36
  throw new TrainError(
37
- 'telegram_user_auth',
37
+ 'telegram_auth',
38
38
  `account '${account.id}' missing apiId/apiHash`,
39
39
  );
40
40
  const tg = new TelegramClient({
@@ -51,7 +51,7 @@ export function createClient(account: UserAccount): UserClient {
51
51
  await tg.connect();
52
52
  } catch (e) {
53
53
  throw new TrainError(
54
- 'telegram_user_auth',
54
+ 'telegram_auth',
55
55
  `account '${account.id}' connect failed: ${errMsg(e)}`,
56
56
  );
57
57
  }
@@ -60,7 +60,7 @@ export function createClient(account: UserAccount): UserClient {
60
60
  me = { username: user.username ?? null, phone: user.phoneNumber ?? null };
61
61
  } catch (e) {
62
62
  process.stderr.write(
63
- `telegram-user[${account.id}] identity unavailable: ${errMsg(e)}\n`,
63
+ `telegram[${account.id}] identity unavailable: ${errMsg(e)}\n`,
64
64
  );
65
65
  }
66
66
  };
@@ -1,172 +1,141 @@
1
- import { errMsg } from '@metro-labs/mcp/log';
2
- import { accounts, lineOf } from './accounts.js';
3
- import { mintId, SELF_URI } from './wire.js';
4
- import { mediaRefOf, saveTelegramMedia } from './attachments.js';
5
- import type { TgMsg, TgReaction, TgReactionCount } from './types.js';
1
+ import type { Chat, Message, Peer, User } from '@mtcute/bun';
2
+ import type { SavedAttachment } from '@metro-labs/mcp/stations/attachments';
3
+ import { lineOf } from './accounts.js';
4
+ import { mintId } from './wire.js';
5
+ import { isDownloadable, pendingDescriptorOf } from './attachments.js';
6
6
 
7
- export type { TgMsg, TgReaction, TgReactionCount };
7
+ const isUser = (peer: Peer): peer is User => peer.type === 'user';
8
+ const isChat = (peer: Peer): peer is Chat => peer.type === 'chat';
8
9
 
9
- function lineForMsg(
10
- accountId: string,
11
- m: TgMsg,
12
- ): { line: string; topicId?: number } {
13
- const topicId = m.is_topic_message ? m.message_thread_id : undefined;
14
- return { line: lineOf(accountId, m.chat.id, topicId), topicId };
10
+ export function senderName(peer: Peer): string | undefined {
11
+ if (isUser(peer)) return peer.username ? `@${peer.username}` : peer.firstName;
12
+ return peer.title;
13
+ }
14
+
15
+ export function displayName(peer: Peer): string | undefined {
16
+ if (isUser(peer)) return peer.firstName;
17
+ return peer.title;
18
+ }
19
+
20
+ function chatName(peer: Peer): string | undefined {
21
+ if (isChat(peer)) return peer.title;
22
+ if (isUser(peer)) return peer.username ? `@${peer.username}` : peer.firstName;
23
+ return undefined;
24
+ }
25
+
26
+ function topicOf(m: Message): number | undefined {
27
+ if (!m.isTopicMessage) return undefined;
28
+ const threadId = m.replyToMessage?.threadId;
29
+ return threadId ?? undefined;
15
30
  }
16
31
 
17
- function stickerTag(s: NonNullable<TgMsg['sticker']>): string {
18
- const set = s.set_name ? ` · ${s.set_name}` : '';
19
- return `[sticker${s.emoji ? ` ${s.emoji}` : ''}${set}]`;
32
+ function mediaTag(m: Message): string | undefined {
33
+ if (m.media === null) return undefined;
34
+ return `[${m.media.type}]`;
20
35
  }
21
36
 
22
- const TAG_EXTRACTORS: ((m: TgMsg) => string | null)[] = [
23
- (m) => (m.photo?.length ? '[image]' : null),
24
- (m) => (m.voice ? '[voice]' : null),
25
- (m) => (m.audio ? `[audio: ${m.audio.file_name ?? 'audio'}]` : null),
26
- (m) => (m.video ? `[video: ${m.video.file_name ?? 'video'}]` : null),
27
- (m) => (m.animation ? `[gif: ${m.animation.file_name ?? 'gif'}]` : null),
28
- (m) => (m.sticker ? stickerTag(m.sticker) : null),
29
- (m) =>
30
- m.document && !m.animation
31
- ? `[file: ${m.document.file_name ?? 'doc'}]`
32
- : null,
33
- (m) =>
34
- m.location
35
- ? `[location: ${m.location.latitude}, ${m.location.longitude}]`
36
- : null,
37
- (m) => (m.dice ? `[dice ${m.dice.emoji} = ${m.dice.value}]` : null),
38
- ];
37
+ function projectText(m: Message): string {
38
+ const tag = mediaTag(m);
39
+ return [m.text, tag].filter(Boolean).join(' ');
40
+ }
39
41
 
40
- function projectText(m: TgMsg): string {
41
- const tags = TAG_EXTRACTORS.map((f) => f(m)).filter(
42
- (t): t is string => t !== null,
43
- );
44
- return [m.text ?? m.caption, ...tags].filter(Boolean).join(' ');
42
+ export function isOwnEcho(m: Message): boolean {
43
+ if (m.isOutgoing) return true;
44
+ const { sender } = m;
45
+ return isUser(sender) && sender.isSelf;
45
46
  }
46
47
 
47
- export function envelope(accountId: string, m: TgMsg): Record<string, unknown> {
48
- const { line } = lineForMsg(accountId, m);
48
+ export function envelope(
49
+ accountId: string,
50
+ m: Message,
51
+ ): Record<string, unknown> {
52
+ const topicId = topicOf(m);
53
+ const line = lineOf(accountId, m.chat.id, topicId);
54
+ const isPrivate = isUser(m.chat);
55
+ const { media } = m;
56
+ const attachments =
57
+ media !== null && isDownloadable(media)
58
+ ? [pendingDescriptorOf(media)]
59
+ : undefined;
49
60
  return {
50
61
  kind: 'inbound',
51
62
  id: mintId(),
52
- ts: new Date(m.date * 1000).toISOString(),
63
+ ts: m.date.toISOString(),
53
64
  station: 'telegram',
54
65
  line,
55
- line_name: m.chat.title ?? m.chat.first_name ?? undefined,
56
- from: `metro://telegram/${accountId}/user/${m.from?.id ?? 'unknown'}`,
57
- from_name: m.from?.username ? `@${m.from.username}` : m.from?.first_name,
58
- from_display_name: m.from?.first_name,
59
- message_id: String(m.message_id),
66
+ line_name: chatName(m.chat),
67
+ from: `metro://telegram/${accountId}/user/${m.sender.id}`,
68
+ from_name: senderName(m.sender),
69
+ from_display_name: displayName(m.sender),
70
+ message_id: String(m.id),
60
71
  text: projectText(m),
61
- payload: m,
62
- is_private: m.chat.type === 'private',
72
+ is_private: isPrivate,
73
+ has_media: media !== null,
74
+ payload: {
75
+ account: accountId,
76
+ message_id: String(m.id),
77
+ ...(attachments ? { attachments } : {}),
78
+ },
63
79
  };
64
80
  }
65
81
 
66
- export function reactionEnvelope(
82
+ export function attachmentSavedEnvelope(
67
83
  accountId: string,
68
- r: TgReaction,
69
- ): Record<string, unknown> | null {
70
- if (r.user?.is_bot) return null;
71
- const newEmojis = r.new_reaction
72
- .filter((x) => x.type === 'emoji')
73
- .map((x) => x.emoji ?? '');
74
- const oldEmojis = r.old_reaction
75
- .filter((x) => x.type === 'emoji')
76
- .map((x) => x.emoji ?? '');
77
- const added = newEmojis.filter((e) => !oldEmojis.includes(e));
78
- if (!added.length) return null;
84
+ line: string,
85
+ sourceEnvId: string,
86
+ saved: SavedAttachment,
87
+ index = 0,
88
+ ): Record<string, unknown> {
79
89
  return {
80
- kind: 'react',
90
+ kind: 'inbound',
81
91
  id: mintId(),
82
- ts: new Date(r.date * 1000).toISOString(),
92
+ ts: new Date().toISOString(),
83
93
  station: 'telegram',
84
- line: lineOf(accountId, r.chat.id),
85
- from: `metro://telegram/${accountId}/user/${r.user?.id ?? 'unknown'}`,
86
- from_name: r.user?.username ? `@${r.user.username}` : r.user?.first_name,
87
- from_display_name: r.user?.first_name,
88
- message_id: String(r.message_id),
89
- emoji: added[0],
90
- event: { type: 'react', emoji: added[0], targetId: String(r.message_id) },
91
- is_private: r.chat.type === 'private',
92
- payload: r,
94
+ line,
95
+ from: `metro://telegram/${accountId}/self`,
96
+ text: `📎 saved: ${saved.path}`,
97
+ payload: {
98
+ account: accountId,
99
+ contentType: 'attachmentSaved',
100
+ attachmentFor: sourceEnvId,
101
+ index,
102
+ attachmentPath: saved.path,
103
+ localPath: saved.path,
104
+ mime: saved.mime,
105
+ name: saved.name,
106
+ },
93
107
  };
94
108
  }
95
109
 
96
- export function reactionCountEnvelope(
97
- accountId: string,
98
- rc: TgReactionCount,
99
- ): Record<string, unknown> | null {
100
- const emojiReactions = rc.reactions.filter(
101
- (r) => r.type.type === 'emoji' && r.type.emoji,
102
- );
103
- if (!emojiReactions.length) return null;
104
- const top = emojiReactions.reduce((a, b) =>
105
- b.total_count > a.total_count ? b : a,
106
- );
107
- const emoji = top.type.emoji ?? '';
108
- if (!emoji) return null;
110
+ export interface ReactionInput {
111
+ accountId: string;
112
+ chatId: number;
113
+ messageId: number;
114
+ emoji: string;
115
+ senderId: number;
116
+ date: Date;
117
+ isPrivate: boolean;
118
+ senderName?: string;
119
+ removed?: boolean;
120
+ }
121
+
122
+ export function reactionEnvelope(r: ReactionInput): Record<string, unknown> {
109
123
  return {
110
124
  kind: 'react',
111
125
  id: mintId(),
112
- ts: new Date(rc.date * 1000).toISOString(),
126
+ ts: r.date.toISOString(),
113
127
  station: 'telegram',
114
- line: lineOf(accountId, rc.chat.id),
115
- from: `metro://telegram/${accountId}/user/unknown`,
116
- message_id: String(rc.message_id),
117
- emoji,
118
- event: { type: 'react', emoji, targetId: String(rc.message_id) },
119
- is_private: rc.chat.type === 'private',
120
- payload: rc,
121
- };
122
- }
123
-
124
- export function emitInbound(
125
- emit: (e: unknown) => void,
126
- accountId: string,
127
- e: Record<string, unknown>,
128
- ): void {
129
- const owner = accounts.get(accountId)?.cfg.owner;
130
- const payload = {
131
- ...(e.payload as Record<string, unknown> | undefined),
132
- account: accountId,
128
+ line: lineOf(r.accountId, r.chatId),
129
+ from: `metro://telegram/${r.accountId}/user/${r.senderId}`,
130
+ from_name: r.senderName,
131
+ message_id: String(r.messageId),
132
+ emoji: r.emoji,
133
+ event: { type: 'react', emoji: r.emoji, targetId: String(r.messageId) },
134
+ is_private: r.isPrivate,
135
+ payload: {
136
+ account: r.accountId,
137
+ message_id: String(r.messageId),
138
+ removed: r.removed === true,
139
+ },
133
140
  };
134
- emit({ ...e, ...(owner ? { to: owner } : {}), account: accountId, payload });
135
- }
136
-
137
- export function saveMediaAndEmit(
138
- emit: (e: unknown) => void,
139
- accountId: string,
140
- m: TgMsg,
141
- sourceEnvId: string,
142
- ): void {
143
- const ref = mediaRefOf(m);
144
- if (!ref) return;
145
- const line = lineForMsg(accountId, m).line;
146
- void saveTelegramMedia(accountId, ref, String(m.message_id), 0)
147
- .then((saved) => {
148
- emitInbound(emit, accountId, {
149
- kind: 'inbound',
150
- id: mintId(),
151
- ts: new Date().toISOString(),
152
- station: 'telegram',
153
- line,
154
- from: SELF_URI || `metro://telegram/${accountId}/self`,
155
- text: `📎 saved: ${saved.path}`,
156
- payload: {
157
- contentType: 'attachmentSaved',
158
- attachmentFor: sourceEnvId,
159
- index: 0,
160
- attachmentPath: saved.path,
161
- localPath: saved.path,
162
- mime: saved.mime,
163
- name: saved.name,
164
- },
165
- });
166
- })
167
- .catch((err: unknown) =>
168
- process.stderr.write(
169
- `telegram media save failed: ${errMsg(err)}\n`,
170
- ),
171
- );
172
141
  }
@@ -140,7 +140,7 @@ async function createPerMember(
140
140
  }
141
141
  if (chatId === null)
142
142
  throw new TrainError(
143
- 'telegram_user_call',
143
+ 'telegram_call',
144
144
  'could not create group: no requested member could be added',
145
145
  );
146
146
  for (const m of pending) outcomes.push(await addOne(client, chatId, m));
@@ -12,7 +12,7 @@ export function clampLimit(limit: unknown): number {
12
12
  }
13
13
 
14
14
  function senderUri(accountId: string, m: Message): string {
15
- return `metro://telegram-user/${accountId}/user/${m.sender.id}`;
15
+ return `metro://telegram/${accountId}/user/${m.sender.id}`;
16
16
  }
17
17
 
18
18
  function textOf(m: Message): string {
@@ -26,14 +26,14 @@ async function saveMediaAndEmit(
26
26
  );
27
27
  } catch (e) {
28
28
  process.stderr.write(
29
- `telegram-user[${accountId}] media save failed: ${errMsg(e)}\n`,
29
+ `telegram[${accountId}] media save failed: ${errMsg(e)}\n`,
30
30
  );
31
31
  }
32
32
  }
33
33
 
34
34
  function emitMessage(client: UserClient, m: Message): void {
35
35
  log.debug(
36
- { train: 'telegram-user', account: client.account.id, messageId: m.id },
36
+ { train: 'telegram', account: client.account.id, messageId: m.id },
37
37
  'inbound message received',
38
38
  );
39
39
  if (isOwnEcho(m)) return;
@@ -42,7 +42,7 @@ function emitMessage(client: UserClient, m: Message): void {
42
42
  if (m.media !== null)
43
43
  saveMediaAndEmit(client, m, env).catch((err: unknown) => {
44
44
  process.stderr.write(
45
- `telegram-user[${client.account.id}] media save failed: ${errMsg(err)}\n`,
45
+ `telegram[${client.account.id}] media save failed: ${errMsg(err)}\n`,
46
46
  );
47
47
  });
48
48
  }
@@ -54,7 +54,7 @@ function subscribe(client: UserClient): void {
54
54
  emitMessage(client, m);
55
55
  } catch (e) {
56
56
  process.stderr.write(
57
- `telegram-user[${accountId}] normalize failed: ${errMsg(e)}\n`,
57
+ `telegram[${accountId}] normalize failed: ${errMsg(e)}\n`,
58
58
  );
59
59
  }
60
60
  };
@@ -69,10 +69,10 @@ export async function startInbound(client: UserClient): Promise<void> {
69
69
  subscribe(client);
70
70
  await client.connect();
71
71
  await client.startUpdates();
72
- process.stderr.write(`telegram-user[${accountId}] inbound connected\n`);
72
+ process.stderr.write(`telegram[${accountId}] inbound connected\n`);
73
73
  } catch (e) {
74
74
  process.stderr.write(
75
- `telegram-user[${accountId}] connect failed: ${errMsg(e)}\n`,
75
+ `telegram[${accountId}] connect failed: ${errMsg(e)}\n`,
76
76
  );
77
77
  }
78
78
  }