agent-messenger 2.32.0 → 2.32.1
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 +7 -0
- package/dist/package.json +2 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.js +67 -18
- package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
- package/dist/src/platforms/discord/commands/channel.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/channel.js +3 -2
- package/dist/src/platforms/discord/commands/channel.js.map +1 -1
- package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/snapshot.js +7 -6
- package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/discord/types.d.ts +17 -0
- package/dist/src/platforms/discord/types.d.ts.map +1 -1
- package/dist/src/platforms/discord/types.js +36 -0
- package/dist/src/platforms/discord/types.js.map +1 -1
- package/dist/src/platforms/slack/index.d.ts +1 -0
- package/dist/src/platforms/slack/index.d.ts.map +1 -1
- package/dist/src/platforms/slack/index.js.map +1 -1
- package/dist/src/platforms/slack/qr-confirmation.d.ts +19 -0
- package/dist/src/platforms/slack/qr-confirmation.d.ts.map +1 -0
- package/dist/src/platforms/slack/qr-confirmation.js +68 -0
- package/dist/src/platforms/slack/qr-confirmation.js.map +1 -0
- package/dist/src/platforms/slack/qr-cookie-jar.d.ts +7 -0
- package/dist/src/platforms/slack/qr-cookie-jar.d.ts.map +1 -0
- package/dist/src/platforms/slack/qr-cookie-jar.js +48 -0
- package/dist/src/platforms/slack/qr-cookie-jar.js.map +1 -0
- package/dist/src/platforms/slack/qr-http-login.d.ts +2 -1
- package/dist/src/platforms/slack/qr-http-login.d.ts.map +1 -1
- package/dist/src/platforms/slack/qr-http-login.js +88 -53
- package/dist/src/platforms/slack/qr-http-login.js.map +1 -1
- package/docs/content/docs/cli/channeltalk.mdx +4 -4
- package/package.json +2 -1
- package/skills/agent-channeltalk/SKILL.md +5 -5
- package/skills/agent-channeltalk/references/authentication.md +9 -5
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +8 -4
- package/skills/agent-discord/references/common-patterns.md +7 -3
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/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 +25 -2
- 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/platforms/channeltalk/token-extractor.test.ts +235 -0
- package/src/platforms/channeltalk/token-extractor.ts +81 -22
- package/src/platforms/discord/commands/channel.test.ts +33 -23
- package/src/platforms/discord/commands/channel.ts +3 -2
- package/src/platforms/discord/commands/snapshot.test.ts +80 -2
- package/src/platforms/discord/commands/snapshot.ts +7 -6
- package/src/platforms/discord/types.test.ts +45 -0
- package/src/platforms/discord/types.ts +41 -0
- package/src/platforms/slack/index.ts +1 -0
- package/src/platforms/slack/qr-confirmation.ts +86 -0
- package/src/platforms/slack/qr-cookie-jar.ts +54 -0
- package/src/platforms/slack/qr-http-login-2fa.test.ts +398 -0
- package/src/platforms/slack/qr-http-login.test.ts +10 -18
- package/src/platforms/slack/qr-http-login.ts +109 -52
|
@@ -6,6 +6,7 @@ import { formatOutput } from '@/shared/utils/output'
|
|
|
6
6
|
|
|
7
7
|
import { DiscordClient } from '../client'
|
|
8
8
|
import { DiscordCredentialManager } from '../credential-manager'
|
|
9
|
+
import { isListableChannel, isMessageReadableChannel } from '../types'
|
|
9
10
|
import type { DiscordChannel } from '../types'
|
|
10
11
|
|
|
11
12
|
export async function snapshotAction(options: {
|
|
@@ -41,8 +42,9 @@ export async function snapshotAction(options: {
|
|
|
41
42
|
|
|
42
43
|
if (!options.usersOnly) {
|
|
43
44
|
const channels = await client.listChannels(serverId)
|
|
45
|
+
const listableChannels = channels.filter(isListableChannel)
|
|
44
46
|
|
|
45
|
-
snapshot.channels =
|
|
47
|
+
snapshot.channels = listableChannels.map((ch) => ({
|
|
46
48
|
id: ch.id,
|
|
47
49
|
name: ch.name,
|
|
48
50
|
type: ch.type,
|
|
@@ -50,11 +52,10 @@ export async function snapshotAction(options: {
|
|
|
50
52
|
}))
|
|
51
53
|
|
|
52
54
|
if (!options.channelsOnly) {
|
|
53
|
-
const
|
|
54
|
-
const textChannels = channels.filter(isTextChannel)
|
|
55
|
+
const readableChannels = listableChannels.filter(isMessageReadableChannel)
|
|
55
56
|
|
|
56
57
|
const channelMessages = await parallelMap(
|
|
57
|
-
|
|
58
|
+
readableChannels,
|
|
58
59
|
async (channel: DiscordChannel) => {
|
|
59
60
|
const messages = await client.getMessages(channel.id, messageLimit)
|
|
60
61
|
return messages.map((msg) => ({
|
|
@@ -88,8 +89,8 @@ export async function snapshotAction(options: {
|
|
|
88
89
|
} else {
|
|
89
90
|
if (!options.usersOnly) {
|
|
90
91
|
const channels = await client.listChannels(serverId)
|
|
91
|
-
const
|
|
92
|
-
snapshot.channels =
|
|
92
|
+
const listableChannels = channels.filter(isListableChannel)
|
|
93
|
+
snapshot.channels = listableChannels.map((ch) => ({ id: ch.id, name: ch.name }))
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
snapshot.hint =
|
|
@@ -2,6 +2,7 @@ import { expect, it } from 'bun:test'
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
DiscordChannelSchema,
|
|
5
|
+
DiscordChannelType,
|
|
5
6
|
DiscordConfigSchema,
|
|
6
7
|
DiscordCredentialsSchema,
|
|
7
8
|
DiscordError,
|
|
@@ -10,6 +11,8 @@ import {
|
|
|
10
11
|
DiscordMessageSchema,
|
|
11
12
|
DiscordReactionSchema,
|
|
12
13
|
DiscordUserSchema,
|
|
14
|
+
isListableChannel,
|
|
15
|
+
isMessageReadableChannel,
|
|
13
16
|
} from './types'
|
|
14
17
|
|
|
15
18
|
it('DiscordGuildSchema validates correct guild', () => {
|
|
@@ -255,3 +258,45 @@ it('DiscordError is instance of Error', () => {
|
|
|
255
258
|
const error = new DiscordError('Test error', 'TEST_CODE')
|
|
256
259
|
expect(error instanceof Error).toBe(true)
|
|
257
260
|
})
|
|
261
|
+
|
|
262
|
+
it('isListableChannel includes text, announcement, voice, stage, forum, media, and directory', () => {
|
|
263
|
+
const listable = [
|
|
264
|
+
DiscordChannelType.GUILD_TEXT,
|
|
265
|
+
DiscordChannelType.GUILD_ANNOUNCEMENT,
|
|
266
|
+
DiscordChannelType.GUILD_VOICE,
|
|
267
|
+
DiscordChannelType.GUILD_STAGE_VOICE,
|
|
268
|
+
DiscordChannelType.GUILD_FORUM,
|
|
269
|
+
DiscordChannelType.GUILD_MEDIA,
|
|
270
|
+
DiscordChannelType.GUILD_DIRECTORY,
|
|
271
|
+
]
|
|
272
|
+
expect(listable.every((type) => isListableChannel({ type }))).toBe(true)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('isListableChannel excludes categories and threads', () => {
|
|
276
|
+
const excluded = [
|
|
277
|
+
DiscordChannelType.GUILD_CATEGORY,
|
|
278
|
+
DiscordChannelType.ANNOUNCEMENT_THREAD,
|
|
279
|
+
DiscordChannelType.PUBLIC_THREAD,
|
|
280
|
+
DiscordChannelType.PRIVATE_THREAD,
|
|
281
|
+
]
|
|
282
|
+
expect(excluded.some((type) => isListableChannel({ type }))).toBe(false)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('isMessageReadableChannel includes text, announcement, voice, and stage', () => {
|
|
286
|
+
const readable = [
|
|
287
|
+
DiscordChannelType.GUILD_TEXT,
|
|
288
|
+
DiscordChannelType.GUILD_ANNOUNCEMENT,
|
|
289
|
+
DiscordChannelType.GUILD_VOICE,
|
|
290
|
+
DiscordChannelType.GUILD_STAGE_VOICE,
|
|
291
|
+
]
|
|
292
|
+
expect(readable.every((type) => isMessageReadableChannel({ type }))).toBe(true)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('isMessageReadableChannel excludes forum, media, and directory containers', () => {
|
|
296
|
+
const notReadable = [
|
|
297
|
+
DiscordChannelType.GUILD_FORUM,
|
|
298
|
+
DiscordChannelType.GUILD_MEDIA,
|
|
299
|
+
DiscordChannelType.GUILD_DIRECTORY,
|
|
300
|
+
]
|
|
301
|
+
expect(notReadable.some((type) => isMessageReadableChannel({ type }))).toBe(false)
|
|
302
|
+
})
|
|
@@ -27,6 +27,47 @@ export interface DiscordChannel {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
|
31
|
+
export const DiscordChannelType = {
|
|
32
|
+
GUILD_TEXT: 0,
|
|
33
|
+
DM: 1,
|
|
34
|
+
GUILD_VOICE: 2,
|
|
35
|
+
GROUP_DM: 3,
|
|
36
|
+
GUILD_CATEGORY: 4,
|
|
37
|
+
GUILD_ANNOUNCEMENT: 5,
|
|
38
|
+
ANNOUNCEMENT_THREAD: 10,
|
|
39
|
+
PUBLIC_THREAD: 11,
|
|
40
|
+
PRIVATE_THREAD: 12,
|
|
41
|
+
GUILD_STAGE_VOICE: 13,
|
|
42
|
+
GUILD_DIRECTORY: 14,
|
|
43
|
+
GUILD_FORUM: 15,
|
|
44
|
+
GUILD_MEDIA: 16,
|
|
45
|
+
} as const
|
|
46
|
+
|
|
47
|
+
const THREAD_CHANNEL_TYPES: ReadonlySet<number> = new Set([
|
|
48
|
+
DiscordChannelType.ANNOUNCEMENT_THREAD,
|
|
49
|
+
DiscordChannelType.PUBLIC_THREAD,
|
|
50
|
+
DiscordChannelType.PRIVATE_THREAD,
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
// Channels with no directly fetchable message timeline:
|
|
54
|
+
// forum/media messages live in child posts/threads, and directory channels
|
|
55
|
+
// are hub listings, not message channels. Fetching messages from these aborts.
|
|
56
|
+
const NON_MESSAGE_CHANNEL_TYPES: ReadonlySet<number> = new Set([
|
|
57
|
+
DiscordChannelType.GUILD_FORUM,
|
|
58
|
+
DiscordChannelType.GUILD_MEDIA,
|
|
59
|
+
DiscordChannelType.GUILD_DIRECTORY,
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
// Voice/stage channels are listed too because they carry embedded text chat.
|
|
63
|
+
export function isListableChannel(channel: Pick<DiscordChannel, 'type'>): boolean {
|
|
64
|
+
return channel.type !== DiscordChannelType.GUILD_CATEGORY && !THREAD_CHANNEL_TYPES.has(channel.type)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isMessageReadableChannel(channel: Pick<DiscordChannel, 'type'>): boolean {
|
|
68
|
+
return isListableChannel(channel) && !NON_MESSAGE_CHANNEL_TYPES.has(channel.type)
|
|
69
|
+
}
|
|
70
|
+
|
|
30
71
|
export interface DiscordMessage {
|
|
31
72
|
id: string
|
|
32
73
|
channel_id: string
|
|
@@ -3,6 +3,7 @@ export { SlackCredentialManager, CredentialManager } from './credential-manager'
|
|
|
3
3
|
export { SlackListener } from './listener'
|
|
4
4
|
export { loginWithQr } from './qr-http-login'
|
|
5
5
|
export type { QrLoginOptions, QrSession } from './qr-http-login'
|
|
6
|
+
export type { SlackConfirmationChallenge, SlackConfirmationCodeRequest } from './qr-confirmation'
|
|
6
7
|
export { decodeSlackQr } from './qr-login'
|
|
7
8
|
export type { SlackQrLogin } from './qr-login'
|
|
8
9
|
export type {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { SlackQrLogin } from './qr-login'
|
|
2
|
+
|
|
3
|
+
export interface SlackConfirmationChallenge {
|
|
4
|
+
readonly email: string
|
|
5
|
+
readonly type: string | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type SlackConfirmationCodeRequest = (challenge: SlackConfirmationChallenge) => Promise<string>
|
|
9
|
+
|
|
10
|
+
type SigninProps = {
|
|
11
|
+
readonly magicLogin: string
|
|
12
|
+
readonly emailAddress: string
|
|
13
|
+
readonly twoFactorType: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseConfirmationPage(html: string): SigninProps | null {
|
|
17
|
+
const match = html.match(/id="props_node"[^>]*\bdata-props="([^"]*)"/i)
|
|
18
|
+
const encoded = match?.[1]
|
|
19
|
+
if (!encoded) return null
|
|
20
|
+
|
|
21
|
+
let parsed: unknown
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(decodeHtml(encoded))
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
if (!isRecord(parsed)) return null
|
|
28
|
+
|
|
29
|
+
const magicLogin = parsed.magicLogin
|
|
30
|
+
const emailAddress = parsed.emailAddress
|
|
31
|
+
if (typeof magicLogin !== 'string' || magicLogin === '') return null
|
|
32
|
+
if (typeof emailAddress !== 'string' || emailAddress === '') return null
|
|
33
|
+
return {
|
|
34
|
+
magicLogin,
|
|
35
|
+
emailAddress,
|
|
36
|
+
twoFactorType: typeof parsed.twoFactorType === 'string' ? parsed.twoFactorType : null,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildConfirmationRequest(
|
|
41
|
+
login: SlackQrLogin,
|
|
42
|
+
finalUrl: string,
|
|
43
|
+
props: SigninProps,
|
|
44
|
+
code: string,
|
|
45
|
+
): { readonly url: string; readonly body: string } | null {
|
|
46
|
+
const expected = expectedConfirmationPath(login)
|
|
47
|
+
if (!expected) return null
|
|
48
|
+
const url = new URL(finalUrl)
|
|
49
|
+
if (url.hostname !== `${login.workspace}.slack.com`) return null
|
|
50
|
+
if (url.pathname !== expected) return null
|
|
51
|
+
|
|
52
|
+
url.search = ''
|
|
53
|
+
url.searchParams.set('domain', login.workspace)
|
|
54
|
+
url.searchParams.set('domainLogin', '1')
|
|
55
|
+
url.searchParams.set('email', props.emailAddress)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
url: url.toString(),
|
|
59
|
+
body: new URLSearchParams({
|
|
60
|
+
'2fa_magiclogin': props.magicLogin,
|
|
61
|
+
remember: '0',
|
|
62
|
+
has_remember: 'false',
|
|
63
|
+
'2fa_code': code.trim(),
|
|
64
|
+
'2fa_action': 'submit_primary',
|
|
65
|
+
}).toString(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function expectedConfirmationPath(login: SlackQrLogin): string | null {
|
|
70
|
+
const qrSecret = new URL(login.url).pathname.split('/').at(-1)
|
|
71
|
+
if (!qrSecret || !qrSecret.startsWith('z-app-')) return null
|
|
72
|
+
return `/login/${qrSecret}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function decodeHtml(value: string): string {
|
|
76
|
+
return value
|
|
77
|
+
.replaceAll('"', '"')
|
|
78
|
+
.replaceAll(''', "'")
|
|
79
|
+
.replaceAll('<', '<')
|
|
80
|
+
.replaceAll('>', '>')
|
|
81
|
+
.replaceAll('&', '&')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
85
|
+
return typeof value === 'object' && value !== null
|
|
86
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CookieJar } from 'tough-cookie'
|
|
2
|
+
|
|
3
|
+
const BROWSER_USER_AGENT =
|
|
4
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'
|
|
5
|
+
|
|
6
|
+
const SESSION_COOKIE_NAME = 'd'
|
|
7
|
+
const SESSION_COOKIE_PREFIX = 'xoxd-'
|
|
8
|
+
|
|
9
|
+
export class SlackCookieJar {
|
|
10
|
+
readonly #jar = new CookieJar()
|
|
11
|
+
|
|
12
|
+
async fetch(doFetch: typeof fetch, url: string, init: RequestInit = {}): Promise<Response> {
|
|
13
|
+
const cookie = await this.#jar.getCookieString(url)
|
|
14
|
+
const response = await doFetch(url, {
|
|
15
|
+
...init,
|
|
16
|
+
redirect: 'manual',
|
|
17
|
+
headers: {
|
|
18
|
+
'User-Agent': BROWSER_USER_AGENT,
|
|
19
|
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
20
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
21
|
+
...init.headers,
|
|
22
|
+
...(cookie ? { Cookie: cookie } : {}),
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
for (const setCookie of getSetCookies(response)) {
|
|
26
|
+
await this.#jar.setCookie(setCookie, url, { ignoreError: true })
|
|
27
|
+
}
|
|
28
|
+
return response
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async sessionCookie(url: string): Promise<string | null> {
|
|
32
|
+
const cookies = await this.#jar.getCookies(url)
|
|
33
|
+
for (const cookie of cookies) {
|
|
34
|
+
if (cookie.key === SESSION_COOKIE_NAME && cookie.value.startsWith(SESSION_COOKIE_PREFIX)) {
|
|
35
|
+
return cookie.value
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setCookieNames(response: Response): readonly string[] {
|
|
43
|
+
return getSetCookies(response).flatMap((setCookie) => {
|
|
44
|
+
const name = setCookie.split('=')[0]?.trim()
|
|
45
|
+
return name ? [name] : []
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getSetCookies(response: Response): readonly string[] {
|
|
50
|
+
const withGetter = response.headers as Headers & { getSetCookie?: () => string[] }
|
|
51
|
+
if (typeof withGetter.getSetCookie === 'function') return withGetter.getSetCookie()
|
|
52
|
+
const single = response.headers.get('set-cookie')
|
|
53
|
+
return single ? [single] : []
|
|
54
|
+
}
|