agent-messenger 2.20.1 → 2.20.3
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/plugin.json +1 -1
- package/bun.lock +41 -0
- package/dist/package.json +2 -1
- package/dist/src/platforms/webex/client.d.ts +2 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +31 -7
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/message.js +17 -13
- package/dist/src/platforms/webex/commands/message.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts +7 -0
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
- package/dist/src/platforms/webex/encryption.js +12 -1
- package/dist/src/platforms/webex/encryption.js.map +1 -1
- package/dist/src/platforms/webex/kms-key-provider.d.ts +20 -0
- package/dist/src/platforms/webex/kms-key-provider.d.ts.map +1 -0
- package/dist/src/platforms/webex/kms-key-provider.js +78 -0
- package/dist/src/platforms/webex/kms-key-provider.js.map +1 -0
- package/dist/src/platforms/webex/markdown-to-html.d.ts +1 -0
- package/dist/src/platforms/webex/markdown-to-html.d.ts.map +1 -1
- package/dist/src/platforms/webex/markdown-to-html.js +1 -1
- package/dist/src/platforms/webex/markdown-to-html.js.map +1 -1
- package/dist/src/tui/adapters/line-adapter.d.ts +3 -0
- package/dist/src/tui/adapters/line-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/line-adapter.js +26 -0
- package/dist/src/tui/adapters/line-adapter.js.map +1 -1
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +8 -0
- package/dist/src/tui/app.js.map +1 -1
- package/docs/content/docs/cli/webex.mdx +1 -1
- package/package.json +2 -1
- 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-instagram/SKILL.md +1 -1
- 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-webex/references/authentication.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/platforms/webex/client.test.ts +25 -0
- package/src/platforms/webex/client.ts +37 -8
- package/src/platforms/webex/commands/message.test.ts +12 -0
- package/src/platforms/webex/commands/message.ts +22 -14
- package/src/platforms/webex/encryption.test.ts +58 -3
- package/src/platforms/webex/encryption.ts +19 -1
- package/src/platforms/webex/kms-key-provider.ts +99 -0
- package/src/platforms/webex/markdown-to-html.ts +1 -1
- package/src/platforms/webex/typings/webex-message-handler.d.ts +45 -0
- package/src/tui/adapters/line-adapter.ts +28 -0
- package/src/tui/app.ts +8 -0
|
@@ -32,6 +32,7 @@ let mockGetMessage: ReturnType<typeof spyOn>
|
|
|
32
32
|
let mockDeleteMessage: ReturnType<typeof spyOn>
|
|
33
33
|
let mockEditMessage: ReturnType<typeof spyOn>
|
|
34
34
|
let mockLogin: ReturnType<typeof spyOn>
|
|
35
|
+
let mockDispose: ReturnType<typeof spyOn>
|
|
35
36
|
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
36
37
|
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
37
38
|
let processExitSpy: ReturnType<typeof spyOn>
|
|
@@ -47,6 +48,7 @@ beforeEach(() => {
|
|
|
47
48
|
mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
|
|
48
49
|
return this
|
|
49
50
|
})
|
|
51
|
+
mockDispose = protoSpy('dispose').mockResolvedValue(undefined)
|
|
50
52
|
mockSendMessage = protoSpy('sendMessage').mockResolvedValue(mockMessage)
|
|
51
53
|
mockSendDirectMessage = protoSpy('sendDirectMessage').mockResolvedValue(mockMessage)
|
|
52
54
|
mockListMessages = protoSpy('listMessages').mockResolvedValue([mockMessage, mockMessage2])
|
|
@@ -78,6 +80,7 @@ it('calls sendMessage with correct args and outputs result', async () => {
|
|
|
78
80
|
expect(output).toContain('msg_123')
|
|
79
81
|
expect(output).toContain('space_456')
|
|
80
82
|
expect(output).toContain('user@example.com')
|
|
83
|
+
expect(mockDispose).toHaveBeenCalled()
|
|
81
84
|
})
|
|
82
85
|
|
|
83
86
|
it('passes markdown option when --markdown flag is set on send', async () => {
|
|
@@ -86,6 +89,14 @@ it('passes markdown option when --markdown flag is set on send', async () => {
|
|
|
86
89
|
expect(mockSendMessage).toHaveBeenCalledWith('space_456', '**bold**', { markdown: true })
|
|
87
90
|
})
|
|
88
91
|
|
|
92
|
+
it('disposes the client when send fails', async () => {
|
|
93
|
+
mockSendMessage.mockRejectedValue(new WebexError('Send failed', 'send_failed'))
|
|
94
|
+
|
|
95
|
+
await sendAction('space_456', 'Hello world', { pretty: false })
|
|
96
|
+
|
|
97
|
+
expect(mockDispose).toHaveBeenCalled()
|
|
98
|
+
})
|
|
99
|
+
|
|
89
100
|
it('exits with code 1 when not authenticated on send', async () => {
|
|
90
101
|
mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
|
|
91
102
|
|
|
@@ -166,6 +177,7 @@ it('calls editMessage with roomId in args and outputs result', async () => {
|
|
|
166
177
|
const output = consoleLogSpy.mock.calls[0][0]
|
|
167
178
|
expect(output).toContain('msg_123')
|
|
168
179
|
expect(output).toContain('Updated message')
|
|
180
|
+
expect(mockDispose).toHaveBeenCalled()
|
|
169
181
|
})
|
|
170
182
|
|
|
171
183
|
it('passes markdown option to editMessage when --markdown flag is set', async () => {
|
|
@@ -6,14 +6,23 @@ import { formatOutput } from '@/shared/utils/output'
|
|
|
6
6
|
import { WebexClient } from '../client'
|
|
7
7
|
import type { WebexMessage } from '../types'
|
|
8
8
|
|
|
9
|
+
async function withWebexClient<T>(run: (client: WebexClient) => Promise<T>): Promise<T> {
|
|
10
|
+
const client = new WebexClient()
|
|
11
|
+
try {
|
|
12
|
+
await client.login()
|
|
13
|
+
return await run(client)
|
|
14
|
+
} finally {
|
|
15
|
+
await client.dispose()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export async function sendAction(
|
|
10
20
|
spaceId: string,
|
|
11
21
|
text: string,
|
|
12
22
|
options: { markdown?: boolean; pretty?: boolean },
|
|
13
23
|
): Promise<void> {
|
|
14
24
|
try {
|
|
15
|
-
const
|
|
16
|
-
const message = await client.sendMessage(spaceId, text, { markdown: options.markdown })
|
|
25
|
+
const message = await withWebexClient((client) => client.sendMessage(spaceId, text, { markdown: options.markdown }))
|
|
17
26
|
|
|
18
27
|
const output = {
|
|
19
28
|
id: message.id,
|
|
@@ -31,9 +40,8 @@ export async function sendAction(
|
|
|
31
40
|
|
|
32
41
|
export async function listAction(spaceId: string, options: { limit?: number; pretty?: boolean }): Promise<void> {
|
|
33
42
|
try {
|
|
34
|
-
const client = await new WebexClient().login()
|
|
35
43
|
const limit = options.limit ?? 50
|
|
36
|
-
const messages = await client.listMessages(spaceId, { max: limit })
|
|
44
|
+
const messages = await withWebexClient((client) => client.listMessages(spaceId, { max: limit }))
|
|
37
45
|
|
|
38
46
|
const output = messages.map((msg: WebexMessage) => ({
|
|
39
47
|
id: msg.id,
|
|
@@ -51,8 +59,7 @@ export async function listAction(spaceId: string, options: { limit?: number; pre
|
|
|
51
59
|
|
|
52
60
|
export async function getAction(messageId: string, options: { pretty?: boolean }): Promise<void> {
|
|
53
61
|
try {
|
|
54
|
-
const
|
|
55
|
-
const message = await client.getMessage(messageId)
|
|
62
|
+
const message = await withWebexClient((client) => client.getMessage(messageId))
|
|
56
63
|
|
|
57
64
|
const output = {
|
|
58
65
|
id: message.id,
|
|
@@ -75,8 +82,7 @@ export async function deleteAction(messageId: string, options: { force?: boolean
|
|
|
75
82
|
return process.exit(0)
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
await client.deleteMessage(messageId)
|
|
85
|
+
await withWebexClient((client) => client.deleteMessage(messageId))
|
|
80
86
|
|
|
81
87
|
console.log(formatOutput({ deleted: messageId }, options.pretty))
|
|
82
88
|
} catch (error) {
|
|
@@ -91,10 +97,11 @@ export async function editAction(
|
|
|
91
97
|
options: { markdown?: boolean; pretty?: boolean },
|
|
92
98
|
): Promise<void> {
|
|
93
99
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const message = await withWebexClient((client) =>
|
|
101
|
+
client.editMessage(messageId, spaceId, text, {
|
|
102
|
+
markdown: options.markdown,
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
98
105
|
|
|
99
106
|
const output = {
|
|
100
107
|
id: message.id,
|
|
@@ -116,8 +123,9 @@ export async function dmAction(
|
|
|
116
123
|
options: { markdown?: boolean; pretty?: boolean },
|
|
117
124
|
): Promise<void> {
|
|
118
125
|
try {
|
|
119
|
-
const
|
|
120
|
-
|
|
126
|
+
const message = await withWebexClient((client) =>
|
|
127
|
+
client.sendDirectMessage(email, text, { markdown: options.markdown }),
|
|
128
|
+
)
|
|
121
129
|
|
|
122
130
|
const output = {
|
|
123
131
|
id: message.id,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
1
|
+
import { describe, expect, it, mock } from 'bun:test'
|
|
2
2
|
|
|
3
3
|
import * as jose from 'node-jose'
|
|
4
4
|
|
|
@@ -11,12 +11,16 @@ const decodeJweHeader = (jwe: string): Record<string, unknown> => {
|
|
|
11
11
|
return JSON.parse(json) as Record<string, unknown>
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const createSerializedKey = async (keyUri: string) => {
|
|
15
15
|
const keystore = jose.JWK.createKeyStore()
|
|
16
16
|
const key = await keystore.generate('oct', 256, { alg: 'A256GCM' })
|
|
17
17
|
const jwk = key.toJSON(true)
|
|
18
|
+
return JSON.stringify({ uri: keyUri, jwk })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const createKeyring = async (keyUri: string) => {
|
|
18
22
|
const rawKeys = new Map<string, string>()
|
|
19
|
-
rawKeys.set(keyUri,
|
|
23
|
+
rawKeys.set(keyUri, await createSerializedKey(keyUri))
|
|
20
24
|
return new WebexEncryptionService(rawKeys)
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -51,4 +55,55 @@ describe('WebexEncryptionService', () => {
|
|
|
51
55
|
|
|
52
56
|
expect(plaintext).toBe('round trip')
|
|
53
57
|
})
|
|
58
|
+
|
|
59
|
+
it('getKey returns cached key without calling provider when key is present', async () => {
|
|
60
|
+
const service = await createKeyring(keyUri)
|
|
61
|
+
const provider = { fetchKey: mock(async () => null as string | null) }
|
|
62
|
+
service.setKeyProvider(provider)
|
|
63
|
+
|
|
64
|
+
const key = await service.getKey(keyUri)
|
|
65
|
+
|
|
66
|
+
expect(key).not.toBeNull()
|
|
67
|
+
expect(provider.fetchKey).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('getKey calls provider and returns key when key is missing', async () => {
|
|
71
|
+
const missingKeyUri = 'kms://kms-aore.wbx2.com/keys/0d7a0dfb-0464-40ce-8f3d-e65a33b61561'
|
|
72
|
+
const serializedKey = await createSerializedKey(missingKeyUri)
|
|
73
|
+
const service = new WebexEncryptionService(new Map())
|
|
74
|
+
const provider = { fetchKey: mock(async () => serializedKey) }
|
|
75
|
+
service.setKeyProvider(provider)
|
|
76
|
+
|
|
77
|
+
const key = await service.getKey(missingKeyUri)
|
|
78
|
+
|
|
79
|
+
expect(key).not.toBeNull()
|
|
80
|
+
expect(provider.fetchKey).toHaveBeenCalledWith(missingKeyUri)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('getKey returns null when provider returns null', async () => {
|
|
84
|
+
const missingKeyUri = 'kms://kms-aore.wbx2.com/keys/13d6256d-f7f1-4b98-8102-4d3d87b2834a'
|
|
85
|
+
const service = new WebexEncryptionService(new Map())
|
|
86
|
+
const provider = { fetchKey: mock(async () => null as string | null) }
|
|
87
|
+
service.setKeyProvider(provider)
|
|
88
|
+
|
|
89
|
+
const key = await service.getKey(missingKeyUri)
|
|
90
|
+
|
|
91
|
+
expect(key).toBeNull()
|
|
92
|
+
expect(provider.fetchKey).toHaveBeenCalledWith(missingKeyUri)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('getKey reuses provider result from raw keys after first fetch', async () => {
|
|
96
|
+
const missingKeyUri = 'kms://kms-aore.wbx2.com/keys/84afb005-5ba5-49c8-bd46-0c5d7ddf1c30'
|
|
97
|
+
const serializedKey = await createSerializedKey(missingKeyUri)
|
|
98
|
+
const service = new WebexEncryptionService(new Map())
|
|
99
|
+
const provider = { fetchKey: mock(async () => serializedKey) }
|
|
100
|
+
service.setKeyProvider(provider)
|
|
101
|
+
|
|
102
|
+
await service.getKey(missingKeyUri)
|
|
103
|
+
;(service as unknown as { keyCache: Map<string, jose.JWK.Key> }).keyCache.clear()
|
|
104
|
+
const key = await service.getKey(missingKeyUri)
|
|
105
|
+
|
|
106
|
+
expect(key).not.toBeNull()
|
|
107
|
+
expect(provider.fetchKey).toHaveBeenCalledTimes(1)
|
|
108
|
+
})
|
|
54
109
|
})
|
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import * as jose from 'node-jose'
|
|
2
2
|
|
|
3
|
+
export interface WebexKeyProvider {
|
|
4
|
+
fetchKey(keyUri: string): Promise<string | null>
|
|
5
|
+
close?(): Promise<void>
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export class WebexEncryptionService {
|
|
4
9
|
private rawKeys: Map<string, string>
|
|
5
10
|
private keyCache: Map<string, jose.JWK.Key> = new Map()
|
|
11
|
+
private keyProvider: WebexKeyProvider | null = null
|
|
6
12
|
|
|
7
13
|
constructor(serializedKeys: Map<string, string>) {
|
|
8
14
|
this.rawKeys = serializedKeys
|
|
9
15
|
}
|
|
10
16
|
|
|
17
|
+
setKeyProvider(provider: WebexKeyProvider): void {
|
|
18
|
+
this.keyProvider = provider
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async close(): Promise<void> {
|
|
22
|
+
await this.keyProvider?.close?.()
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
async getKey(keyUri: string): Promise<jose.JWK.Key | null> {
|
|
12
26
|
const cached = this.keyCache.get(keyUri)
|
|
13
27
|
if (cached) return cached
|
|
14
28
|
|
|
15
|
-
|
|
29
|
+
let raw = this.rawKeys.get(keyUri)
|
|
30
|
+
if (!raw && this.keyProvider) {
|
|
31
|
+
raw = (await this.keyProvider.fetchKey(keyUri)) ?? undefined
|
|
32
|
+
}
|
|
16
33
|
if (!raw) return null
|
|
17
34
|
|
|
18
35
|
try {
|
|
19
36
|
const parsed = JSON.parse(raw) as { jwk: object }
|
|
20
37
|
const joseKey = await jose.JWK.asKey(parsed.jwk)
|
|
38
|
+
this.rawKeys.set(keyUri, raw)
|
|
21
39
|
this.keyCache.set(keyUri, joseKey)
|
|
22
40
|
return joseKey
|
|
23
41
|
} catch {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { DeviceManager, KmsClient, MercurySocket, noopLogger } from 'webex-message-handler'
|
|
2
|
+
import WebSocket from 'ws'
|
|
3
|
+
|
|
4
|
+
interface KmsKeyProviderOptions {
|
|
5
|
+
token: string
|
|
6
|
+
logger?: { debug(message: string): void }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Registration {
|
|
10
|
+
webSocketUrl: string
|
|
11
|
+
deviceUrl: string
|
|
12
|
+
userId: string
|
|
13
|
+
encryptionServiceUrl: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type HttpRequest = {
|
|
17
|
+
url: string
|
|
18
|
+
method: string
|
|
19
|
+
headers: Record<string, string>
|
|
20
|
+
body?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class KmsKeyProvider {
|
|
24
|
+
private token: string
|
|
25
|
+
private logger?: { debug(message: string): void }
|
|
26
|
+
private mercury: MercurySocket | null = null
|
|
27
|
+
private kms: KmsClient | null = null
|
|
28
|
+
private readyPromise: Promise<void> | null = null
|
|
29
|
+
|
|
30
|
+
constructor(options: KmsKeyProviderOptions) {
|
|
31
|
+
this.token = options.token
|
|
32
|
+
this.logger = options.logger
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async fetchKey(keyUri: string): Promise<string | null> {
|
|
36
|
+
try {
|
|
37
|
+
await this.ensureReady()
|
|
38
|
+
const key = await this.kms?.getKey(keyUri)
|
|
39
|
+
if (!key) return null
|
|
40
|
+
return JSON.stringify({ uri: keyUri, jwk: key.toJSON(true) })
|
|
41
|
+
} catch (error) {
|
|
42
|
+
this.logger?.debug(`Webex KMS key fetch failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
43
|
+
await this.close()
|
|
44
|
+
this.readyPromise = null
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async close(): Promise<void> {
|
|
50
|
+
await this.mercury?.disconnect().catch(() => undefined)
|
|
51
|
+
this.mercury = null
|
|
52
|
+
this.kms = null
|
|
53
|
+
this.readyPromise = null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async ensureReady(): Promise<void> {
|
|
57
|
+
this.readyPromise ??= this.initialize()
|
|
58
|
+
await this.readyPromise
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async initialize(): Promise<void> {
|
|
62
|
+
const httpDo = async (req: HttpRequest) => {
|
|
63
|
+
const res = await fetch(req.url, {
|
|
64
|
+
method: req.method,
|
|
65
|
+
headers: req.headers,
|
|
66
|
+
body: req.body,
|
|
67
|
+
})
|
|
68
|
+
return {
|
|
69
|
+
status: res.status,
|
|
70
|
+
ok: res.ok,
|
|
71
|
+
json: () => res.json(),
|
|
72
|
+
text: () => res.text(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const wsFactory = (url: string) => new WebSocket(url) as never
|
|
76
|
+
const dm = new DeviceManager({ logger: noopLogger, httpDo })
|
|
77
|
+
const reg = (await dm.register(this.token)) as Registration
|
|
78
|
+
const mercury = new MercurySocket({ logger: noopLogger, wsFactory })
|
|
79
|
+
const kms = new KmsClient({
|
|
80
|
+
token: this.token,
|
|
81
|
+
deviceUrl: reg.deviceUrl,
|
|
82
|
+
userId: reg.userId,
|
|
83
|
+
encryptionServiceUrl: reg.encryptionServiceUrl,
|
|
84
|
+
logger: noopLogger,
|
|
85
|
+
httpDo,
|
|
86
|
+
})
|
|
87
|
+
mercury.on('kms:response', (data: unknown) => kms.handleKmsMessage(data))
|
|
88
|
+
await mercury.connect(reg.webSocketUrl, this.token)
|
|
89
|
+
this.mercury = mercury
|
|
90
|
+
try {
|
|
91
|
+
await kms.initialize()
|
|
92
|
+
} catch (error) {
|
|
93
|
+
await mercury.disconnect().catch(() => undefined)
|
|
94
|
+
this.mercury = null
|
|
95
|
+
throw error
|
|
96
|
+
}
|
|
97
|
+
this.kms = kms
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -184,7 +184,7 @@ function isSafeUrl(url: string): boolean {
|
|
|
184
184
|
return SAFE_URL_PATTERN.test(url.trim())
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function escapeHtml(value: string): string {
|
|
187
|
+
export function escapeHtml(value: string): string {
|
|
188
188
|
return value
|
|
189
189
|
.replaceAll('&', '&')
|
|
190
190
|
.replaceAll('<', '<')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export {}
|
|
2
|
+
|
|
3
|
+
declare module 'webex-message-handler' {
|
|
4
|
+
import type * as jose from 'node-jose'
|
|
5
|
+
|
|
6
|
+
type Logger = Record<string, (...args: unknown[]) => void>
|
|
7
|
+
type HttpRequest = { url: string; method: string; headers: Record<string, string>; body?: string }
|
|
8
|
+
type HttpResponse = { status: number; ok: boolean; json(): Promise<unknown>; text(): Promise<string> }
|
|
9
|
+
type HttpDo = (req: HttpRequest) => Promise<HttpResponse>
|
|
10
|
+
|
|
11
|
+
export const noopLogger: Logger
|
|
12
|
+
export const consoleLogger: Logger
|
|
13
|
+
|
|
14
|
+
export class DeviceManager {
|
|
15
|
+
constructor(options: { logger: Logger; httpDo: HttpDo })
|
|
16
|
+
register(token: string): Promise<{
|
|
17
|
+
webSocketUrl: string
|
|
18
|
+
deviceUrl: string
|
|
19
|
+
userId: string
|
|
20
|
+
services: unknown
|
|
21
|
+
encryptionServiceUrl: string
|
|
22
|
+
}>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class MercurySocket {
|
|
26
|
+
constructor(options: { logger: Logger; wsFactory: (url: string) => unknown })
|
|
27
|
+
on(event: 'kms:response', handler: (data: unknown) => void): void
|
|
28
|
+
connect(webSocketUrl: string, token: string): Promise<void>
|
|
29
|
+
disconnect(): Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class KmsClient {
|
|
33
|
+
constructor(options: {
|
|
34
|
+
token: string
|
|
35
|
+
deviceUrl: string
|
|
36
|
+
userId: string
|
|
37
|
+
encryptionServiceUrl: string
|
|
38
|
+
logger: Logger
|
|
39
|
+
httpDo: HttpDo
|
|
40
|
+
})
|
|
41
|
+
initialize(): Promise<void>
|
|
42
|
+
getKey(keyUri: string): Promise<jose.JWK.Key | null>
|
|
43
|
+
handleKmsMessage(data: unknown): void
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { LineClient } from '@/platforms/line/client'
|
|
2
2
|
import { LineCredentialManager } from '@/platforms/line/credential-manager'
|
|
3
|
+
import { LineListener } from '@/platforms/line/listener'
|
|
4
|
+
import type { LinePushMessageEvent } from '@/platforms/line/types'
|
|
3
5
|
|
|
4
6
|
import type { AuthHint, AuthIO, PlatformAdapter, UnifiedChannel, UnifiedMessage, Workspace } from './types'
|
|
5
7
|
|
|
@@ -7,6 +9,7 @@ export class LineAdapter implements PlatformAdapter {
|
|
|
7
9
|
readonly name = 'LINE'
|
|
8
10
|
|
|
9
11
|
private client: LineClient | null = null
|
|
12
|
+
private listener: LineListener | null = null
|
|
10
13
|
private credManager = new LineCredentialManager()
|
|
11
14
|
private currentAccount: Workspace | null = null
|
|
12
15
|
|
|
@@ -48,6 +51,28 @@ export class LineAdapter implements PlatformAdapter {
|
|
|
48
51
|
await client.sendMessage(channelId, text)
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
async startListening(onMessage: (msg: UnifiedMessage) => void): Promise<void> {
|
|
55
|
+
const client = this.ensureClient()
|
|
56
|
+
const listener = new LineListener(client)
|
|
57
|
+
await listener.start()
|
|
58
|
+
listener.on('message', (event: LinePushMessageEvent) => {
|
|
59
|
+
if (event.text === null) return
|
|
60
|
+
onMessage({
|
|
61
|
+
id: event.message_id,
|
|
62
|
+
channelId: event.chat_id,
|
|
63
|
+
author: event.author_id || 'unknown',
|
|
64
|
+
content: event.text,
|
|
65
|
+
timestamp: event.sent_at,
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
this.listener = listener
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
stopListening(): void {
|
|
72
|
+
this.listener?.stop()
|
|
73
|
+
this.listener = null
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
async getWorkspaces(): Promise<Workspace[]> {
|
|
52
77
|
const accounts = await this.credManager.listAccounts()
|
|
53
78
|
return accounts.map((acct) => ({
|
|
@@ -62,6 +87,9 @@ export class LineAdapter implements PlatformAdapter {
|
|
|
62
87
|
|
|
63
88
|
const client = new LineClient()
|
|
64
89
|
await client.login(creds)
|
|
90
|
+
// Persist the selected account so the listener's credential-less reconnects
|
|
91
|
+
// resolve this account instead of a stale current_account.
|
|
92
|
+
await this.credManager.setCurrentAccount(accountId)
|
|
65
93
|
this.client = client
|
|
66
94
|
this.currentAccount = { id: creds.account_id, name: creds.display_name ?? creds.account_id }
|
|
67
95
|
}
|
package/src/tui/app.ts
CHANGED
|
@@ -647,6 +647,10 @@ export async function createApp(): Promise<void> {
|
|
|
647
647
|
const ws = p.workspaces[selectedIndex]
|
|
648
648
|
try {
|
|
649
649
|
await p.adapter.switchWorkspace?.(ws.id)
|
|
650
|
+
if (p.listening) {
|
|
651
|
+
p.adapter.stopListening?.()
|
|
652
|
+
p.listening = false
|
|
653
|
+
}
|
|
650
654
|
p.channels = null
|
|
651
655
|
renderHeader()
|
|
652
656
|
} catch {}
|
|
@@ -744,6 +748,10 @@ export async function createApp(): Promise<void> {
|
|
|
744
748
|
p.adapter
|
|
745
749
|
.switchWorkspace(workspace.id)
|
|
746
750
|
.then(() => {
|
|
751
|
+
if (p.listening) {
|
|
752
|
+
p.adapter.stopListening?.()
|
|
753
|
+
p.listening = false
|
|
754
|
+
}
|
|
747
755
|
p.channels = null
|
|
748
756
|
p.workspaces = null
|
|
749
757
|
renderHeader()
|