@zooid/transport-matrix 0.7.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/LICENSE +21 -0
- package/dist/index.d.ts +288 -0
- package/dist/index.js +1076 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/bot-pool.test.ts +307 -0
- package/src/bot-pool.ts +112 -0
- package/src/context-provider.test.ts +317 -0
- package/src/context-provider.ts +187 -0
- package/src/event-encoders.test.ts +124 -0
- package/src/event-encoders.ts +66 -0
- package/src/index.ts +26 -0
- package/src/markdown-to-matrix-html.test.ts +102 -0
- package/src/markdown-to-matrix-html.ts +41 -0
- package/src/matrix-client.test.ts +307 -0
- package/src/matrix-client.ts +361 -0
- package/src/mentions.test.ts +90 -0
- package/src/mentions.ts +38 -0
- package/src/registration.test.ts +41 -0
- package/src/registration.ts +44 -0
- package/src/router.test.ts +90 -0
- package/src/router.ts +72 -0
- package/src/space-provisioner.test.ts +89 -0
- package/src/space-provisioner.ts +34 -0
- package/src/transport.test.ts +1164 -0
- package/src/transport.ts +521 -0
- package/src/workforce-publisher.test.ts +76 -0
- package/src/workforce-publisher.ts +53 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { extractMentions } from './mentions.js'
|
|
2
|
+
|
|
3
|
+
export interface AgentBinding {
|
|
4
|
+
name: string
|
|
5
|
+
userId: string
|
|
6
|
+
/** Optional human-readable display name. Falls back to the user_id localpart. */
|
|
7
|
+
displayName?: string
|
|
8
|
+
rooms: string[]
|
|
9
|
+
trigger: 'mention' | 'any'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ThreadState {
|
|
13
|
+
/** Agent names that have posted in this thread, in order. */
|
|
14
|
+
participants: string[]
|
|
15
|
+
/** Agent names @mentioned in the thread root event (or subsequently). */
|
|
16
|
+
rootMentions: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface MaybeEvent {
|
|
20
|
+
type?: string
|
|
21
|
+
room_id?: string
|
|
22
|
+
sender?: string
|
|
23
|
+
content?: {
|
|
24
|
+
msgtype?: string
|
|
25
|
+
'm.relates_to'?: { rel_type?: string; event_id?: string }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type RouteMatch = AgentBinding
|
|
30
|
+
|
|
31
|
+
function inboundThreadRoot(event: MaybeEvent): string | undefined {
|
|
32
|
+
const r = event.content?.['m.relates_to']
|
|
33
|
+
return r?.rel_type === 'm.thread' && r.event_id ? r.event_id : undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function route(
|
|
37
|
+
event: MaybeEvent,
|
|
38
|
+
agents: AgentBinding[],
|
|
39
|
+
threadStates?: Map<string, ThreadState>,
|
|
40
|
+
): RouteMatch[] {
|
|
41
|
+
if (event.type !== 'm.room.message') return []
|
|
42
|
+
if (!event.content?.msgtype) return []
|
|
43
|
+
const mentions = new Set(extractMentions(event as never))
|
|
44
|
+
const matches: RouteMatch[] = []
|
|
45
|
+
const threadRoot = inboundThreadRoot(event)
|
|
46
|
+
const threadState = threadRoot ? threadStates?.get(threadRoot) : undefined
|
|
47
|
+
|
|
48
|
+
for (const a of agents) {
|
|
49
|
+
if (event.sender === a.userId) continue
|
|
50
|
+
if (!a.rooms.includes(event.room_id ?? '')) continue
|
|
51
|
+
if (a.trigger === 'any') {
|
|
52
|
+
matches.push(a)
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
// trigger === 'mention'
|
|
56
|
+
if (mentions.has(a.userId)) {
|
|
57
|
+
matches.push(a)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
// Implicit trigger in a thread: most-recent-poster, or root-mention
|
|
61
|
+
// inheritance if no agent has posted yet.
|
|
62
|
+
if (threadState) {
|
|
63
|
+
const lastPoster = threadState.participants.at(-1)
|
|
64
|
+
if (lastPoster) {
|
|
65
|
+
if (lastPoster === a.name) matches.push(a)
|
|
66
|
+
} else if (threadState.rootMentions.includes(a.name)) {
|
|
67
|
+
matches.push(a)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return matches
|
|
72
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { MatrixClient } from './matrix-client.js'
|
|
3
|
+
import { ensureWorkforceSpace, serverNameFromMxid } from './space-provisioner.js'
|
|
4
|
+
|
|
5
|
+
function clientWithFetches(...handlers: Array<(url: string, init?: RequestInit) => Response>) {
|
|
6
|
+
let i = 0
|
|
7
|
+
const fetch = vi.fn(async (url: string, init?: RequestInit) => {
|
|
8
|
+
const h = handlers[i++]
|
|
9
|
+
if (!h) throw new Error(`unexpected fetch #${i}: ${url}`)
|
|
10
|
+
return h(url, init)
|
|
11
|
+
})
|
|
12
|
+
const client = new MatrixClient({
|
|
13
|
+
homeserver: 'https://hs.zoon.local',
|
|
14
|
+
asToken: 'as-tok',
|
|
15
|
+
fetch: fetch as unknown as typeof globalThis.fetch,
|
|
16
|
+
})
|
|
17
|
+
return { client, fetch }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('ensureWorkforceSpace', () => {
|
|
21
|
+
it('returns the room ID when the alias already resolves', async () => {
|
|
22
|
+
const { client, fetch } = clientWithFetches((url) => {
|
|
23
|
+
expect(url).toContain('/_matrix/client/v3/directory/room/%23dev%3Ahs.zoon.local')
|
|
24
|
+
return new Response(JSON.stringify({ room_id: '!existing:hs.zoon.local' }), { status: 200 })
|
|
25
|
+
})
|
|
26
|
+
const id = await ensureWorkforceSpace({
|
|
27
|
+
client,
|
|
28
|
+
asUserId: '@zooid:hs.zoon.local',
|
|
29
|
+
serverName: 'hs.zoon.local',
|
|
30
|
+
spaceLocalpart: 'dev',
|
|
31
|
+
preset: 'public_chat',
|
|
32
|
+
})
|
|
33
|
+
expect(id).toBe('!existing:hs.zoon.local')
|
|
34
|
+
expect(fetch).toHaveBeenCalledTimes(1)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('creates the space when the alias is unknown', async () => {
|
|
38
|
+
const { client, fetch } = clientWithFetches(
|
|
39
|
+
() => new Response(JSON.stringify({ errcode: 'M_NOT_FOUND' }), { status: 404 }),
|
|
40
|
+
(url, init) => {
|
|
41
|
+
expect(url).toContain('/_matrix/client/v3/createRoom')
|
|
42
|
+
const body = JSON.parse(init!.body as string)
|
|
43
|
+
expect(body).toMatchObject({
|
|
44
|
+
room_alias_name: 'dev',
|
|
45
|
+
name: 'Dev',
|
|
46
|
+
preset: 'public_chat',
|
|
47
|
+
creation_content: { type: 'm.space' },
|
|
48
|
+
})
|
|
49
|
+
return new Response(JSON.stringify({ room_id: '!new:hs.zoon.local' }), { status: 200 })
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
const id = await ensureWorkforceSpace({
|
|
53
|
+
client,
|
|
54
|
+
asUserId: '@zooid:hs.zoon.local',
|
|
55
|
+
serverName: 'hs.zoon.local',
|
|
56
|
+
spaceLocalpart: 'dev',
|
|
57
|
+
preset: 'public_chat',
|
|
58
|
+
})
|
|
59
|
+
expect(id).toBe('!new:hs.zoon.local')
|
|
60
|
+
expect(fetch).toHaveBeenCalledTimes(2)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('throws on resolveAlias errors other than 404', async () => {
|
|
64
|
+
const { client } = clientWithFetches(() => new Response('boom', { status: 500 }))
|
|
65
|
+
await expect(
|
|
66
|
+
ensureWorkforceSpace({
|
|
67
|
+
client,
|
|
68
|
+
asUserId: '@zooid:hs.zoon.local',
|
|
69
|
+
serverName: 'hs.zoon.local',
|
|
70
|
+
spaceLocalpart: 'dev',
|
|
71
|
+
preset: 'public_chat',
|
|
72
|
+
}),
|
|
73
|
+
).rejects.toThrow(/500/)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('serverNameFromMxid', () => {
|
|
78
|
+
it('returns the part after the first colon', () => {
|
|
79
|
+
expect(serverNameFromMxid('@zooid:zoon.local')).toBe('zoon.local')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('handles federated server names with ports', () => {
|
|
83
|
+
expect(serverNameFromMxid('@zooid:hs.example.com:8448')).toBe('hs.example.com:8448')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('throws on an mxid without a server', () => {
|
|
87
|
+
expect(() => serverNameFromMxid('@zooid')).toThrow(/server/)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MatrixClient } from './matrix-client.js'
|
|
2
|
+
|
|
3
|
+
export interface EnsureSpaceOpts {
|
|
4
|
+
client: MatrixClient
|
|
5
|
+
asUserId: string
|
|
6
|
+
serverName: string
|
|
7
|
+
spaceLocalpart: string
|
|
8
|
+
preset: 'public_chat' | 'private_chat'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function ensureWorkforceSpace(opts: EnsureSpaceOpts): Promise<string> {
|
|
12
|
+
const alias = `#${opts.spaceLocalpart}:${opts.serverName}`
|
|
13
|
+
const existing = await opts.client.resolveAlias(alias)
|
|
14
|
+
if (existing) return existing
|
|
15
|
+
|
|
16
|
+
const display = opts.spaceLocalpart.charAt(0).toUpperCase() + opts.spaceLocalpart.slice(1)
|
|
17
|
+
return opts.client.createRoomRaw({
|
|
18
|
+
asUserId: opts.asUserId,
|
|
19
|
+
body: {
|
|
20
|
+
room_alias_name: opts.spaceLocalpart,
|
|
21
|
+
name: display,
|
|
22
|
+
preset: opts.preset,
|
|
23
|
+
creation_content: { type: 'm.space' },
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function serverNameFromMxid(mxid: string): string {
|
|
29
|
+
const colon = mxid.indexOf(':')
|
|
30
|
+
if (colon < 0) {
|
|
31
|
+
throw new Error(`mxid lacks server: ${mxid}`)
|
|
32
|
+
}
|
|
33
|
+
return mxid.slice(colon + 1)
|
|
34
|
+
}
|