@archipelagolab/lobi 1.0.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.
Files changed (315) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/ENDOFFILE +0 -0
  3. package/EOF +0 -0
  4. package/LICENSE +21 -0
  5. package/SPEC-SUPPORT.md +116 -0
  6. package/YAMLEND +0 -0
  7. package/api.ts +18 -0
  8. package/archipelagolab-lobi-1.0.0.tgz +0 -0
  9. package/auth-presence.ts +56 -0
  10. package/channel-plugin-api.ts +3 -0
  11. package/cli-metadata.ts +11 -0
  12. package/contract-api.ts +17 -0
  13. package/docs/CHECKLIST.md +83 -0
  14. package/docs/FORK_SDK_GUIDE.md +279 -0
  15. package/helper-api.ts +3 -0
  16. package/index.test.ts +61 -0
  17. package/index.ts +65 -0
  18. package/openclaw.plugin.json +23 -0
  19. package/package.json +52 -0
  20. package/plugin-entry.handlers.runtime.ts +1 -0
  21. package/runtime-api.ts +54 -0
  22. package/runtime-heavy-api.ts +1 -0
  23. package/scripts/migrate-to-lobi.sh +72 -0
  24. package/secret-contract-api.ts +5 -0
  25. package/setup-entry.ts +13 -0
  26. package/src/account-selection.test.ts +124 -0
  27. package/src/account-selection.ts +226 -0
  28. package/src/actions.account-propagation.test.ts +251 -0
  29. package/src/actions.test.ts +251 -0
  30. package/src/actions.ts +336 -0
  31. package/src/approval-auth.test.ts +23 -0
  32. package/src/approval-auth.ts +25 -0
  33. package/src/approval-handler.runtime.test.ts +46 -0
  34. package/src/approval-handler.runtime.ts +400 -0
  35. package/src/approval-ids.ts +6 -0
  36. package/src/approval-native.test.ts +329 -0
  37. package/src/approval-native.ts +336 -0
  38. package/src/approval-reactions.test.ts +107 -0
  39. package/src/approval-reactions.ts +158 -0
  40. package/src/auth-precedence.ts +61 -0
  41. package/src/channel-account-paths.ts +92 -0
  42. package/src/channel.account-paths.test.ts +102 -0
  43. package/src/channel.directory.test.ts +601 -0
  44. package/src/channel.resolve.test.ts +38 -0
  45. package/src/channel.runtime.ts +16 -0
  46. package/src/channel.setup.test.ts +269 -0
  47. package/src/channel.ts +570 -0
  48. package/src/cli-metadata.ts +19 -0
  49. package/src/cli.test.ts +1015 -0
  50. package/src/cli.ts +1198 -0
  51. package/src/config-adapter.ts +41 -0
  52. package/src/config-schema.test.ts +90 -0
  53. package/src/config-schema.ts +114 -0
  54. package/src/directory-live.test.ts +200 -0
  55. package/src/directory-live.ts +238 -0
  56. package/src/doctor-contract.ts +287 -0
  57. package/src/doctor.test.ts +440 -0
  58. package/src/doctor.ts +262 -0
  59. package/src/env-vars.ts +92 -0
  60. package/src/exec-approval-resolver.test.ts +68 -0
  61. package/src/exec-approval-resolver.ts +23 -0
  62. package/src/exec-approvals.test.ts +483 -0
  63. package/src/exec-approvals.ts +290 -0
  64. package/src/group-mentions.ts +41 -0
  65. package/src/legacy-crypto-inspector-availability.test.ts +81 -0
  66. package/src/legacy-crypto-inspector-availability.ts +60 -0
  67. package/src/legacy-crypto.test.ts +234 -0
  68. package/src/legacy-crypto.ts +549 -0
  69. package/src/legacy-state.test.ts +86 -0
  70. package/src/legacy-state.ts +156 -0
  71. package/src/matrix/account-config.ts +150 -0
  72. package/src/matrix/accounts.readiness.test.ts +27 -0
  73. package/src/matrix/accounts.test.ts +757 -0
  74. package/src/matrix/accounts.ts +194 -0
  75. package/src/matrix/actions/client.test.ts +215 -0
  76. package/src/matrix/actions/client.ts +31 -0
  77. package/src/matrix/actions/devices.test.ts +114 -0
  78. package/src/matrix/actions/devices.ts +34 -0
  79. package/src/matrix/actions/limits.test.ts +15 -0
  80. package/src/matrix/actions/limits.ts +6 -0
  81. package/src/matrix/actions/messages.test.ts +289 -0
  82. package/src/matrix/actions/messages.ts +123 -0
  83. package/src/matrix/actions/pins.test.ts +74 -0
  84. package/src/matrix/actions/pins.ts +64 -0
  85. package/src/matrix/actions/polls.test.ts +71 -0
  86. package/src/matrix/actions/polls.ts +109 -0
  87. package/src/matrix/actions/profile.test.ts +109 -0
  88. package/src/matrix/actions/profile.ts +37 -0
  89. package/src/matrix/actions/reactions.test.ts +135 -0
  90. package/src/matrix/actions/reactions.ts +59 -0
  91. package/src/matrix/actions/room.test.ts +79 -0
  92. package/src/matrix/actions/room.ts +71 -0
  93. package/src/matrix/actions/summary.test.ts +87 -0
  94. package/src/matrix/actions/summary.ts +88 -0
  95. package/src/matrix/actions/types.ts +82 -0
  96. package/src/matrix/actions/verification.test.ts +105 -0
  97. package/src/matrix/actions/verification.ts +237 -0
  98. package/src/matrix/actions.ts +37 -0
  99. package/src/matrix/active-client.ts +26 -0
  100. package/src/matrix/async-lock.ts +18 -0
  101. package/src/matrix/backup-health.ts +115 -0
  102. package/src/matrix/client/config-runtime-api.ts +14 -0
  103. package/src/matrix/client/config-secret-input.runtime.ts +1 -0
  104. package/src/matrix/client/config.ts +982 -0
  105. package/src/matrix/client/create-client.test.ts +115 -0
  106. package/src/matrix/client/create-client.ts +101 -0
  107. package/src/matrix/client/env-auth.ts +6 -0
  108. package/src/matrix/client/file-sync-store.test.ts +265 -0
  109. package/src/matrix/client/file-sync-store.ts +289 -0
  110. package/src/matrix/client/logging.ts +123 -0
  111. package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
  112. package/src/matrix/client/private-network-host.ts +56 -0
  113. package/src/matrix/client/runtime.ts +4 -0
  114. package/src/matrix/client/shared.test.ts +344 -0
  115. package/src/matrix/client/shared.ts +306 -0
  116. package/src/matrix/client/storage.test.ts +634 -0
  117. package/src/matrix/client/storage.ts +544 -0
  118. package/src/matrix/client/types.ts +50 -0
  119. package/src/matrix/client-bootstrap.test.ts +84 -0
  120. package/src/matrix/client-bootstrap.ts +164 -0
  121. package/src/matrix/client-resolver.test-helpers.ts +147 -0
  122. package/src/matrix/client.test.ts +1521 -0
  123. package/src/matrix/client.ts +23 -0
  124. package/src/matrix/config-paths.ts +31 -0
  125. package/src/matrix/config-update.test.ts +237 -0
  126. package/src/matrix/config-update.ts +291 -0
  127. package/src/matrix/credentials-read.ts +206 -0
  128. package/src/matrix/credentials-write.runtime.ts +26 -0
  129. package/src/matrix/credentials.test.ts +501 -0
  130. package/src/matrix/credentials.ts +95 -0
  131. package/src/matrix/deps.test.ts +74 -0
  132. package/src/matrix/deps.ts +225 -0
  133. package/src/matrix/device-health.test.ts +45 -0
  134. package/src/matrix/device-health.ts +31 -0
  135. package/src/matrix/direct-management.test.ts +350 -0
  136. package/src/matrix/direct-management.ts +347 -0
  137. package/src/matrix/direct-room.test.ts +61 -0
  138. package/src/matrix/direct-room.ts +128 -0
  139. package/src/matrix/draft-stream.test.ts +406 -0
  140. package/src/matrix/draft-stream.ts +216 -0
  141. package/src/matrix/encryption-guidance.ts +27 -0
  142. package/src/matrix/errors.ts +21 -0
  143. package/src/matrix/format.test.ts +340 -0
  144. package/src/matrix/format.ts +428 -0
  145. package/src/matrix/legacy-crypto-inspector.ts +95 -0
  146. package/src/matrix/media-errors.ts +20 -0
  147. package/src/matrix/media-text.ts +169 -0
  148. package/src/matrix/monitor/access-state.test.ts +45 -0
  149. package/src/matrix/monitor/access-state.ts +77 -0
  150. package/src/matrix/monitor/ack-config.test.ts +57 -0
  151. package/src/matrix/monitor/ack-config.ts +26 -0
  152. package/src/matrix/monitor/allowlist.test.ts +45 -0
  153. package/src/matrix/monitor/allowlist.ts +94 -0
  154. package/src/matrix/monitor/auto-join.test.ts +203 -0
  155. package/src/matrix/monitor/auto-join.ts +86 -0
  156. package/src/matrix/monitor/config.test.ts +197 -0
  157. package/src/matrix/monitor/config.ts +303 -0
  158. package/src/matrix/monitor/context-summary.ts +43 -0
  159. package/src/matrix/monitor/direct.test.ts +529 -0
  160. package/src/matrix/monitor/direct.ts +270 -0
  161. package/src/matrix/monitor/events.test.ts +1524 -0
  162. package/src/matrix/monitor/events.ts +213 -0
  163. package/src/matrix/monitor/handler.body-for-agent.test.ts +396 -0
  164. package/src/matrix/monitor/handler.group-history.test.ts +648 -0
  165. package/src/matrix/monitor/handler.media-failure.test.ts +267 -0
  166. package/src/matrix/monitor/handler.test-helpers.ts +308 -0
  167. package/src/matrix/monitor/handler.test.ts +2952 -0
  168. package/src/matrix/monitor/handler.thread-root-media.test.ts +82 -0
  169. package/src/matrix/monitor/handler.ts +1679 -0
  170. package/src/matrix/monitor/inbound-dedupe.test.ts +146 -0
  171. package/src/matrix/monitor/inbound-dedupe.ts +267 -0
  172. package/src/matrix/monitor/index.test.ts +920 -0
  173. package/src/matrix/monitor/index.ts +434 -0
  174. package/src/matrix/monitor/legacy-crypto-restore.test.ts +206 -0
  175. package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
  176. package/src/matrix/monitor/location.ts +100 -0
  177. package/src/matrix/monitor/media.test.ts +159 -0
  178. package/src/matrix/monitor/media.ts +119 -0
  179. package/src/matrix/monitor/mentions.test.ts +289 -0
  180. package/src/matrix/monitor/mentions.ts +177 -0
  181. package/src/matrix/monitor/reaction-events.test.ts +326 -0
  182. package/src/matrix/monitor/reaction-events.ts +187 -0
  183. package/src/matrix/monitor/recent-invite.test.ts +92 -0
  184. package/src/matrix/monitor/recent-invite.ts +30 -0
  185. package/src/matrix/monitor/replies.test.ts +265 -0
  186. package/src/matrix/monitor/replies.ts +136 -0
  187. package/src/matrix/monitor/reply-context.test.ts +276 -0
  188. package/src/matrix/monitor/reply-context.ts +92 -0
  189. package/src/matrix/monitor/room-history.test.ts +258 -0
  190. package/src/matrix/monitor/room-history.ts +301 -0
  191. package/src/matrix/monitor/room-info.test.ts +201 -0
  192. package/src/matrix/monitor/room-info.ts +126 -0
  193. package/src/matrix/monitor/rooms.test.ts +121 -0
  194. package/src/matrix/monitor/rooms.ts +52 -0
  195. package/src/matrix/monitor/route.test.ts +255 -0
  196. package/src/matrix/monitor/route.ts +178 -0
  197. package/src/matrix/monitor/runtime-api.ts +31 -0
  198. package/src/matrix/monitor/startup-verification.test.ts +294 -0
  199. package/src/matrix/monitor/startup-verification.ts +237 -0
  200. package/src/matrix/monitor/startup.test.ts +257 -0
  201. package/src/matrix/monitor/startup.ts +218 -0
  202. package/src/matrix/monitor/status.ts +111 -0
  203. package/src/matrix/monitor/sync-lifecycle.test.ts +224 -0
  204. package/src/matrix/monitor/sync-lifecycle.ts +91 -0
  205. package/src/matrix/monitor/task-runner.ts +38 -0
  206. package/src/matrix/monitor/thread-context.test.ts +149 -0
  207. package/src/matrix/monitor/thread-context.ts +108 -0
  208. package/src/matrix/monitor/threads.test.ts +68 -0
  209. package/src/matrix/monitor/threads.ts +85 -0
  210. package/src/matrix/monitor/types.ts +30 -0
  211. package/src/matrix/monitor/verification-events.ts +627 -0
  212. package/src/matrix/monitor/verification-utils.test.ts +47 -0
  213. package/src/matrix/monitor/verification-utils.ts +46 -0
  214. package/src/matrix/outbound-media-runtime.ts +1 -0
  215. package/src/matrix/poll-summary.ts +110 -0
  216. package/src/matrix/poll-types.test.ts +205 -0
  217. package/src/matrix/poll-types.ts +433 -0
  218. package/src/matrix/probe.runtime.ts +4 -0
  219. package/src/matrix/probe.test.ts +154 -0
  220. package/src/matrix/probe.ts +96 -0
  221. package/src/matrix/profile.test.ts +154 -0
  222. package/src/matrix/profile.ts +184 -0
  223. package/src/matrix/reaction-common.test.ts +96 -0
  224. package/src/matrix/reaction-common.ts +147 -0
  225. package/src/matrix/sdk/crypto-bootstrap.test.ts +505 -0
  226. package/src/matrix/sdk/crypto-bootstrap.ts +341 -0
  227. package/src/matrix/sdk/crypto-facade.test.ts +197 -0
  228. package/src/matrix/sdk/crypto-facade.ts +207 -0
  229. package/src/matrix/sdk/crypto-node.runtime.test.ts +27 -0
  230. package/src/matrix/sdk/crypto-node.runtime.ts +9 -0
  231. package/src/matrix/sdk/crypto-runtime.ts +11 -0
  232. package/src/matrix/sdk/decrypt-bridge.ts +356 -0
  233. package/src/matrix/sdk/event-helpers.test.ts +60 -0
  234. package/src/matrix/sdk/event-helpers.ts +71 -0
  235. package/src/matrix/sdk/http-client.test.ts +134 -0
  236. package/src/matrix/sdk/http-client.ts +87 -0
  237. package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
  238. package/src/matrix/sdk/idb-persistence.lock-order.test.ts +108 -0
  239. package/src/matrix/sdk/idb-persistence.test-helpers.ts +88 -0
  240. package/src/matrix/sdk/idb-persistence.test.ts +149 -0
  241. package/src/matrix/sdk/idb-persistence.ts +283 -0
  242. package/src/matrix/sdk/logger.test.ts +25 -0
  243. package/src/matrix/sdk/logger.ts +108 -0
  244. package/src/matrix/sdk/read-response-with-limit.ts +19 -0
  245. package/src/matrix/sdk/recovery-key-store.test.ts +385 -0
  246. package/src/matrix/sdk/recovery-key-store.ts +430 -0
  247. package/src/matrix/sdk/transport.test.ts +161 -0
  248. package/src/matrix/sdk/transport.ts +344 -0
  249. package/src/matrix/sdk/types.ts +236 -0
  250. package/src/matrix/sdk/verification-manager.test.ts +509 -0
  251. package/src/matrix/sdk/verification-manager.ts +694 -0
  252. package/src/matrix/sdk/verification-status.ts +23 -0
  253. package/src/matrix/sdk.test.ts +2568 -0
  254. package/src/matrix/sdk.ts +1789 -0
  255. package/src/matrix/send/client.test.ts +174 -0
  256. package/src/matrix/send/client.ts +90 -0
  257. package/src/matrix/send/formatting.ts +189 -0
  258. package/src/matrix/send/media.ts +244 -0
  259. package/src/matrix/send/targets.test.ts +254 -0
  260. package/src/matrix/send/targets.ts +104 -0
  261. package/src/matrix/send/types.ts +134 -0
  262. package/src/matrix/send.test.ts +958 -0
  263. package/src/matrix/send.ts +609 -0
  264. package/src/matrix/session-store-metadata.ts +108 -0
  265. package/src/matrix/startup-abort.ts +44 -0
  266. package/src/matrix/sync-state.ts +27 -0
  267. package/src/matrix/target-ids.ts +102 -0
  268. package/src/matrix/thread-bindings-shared.ts +201 -0
  269. package/src/matrix/thread-bindings.test.ts +673 -0
  270. package/src/matrix/thread-bindings.ts +577 -0
  271. package/src/matrix-migration.runtime.ts +9 -0
  272. package/src/migration-config.test.ts +228 -0
  273. package/src/migration-config.ts +243 -0
  274. package/src/migration-snapshot-backup.ts +117 -0
  275. package/src/migration-snapshot.test.ts +184 -0
  276. package/src/migration-snapshot.ts +55 -0
  277. package/src/onboarding.resolve.test.ts +55 -0
  278. package/src/onboarding.test-harness.ts +158 -0
  279. package/src/onboarding.test.ts +665 -0
  280. package/src/onboarding.ts +773 -0
  281. package/src/outbound.test.ts +173 -0
  282. package/src/outbound.ts +78 -0
  283. package/src/plugin-entry.runtime.js +159 -0
  284. package/src/plugin-entry.runtime.test.ts +108 -0
  285. package/src/plugin-entry.runtime.ts +68 -0
  286. package/src/profile-update.ts +68 -0
  287. package/src/record-shared.ts +3 -0
  288. package/src/resolve-targets.test.ts +178 -0
  289. package/src/resolve-targets.ts +175 -0
  290. package/src/resolver.ts +21 -0
  291. package/src/runtime-api.ts +144 -0
  292. package/src/runtime.ts +7 -0
  293. package/src/secret-contract.ts +174 -0
  294. package/src/session-route.test.ts +315 -0
  295. package/src/session-route.ts +113 -0
  296. package/src/setup-bootstrap.ts +94 -0
  297. package/src/setup-config.ts +222 -0
  298. package/src/setup-contract.ts +89 -0
  299. package/src/setup-core.test.ts +326 -0
  300. package/src/setup-core.ts +50 -0
  301. package/src/setup-surface.ts +4 -0
  302. package/src/startup-maintenance.test.ts +227 -0
  303. package/src/startup-maintenance.ts +114 -0
  304. package/src/storage-paths.ts +92 -0
  305. package/src/test-helpers.ts +42 -0
  306. package/src/test-mocks.ts +55 -0
  307. package/src/test-runtime.ts +72 -0
  308. package/src/test-support/monitor-route-test-support.ts +8 -0
  309. package/src/tool-actions.runtime.ts +1 -0
  310. package/src/tool-actions.test.ts +422 -0
  311. package/src/tool-actions.ts +498 -0
  312. package/src/types.ts +230 -0
  313. package/test-api.ts +2 -0
  314. package/thread-bindings-runtime.ts +4 -0
  315. package/tsconfig.json +16 -0
@@ -0,0 +1,109 @@
1
+ import {
2
+ buildPollResponseContent,
3
+ isPollStartType,
4
+ parsePollStart,
5
+ type PollStartContent,
6
+ } from "../poll-types.js";
7
+ import { withResolvedRoomAction } from "./client.js";
8
+ import type { MatrixActionClientOpts } from "./types.js";
9
+
10
+ function normalizeOptionIndexes(indexes: number[]): number[] {
11
+ const normalized = indexes
12
+ .map((index) => Math.trunc(index))
13
+ .filter((index) => Number.isFinite(index) && index > 0);
14
+ return Array.from(new Set(normalized));
15
+ }
16
+
17
+ function normalizeOptionIds(optionIds: string[]): string[] {
18
+ return Array.from(
19
+ new Set(optionIds.map((optionId) => optionId.trim()).filter((optionId) => optionId.length > 0)),
20
+ );
21
+ }
22
+
23
+ function resolveSelectedAnswerIds(params: {
24
+ optionIds?: string[];
25
+ optionIndexes?: number[];
26
+ pollContent: PollStartContent;
27
+ }): { answerIds: string[]; labels: string[]; maxSelections: number } {
28
+ const parsed = parsePollStart(params.pollContent);
29
+ if (!parsed) {
30
+ throw new Error("Matrix poll vote requires a valid poll start event.");
31
+ }
32
+
33
+ const selectedById = normalizeOptionIds(params.optionIds ?? []);
34
+ const selectedByIndex = normalizeOptionIndexes(params.optionIndexes ?? []).map((index) => {
35
+ const answer = parsed.answers[index - 1];
36
+ if (!answer) {
37
+ throw new Error(
38
+ `Matrix poll option index ${index} is out of range for a poll with ${parsed.answers.length} options.`,
39
+ );
40
+ }
41
+ return answer.id;
42
+ });
43
+
44
+ const answerIds = normalizeOptionIds([...selectedById, ...selectedByIndex]);
45
+ if (answerIds.length === 0) {
46
+ throw new Error("Matrix poll vote requires at least one poll option id or index.");
47
+ }
48
+ if (answerIds.length > parsed.maxSelections) {
49
+ throw new Error(
50
+ `Matrix poll allows at most ${parsed.maxSelections} selection${parsed.maxSelections === 1 ? "" : "s"}.`,
51
+ );
52
+ }
53
+
54
+ const answerMap = new Map(parsed.answers.map((answer) => [answer.id, answer.text] as const));
55
+ const labels = answerIds.map((answerId) => {
56
+ const label = answerMap.get(answerId);
57
+ if (!label) {
58
+ throw new Error(
59
+ `Matrix poll option id "${answerId}" is not valid for poll ${parsed.question}.`,
60
+ );
61
+ }
62
+ return label;
63
+ });
64
+
65
+ return {
66
+ answerIds,
67
+ labels,
68
+ maxSelections: parsed.maxSelections,
69
+ };
70
+ }
71
+
72
+ export async function voteMatrixPoll(
73
+ roomId: string,
74
+ pollId: string,
75
+ opts: MatrixActionClientOpts & {
76
+ optionId?: string;
77
+ optionIds?: string[];
78
+ optionIndex?: number;
79
+ optionIndexes?: number[];
80
+ } = {},
81
+ ) {
82
+ return await withResolvedRoomAction(roomId, opts, async (client, resolvedRoom) => {
83
+ const pollEvent = await client.getEvent(resolvedRoom, pollId);
84
+ const eventType = typeof pollEvent.type === "string" ? pollEvent.type : "";
85
+ if (!isPollStartType(eventType)) {
86
+ throw new Error(`Event ${pollId} is not a Matrix poll start event.`);
87
+ }
88
+
89
+ const { answerIds, labels, maxSelections } = resolveSelectedAnswerIds({
90
+ optionIds: [...(opts.optionIds ?? []), ...(opts.optionId ? [opts.optionId] : [])],
91
+ optionIndexes: [
92
+ ...(opts.optionIndexes ?? []),
93
+ ...(opts.optionIndex !== undefined ? [opts.optionIndex] : []),
94
+ ],
95
+ pollContent: pollEvent.content as PollStartContent,
96
+ });
97
+
98
+ const content = buildPollResponseContent(pollId, answerIds);
99
+ const eventId = await client.sendEvent(resolvedRoom, "m.poll.response", content);
100
+ return {
101
+ eventId: eventId ?? null,
102
+ roomId: resolvedRoom,
103
+ pollId,
104
+ answerIds,
105
+ labels,
106
+ maxSelections,
107
+ };
108
+ });
109
+ }
@@ -0,0 +1,109 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const loadWebMediaMock = vi.fn();
4
+ const syncMatrixOwnProfileMock = vi.fn();
5
+ const withResolvedActionClientMock = vi.fn();
6
+
7
+ vi.mock("../../runtime.js", () => ({
8
+ getMatrixRuntime: () => ({
9
+ media: {
10
+ loadWebMedia: (...args: unknown[]) => loadWebMediaMock(...args),
11
+ },
12
+ }),
13
+ }));
14
+
15
+ vi.mock("../profile.js", () => ({
16
+ syncMatrixOwnProfile: (...args: unknown[]) => syncMatrixOwnProfileMock(...args),
17
+ }));
18
+
19
+ vi.mock("./client.js", () => ({
20
+ withResolvedActionClient: (...args: unknown[]) => withResolvedActionClientMock(...args),
21
+ }));
22
+
23
+ const { updateMatrixOwnProfile } = await import("./profile.js");
24
+
25
+ describe("matrix profile actions", () => {
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ loadWebMediaMock.mockResolvedValue({
29
+ buffer: Buffer.from("avatar"),
30
+ contentType: "image/png",
31
+ fileName: "avatar.png",
32
+ });
33
+ syncMatrixOwnProfileMock.mockResolvedValue({
34
+ skipped: false,
35
+ displayNameUpdated: true,
36
+ avatarUpdated: true,
37
+ resolvedAvatarUrl: "mxc://example/avatar",
38
+ convertedAvatarFromHttp: true,
39
+ uploadedAvatarSource: "http",
40
+ });
41
+ });
42
+
43
+ it("trims profile fields and persists through the action client wrapper", async () => {
44
+ withResolvedActionClientMock.mockImplementation(async (_opts, run) => {
45
+ return await run({
46
+ getUserId: vi.fn(async () => "@bot:example.org"),
47
+ });
48
+ });
49
+
50
+ await updateMatrixOwnProfile({
51
+ accountId: "ops",
52
+ displayName: " Ops Bot ",
53
+ avatarUrl: " mxc://example/avatar ",
54
+ avatarPath: " /tmp/avatar.png ",
55
+ });
56
+
57
+ expect(withResolvedActionClientMock).toHaveBeenCalledWith(
58
+ {
59
+ accountId: "ops",
60
+ displayName: " Ops Bot ",
61
+ avatarUrl: " mxc://example/avatar ",
62
+ avatarPath: " /tmp/avatar.png ",
63
+ },
64
+ expect.any(Function),
65
+ "persist",
66
+ );
67
+ expect(syncMatrixOwnProfileMock).toHaveBeenCalledWith(
68
+ expect.objectContaining({
69
+ userId: "@bot:example.org",
70
+ displayName: "Ops Bot",
71
+ avatarUrl: "mxc://example/avatar",
72
+ avatarPath: "/tmp/avatar.png",
73
+ }),
74
+ );
75
+ });
76
+
77
+ it("bridges avatar loaders through Matrix runtime media helpers", async () => {
78
+ withResolvedActionClientMock.mockImplementation(async (_opts, run) => {
79
+ return await run({
80
+ getUserId: vi.fn(async () => "@bot:example.org"),
81
+ });
82
+ });
83
+
84
+ await updateMatrixOwnProfile({
85
+ avatarUrl: "https://cdn.example.org/avatar.png",
86
+ avatarPath: "/tmp/avatar.png",
87
+ });
88
+
89
+ const call = syncMatrixOwnProfileMock.mock.calls[0]?.[0] as
90
+ | {
91
+ loadAvatarFromUrl: (url: string, maxBytes: number) => Promise<unknown>;
92
+ loadAvatarFromPath: (path: string, maxBytes: number) => Promise<unknown>;
93
+ }
94
+ | undefined;
95
+
96
+ if (!call) {
97
+ throw new Error("syncMatrixOwnProfile was not called");
98
+ }
99
+
100
+ await call.loadAvatarFromUrl("https://cdn.example.org/avatar.png", 123);
101
+ await call.loadAvatarFromPath("/tmp/avatar.png", 456);
102
+
103
+ expect(loadWebMediaMock).toHaveBeenNthCalledWith(1, "https://cdn.example.org/avatar.png", 123);
104
+ expect(loadWebMediaMock).toHaveBeenNthCalledWith(2, "/tmp/avatar.png", {
105
+ maxBytes: 456,
106
+ localRoots: undefined,
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,37 @@
1
+ import { getMatrixRuntime } from "../../runtime.js";
2
+ import { syncMatrixOwnProfile, type MatrixProfileSyncResult } from "../profile.js";
3
+ import { withResolvedActionClient } from "./client.js";
4
+ import type { MatrixActionClientOpts } from "./types.js";
5
+
6
+ export async function updateMatrixOwnProfile(
7
+ opts: MatrixActionClientOpts & {
8
+ displayName?: string;
9
+ avatarUrl?: string;
10
+ avatarPath?: string;
11
+ } = {},
12
+ ): Promise<MatrixProfileSyncResult> {
13
+ const displayName = opts.displayName?.trim();
14
+ const avatarUrl = opts.avatarUrl?.trim();
15
+ const avatarPath = opts.avatarPath?.trim();
16
+ const runtime = getMatrixRuntime();
17
+ return await withResolvedActionClient(
18
+ opts,
19
+ async (client) => {
20
+ const userId = await client.getUserId();
21
+ return await syncMatrixOwnProfile({
22
+ client,
23
+ userId,
24
+ displayName: displayName || undefined,
25
+ avatarUrl: avatarUrl || undefined,
26
+ avatarPath: avatarPath || undefined,
27
+ loadAvatarFromUrl: async (url, maxBytes) => await runtime.media.loadWebMedia(url, maxBytes),
28
+ loadAvatarFromPath: async (path, maxBytes) =>
29
+ await runtime.media.loadWebMedia(path, {
30
+ maxBytes,
31
+ localRoots: opts.mediaLocalRoots,
32
+ }),
33
+ });
34
+ },
35
+ "persist",
36
+ );
37
+ }
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { MatrixClient } from "../sdk.js";
3
+ import { listMatrixReactions, removeMatrixReactions } from "./reactions.js";
4
+
5
+ function createReactionsClient(params: {
6
+ chunk: Array<{
7
+ event_id?: string;
8
+ sender?: string;
9
+ key?: string;
10
+ }>;
11
+ userId?: string | null;
12
+ }) {
13
+ const doRequest = vi.fn(async (_method: string, _path: string, _query: unknown) => ({
14
+ chunk: params.chunk.map((item) => ({
15
+ event_id: item.event_id ?? "",
16
+ sender: item.sender ?? "",
17
+ content: item.key
18
+ ? {
19
+ "m.relates_to": {
20
+ rel_type: "m.annotation",
21
+ event_id: "$target",
22
+ key: item.key,
23
+ },
24
+ }
25
+ : {},
26
+ })),
27
+ }));
28
+ const getUserId = vi.fn(async () => params.userId ?? null);
29
+ const redactEvent = vi.fn(async () => undefined);
30
+
31
+ return {
32
+ client: {
33
+ doRequest,
34
+ getUserId,
35
+ redactEvent,
36
+ stop: vi.fn(),
37
+ } as unknown as MatrixClient,
38
+ doRequest,
39
+ redactEvent,
40
+ };
41
+ }
42
+
43
+ describe("matrix reaction actions", () => {
44
+ it("aggregates reactions by key and unique sender", async () => {
45
+ const { client, doRequest } = createReactionsClient({
46
+ chunk: [
47
+ { event_id: "$1", sender: "@alice:example.org", key: "👍" },
48
+ { event_id: "$2", sender: "@bob:example.org", key: "👍" },
49
+ { event_id: "$3", sender: "@alice:example.org", key: "👎" },
50
+ { event_id: "$4", sender: "@bot:example.org" },
51
+ ],
52
+ userId: "@bot:example.org",
53
+ });
54
+
55
+ const result = await listMatrixReactions("!room:example.org", "$msg", { client, limit: 2.9 });
56
+
57
+ expect(doRequest).toHaveBeenCalledWith(
58
+ "GET",
59
+ expect.stringContaining("/rooms/!room%3Aexample.org/relations/%24msg/"),
60
+ expect.objectContaining({ limit: 2 }),
61
+ );
62
+ expect(result).toEqual(
63
+ expect.arrayContaining([
64
+ expect.objectContaining({
65
+ key: "👍",
66
+ count: 2,
67
+ users: expect.arrayContaining(["@alice:example.org", "@bob:example.org"]),
68
+ }),
69
+ expect.objectContaining({
70
+ key: "👎",
71
+ count: 1,
72
+ users: ["@alice:example.org"],
73
+ }),
74
+ ]),
75
+ );
76
+ });
77
+
78
+ it("removes only current-user reactions matching emoji filter", async () => {
79
+ const { client, redactEvent } = createReactionsClient({
80
+ chunk: [
81
+ { event_id: "$1", sender: "@me:example.org", key: "👍" },
82
+ { event_id: "$2", sender: "@me:example.org", key: "👎" },
83
+ { event_id: "$3", sender: "@other:example.org", key: "👍" },
84
+ ],
85
+ userId: "@me:example.org",
86
+ });
87
+
88
+ const result = await removeMatrixReactions("!room:example.org", "$msg", {
89
+ client,
90
+ emoji: "👍",
91
+ });
92
+
93
+ expect(result).toEqual({ removed: 1 });
94
+ expect(redactEvent).toHaveBeenCalledTimes(1);
95
+ expect(redactEvent).toHaveBeenCalledWith("!room:example.org", "$1");
96
+ });
97
+
98
+ it("returns removed=0 when current user id is unavailable", async () => {
99
+ const { client, redactEvent } = createReactionsClient({
100
+ chunk: [{ event_id: "$1", sender: "@me:example.org", key: "👍" }],
101
+ userId: null,
102
+ });
103
+
104
+ const result = await removeMatrixReactions("!room:example.org", "$msg", { client });
105
+
106
+ expect(result).toEqual({ removed: 0 });
107
+ expect(redactEvent).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it("returns an empty list when the relations response is malformed", async () => {
111
+ const doRequest = vi.fn(async () => ({ chunk: null }));
112
+ const client = {
113
+ doRequest,
114
+ getUserId: vi.fn(async () => "@me:example.org"),
115
+ redactEvent: vi.fn(async () => undefined),
116
+ stop: vi.fn(),
117
+ } as unknown as MatrixClient;
118
+
119
+ const result = await listMatrixReactions("!room:example.org", "$msg", { client });
120
+
121
+ expect(result).toEqual([]);
122
+ });
123
+
124
+ it("rejects blank message ids before querying Matrix relations", async () => {
125
+ const { client, doRequest } = createReactionsClient({
126
+ chunk: [],
127
+ userId: "@me:example.org",
128
+ });
129
+
130
+ await expect(listMatrixReactions("!room:example.org", " ", { client })).rejects.toThrow(
131
+ "messageId",
132
+ );
133
+ expect(doRequest).not.toHaveBeenCalled();
134
+ });
135
+ });
@@ -0,0 +1,59 @@
1
+ import {
2
+ buildMatrixReactionRelationsPath,
3
+ selectOwnMatrixReactionEventIds,
4
+ summarizeMatrixReactionEvents,
5
+ } from "../reaction-common.js";
6
+ import { withResolvedRoomAction } from "./client.js";
7
+ import { resolveMatrixActionLimit } from "./limits.js";
8
+ import {
9
+ type MatrixActionClientOpts,
10
+ type MatrixRawEvent,
11
+ type MatrixReactionSummary,
12
+ } from "./types.js";
13
+
14
+ type ActionClient = NonNullable<MatrixActionClientOpts["client"]>;
15
+
16
+ async function listMatrixReactionEvents(
17
+ client: ActionClient,
18
+ roomId: string,
19
+ messageId: string,
20
+ limit: number,
21
+ ): Promise<MatrixRawEvent[]> {
22
+ const res = (await client.doRequest("GET", buildMatrixReactionRelationsPath(roomId, messageId), {
23
+ dir: "b",
24
+ limit,
25
+ })) as { chunk?: MatrixRawEvent[] };
26
+ return Array.isArray(res.chunk) ? res.chunk : [];
27
+ }
28
+
29
+ export async function listMatrixReactions(
30
+ roomId: string,
31
+ messageId: string,
32
+ opts: MatrixActionClientOpts & { limit?: number } = {},
33
+ ): Promise<MatrixReactionSummary[]> {
34
+ return await withResolvedRoomAction(roomId, opts, async (client, resolvedRoom) => {
35
+ const limit = resolveMatrixActionLimit(opts.limit, 100);
36
+ const chunk = await listMatrixReactionEvents(client, resolvedRoom, messageId, limit);
37
+ return summarizeMatrixReactionEvents(chunk);
38
+ });
39
+ }
40
+
41
+ export async function removeMatrixReactions(
42
+ roomId: string,
43
+ messageId: string,
44
+ opts: MatrixActionClientOpts & { emoji?: string } = {},
45
+ ): Promise<{ removed: number }> {
46
+ return await withResolvedRoomAction(roomId, opts, async (client, resolvedRoom) => {
47
+ const chunk = await listMatrixReactionEvents(client, resolvedRoom, messageId, 200);
48
+ const userId = await client.getUserId();
49
+ if (!userId) {
50
+ return { removed: 0 };
51
+ }
52
+ const toRemove = selectOwnMatrixReactionEventIds(chunk, userId, opts.emoji);
53
+ if (toRemove.length === 0) {
54
+ return { removed: 0 };
55
+ }
56
+ await Promise.all(toRemove.map((id) => client.redactEvent(resolvedRoom, id)));
57
+ return { removed: toRemove.length };
58
+ });
59
+ }
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { MatrixClient } from "../sdk.js";
3
+ import { getMatrixMemberInfo, getMatrixRoomInfo } from "./room.js";
4
+
5
+ function createRoomClient() {
6
+ const getRoomStateEvent = vi.fn(async (_roomId: string, eventType: string) => {
7
+ switch (eventType) {
8
+ case "m.room.name":
9
+ return { name: "Ops Room" };
10
+ case "m.room.topic":
11
+ return { topic: "Incidents" };
12
+ case "m.room.canonical_alias":
13
+ return { alias: "#ops:example.org" };
14
+ default:
15
+ throw new Error(`unexpected state event ${eventType}`);
16
+ }
17
+ });
18
+ const getJoinedRoomMembers = vi.fn(async () => [
19
+ { user_id: "@alice:example.org" },
20
+ { user_id: "@bot:example.org" },
21
+ ]);
22
+ const getUserProfile = vi.fn(async () => ({
23
+ displayname: "Alice",
24
+ avatar_url: "mxc://example.org/alice",
25
+ }));
26
+
27
+ return {
28
+ client: {
29
+ getRoomStateEvent,
30
+ getJoinedRoomMembers,
31
+ getUserProfile,
32
+ stop: vi.fn(),
33
+ } as unknown as MatrixClient,
34
+ getRoomStateEvent,
35
+ getJoinedRoomMembers,
36
+ getUserProfile,
37
+ };
38
+ }
39
+
40
+ describe("matrix room actions", () => {
41
+ it("returns room details from the resolved Matrix room id", async () => {
42
+ const { client, getJoinedRoomMembers, getRoomStateEvent } = createRoomClient();
43
+
44
+ const result = await getMatrixRoomInfo("room:!ops:example.org", { client });
45
+
46
+ expect(getRoomStateEvent).toHaveBeenCalledWith("!ops:example.org", "m.room.name", "");
47
+ expect(getJoinedRoomMembers).toHaveBeenCalledWith("!ops:example.org");
48
+ expect(result).toEqual({
49
+ roomId: "!ops:example.org",
50
+ name: "Ops Room",
51
+ topic: "Incidents",
52
+ canonicalAlias: "#ops:example.org",
53
+ altAliases: [],
54
+ memberCount: 2,
55
+ });
56
+ });
57
+
58
+ it("resolves optional room ids when looking up member info", async () => {
59
+ const { client, getUserProfile } = createRoomClient();
60
+
61
+ const result = await getMatrixMemberInfo("@alice:example.org", {
62
+ client,
63
+ roomId: "room:!ops:example.org",
64
+ });
65
+
66
+ expect(getUserProfile).toHaveBeenCalledWith("@alice:example.org");
67
+ expect(result).toEqual({
68
+ userId: "@alice:example.org",
69
+ profile: {
70
+ displayName: "Alice",
71
+ avatarUrl: "mxc://example.org/alice",
72
+ },
73
+ membership: null,
74
+ powerLevel: null,
75
+ displayName: "Alice",
76
+ roomId: "!ops:example.org",
77
+ });
78
+ });
79
+ });
@@ -0,0 +1,71 @@
1
+ import { resolveMatrixRoomId } from "../send.js";
2
+ import { withResolvedActionClient, withResolvedRoomAction } from "./client.js";
3
+ import { EventType, type MatrixActionClientOpts } from "./types.js";
4
+
5
+ export async function getMatrixMemberInfo(
6
+ userId: string,
7
+ opts: MatrixActionClientOpts & { roomId?: string } = {},
8
+ ) {
9
+ return await withResolvedActionClient(opts, async (client) => {
10
+ const roomId = opts.roomId ? await resolveMatrixRoomId(client, opts.roomId) : undefined;
11
+ const profile = await client.getUserProfile(userId);
12
+ // Membership and power levels are not included in profile calls; fetch state separately if needed.
13
+ return {
14
+ userId,
15
+ profile: {
16
+ displayName: profile?.displayname ?? null,
17
+ avatarUrl: profile?.avatar_url ?? null,
18
+ },
19
+ membership: null, // Would need separate room state query
20
+ powerLevel: null, // Would need separate power levels state query
21
+ displayName: profile?.displayname ?? null,
22
+ roomId: roomId ?? null,
23
+ };
24
+ });
25
+ }
26
+
27
+ export async function getMatrixRoomInfo(roomId: string, opts: MatrixActionClientOpts = {}) {
28
+ return await withResolvedRoomAction(roomId, opts, async (client, resolvedRoom) => {
29
+ let name: string | null = null;
30
+ let topic: string | null = null;
31
+ let canonicalAlias: string | null = null;
32
+ let memberCount: number | null = null;
33
+
34
+ try {
35
+ const nameState = await client.getRoomStateEvent(resolvedRoom, "m.room.name", "");
36
+ name = typeof nameState?.name === "string" ? nameState.name : null;
37
+ } catch {
38
+ // ignore
39
+ }
40
+
41
+ try {
42
+ const topicState = await client.getRoomStateEvent(resolvedRoom, EventType.RoomTopic, "");
43
+ topic = typeof topicState?.topic === "string" ? topicState.topic : null;
44
+ } catch {
45
+ // ignore
46
+ }
47
+
48
+ try {
49
+ const aliasState = await client.getRoomStateEvent(resolvedRoom, "m.room.canonical_alias", "");
50
+ canonicalAlias = typeof aliasState?.alias === "string" ? aliasState.alias : null;
51
+ } catch {
52
+ // ignore
53
+ }
54
+
55
+ try {
56
+ const members = await client.getJoinedRoomMembers(resolvedRoom);
57
+ memberCount = members.length;
58
+ } catch {
59
+ // ignore
60
+ }
61
+
62
+ return {
63
+ roomId: resolvedRoom,
64
+ name,
65
+ topic,
66
+ canonicalAlias,
67
+ altAliases: [], // Would need separate query
68
+ memberCount,
69
+ };
70
+ });
71
+ }
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { summarizeMatrixRawEvent } from "./summary.js";
3
+
4
+ describe("summarizeMatrixRawEvent", () => {
5
+ it("replaces bare media filenames with a media marker", () => {
6
+ const summary = summarizeMatrixRawEvent({
7
+ event_id: "$image",
8
+ sender: "@gum:matrix.example.org",
9
+ type: "m.room.message",
10
+ origin_server_ts: 123,
11
+ content: {
12
+ msgtype: "m.image",
13
+ body: "photo.jpg",
14
+ },
15
+ });
16
+
17
+ expect(summary).toMatchObject({
18
+ eventId: "$image",
19
+ msgtype: "m.image",
20
+ attachment: {
21
+ kind: "image",
22
+ filename: "photo.jpg",
23
+ },
24
+ });
25
+ expect(summary.body).toBeUndefined();
26
+ });
27
+
28
+ it("preserves captions while marking media summaries", () => {
29
+ const summary = summarizeMatrixRawEvent({
30
+ event_id: "$image",
31
+ sender: "@gum:matrix.example.org",
32
+ type: "m.room.message",
33
+ origin_server_ts: 123,
34
+ content: {
35
+ msgtype: "m.image",
36
+ body: "can you see this?",
37
+ filename: "photo.jpg",
38
+ },
39
+ });
40
+
41
+ expect(summary).toMatchObject({
42
+ body: "can you see this?",
43
+ attachment: {
44
+ kind: "image",
45
+ caption: "can you see this?",
46
+ filename: "photo.jpg",
47
+ },
48
+ });
49
+ });
50
+
51
+ it("does not treat a sentence ending in a file extension as a bare filename", () => {
52
+ const summary = summarizeMatrixRawEvent({
53
+ event_id: "$image",
54
+ sender: "@gum:matrix.example.org",
55
+ type: "m.room.message",
56
+ origin_server_ts: 123,
57
+ content: {
58
+ msgtype: "m.image",
59
+ body: "see image.png",
60
+ },
61
+ });
62
+
63
+ expect(summary).toMatchObject({
64
+ body: "see image.png",
65
+ attachment: {
66
+ kind: "image",
67
+ caption: "see image.png",
68
+ },
69
+ });
70
+ });
71
+
72
+ it("leaves text messages unchanged", () => {
73
+ const summary = summarizeMatrixRawEvent({
74
+ event_id: "$text",
75
+ sender: "@gum:matrix.example.org",
76
+ type: "m.room.message",
77
+ origin_server_ts: 123,
78
+ content: {
79
+ msgtype: "m.text",
80
+ body: "hello",
81
+ },
82
+ });
83
+
84
+ expect(summary.body).toBe("hello");
85
+ expect(summary.attachment).toBeUndefined();
86
+ });
87
+ });