agent-messenger 2.27.1 → 2.28.0
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.
- package/.claude-plugin/README.md +11 -1
- package/.claude-plugin/marketplace.json +14 -1
- package/.claude-plugin/plugin.json +4 -2
- package/AGENTS.md +4 -0
- package/README.md +40 -9
- package/dist/package.json +10 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/discordbot/cli.js +1 -1
- package/dist/src/platforms/imessage/cli.d.ts +5 -0
- package/dist/src/platforms/imessage/cli.d.ts.map +1 -0
- package/dist/src/platforms/imessage/cli.js +31 -0
- package/dist/src/platforms/imessage/cli.js.map +1 -0
- package/dist/src/platforms/imessage/client.d.ts +43 -0
- package/dist/src/platforms/imessage/client.d.ts.map +1 -0
- package/dist/src/platforms/imessage/client.js +190 -0
- package/dist/src/platforms/imessage/client.js.map +1 -0
- package/dist/src/platforms/imessage/commands/auth.d.ts +31 -0
- package/dist/src/platforms/imessage/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/auth.js +100 -0
- package/dist/src/platforms/imessage/commands/auth.js.map +1 -0
- package/dist/src/platforms/imessage/commands/chat.d.ts +3 -0
- package/dist/src/platforms/imessage/commands/chat.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/chat.js +42 -0
- package/dist/src/platforms/imessage/commands/chat.js.map +1 -0
- package/dist/src/platforms/imessage/commands/doctor.d.ts +25 -0
- package/dist/src/platforms/imessage/commands/doctor.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/doctor.js +101 -0
- package/dist/src/platforms/imessage/commands/doctor.js.map +1 -0
- package/dist/src/platforms/imessage/commands/index.d.ts +7 -0
- package/dist/src/platforms/imessage/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/index.js +7 -0
- package/dist/src/platforms/imessage/commands/index.js.map +1 -0
- package/dist/src/platforms/imessage/commands/message.d.ts +5 -0
- package/dist/src/platforms/imessage/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/message.js +127 -0
- package/dist/src/platforms/imessage/commands/message.js.map +1 -0
- package/dist/src/platforms/imessage/commands/setup.d.ts +20 -0
- package/dist/src/platforms/imessage/commands/setup.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/setup.js +57 -0
- package/dist/src/platforms/imessage/commands/setup.js.map +1 -0
- package/dist/src/platforms/imessage/commands/shared.d.ts +10 -0
- package/dist/src/platforms/imessage/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/shared.js +53 -0
- package/dist/src/platforms/imessage/commands/shared.js.map +1 -0
- package/dist/src/platforms/imessage/commands/whoami.d.ts +3 -0
- package/dist/src/platforms/imessage/commands/whoami.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/whoami.js +20 -0
- package/dist/src/platforms/imessage/commands/whoami.js.map +1 -0
- package/dist/src/platforms/imessage/credential-manager.d.ts +22 -0
- package/dist/src/platforms/imessage/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/imessage/credential-manager.js +111 -0
- package/dist/src/platforms/imessage/credential-manager.js.map +1 -0
- package/dist/src/platforms/imessage/ensure-auth.d.ts +2 -0
- package/dist/src/platforms/imessage/ensure-auth.d.ts.map +1 -0
- package/dist/src/platforms/imessage/ensure-auth.js +13 -0
- package/dist/src/platforms/imessage/ensure-auth.js.map +1 -0
- package/dist/src/platforms/imessage/errors.d.ts +3 -0
- package/dist/src/platforms/imessage/errors.d.ts.map +1 -0
- package/dist/src/platforms/imessage/errors.js +25 -0
- package/dist/src/platforms/imessage/errors.js.map +1 -0
- package/dist/src/platforms/imessage/index.d.ts +7 -0
- package/dist/src/platforms/imessage/index.d.ts.map +1 -0
- package/dist/src/platforms/imessage/index.js +5 -0
- package/dist/src/platforms/imessage/index.js.map +1 -0
- package/dist/src/platforms/imessage/rpc.d.ts +20 -0
- package/dist/src/platforms/imessage/rpc.d.ts.map +1 -0
- package/dist/src/platforms/imessage/rpc.js +133 -0
- package/dist/src/platforms/imessage/rpc.js.map +1 -0
- package/dist/src/platforms/imessage/types.d.ts +59 -0
- package/dist/src/platforms/imessage/types.d.ts.map +1 -0
- package/dist/src/platforms/imessage/types.js +29 -0
- package/dist/src/platforms/imessage/types.js.map +1 -0
- package/dist/src/platforms/instagram/cli.js +1 -1
- package/dist/src/platforms/instagram/client.d.ts +6 -1
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +17 -0
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/hybrid-listener.d.ts +48 -0
- package/dist/src/platforms/instagram/hybrid-listener.d.ts.map +1 -0
- package/dist/src/platforms/instagram/hybrid-listener.js +154 -0
- package/dist/src/platforms/instagram/hybrid-listener.js.map +1 -0
- package/dist/src/platforms/instagram/index.d.ts +2 -0
- package/dist/src/platforms/instagram/index.d.ts.map +1 -1
- package/dist/src/platforms/instagram/index.js +2 -0
- package/dist/src/platforms/instagram/index.js.map +1 -1
- package/dist/src/platforms/instagram/mqtt/connection.d.ts +4 -0
- package/dist/src/platforms/instagram/mqtt/connection.d.ts.map +1 -0
- package/dist/src/platforms/instagram/mqtt/connection.js +66 -0
- package/dist/src/platforms/instagram/mqtt/connection.js.map +1 -0
- package/dist/src/platforms/instagram/mqtt/thrift.d.ts +22 -0
- package/dist/src/platforms/instagram/mqtt/thrift.d.ts.map +1 -0
- package/dist/src/platforms/instagram/mqtt/thrift.js +143 -0
- package/dist/src/platforms/instagram/mqtt/thrift.js.map +1 -0
- package/dist/src/platforms/instagram/mqtt/transport.d.ts +42 -0
- package/dist/src/platforms/instagram/mqtt/transport.d.ts.map +1 -0
- package/dist/src/platforms/instagram/mqtt/transport.js +246 -0
- package/dist/src/platforms/instagram/mqtt/transport.js.map +1 -0
- package/dist/src/platforms/instagram/realtime-listener.d.ts +33 -0
- package/dist/src/platforms/instagram/realtime-listener.d.ts.map +1 -0
- package/dist/src/platforms/instagram/realtime-listener.js +151 -0
- package/dist/src/platforms/instagram/realtime-listener.js.map +1 -0
- package/dist/src/platforms/kakaotalk/cli.js +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +4 -4
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/line/cli.js +1 -1
- package/dist/src/platforms/webex/cli.js +1 -1
- package/dist/src/platforms/webexbot/cli.js +1 -1
- package/dist/src/platforms/wechatbot/cli.js +1 -1
- package/dist/src/platforms/whatsapp/cli.js +1 -1
- package/dist/src/platforms/whatsappbot/cli.js +1 -1
- package/dist/src/tui/adapters/imessage-adapter.d.ts +21 -0
- package/dist/src/tui/adapters/imessage-adapter.d.ts.map +1 -0
- package/dist/src/tui/adapters/imessage-adapter.js +110 -0
- package/dist/src/tui/adapters/imessage-adapter.js.map +1 -0
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +9 -0
- package/dist/src/tui/app.js.map +1 -1
- package/docs/content/docs/agent-skills.mdx +1 -1
- package/docs/content/docs/cli/imessage.mdx +99 -0
- package/docs/content/docs/cli/instagram.mdx +1 -1
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/index.mdx +9 -6
- package/docs/content/docs/quick-start.mdx +2 -0
- package/docs/content/docs/sdk/instagram.mdx +90 -5
- package/docs/content/docs/tui.mdx +4 -3
- package/package.json +10 -2
- package/scripts/postbuild.ts +12 -15
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +133 -0
- package/skills/agent-imessage/references/authentication.md +66 -0
- package/skills/agent-imessage/references/permissions.md +31 -0
- package/skills/agent-imessage/references/setup.md +49 -0
- package/skills/agent-instagram/SKILL.md +72 -2
- package/skills/agent-instagram/references/common-patterns.md +36 -0
- package/skills/agent-instagram/templates/monitor-chat.sh +5 -0
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/cli.ts +4 -0
- package/src/platforms/imessage/cli.ts +39 -0
- package/src/platforms/imessage/client.test.ts +137 -0
- package/src/platforms/imessage/client.ts +253 -0
- package/src/platforms/imessage/commands/auth.ts +142 -0
- package/src/platforms/imessage/commands/chat.ts +51 -0
- package/src/platforms/imessage/commands/doctor.test.ts +80 -0
- package/src/platforms/imessage/commands/doctor.ts +139 -0
- package/src/platforms/imessage/commands/index.ts +6 -0
- package/src/platforms/imessage/commands/message.test.ts +20 -0
- package/src/platforms/imessage/commands/message.ts +151 -0
- package/src/platforms/imessage/commands/setup.test.ts +52 -0
- package/src/platforms/imessage/commands/setup.ts +76 -0
- package/src/platforms/imessage/commands/shared.test.ts +19 -0
- package/src/platforms/imessage/commands/shared.ts +69 -0
- package/src/platforms/imessage/commands/whoami.ts +22 -0
- package/src/platforms/imessage/credential-manager.test.ts +122 -0
- package/src/platforms/imessage/credential-manager.ts +123 -0
- package/src/platforms/imessage/ensure-auth.ts +18 -0
- package/src/platforms/imessage/errors.ts +27 -0
- package/src/platforms/imessage/index.test.ts +13 -0
- package/src/platforms/imessage/index.ts +16 -0
- package/src/platforms/imessage/rpc.ts +157 -0
- package/src/platforms/imessage/test-stub-imsg.mjs +147 -0
- package/src/platforms/imessage/types.test.ts +47 -0
- package/src/platforms/imessage/types.ts +95 -0
- package/src/platforms/instagram/client.ts +23 -0
- package/src/platforms/instagram/hybrid-listener.test.ts +135 -0
- package/src/platforms/instagram/hybrid-listener.ts +196 -0
- package/src/platforms/instagram/index.ts +10 -0
- package/src/platforms/instagram/mqtt/connection.ts +72 -0
- package/src/platforms/instagram/mqtt/thrift.test.ts +91 -0
- package/src/platforms/instagram/mqtt/thrift.ts +159 -0
- package/src/platforms/instagram/mqtt/transport.ts +285 -0
- package/src/platforms/instagram/realtime-listener.test.ts +34 -0
- package/src/platforms/instagram/realtime-listener.ts +201 -0
- package/src/platforms/kakaotalk/client.test.ts +40 -0
- package/src/platforms/kakaotalk/client.ts +6 -4
- package/src/platforms/webexbot/cli.ts +0 -0
- package/src/tui/adapters/imessage-adapter.ts +131 -0
- package/src/tui/app.ts +13 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
|
|
3
|
+
import type { InstagramClient } from './client'
|
|
4
|
+
import { InstagramListener } from './listener'
|
|
5
|
+
import { InstagramRealtimeListener } from './realtime-listener'
|
|
6
|
+
import type { InstagramMessageSummary } from './types'
|
|
7
|
+
|
|
8
|
+
export interface InstagramHybridListenerEventMap {
|
|
9
|
+
message: [InstagramMessageSummary]
|
|
10
|
+
error: [Error]
|
|
11
|
+
connected: [{ userId: string; transport: 'realtime' | 'polling' }]
|
|
12
|
+
disconnected: []
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type EventKey = keyof InstagramHybridListenerEventMap
|
|
16
|
+
|
|
17
|
+
const DEFAULT_REALTIME_RETRY_BASE_MS = 30_000
|
|
18
|
+
const DEFAULT_REALTIME_RETRY_MAX_MS = 5 * 60_000
|
|
19
|
+
|
|
20
|
+
export interface InstagramHybridListenerOptions {
|
|
21
|
+
pollInterval?: number
|
|
22
|
+
realtimeRetryBaseMs?: number
|
|
23
|
+
realtimeRetryMaxMs?: number
|
|
24
|
+
disableRealtime?: boolean
|
|
25
|
+
connackTimeoutMs?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class InstagramHybridListener {
|
|
29
|
+
private client: InstagramClient
|
|
30
|
+
private emitter = new EventEmitter()
|
|
31
|
+
private realtime: InstagramRealtimeListener | null = null
|
|
32
|
+
private poller: InstagramListener | null = null
|
|
33
|
+
private options: Required<
|
|
34
|
+
Pick<InstagramHybridListenerOptions, 'realtimeRetryBaseMs' | 'realtimeRetryMaxMs' | 'disableRealtime'>
|
|
35
|
+
> &
|
|
36
|
+
Pick<InstagramHybridListenerOptions, 'pollInterval' | 'connackTimeoutMs'>
|
|
37
|
+
private running = false
|
|
38
|
+
private realtimeRetryTimer: ReturnType<typeof setTimeout> | null = null
|
|
39
|
+
private realtimeFailures = 0
|
|
40
|
+
private activeTransport: 'realtime' | 'polling' | null = null
|
|
41
|
+
private tearingDownRealtime = false
|
|
42
|
+
|
|
43
|
+
constructor(client: InstagramClient, options: InstagramHybridListenerOptions = {}) {
|
|
44
|
+
this.client = client
|
|
45
|
+
this.options = {
|
|
46
|
+
pollInterval: options.pollInterval,
|
|
47
|
+
realtimeRetryBaseMs: options.realtimeRetryBaseMs ?? DEFAULT_REALTIME_RETRY_BASE_MS,
|
|
48
|
+
realtimeRetryMaxMs: options.realtimeRetryMaxMs ?? DEFAULT_REALTIME_RETRY_MAX_MS,
|
|
49
|
+
disableRealtime: options.disableRealtime ?? false,
|
|
50
|
+
connackTimeoutMs: options.connackTimeoutMs,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async start(): Promise<void> {
|
|
55
|
+
if (this.running) return
|
|
56
|
+
this.running = true
|
|
57
|
+
|
|
58
|
+
if (this.options.disableRealtime) {
|
|
59
|
+
this.startPolling()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await this.tryRealtime()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
stop(): void {
|
|
67
|
+
this.running = false
|
|
68
|
+
this.activeTransport = null
|
|
69
|
+
this.clearRealtimeRetry()
|
|
70
|
+
this.teardownRealtime()
|
|
71
|
+
this.teardownPolling()
|
|
72
|
+
this.emitter.emit('disconnected')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
on<K extends EventKey>(event: K, listener: (...args: InstagramHybridListenerEventMap[K]) => void): this {
|
|
76
|
+
this.emitter.on(event, listener as (...args: unknown[]) => void)
|
|
77
|
+
return this
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
off<K extends EventKey>(event: K, listener: (...args: InstagramHybridListenerEventMap[K]) => void): this {
|
|
81
|
+
this.emitter.off(event, listener as (...args: unknown[]) => void)
|
|
82
|
+
return this
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
once<K extends EventKey>(event: K, listener: (...args: InstagramHybridListenerEventMap[K]) => void): this {
|
|
86
|
+
this.emitter.once(event, listener as (...args: unknown[]) => void)
|
|
87
|
+
return this
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected createRealtimeListener(): InstagramRealtimeListener {
|
|
91
|
+
return new InstagramRealtimeListener(this.client, { connackTimeoutMs: this.options.connackTimeoutMs })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async tryRealtime(): Promise<void> {
|
|
95
|
+
const realtime = this.createRealtimeListener()
|
|
96
|
+
this.realtime = realtime
|
|
97
|
+
|
|
98
|
+
realtime.on('message', (message) => this.emitter.emit('message', message))
|
|
99
|
+
realtime.on('connected', ({ userId }) => {
|
|
100
|
+
this.realtimeFailures = 0
|
|
101
|
+
this.activeTransport = 'realtime'
|
|
102
|
+
this.teardownPolling()
|
|
103
|
+
this.emitter.emit('connected', { userId, transport: 'realtime' })
|
|
104
|
+
})
|
|
105
|
+
realtime.on('error', (error) => this.onRealtimeFailure(error))
|
|
106
|
+
realtime.on('disconnected', () => {
|
|
107
|
+
// Ignore the 'disconnected' that our own teardown triggers; only an
|
|
108
|
+
// unexpected close (still marked realtime-active) is a real failure.
|
|
109
|
+
if (this.tearingDownRealtime) return
|
|
110
|
+
if (this.running && this.activeTransport === 'realtime') {
|
|
111
|
+
this.onRealtimeFailure(new Error('Realtime connection closed'))
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await realtime.start()
|
|
117
|
+
// stop() may have run during the bootstrap await; if so, don't leave an MQTT
|
|
118
|
+
// connection alive after the hybrid listener was already torn down.
|
|
119
|
+
if (!this.running) {
|
|
120
|
+
realtime.stop()
|
|
121
|
+
if (this.realtime === realtime) this.realtime = null
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.onRealtimeFailure(error instanceof Error ? error : new Error(String(error)))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private onRealtimeFailure(error: Error): void {
|
|
129
|
+
if (!this.running) return
|
|
130
|
+
|
|
131
|
+
this.activeTransport = null
|
|
132
|
+
this.emitter.emit('error', error)
|
|
133
|
+
this.realtimeFailures++
|
|
134
|
+
// teardownRealtime() suppresses the 'disconnected' it triggers, so this path
|
|
135
|
+
// does not re-enter via the realtime disconnected handler.
|
|
136
|
+
this.teardownRealtime()
|
|
137
|
+
this.startPolling()
|
|
138
|
+
this.scheduleRealtimeRetry()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private scheduleRealtimeRetry(): void {
|
|
142
|
+
if (!this.running || this.realtimeRetryTimer) return
|
|
143
|
+
|
|
144
|
+
const delay = Math.min(
|
|
145
|
+
this.options.realtimeRetryBaseMs * 2 ** (this.realtimeFailures - 1),
|
|
146
|
+
this.options.realtimeRetryMaxMs,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
this.realtimeRetryTimer = setTimeout(() => {
|
|
150
|
+
this.realtimeRetryTimer = null
|
|
151
|
+
if (this.running) void this.tryRealtime()
|
|
152
|
+
}, delay)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private startPolling(): void {
|
|
156
|
+
if (this.poller) return
|
|
157
|
+
|
|
158
|
+
const poller = new InstagramListener(this.client, { pollInterval: this.options.pollInterval })
|
|
159
|
+
this.poller = poller
|
|
160
|
+
|
|
161
|
+
poller.on('message', (message) => this.emitter.emit('message', message))
|
|
162
|
+
poller.on('error', (error) => this.emitter.emit('error', error))
|
|
163
|
+
poller.on('connected', ({ userId }) => {
|
|
164
|
+
this.activeTransport = 'polling'
|
|
165
|
+
this.emitter.emit('connected', { userId, transport: 'polling' })
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
void poller.start()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private teardownRealtime(): void {
|
|
172
|
+
if (this.realtime) {
|
|
173
|
+
this.tearingDownRealtime = true
|
|
174
|
+
try {
|
|
175
|
+
this.realtime.stop()
|
|
176
|
+
} finally {
|
|
177
|
+
this.tearingDownRealtime = false
|
|
178
|
+
}
|
|
179
|
+
this.realtime = null
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private teardownPolling(): void {
|
|
184
|
+
if (this.poller) {
|
|
185
|
+
this.poller.stop()
|
|
186
|
+
this.poller = null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private clearRealtimeRetry(): void {
|
|
191
|
+
if (this.realtimeRetryTimer) {
|
|
192
|
+
clearTimeout(this.realtimeRetryTimer)
|
|
193
|
+
this.realtimeRetryTimer = null
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export { InstagramClient } from './client'
|
|
2
2
|
export { InstagramCredentialManager } from './credential-manager'
|
|
3
|
+
export {
|
|
4
|
+
InstagramHybridListener,
|
|
5
|
+
type InstagramHybridListenerEventMap,
|
|
6
|
+
type InstagramHybridListenerOptions,
|
|
7
|
+
} from './hybrid-listener'
|
|
3
8
|
export { InstagramListener, type InstagramListenerEventMap } from './listener'
|
|
9
|
+
export {
|
|
10
|
+
InstagramRealtimeListener,
|
|
11
|
+
type InstagramRealtimeListenerEventMap,
|
|
12
|
+
type InstagramRealtimeListenerOptions,
|
|
13
|
+
} from './realtime-listener'
|
|
4
14
|
export { InstagramTokenExtractor, type ExtractedInstagramCookies } from './token-extractor'
|
|
5
15
|
export {
|
|
6
16
|
createAccountId,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { deflateSync } from 'node:zlib'
|
|
2
|
+
|
|
3
|
+
import type { InstagramSessionState } from '../types'
|
|
4
|
+
import { ThriftCompactWriter } from './thrift'
|
|
5
|
+
|
|
6
|
+
const IG_APP_ID = 567067343352427n
|
|
7
|
+
const IG_VERSION = '312.1.0.34.111'
|
|
8
|
+
const CLIENT_CAPABILITIES = 183n
|
|
9
|
+
const PUBLISH_FORMAT = 1
|
|
10
|
+
const NETWORK_TYPE_WIFI = 1
|
|
11
|
+
const CLIENT_STACK = 3
|
|
12
|
+
const CLIENT_IDENTIFIER_MAX_LENGTH = 20
|
|
13
|
+
const SESSION_ID_MASK = 0xffffffffn
|
|
14
|
+
|
|
15
|
+
const CONNECT_SUBSCRIBE_TOPIC_IDS = [88, 135, 149, 150, 133, 146]
|
|
16
|
+
|
|
17
|
+
export function extractSessionId(session: InstagramSessionState): string {
|
|
18
|
+
const authorization = session.authorization ?? ''
|
|
19
|
+
const igTokenMatch = authorization.match(/^Bearer IGT:2:(.+)$/)
|
|
20
|
+
if (igTokenMatch) {
|
|
21
|
+
try {
|
|
22
|
+
const decoded = JSON.parse(Buffer.from(igTokenMatch[1]!, 'base64').toString('utf8')) as { sessionid?: string }
|
|
23
|
+
if (decoded.sessionid) return decoded.sessionid
|
|
24
|
+
} catch {
|
|
25
|
+
// Malformed/stale token: fall through to the cookie-based sessionid below.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cookieMatch = session.cookies.match(/sessionid=([^;]+)/)
|
|
30
|
+
if (cookieMatch) return cookieMatch[1]!
|
|
31
|
+
|
|
32
|
+
throw new Error('No sessionid found in Instagram session (checked authorization token and cookies)')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildConnectPayload(session: InstagramSessionState): Buffer {
|
|
36
|
+
const sessionId = extractSessionId(session)
|
|
37
|
+
const userId = BigInt(session.user_id ?? '0')
|
|
38
|
+
const deviceId = session.device.phone_id
|
|
39
|
+
const userAgent = `Instagram ${IG_VERSION} Android (${session.device.device_string})`
|
|
40
|
+
|
|
41
|
+
const thrift = new ThriftCompactWriter()
|
|
42
|
+
.binary(1, deviceId.substring(0, CLIENT_IDENTIFIER_MAX_LENGTH))
|
|
43
|
+
.structStart(4)
|
|
44
|
+
.i64(1, userId)
|
|
45
|
+
.binary(2, userAgent)
|
|
46
|
+
.i64(3, CLIENT_CAPABILITIES)
|
|
47
|
+
.i64(4, 0)
|
|
48
|
+
.i32(5, PUBLISH_FORMAT)
|
|
49
|
+
.bool(6, false)
|
|
50
|
+
.bool(7, true)
|
|
51
|
+
.binary(8, deviceId)
|
|
52
|
+
.bool(9, true)
|
|
53
|
+
.i32(10, NETWORK_TYPE_WIFI)
|
|
54
|
+
.i32(11, 0)
|
|
55
|
+
.i64(12, BigInt(Date.now()) & SESSION_ID_MASK)
|
|
56
|
+
.listOfI32(14, CONNECT_SUBSCRIBE_TOPIC_IDS)
|
|
57
|
+
.binary(15, 'cookie_auth')
|
|
58
|
+
.i64(16, IG_APP_ID)
|
|
59
|
+
.binary(20, '')
|
|
60
|
+
.byte(21, CLIENT_STACK)
|
|
61
|
+
.structEnd()
|
|
62
|
+
.binary(5, `sessionid=${sessionId}`)
|
|
63
|
+
.mapBinaryBinary(10, {
|
|
64
|
+
platform: 'android',
|
|
65
|
+
ig_mqtt_route: 'django',
|
|
66
|
+
pubsub_msg_type_blacklist: 'direct, typing_type',
|
|
67
|
+
auth_cache_enabled: '0',
|
|
68
|
+
})
|
|
69
|
+
.finish()
|
|
70
|
+
|
|
71
|
+
return deflateSync(thrift, { level: 9 })
|
|
72
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { ThriftCompactWriter } from '@/platforms/instagram/mqtt/thrift'
|
|
4
|
+
|
|
5
|
+
describe('ThriftCompactWriter', () => {
|
|
6
|
+
it('encodes a true bool as a packed field header with a trailing stop byte', () => {
|
|
7
|
+
// field id 1 (delta 1) << 4 | TYPE_TRUE(0x01) = 0x11, then struct stop 0x00
|
|
8
|
+
const out = new ThriftCompactWriter().bool(1, true).finish()
|
|
9
|
+
expect([...out]).toEqual([0x11, 0x00])
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('encodes a false bool with the false type code', () => {
|
|
13
|
+
// field id 1 (delta 1) << 4 | TYPE_FALSE(0x02) = 0x12
|
|
14
|
+
const out = new ThriftCompactWriter().bool(1, false).finish()
|
|
15
|
+
expect([...out]).toEqual([0x12, 0x00])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('encodes i32 using zigzag varint', () => {
|
|
19
|
+
// field 1 i32(0x05) = 0x15, zigzag(1) = 2
|
|
20
|
+
const out = new ThriftCompactWriter().i32(1, 1).finish()
|
|
21
|
+
expect([...out]).toEqual([0x15, 0x02, 0x00])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('encodes negative i32 via zigzag', () => {
|
|
25
|
+
// zigzag(-1) = 1
|
|
26
|
+
const out = new ThriftCompactWriter().i32(1, -1).finish()
|
|
27
|
+
expect([...out]).toEqual([0x15, 0x01, 0x00])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('encodes a large positive i32 as an unsigned <=5-byte varint', () => {
|
|
31
|
+
// 0x40000000: zigzag(1073741824) = 2147483648 = 0x80000000 -> varint 80 80 80 80 08
|
|
32
|
+
const out = new ThriftCompactWriter().i32(1, 0x40000000).finish()
|
|
33
|
+
expect([...out]).toEqual([0x15, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('encodes INT32_MIN without overflowing into a 64-bit varint', () => {
|
|
37
|
+
// zigzag(-2147483648) = 4294967295 = 0xFFFFFFFF -> varint ff ff ff ff 0f (5 bytes, not 9)
|
|
38
|
+
const out = new ThriftCompactWriter().i32(1, -2147483648).finish()
|
|
39
|
+
expect([...out]).toEqual([0x15, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x00])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('encodes binary as length-prefixed utf8', () => {
|
|
43
|
+
// field 1 binary(0x08) = 0x18, length 2, "hi"
|
|
44
|
+
const out = new ThriftCompactWriter().binary(1, 'hi').finish()
|
|
45
|
+
expect([...out]).toEqual([0x18, 0x02, 0x68, 0x69, 0x00])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('packs small field-id deltas across multiple fields', () => {
|
|
49
|
+
// field 1 binary = 0x18, then field 3 binary uses delta 2 = 0x28
|
|
50
|
+
const out = new ThriftCompactWriter().binary(1, 'a').binary(3, 'b').finish()
|
|
51
|
+
expect([...out]).toEqual([0x18, 0x01, 0x61, 0x28, 0x01, 0x62, 0x00])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('falls back to explicit zigzag id when delta exceeds 15', () => {
|
|
55
|
+
// field 20 binary: delta 20 > 15, so type byte 0x08 then zigzag(20)=40=0x28
|
|
56
|
+
const out = new ThriftCompactWriter().binary(20, 'x').finish()
|
|
57
|
+
expect([...out]).toEqual([0x08, 0x28, 0x01, 0x78, 0x00])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('encodes a short i32 list with size packed into the header nibble', () => {
|
|
61
|
+
// field 1 list = 0x19, list header size 2 << 4 | i32(0x05) = 0x25, zigzag(88)=176=0xb0 0x01, zigzag(146)=292=0xa4 0x02
|
|
62
|
+
const out = new ThriftCompactWriter().listOfI32(1, [88, 146]).finish()
|
|
63
|
+
expect([...out]).toEqual([0x19, 0x25, 0xb0, 0x01, 0xa4, 0x02, 0x00])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('encodes an empty map as a single zero byte', () => {
|
|
67
|
+
// field 1 map = 0x1b, empty map size 0x00
|
|
68
|
+
const out = new ThriftCompactWriter().mapBinaryBinary(1, {}).finish()
|
|
69
|
+
expect([...out]).toEqual([0x1b, 0x00, 0x00])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('encodes a binary->binary map with the key/value type byte', () => {
|
|
73
|
+
// field 1 map = 0x1b, size 1, types (binary<<4|binary)=0x88, key "k", value "v"
|
|
74
|
+
const out = new ThriftCompactWriter().mapBinaryBinary(1, { k: 'v' }).finish()
|
|
75
|
+
expect([...out]).toEqual([0x1b, 0x01, 0x88, 0x01, 0x6b, 0x01, 0x76, 0x00])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('resets field-id delta tracking inside nested structs', () => {
|
|
79
|
+
// field 4 struct = 0x4c, inner field 1 binary = 0x18 "a", inner stop 0x00, outer stop 0x00
|
|
80
|
+
const out = new ThriftCompactWriter().structStart(4).binary(1, 'a').structEnd().finish()
|
|
81
|
+
expect([...out]).toEqual([0x4c, 0x18, 0x01, 0x61, 0x00, 0x00])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('encodes i64 values larger than 32 bits', () => {
|
|
85
|
+
// field 16 i64: delta 16 > 15 -> type 0x06, zigzag id(16)=32=0x20, then zigzag64(567067343352427)
|
|
86
|
+
const out = new ThriftCompactWriter().i64(16, 567067343352427n).finish()
|
|
87
|
+
expect(out[0]).toBe(0x06)
|
|
88
|
+
expect(out[1]).toBe(0x20)
|
|
89
|
+
expect(out[out.length - 1]).toBe(0x00)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const TYPE_TRUE = 0x01
|
|
2
|
+
const TYPE_FALSE = 0x02
|
|
3
|
+
const TYPE_BYTE = 0x03
|
|
4
|
+
const TYPE_I16 = 0x04
|
|
5
|
+
const TYPE_I32 = 0x05
|
|
6
|
+
const TYPE_I64 = 0x06
|
|
7
|
+
const TYPE_BINARY = 0x08
|
|
8
|
+
const TYPE_LIST = 0x09
|
|
9
|
+
const TYPE_MAP = 0x0b
|
|
10
|
+
const TYPE_STRUCT = 0x0c
|
|
11
|
+
|
|
12
|
+
const VARINT_CONTINUE = 0x80
|
|
13
|
+
const VARINT_MASK = 0x7f
|
|
14
|
+
const U64_MASK = 0xffffffffffffffffn
|
|
15
|
+
|
|
16
|
+
export class ThriftCompactWriter {
|
|
17
|
+
private bytes: number[] = []
|
|
18
|
+
private parentFieldStack: number[] = []
|
|
19
|
+
private lastFieldId = 0
|
|
20
|
+
|
|
21
|
+
bool(id: number, value: boolean): this {
|
|
22
|
+
this.writeFieldHeader(value ? TYPE_TRUE : TYPE_FALSE, id)
|
|
23
|
+
return this
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
byte(id: number, value: number): this {
|
|
27
|
+
this.writeFieldHeader(TYPE_BYTE, id)
|
|
28
|
+
this.bytes.push(value & 0xff)
|
|
29
|
+
return this
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
i16(id: number, value: number): this {
|
|
33
|
+
this.writeFieldHeader(TYPE_I16, id)
|
|
34
|
+
this.writeVarint(zigzag32(value))
|
|
35
|
+
return this
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
i32(id: number, value: number): this {
|
|
39
|
+
this.writeFieldHeader(TYPE_I32, id)
|
|
40
|
+
this.writeVarint(zigzag32(value))
|
|
41
|
+
return this
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
i64(id: number, value: number | bigint): this {
|
|
45
|
+
this.writeFieldHeader(TYPE_I64, id)
|
|
46
|
+
this.writeVarint(zigzag64(BigInt(value)))
|
|
47
|
+
return this
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
binary(id: number, value: string | Buffer): this {
|
|
51
|
+
this.writeFieldHeader(TYPE_BINARY, id)
|
|
52
|
+
this.writeBinaryValue(value)
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
listOfI32(id: number, values: number[]): this {
|
|
57
|
+
this.writeFieldHeader(TYPE_LIST, id)
|
|
58
|
+
this.writeListHeader(TYPE_I32, values.length)
|
|
59
|
+
for (const value of values) this.writeVarint(zigzag32(value))
|
|
60
|
+
return this
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
listOfBinary(id: number, values: string[]): this {
|
|
64
|
+
this.writeFieldHeader(TYPE_LIST, id)
|
|
65
|
+
this.writeListHeader(TYPE_BINARY, values.length)
|
|
66
|
+
for (const value of values) this.writeBinaryValue(value)
|
|
67
|
+
return this
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
mapBinaryBinary(id: number, entries: Record<string, string>): this {
|
|
71
|
+
this.writeFieldHeader(TYPE_MAP, id)
|
|
72
|
+
const keys = Object.keys(entries)
|
|
73
|
+
|
|
74
|
+
// Compact protocol encodes an empty map as a single zero byte (size only).
|
|
75
|
+
if (keys.length === 0) {
|
|
76
|
+
this.bytes.push(0)
|
|
77
|
+
return this
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.writeVarint(keys.length)
|
|
81
|
+
this.bytes.push((TYPE_BINARY << 4) | TYPE_BINARY)
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
this.writeBinaryValue(key)
|
|
84
|
+
this.writeBinaryValue(entries[key]!)
|
|
85
|
+
}
|
|
86
|
+
return this
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
structStart(id: number): this {
|
|
90
|
+
this.writeFieldHeader(TYPE_STRUCT, id)
|
|
91
|
+
this.parentFieldStack.push(this.lastFieldId)
|
|
92
|
+
this.lastFieldId = 0
|
|
93
|
+
return this
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
structEnd(): this {
|
|
97
|
+
this.bytes.push(0)
|
|
98
|
+
this.lastFieldId = this.parentFieldStack.pop() ?? 0
|
|
99
|
+
return this
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
finish(): Buffer {
|
|
103
|
+
this.bytes.push(0)
|
|
104
|
+
return Buffer.from(this.bytes)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Compact protocol packs a small field-id delta and the type into one byte
|
|
108
|
+
// (delta << 4 | type); deltas outside 1..15 fall back to a zigzag varint id.
|
|
109
|
+
private writeFieldHeader(type: number, id: number): void {
|
|
110
|
+
const delta = id - this.lastFieldId
|
|
111
|
+
if (delta > 0 && delta <= 15) {
|
|
112
|
+
this.bytes.push((delta << 4) | type)
|
|
113
|
+
} else {
|
|
114
|
+
this.bytes.push(type)
|
|
115
|
+
this.writeVarint(zigzag32(id))
|
|
116
|
+
}
|
|
117
|
+
this.lastFieldId = id
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Lists with fewer than 15 elements pack size into the header nibble;
|
|
121
|
+
// larger lists set the nibble to 0xf and append the size as a varint.
|
|
122
|
+
private writeListHeader(elementType: number, size: number): void {
|
|
123
|
+
if (size < 15) {
|
|
124
|
+
this.bytes.push((size << 4) | elementType)
|
|
125
|
+
} else {
|
|
126
|
+
this.bytes.push(0xf0 | elementType)
|
|
127
|
+
this.writeVarint(size)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private writeBinaryValue(value: string | Buffer): void {
|
|
132
|
+
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value, 'utf8')
|
|
133
|
+
this.writeVarint(buf.length)
|
|
134
|
+
for (const b of buf) this.bytes.push(b)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private writeVarint(value: number | bigint): void {
|
|
138
|
+
let remaining = BigInt(value) & U64_MASK
|
|
139
|
+
while (true) {
|
|
140
|
+
const chunk = Number(remaining & BigInt(VARINT_MASK))
|
|
141
|
+
remaining >>= 7n
|
|
142
|
+
if (remaining === 0n) {
|
|
143
|
+
this.bytes.push(chunk)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
this.bytes.push(chunk | VARINT_CONTINUE)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function zigzag32(value: number): number {
|
|
152
|
+
// `>>> 0` coerces the signed int32 bitwise result to an unsigned 32-bit value,
|
|
153
|
+
// so large positives and INT32_MIN encode as a <=5-byte varint, not a 64-bit one.
|
|
154
|
+
return ((value << 1) ^ (value >> 31)) >>> 0
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function zigzag64(value: bigint): bigint {
|
|
158
|
+
return BigInt.asUintN(64, value << 1n) ^ (value >> 63n)
|
|
159
|
+
}
|