agent-messenger 2.28.0 → 2.29.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/README.md +29 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/client.d.ts +6 -2
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +10 -3
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/instagram/client.d.ts +24 -2
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +343 -53
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +123 -11
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.js +17 -1
- package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
- package/dist/src/platforms/instagram/types.d.ts +10 -8
- package/dist/src/platforms/instagram/types.d.ts.map +1 -1
- package/dist/src/platforms/instagram/types.js.map +1 -1
- package/dist/src/platforms/teams/app-config.d.ts +36 -0
- package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
- package/dist/src/platforms/teams/app-config.js +69 -0
- package/dist/src/platforms/teams/app-config.js.map +1 -0
- package/dist/src/platforms/teams/client.d.ts +20 -2
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +311 -7
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +137 -3
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/file.d.ts +3 -0
- package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/file.js +59 -2
- package/dist/src/platforms/teams/commands/file.js.map +1 -1
- package/dist/src/platforms/teams/commands/message.d.ts +11 -0
- package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/message.js +121 -2
- package/dist/src/platforms/teams/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/teams/credential-manager.js +46 -2
- package/dist/src/platforms/teams/credential-manager.js.map +1 -1
- package/dist/src/platforms/teams/device-code.d.ts +48 -0
- package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-code.js +199 -0
- package/dist/src/platforms/teams/device-code.js.map +1 -0
- package/dist/src/platforms/teams/device-login.d.ts +37 -0
- package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-login.js +129 -0
- package/dist/src/platforms/teams/device-login.js.map +1 -0
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +12 -0
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/index.d.ts +7 -3
- package/dist/src/platforms/teams/index.d.ts.map +1 -1
- package/dist/src/platforms/teams/index.js +5 -2
- package/dist/src/platforms/teams/index.js.map +1 -1
- package/dist/src/platforms/teams/listener.d.ts +40 -0
- package/dist/src/platforms/teams/listener.d.ts.map +1 -0
- package/dist/src/platforms/teams/listener.js +286 -0
- package/dist/src/platforms/teams/listener.js.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.js +33 -0
- package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
- package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +60 -0
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/token-provider.d.ts +25 -0
- package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
- package/dist/src/platforms/teams/token-provider.js +190 -0
- package/dist/src/platforms/teams/token-provider.js.map +1 -0
- package/dist/src/platforms/teams/trouter.d.ts +38 -0
- package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
- package/dist/src/platforms/teams/trouter.js +266 -0
- package/dist/src/platforms/teams/trouter.js.map +1 -0
- package/dist/src/platforms/teams/types.d.ts +103 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js +30 -0
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +1 -1
- package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/instagram-adapter.js +16 -0
- package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
- package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.js +21 -0
- package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
- package/dist/src/tui/app.js +3 -3
- package/docs/content/docs/cli/instagram.mdx +1 -0
- package/docs/content/docs/cli/teams.mdx +36 -0
- package/docs/content/docs/sdk/teams.mdx +30 -0
- package/package.json +1 -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-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-instagram/references/authentication.md +11 -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 +82 -6
- package/skills/agent-teams/references/authentication.md +31 -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/discord/client.test.ts +86 -0
- package/src/platforms/discord/client.ts +10 -3
- package/src/platforms/instagram/client.test.ts +478 -33
- package/src/platforms/instagram/client.ts +449 -68
- package/src/platforms/instagram/commands/auth.test.ts +177 -1
- package/src/platforms/instagram/commands/auth.ts +182 -11
- package/src/platforms/instagram/credential-manager.test.ts +30 -0
- package/src/platforms/instagram/credential-manager.ts +23 -1
- package/src/platforms/instagram/types.ts +11 -8
- package/src/platforms/teams/app-config.ts +92 -0
- package/src/platforms/teams/client.test.ts +335 -1
- package/src/platforms/teams/client.ts +370 -7
- package/src/platforms/teams/commands/auth.test.ts +93 -1
- package/src/platforms/teams/commands/auth.ts +182 -3
- package/src/platforms/teams/commands/file.test.ts +92 -1
- package/src/platforms/teams/commands/file.ts +76 -1
- package/src/platforms/teams/commands/message.test.ts +95 -1
- package/src/platforms/teams/commands/message.ts +145 -3
- package/src/platforms/teams/credential-manager.test.ts +20 -0
- package/src/platforms/teams/credential-manager.ts +68 -3
- package/src/platforms/teams/device-code.test.ts +311 -0
- package/src/platforms/teams/device-code.ts +277 -0
- package/src/platforms/teams/device-login.test.ts +288 -0
- package/src/platforms/teams/device-login.ts +206 -0
- package/src/platforms/teams/ensure-auth.ts +11 -0
- package/src/platforms/teams/index.test.ts +10 -0
- package/src/platforms/teams/index.ts +17 -1
- package/src/platforms/teams/listener.test.ts +243 -0
- package/src/platforms/teams/listener.ts +351 -0
- package/src/platforms/teams/realm-discovery.test.ts +73 -0
- package/src/platforms/teams/realm-discovery.ts +42 -0
- package/src/platforms/teams/token-extractor.ts +69 -0
- package/src/platforms/teams/token-provider.test.ts +227 -0
- package/src/platforms/teams/token-provider.ts +284 -0
- package/src/platforms/teams/trouter.test.ts +174 -0
- package/src/platforms/teams/trouter.ts +323 -0
- package/src/platforms/teams/types.test.ts +17 -0
- package/src/platforms/teams/types.ts +96 -0
- package/src/tui/adapters/instagram-adapter.ts +13 -0
- package/src/tui/adapters/teams-adapter.ts +23 -0
- package/src/tui/app.ts +3 -3
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { getTeamsAppClientId } from './app-config'
|
|
2
|
+
import { TeamsClient } from './client'
|
|
3
|
+
import { TeamsCredentialManager } from './credential-manager'
|
|
4
|
+
import {
|
|
5
|
+
type AadToken,
|
|
6
|
+
type DeviceCodeInfo,
|
|
7
|
+
decodeJwtTid,
|
|
8
|
+
exchangeDeviceCode,
|
|
9
|
+
exchangeForSkypeScope,
|
|
10
|
+
isConsumerTenant,
|
|
11
|
+
mintSkypeToken,
|
|
12
|
+
pollDeviceToken,
|
|
13
|
+
requestDeviceCode,
|
|
14
|
+
resolveWorkTenantId,
|
|
15
|
+
} from './device-code'
|
|
16
|
+
import { type TeamsAccountType, TeamsError } from './types'
|
|
17
|
+
|
|
18
|
+
const PERSONAL_ACCOUNT_HINT =
|
|
19
|
+
'This looks like a personal Microsoft account. Run `agent-teams auth login --account-type personal` to sign in.'
|
|
20
|
+
|
|
21
|
+
export interface DeviceCodePrompt {
|
|
22
|
+
verificationUri: string
|
|
23
|
+
verificationUriComplete: string
|
|
24
|
+
userCode: string
|
|
25
|
+
expiresAt: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DeviceLoginResult {
|
|
29
|
+
accountType: TeamsAccountType
|
|
30
|
+
userName: string
|
|
31
|
+
teams: Array<{ id: string; name: string }>
|
|
32
|
+
current: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface LoginCallbacks {
|
|
36
|
+
onCode: (prompt: DeviceCodePrompt) => void | Promise<void>
|
|
37
|
+
onPending?: () => void
|
|
38
|
+
debug?: (message: string) => void
|
|
39
|
+
clientId?: string
|
|
40
|
+
accountType?: TeamsAccountType
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function startDeviceCode(
|
|
44
|
+
clientIdOverride?: string,
|
|
45
|
+
accountType: TeamsAccountType = 'personal',
|
|
46
|
+
): Promise<{ info: DeviceCodeInfo; clientId: string }> {
|
|
47
|
+
const clientId = clientIdOverride ?? getTeamsAppClientId(accountType).clientId
|
|
48
|
+
const info = await requestDeviceCode(clientId, accountType)
|
|
49
|
+
return { info, clientId }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function completeDeviceCode(
|
|
53
|
+
deviceCode: string,
|
|
54
|
+
clientId: string,
|
|
55
|
+
credManager: TeamsCredentialManager = new TeamsCredentialManager(),
|
|
56
|
+
accountType: TeamsAccountType = 'personal',
|
|
57
|
+
): Promise<DeviceLoginResult> {
|
|
58
|
+
const first = await exchangeDeviceCode(deviceCode, clientId, accountType)
|
|
59
|
+
if (first.status === 'pending' || first.status === 'slow_down') {
|
|
60
|
+
throw new PendingApprovalError()
|
|
61
|
+
}
|
|
62
|
+
if (first.status !== 'success') {
|
|
63
|
+
throw new Error(
|
|
64
|
+
first.status === 'expired'
|
|
65
|
+
? 'Device code expired.'
|
|
66
|
+
: first.status === 'declined'
|
|
67
|
+
? 'Authorization declined.'
|
|
68
|
+
: `Login failed: ${first.message}`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return finalize(first.token, clientId, accountType, credManager)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function loginWithDeviceCode(
|
|
75
|
+
callbacks: LoginCallbacks,
|
|
76
|
+
credManager: TeamsCredentialManager = new TeamsCredentialManager(),
|
|
77
|
+
): Promise<DeviceLoginResult> {
|
|
78
|
+
const accountType = callbacks.accountType ?? 'personal'
|
|
79
|
+
const { info, clientId } = await startDeviceCode(callbacks.clientId, accountType)
|
|
80
|
+
await callbacks.onCode({
|
|
81
|
+
verificationUri: info.verificationUri,
|
|
82
|
+
verificationUriComplete: info.verificationUriComplete,
|
|
83
|
+
userCode: info.userCode,
|
|
84
|
+
expiresAt: Date.now() + info.expiresIn * 1000,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const aad = await pollDeviceToken(info.deviceCode, info.interval, info.expiresIn, clientId, accountType)
|
|
88
|
+
|
|
89
|
+
// A work login can resolve to a personal (MSA) account, whose token carries a
|
|
90
|
+
// consumer tenant. Such accounts have no org tenant and can't be finished on
|
|
91
|
+
// the work flow, so stop with an actionable hint rather than fail obscurely.
|
|
92
|
+
if (accountType === 'work' && isPersonalAccount(aad)) {
|
|
93
|
+
throw new TeamsError(PERSONAL_ACCOUNT_HINT, 'teams_personal_account_on_work_login')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
callbacks.onPending?.()
|
|
97
|
+
return finalize(aad, clientId, accountType, credManager, callbacks.debug)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isPersonalAccount(aad: AadToken): boolean {
|
|
101
|
+
const tid = decodeJwtTid(aad.accessToken)
|
|
102
|
+
return tid !== undefined && isConsumerTenant(tid)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function finalize(
|
|
106
|
+
initialAad: AadToken,
|
|
107
|
+
clientId: string,
|
|
108
|
+
accountType: TeamsAccountType,
|
|
109
|
+
credManager: TeamsCredentialManager,
|
|
110
|
+
debug?: (message: string) => void,
|
|
111
|
+
): Promise<DeviceLoginResult> {
|
|
112
|
+
if (!initialAad.refreshToken) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Sign-in did not return a refresh token (needed to mint the Teams token). Retry `auth login`, or use `auth extract` if this persists.',
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let tenantId: string | undefined
|
|
119
|
+
if (accountType === 'work') {
|
|
120
|
+
debug?.('Resolving Teams tenant...')
|
|
121
|
+
tenantId = await resolveWorkTenantId(initialAad.accessToken)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
debug?.('Exchanging token for skype audience...')
|
|
125
|
+
const skypeScoped = await exchangeForSkypeScope(initialAad.refreshToken, clientId, accountType, tenantId)
|
|
126
|
+
|
|
127
|
+
debug?.('Minting skype token...')
|
|
128
|
+
const minted = await mintSkypeToken(skypeScoped.accessToken, accountType)
|
|
129
|
+
|
|
130
|
+
const client = await new TeamsClient().login({ token: minted.skypeToken, accountType })
|
|
131
|
+
const authInfo = await client.testAuth()
|
|
132
|
+
const teams = await client.listTeams()
|
|
133
|
+
|
|
134
|
+
const teamMap: Record<string, { team_id: string; team_name: string }> = {}
|
|
135
|
+
for (const team of teams) {
|
|
136
|
+
teamMap[team.id] = { team_id: team.id, team_name: team.name }
|
|
137
|
+
}
|
|
138
|
+
const currentTeam = teams[0]?.id ?? null
|
|
139
|
+
|
|
140
|
+
await credManager.setDeviceCodeAccount({
|
|
141
|
+
accountType,
|
|
142
|
+
token: minted.skypeToken,
|
|
143
|
+
tokenExpiresAt: minted.skypeTokenExpiresAt,
|
|
144
|
+
aadRefreshToken: skypeScoped.refreshToken,
|
|
145
|
+
aadClientId: clientId,
|
|
146
|
+
aadTenantId: tenantId,
|
|
147
|
+
region: minted.region ?? (accountType === 'work' ? client.getRegion() : undefined),
|
|
148
|
+
userName: authInfo.displayName,
|
|
149
|
+
teams: teamMap,
|
|
150
|
+
currentTeam,
|
|
151
|
+
authMethod: 'device-code',
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
accountType,
|
|
156
|
+
userName: authInfo.displayName,
|
|
157
|
+
teams: teams.map((t) => ({ id: t.id, name: t.name })),
|
|
158
|
+
current: currentTeam,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function refreshDeviceCodeAccount(
|
|
163
|
+
accountType: TeamsAccountType,
|
|
164
|
+
credManager: TeamsCredentialManager = new TeamsCredentialManager(),
|
|
165
|
+
debug?: (message: string) => void,
|
|
166
|
+
): Promise<boolean> {
|
|
167
|
+
const config = await credManager.loadConfig()
|
|
168
|
+
const account = config?.accounts[accountType]
|
|
169
|
+
if (!account || account.auth_method !== 'device-code' || !account.aad_refresh_token) {
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
const clientId = account.aad_client_id ?? getTeamsAppClientId(accountType).clientId
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
debug?.('Silently refreshing skype token...')
|
|
176
|
+
const tenantId = accountType === 'work' ? account.aad_tenant_id : undefined
|
|
177
|
+
const skypeScoped = await exchangeForSkypeScope(account.aad_refresh_token, clientId, accountType, tenantId)
|
|
178
|
+
const minted = await mintSkypeToken(skypeScoped.accessToken, accountType)
|
|
179
|
+
|
|
180
|
+
await credManager.setDeviceCodeAccount({
|
|
181
|
+
accountType,
|
|
182
|
+
token: minted.skypeToken,
|
|
183
|
+
tokenExpiresAt: minted.skypeTokenExpiresAt,
|
|
184
|
+
aadRefreshToken: skypeScoped.refreshToken,
|
|
185
|
+
aadClientId: clientId,
|
|
186
|
+
aadTenantId: tenantId,
|
|
187
|
+
region: minted.region ?? account.region,
|
|
188
|
+
userName: account.user_name,
|
|
189
|
+
teams: account.teams,
|
|
190
|
+
currentTeam: account.current_team,
|
|
191
|
+
authMethod: 'device-code',
|
|
192
|
+
makeCurrent: false,
|
|
193
|
+
})
|
|
194
|
+
return true
|
|
195
|
+
} catch (error) {
|
|
196
|
+
debug?.(`Silent refresh failed: ${(error as Error).message}`)
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class PendingApprovalError extends Error {
|
|
202
|
+
constructor() {
|
|
203
|
+
super('Authorization pending. Approve in the browser, then retry.')
|
|
204
|
+
this.name = 'PendingApprovalError'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -2,6 +2,7 @@ import { warn } from '@/shared/utils/stderr'
|
|
|
2
2
|
|
|
3
3
|
import { TeamsClient } from './client'
|
|
4
4
|
import { TeamsCredentialManager } from './credential-manager'
|
|
5
|
+
import { refreshDeviceCodeAccount } from './device-login'
|
|
5
6
|
import { TeamsTokenExtractor } from './token-extractor'
|
|
6
7
|
import type { TeamsAccount, TeamsAccountType, TeamsConfig } from './types'
|
|
7
8
|
|
|
@@ -12,6 +13,8 @@ export async function ensureTeamsAuth(): Promise<void> {
|
|
|
12
13
|
|
|
13
14
|
if (config && hasValidToken(config)) return
|
|
14
15
|
|
|
16
|
+
if (config && (await trySilentRefresh(config, credManager))) return
|
|
17
|
+
|
|
15
18
|
const extractor = new TeamsTokenExtractor()
|
|
16
19
|
const extracted = await extractor.extract()
|
|
17
20
|
if (extracted.length === 0) return
|
|
@@ -92,6 +95,14 @@ async function resolveAccountType(
|
|
|
92
95
|
throw lastError ?? new Error('Token validation failed')
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
async function trySilentRefresh(config: TeamsConfig, credManager: TeamsCredentialManager): Promise<boolean> {
|
|
99
|
+
const key = TeamsCredentialManager.accountOverride ?? config.current_account
|
|
100
|
+
if (!key) return false
|
|
101
|
+
const account = config.accounts[key]
|
|
102
|
+
if (account?.auth_method !== 'device-code' || !account.aad_refresh_token) return false
|
|
103
|
+
return refreshDeviceCodeAccount(key as TeamsAccountType, credManager)
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
function hasValidToken(config: TeamsConfig): boolean {
|
|
96
107
|
const key = TeamsCredentialManager.accountOverride ?? config.current_account
|
|
97
108
|
if (!key) return false
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
TeamsFileSchema,
|
|
14
14
|
TeamsMessageSchema,
|
|
15
15
|
TeamsReactionSchema,
|
|
16
|
+
TeamsSearchResultSchema,
|
|
16
17
|
TeamsTeamSchema,
|
|
18
|
+
TeamsTokenProvider,
|
|
17
19
|
TeamsUserSchema,
|
|
18
20
|
} from '@/platforms/teams/index'
|
|
19
21
|
|
|
@@ -29,6 +31,10 @@ it('TeamsCredentialManager is exported from barrel', () => {
|
|
|
29
31
|
expect(typeof TeamsCredentialManager).toBe('function')
|
|
30
32
|
})
|
|
31
33
|
|
|
34
|
+
it('TeamsTokenProvider is exported from barrel', () => {
|
|
35
|
+
expect(typeof TeamsTokenProvider).toBe('function')
|
|
36
|
+
})
|
|
37
|
+
|
|
32
38
|
it('TeamsTeamSchema is exported from barrel', () => {
|
|
33
39
|
expect(typeof TeamsTeamSchema.parse).toBe('function')
|
|
34
40
|
})
|
|
@@ -41,6 +47,10 @@ it('TeamsMessageSchema is exported from barrel', () => {
|
|
|
41
47
|
expect(typeof TeamsMessageSchema.parse).toBe('function')
|
|
42
48
|
})
|
|
43
49
|
|
|
50
|
+
it('TeamsSearchResultSchema is exported from barrel', () => {
|
|
51
|
+
expect(typeof TeamsSearchResultSchema.parse).toBe('function')
|
|
52
|
+
})
|
|
53
|
+
|
|
44
54
|
it('TeamsUserSchema is exported from barrel', () => {
|
|
45
55
|
expect(typeof TeamsUserSchema.parse).toBe('function')
|
|
46
56
|
})
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export { TeamsClient } from './client'
|
|
2
2
|
export { TeamsCredentialManager } from './credential-manager'
|
|
3
|
-
export {
|
|
3
|
+
export { TeamsTokenProvider } from './token-provider'
|
|
4
|
+
export {
|
|
5
|
+
completeDeviceCode,
|
|
6
|
+
loginWithDeviceCode,
|
|
7
|
+
PendingApprovalError,
|
|
8
|
+
refreshDeviceCodeAccount,
|
|
9
|
+
startDeviceCode,
|
|
10
|
+
} from './device-login'
|
|
11
|
+
export type { DeviceCodePrompt, DeviceLoginResult } from './device-login'
|
|
12
|
+
export { TeamsListener } from './listener'
|
|
13
|
+
export { TeamsAuthCapabilityError, TeamsError } from './types'
|
|
4
14
|
export type {
|
|
5
15
|
TeamsAccount,
|
|
6
16
|
TeamsAccountType,
|
|
@@ -9,9 +19,14 @@ export type {
|
|
|
9
19
|
TeamsConfigLegacy,
|
|
10
20
|
TeamsCredentials,
|
|
11
21
|
TeamsFile,
|
|
22
|
+
TeamsListenerEventMap,
|
|
23
|
+
TeamsMention,
|
|
12
24
|
TeamsMessage,
|
|
25
|
+
TeamsRealtimeMessage,
|
|
13
26
|
TeamsReaction,
|
|
27
|
+
TeamsSearchResult,
|
|
14
28
|
TeamsTeam,
|
|
29
|
+
TeamsTrouterGenericEvent,
|
|
15
30
|
TeamsUser,
|
|
16
31
|
} from './types'
|
|
17
32
|
export {
|
|
@@ -24,6 +39,7 @@ export {
|
|
|
24
39
|
TeamsFileSchema,
|
|
25
40
|
TeamsMessageSchema,
|
|
26
41
|
TeamsReactionSchema,
|
|
42
|
+
TeamsSearchResultSchema,
|
|
27
43
|
TeamsTeamSchema,
|
|
28
44
|
TeamsUserSchema,
|
|
29
45
|
} from './types'
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { TeamsListener } from '@/platforms/teams/listener'
|
|
4
|
+
import type { TeamsRealtimeMessage } from '@/platforms/teams/types'
|
|
5
|
+
|
|
6
|
+
type WsHandler = (...args: any[]) => void
|
|
7
|
+
|
|
8
|
+
let mockWsInstance: MockWs
|
|
9
|
+
|
|
10
|
+
class MockWs {
|
|
11
|
+
static OPEN = 1
|
|
12
|
+
static CLOSED = 3
|
|
13
|
+
readyState = MockWs.OPEN
|
|
14
|
+
|
|
15
|
+
private handlers = new Map<string, WsHandler[]>()
|
|
16
|
+
sent: string[] = []
|
|
17
|
+
|
|
18
|
+
constructor(_url: string, _options?: any) {
|
|
19
|
+
// oxlint-disable-next-line typescript-eslint/no-this-alias
|
|
20
|
+
mockWsInstance = this
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
on(event: string, handler: WsHandler) {
|
|
24
|
+
const list = this.handlers.get(event) ?? []
|
|
25
|
+
list.push(handler)
|
|
26
|
+
this.handlers.set(event, list)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
send(data: string) {
|
|
30
|
+
this.sent.push(data)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
close() {
|
|
34
|
+
this.readyState = MockWs.CLOSED
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
emit(event: string, ...args: any[]) {
|
|
38
|
+
for (const handler of this.handlers.get(event) ?? []) {
|
|
39
|
+
handler(...args)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
simulateFrame(frame: string) {
|
|
44
|
+
this.emit('message', Buffer.from(frame))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
mock.module('ws', () => ({ default: MockWs, __esModule: true }))
|
|
49
|
+
|
|
50
|
+
const actualTrouter = await import('@/platforms/teams/trouter')
|
|
51
|
+
mock.module('@/platforms/teams/trouter', () => ({
|
|
52
|
+
...actualTrouter,
|
|
53
|
+
fetchTrouterInfo: mock(() => Promise.resolve({ socketio: 'wss://fake/', surl: 'https://fake/', connectparams: {} })),
|
|
54
|
+
fetchTrouterSessionId: mock(() => Promise.resolve('SESSION')),
|
|
55
|
+
registerEndpoint: mock(() => Promise.resolve()),
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
function messageFrame(resource: Record<string, unknown>): string {
|
|
59
|
+
const body = JSON.stringify({ resourceType: 'NewMessage', resource })
|
|
60
|
+
return `3:::${JSON.stringify({ id: 1, url: '/v4/f/HASH/messaging', body })}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function conversationLink(conversationId: string): string {
|
|
64
|
+
return `https://notifications.skype.net/v1/users/ME/conversations/${conversationId}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createMockClient(channelTeamMap: Map<string, string> = new Map()) {
|
|
68
|
+
return {
|
|
69
|
+
getToken: mock(() => 'skype-token'),
|
|
70
|
+
getIdToken: mock(() => Promise.resolve('id-token')),
|
|
71
|
+
getAccountType: mock(() => 'work'),
|
|
72
|
+
buildChannelTeamMap: mock(() => Promise.resolve(channelTeamMap)),
|
|
73
|
+
} as any
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function startAndCapture(
|
|
77
|
+
client: ReturnType<typeof createMockClient>,
|
|
78
|
+
): Promise<{ listener: TeamsListener; messages: TeamsRealtimeMessage[] }> {
|
|
79
|
+
const listener = new TeamsListener(client)
|
|
80
|
+
const messages: TeamsRealtimeMessage[] = []
|
|
81
|
+
listener.on('message', (message) => messages.push(message))
|
|
82
|
+
await listener.start()
|
|
83
|
+
return { listener, messages }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function flushMicrotasks(): Promise<void> {
|
|
87
|
+
for (let i = 0; i < 5; i++) await Promise.resolve()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('TeamsListener.emitMessage', () => {
|
|
91
|
+
let listener: TeamsListener
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
listener?.stop()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('classifies a 1:1 chat and carries no team/channel context', async () => {
|
|
98
|
+
const client = createMockClient()
|
|
99
|
+
const started = await startAndCapture(client)
|
|
100
|
+
listener = started.listener
|
|
101
|
+
|
|
102
|
+
mockWsInstance.simulateFrame(
|
|
103
|
+
messageFrame({
|
|
104
|
+
id: 'm1',
|
|
105
|
+
messagetype: 'RichText/Html',
|
|
106
|
+
content: 'hello there',
|
|
107
|
+
from: 'https://x/contacts/8:orgid:alice',
|
|
108
|
+
imdisplayname: 'Alice',
|
|
109
|
+
conversationLink: conversationLink('19:uni01_abc@unq.gbl.spaces'),
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
await flushMicrotasks()
|
|
113
|
+
|
|
114
|
+
expect(started.messages).toHaveLength(1)
|
|
115
|
+
const message = started.messages[0]
|
|
116
|
+
expect(message.chatId).toBe('19:uni01_abc@unq.gbl.spaces')
|
|
117
|
+
expect(message.conversationType).toBe('chat')
|
|
118
|
+
expect(message.teamId).toBeUndefined()
|
|
119
|
+
expect(message.channelId).toBeUndefined()
|
|
120
|
+
expect(message.content).toBe('hello there')
|
|
121
|
+
expect(message.mentions).toEqual([])
|
|
122
|
+
expect(message.author).toEqual({ id: '8:orgid:alice', displayName: 'Alice' })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('classifies a group chat thread with no groupId as a chat', async () => {
|
|
126
|
+
const client = createMockClient()
|
|
127
|
+
const started = await startAndCapture(client)
|
|
128
|
+
listener = started.listener
|
|
129
|
+
|
|
130
|
+
mockWsInstance.simulateFrame(
|
|
131
|
+
messageFrame({
|
|
132
|
+
id: 'm2',
|
|
133
|
+
messagetype: 'RichText/Html',
|
|
134
|
+
content: 'group hi',
|
|
135
|
+
from: 'https://x/contacts/8:orgid:bob',
|
|
136
|
+
imdisplayname: 'Bob',
|
|
137
|
+
conversationLink: conversationLink('19:group_xyz@thread.tacv2'),
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
await flushMicrotasks()
|
|
141
|
+
|
|
142
|
+
expect(started.messages).toHaveLength(1)
|
|
143
|
+
const message = started.messages[0]
|
|
144
|
+
expect(message.chatId).toBe('19:group_xyz@thread.tacv2')
|
|
145
|
+
expect(message.conversationType).toBe('chat')
|
|
146
|
+
expect(message.teamId).toBeUndefined()
|
|
147
|
+
expect(message.channelId).toBeUndefined()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('does not refresh the channel map for every group-chat message', async () => {
|
|
151
|
+
const client = createMockClient()
|
|
152
|
+
const started = await startAndCapture(client)
|
|
153
|
+
listener = started.listener
|
|
154
|
+
|
|
155
|
+
for (const id of ['g1', 'g2', 'g3']) {
|
|
156
|
+
mockWsInstance.simulateFrame(
|
|
157
|
+
messageFrame({
|
|
158
|
+
id,
|
|
159
|
+
messagetype: 'RichText/Html',
|
|
160
|
+
content: 'group hi',
|
|
161
|
+
from: 'https://x/contacts/8:orgid:bob',
|
|
162
|
+
imdisplayname: 'Bob',
|
|
163
|
+
conversationLink: conversationLink('19:group_xyz@thread.tacv2'),
|
|
164
|
+
}),
|
|
165
|
+
)
|
|
166
|
+
await flushMicrotasks()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
expect(started.messages).toHaveLength(3)
|
|
170
|
+
// One build at start() + one refresh on the first miss; the negative cache
|
|
171
|
+
// prevents a fetch on the 2nd and 3rd messages.
|
|
172
|
+
expect(client.buildChannelTeamMap).toHaveBeenCalledTimes(2)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('classifies a team channel message and resolves teamId plus mentions', async () => {
|
|
176
|
+
const channelId = '19:channel_abc@thread.tacv2'
|
|
177
|
+
const client = createMockClient(new Map([[channelId, 'team-group-id']]))
|
|
178
|
+
const started = await startAndCapture(client)
|
|
179
|
+
listener = started.listener
|
|
180
|
+
|
|
181
|
+
mockWsInstance.simulateFrame(
|
|
182
|
+
messageFrame({
|
|
183
|
+
id: 'm3',
|
|
184
|
+
messagetype: 'RichText/Html',
|
|
185
|
+
content: '<span itemtype="http://schema.skype.com/Mention" itemscope itemid="0">Alice</span> please review',
|
|
186
|
+
from: 'https://x/contacts/8:orgid:bob',
|
|
187
|
+
imdisplayname: 'Bob',
|
|
188
|
+
conversationLink: conversationLink(channelId),
|
|
189
|
+
properties: {
|
|
190
|
+
mentions: [{ itemid: '0', mri: '8:orgid:alice', mentionType: 'person', displayName: 'Alice' }],
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
)
|
|
194
|
+
await flushMicrotasks()
|
|
195
|
+
|
|
196
|
+
expect(started.messages).toHaveLength(1)
|
|
197
|
+
const message = started.messages[0]
|
|
198
|
+
expect(message.conversationType).toBe('channel')
|
|
199
|
+
expect(message.teamId).toBe('team-group-id')
|
|
200
|
+
expect(message.channelId).toBe(channelId)
|
|
201
|
+
expect(message.content).toBe('Alice please review')
|
|
202
|
+
expect(message.mentions).toEqual([{ id: '0', mri: '8:orgid:alice', displayName: 'Alice' }])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('does not re-emit a message with a duplicate id', async () => {
|
|
206
|
+
const client = createMockClient()
|
|
207
|
+
const started = await startAndCapture(client)
|
|
208
|
+
listener = started.listener
|
|
209
|
+
|
|
210
|
+
const frame = messageFrame({
|
|
211
|
+
id: 'dup',
|
|
212
|
+
messagetype: 'Text',
|
|
213
|
+
content: 'once',
|
|
214
|
+
from: 'https://x/contacts/8:orgid:alice',
|
|
215
|
+
imdisplayname: 'Alice',
|
|
216
|
+
conversationLink: conversationLink('19:uni01_abc@unq.gbl.spaces'),
|
|
217
|
+
})
|
|
218
|
+
mockWsInstance.simulateFrame(frame)
|
|
219
|
+
await flushMicrotasks()
|
|
220
|
+
mockWsInstance.simulateFrame(frame)
|
|
221
|
+
await flushMicrotasks()
|
|
222
|
+
|
|
223
|
+
expect(started.messages).toHaveLength(1)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('ignores non-text message types', async () => {
|
|
227
|
+
const client = createMockClient()
|
|
228
|
+
const started = await startAndCapture(client)
|
|
229
|
+
listener = started.listener
|
|
230
|
+
|
|
231
|
+
mockWsInstance.simulateFrame(
|
|
232
|
+
messageFrame({
|
|
233
|
+
id: 'sys',
|
|
234
|
+
messagetype: 'ThreadActivity/AddMember',
|
|
235
|
+
content: '',
|
|
236
|
+
conversationLink: conversationLink('19:uni01_abc@unq.gbl.spaces'),
|
|
237
|
+
}),
|
|
238
|
+
)
|
|
239
|
+
await flushMicrotasks()
|
|
240
|
+
|
|
241
|
+
expect(started.messages).toHaveLength(0)
|
|
242
|
+
})
|
|
243
|
+
})
|