@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,154 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ isSupportedMatrixAvatarSource,
4
+ syncMatrixOwnProfile,
5
+ type MatrixProfileSyncResult,
6
+ } from "./profile.js";
7
+
8
+ function createClientStub() {
9
+ return {
10
+ getUserProfile: vi.fn(async () => ({})),
11
+ setDisplayName: vi.fn(async () => {}),
12
+ setAvatarUrl: vi.fn(async () => {}),
13
+ uploadContent: vi.fn(async () => "mxc://example/avatar"),
14
+ };
15
+ }
16
+
17
+ function expectNoUpdates(result: MatrixProfileSyncResult) {
18
+ expect(result.displayNameUpdated).toBe(false);
19
+ expect(result.avatarUpdated).toBe(false);
20
+ }
21
+
22
+ describe("matrix profile sync", () => {
23
+ it("skips when no desired profile values are provided", async () => {
24
+ const client = createClientStub();
25
+ const result = await syncMatrixOwnProfile({
26
+ client,
27
+ userId: "@bot:example.org",
28
+ });
29
+
30
+ expect(result.skipped).toBe(true);
31
+ expectNoUpdates(result);
32
+ expect(result.uploadedAvatarSource).toBeNull();
33
+ expect(client.setDisplayName).not.toHaveBeenCalled();
34
+ expect(client.setAvatarUrl).not.toHaveBeenCalled();
35
+ });
36
+
37
+ it("updates display name when desired name differs", async () => {
38
+ const client = createClientStub();
39
+ client.getUserProfile.mockResolvedValue({
40
+ displayname: "Old Name",
41
+ avatar_url: "mxc://example/existing",
42
+ });
43
+
44
+ const result = await syncMatrixOwnProfile({
45
+ client,
46
+ userId: "@bot:example.org",
47
+ displayName: "New Name",
48
+ });
49
+
50
+ expect(result.skipped).toBe(false);
51
+ expect(result.displayNameUpdated).toBe(true);
52
+ expect(result.avatarUpdated).toBe(false);
53
+ expect(result.uploadedAvatarSource).toBeNull();
54
+ expect(client.setDisplayName).toHaveBeenCalledWith("New Name");
55
+ });
56
+
57
+ it("does not update when name and avatar already match", async () => {
58
+ const client = createClientStub();
59
+ client.getUserProfile.mockResolvedValue({
60
+ displayname: "Bot",
61
+ avatar_url: "mxc://example/avatar",
62
+ });
63
+
64
+ const result = await syncMatrixOwnProfile({
65
+ client,
66
+ userId: "@bot:example.org",
67
+ displayName: "Bot",
68
+ avatarUrl: "mxc://example/avatar",
69
+ });
70
+
71
+ expect(result.skipped).toBe(false);
72
+ expectNoUpdates(result);
73
+ expect(client.setDisplayName).not.toHaveBeenCalled();
74
+ expect(client.setAvatarUrl).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it("converts http avatar URL by uploading and then updates profile avatar", async () => {
78
+ const client = createClientStub();
79
+ client.getUserProfile.mockResolvedValue({
80
+ displayname: "Bot",
81
+ avatar_url: "mxc://example/old",
82
+ });
83
+ client.uploadContent.mockResolvedValue("mxc://example/new-avatar");
84
+ const loadAvatarFromUrl = vi.fn(async () => ({
85
+ buffer: Buffer.from("avatar-bytes"),
86
+ contentType: "image/png",
87
+ fileName: "avatar.png",
88
+ }));
89
+
90
+ const result = await syncMatrixOwnProfile({
91
+ client,
92
+ userId: "@bot:example.org",
93
+ avatarUrl: "https://cdn.example.org/avatar.png",
94
+ loadAvatarFromUrl,
95
+ });
96
+
97
+ expect(result.convertedAvatarFromHttp).toBe(true);
98
+ expect(result.uploadedAvatarSource).toBe("http");
99
+ expect(result.resolvedAvatarUrl).toBe("mxc://example/new-avatar");
100
+ expect(result.avatarUpdated).toBe(true);
101
+ expect(loadAvatarFromUrl).toHaveBeenCalledWith(
102
+ "https://cdn.example.org/avatar.png",
103
+ 10 * 1024 * 1024,
104
+ );
105
+ expect(client.setAvatarUrl).toHaveBeenCalledWith("mxc://example/new-avatar");
106
+ });
107
+
108
+ it("uploads avatar media from a local path and then updates profile avatar", async () => {
109
+ const client = createClientStub();
110
+ client.getUserProfile.mockResolvedValue({
111
+ displayname: "Bot",
112
+ avatar_url: "mxc://example/old",
113
+ });
114
+ client.uploadContent.mockResolvedValue("mxc://example/path-avatar");
115
+ const loadAvatarFromPath = vi.fn(async () => ({
116
+ buffer: Buffer.from("avatar-bytes"),
117
+ contentType: "image/jpeg",
118
+ fileName: "avatar.jpg",
119
+ }));
120
+
121
+ const result = await syncMatrixOwnProfile({
122
+ client,
123
+ userId: "@bot:example.org",
124
+ avatarPath: "/tmp/avatar.jpg",
125
+ loadAvatarFromPath,
126
+ });
127
+
128
+ expect(result.convertedAvatarFromHttp).toBe(false);
129
+ expect(result.uploadedAvatarSource).toBe("path");
130
+ expect(result.resolvedAvatarUrl).toBe("mxc://example/path-avatar");
131
+ expect(result.avatarUpdated).toBe(true);
132
+ expect(loadAvatarFromPath).toHaveBeenCalledWith("/tmp/avatar.jpg", 10 * 1024 * 1024);
133
+ expect(client.setAvatarUrl).toHaveBeenCalledWith("mxc://example/path-avatar");
134
+ });
135
+
136
+ it("rejects unsupported avatar URL schemes", async () => {
137
+ const client = createClientStub();
138
+
139
+ await expect(
140
+ syncMatrixOwnProfile({
141
+ client,
142
+ userId: "@bot:example.org",
143
+ avatarUrl: "file:///tmp/avatar.png",
144
+ }),
145
+ ).rejects.toThrow("Matrix avatar URL must be an mxc:// URI or an http(s) URL.");
146
+ });
147
+
148
+ it("recognizes supported avatar sources", () => {
149
+ expect(isSupportedMatrixAvatarSource("mxc://example/avatar")).toBe(true);
150
+ expect(isSupportedMatrixAvatarSource("https://example.org/avatar.png")).toBe(true);
151
+ expect(isSupportedMatrixAvatarSource("http://example.org/avatar.png")).toBe(true);
152
+ expect(isSupportedMatrixAvatarSource("ftp://example.org/avatar.png")).toBe(false);
153
+ });
154
+ });
@@ -0,0 +1,184 @@
1
+ import {
2
+ normalizeLowercaseStringOrEmpty,
3
+ normalizeOptionalString,
4
+ } from "openclaw/plugin-sdk/text-runtime";
5
+ import type { MatrixClient } from "./sdk.js";
6
+
7
+ export const MATRIX_PROFILE_AVATAR_MAX_BYTES = 10 * 1024 * 1024;
8
+
9
+ type MatrixProfileClient = Pick<
10
+ MatrixClient,
11
+ "getUserProfile" | "setDisplayName" | "setAvatarUrl" | "uploadContent"
12
+ >;
13
+
14
+ type MatrixProfileLoadResult = {
15
+ buffer: Buffer;
16
+ contentType?: string;
17
+ fileName?: string;
18
+ };
19
+
20
+ export type MatrixProfileSyncResult = {
21
+ skipped: boolean;
22
+ displayNameUpdated: boolean;
23
+ avatarUpdated: boolean;
24
+ resolvedAvatarUrl: string | null;
25
+ uploadedAvatarSource: "http" | "path" | null;
26
+ convertedAvatarFromHttp: boolean;
27
+ };
28
+
29
+ export function isMatrixMxcUri(value: string): boolean {
30
+ return normalizeLowercaseStringOrEmpty(normalizeOptionalString(value)).startsWith("mxc://");
31
+ }
32
+
33
+ export function isMatrixHttpAvatarUri(value: string): boolean {
34
+ const normalized = normalizeLowercaseStringOrEmpty(normalizeOptionalString(value));
35
+ return normalized.startsWith("https://") || normalized.startsWith("http://");
36
+ }
37
+
38
+ export function isSupportedMatrixAvatarSource(value: string): boolean {
39
+ return isMatrixMxcUri(value) || isMatrixHttpAvatarUri(value);
40
+ }
41
+
42
+ async function uploadAvatarMedia(params: {
43
+ client: MatrixProfileClient;
44
+ avatarSource: string;
45
+ avatarMaxBytes: number;
46
+ loadAvatar: (source: string, maxBytes: number) => Promise<MatrixProfileLoadResult>;
47
+ }): Promise<string> {
48
+ const media = await params.loadAvatar(params.avatarSource, params.avatarMaxBytes);
49
+ return await params.client.uploadContent(
50
+ media.buffer,
51
+ media.contentType,
52
+ media.fileName || "avatar",
53
+ );
54
+ }
55
+
56
+ async function resolveAvatarUrl(params: {
57
+ client: MatrixProfileClient;
58
+ avatarUrl: string | null;
59
+ avatarPath?: string | null;
60
+ avatarMaxBytes: number;
61
+ loadAvatarFromUrl?: (url: string, maxBytes: number) => Promise<MatrixProfileLoadResult>;
62
+ loadAvatarFromPath?: (path: string, maxBytes: number) => Promise<MatrixProfileLoadResult>;
63
+ }): Promise<{
64
+ resolvedAvatarUrl: string | null;
65
+ uploadedAvatarSource: "http" | "path" | null;
66
+ convertedAvatarFromHttp: boolean;
67
+ }> {
68
+ const avatarPath = normalizeOptionalString(params.avatarPath) ?? null;
69
+ if (avatarPath) {
70
+ if (!params.loadAvatarFromPath) {
71
+ throw new Error("Matrix avatar path upload requires a media loader.");
72
+ }
73
+ return {
74
+ resolvedAvatarUrl: await uploadAvatarMedia({
75
+ client: params.client,
76
+ avatarSource: avatarPath,
77
+ avatarMaxBytes: params.avatarMaxBytes,
78
+ loadAvatar: params.loadAvatarFromPath,
79
+ }),
80
+ uploadedAvatarSource: "path",
81
+ convertedAvatarFromHttp: false,
82
+ };
83
+ }
84
+
85
+ const avatarUrl = normalizeOptionalString(params.avatarUrl) ?? null;
86
+ if (!avatarUrl) {
87
+ return {
88
+ resolvedAvatarUrl: null,
89
+ uploadedAvatarSource: null,
90
+ convertedAvatarFromHttp: false,
91
+ };
92
+ }
93
+
94
+ if (isMatrixMxcUri(avatarUrl)) {
95
+ return {
96
+ resolvedAvatarUrl: avatarUrl,
97
+ uploadedAvatarSource: null,
98
+ convertedAvatarFromHttp: false,
99
+ };
100
+ }
101
+
102
+ if (!isMatrixHttpAvatarUri(avatarUrl)) {
103
+ throw new Error("Matrix avatar URL must be an mxc:// URI or an http(s) URL.");
104
+ }
105
+
106
+ if (!params.loadAvatarFromUrl) {
107
+ throw new Error("Matrix avatar URL conversion requires a media loader.");
108
+ }
109
+
110
+ return {
111
+ resolvedAvatarUrl: await uploadAvatarMedia({
112
+ client: params.client,
113
+ avatarSource: avatarUrl,
114
+ avatarMaxBytes: params.avatarMaxBytes,
115
+ loadAvatar: params.loadAvatarFromUrl,
116
+ }),
117
+ uploadedAvatarSource: "http",
118
+ convertedAvatarFromHttp: true,
119
+ };
120
+ }
121
+
122
+ export async function syncMatrixOwnProfile(params: {
123
+ client: MatrixProfileClient;
124
+ userId: string;
125
+ displayName?: string | null;
126
+ avatarUrl?: string | null;
127
+ avatarPath?: string | null;
128
+ avatarMaxBytes?: number;
129
+ loadAvatarFromUrl?: (url: string, maxBytes: number) => Promise<MatrixProfileLoadResult>;
130
+ loadAvatarFromPath?: (path: string, maxBytes: number) => Promise<MatrixProfileLoadResult>;
131
+ }): Promise<MatrixProfileSyncResult> {
132
+ const desiredDisplayName = normalizeOptionalString(params.displayName) ?? null;
133
+ const avatar = await resolveAvatarUrl({
134
+ client: params.client,
135
+ avatarUrl: params.avatarUrl ?? null,
136
+ avatarPath: params.avatarPath ?? null,
137
+ avatarMaxBytes: params.avatarMaxBytes ?? MATRIX_PROFILE_AVATAR_MAX_BYTES,
138
+ loadAvatarFromUrl: params.loadAvatarFromUrl,
139
+ loadAvatarFromPath: params.loadAvatarFromPath,
140
+ });
141
+ const desiredAvatarUrl = avatar.resolvedAvatarUrl;
142
+
143
+ if (!desiredDisplayName && !desiredAvatarUrl) {
144
+ return {
145
+ skipped: true,
146
+ displayNameUpdated: false,
147
+ avatarUpdated: false,
148
+ resolvedAvatarUrl: null,
149
+ uploadedAvatarSource: avatar.uploadedAvatarSource,
150
+ convertedAvatarFromHttp: avatar.convertedAvatarFromHttp,
151
+ };
152
+ }
153
+
154
+ let currentDisplayName: string | undefined;
155
+ let currentAvatarUrl: string | undefined;
156
+ try {
157
+ const currentProfile = await params.client.getUserProfile(params.userId);
158
+ currentDisplayName = normalizeOptionalString(currentProfile.displayname);
159
+ currentAvatarUrl = normalizeOptionalString(currentProfile.avatar_url);
160
+ } catch {
161
+ // If profile fetch fails, attempt writes directly.
162
+ }
163
+
164
+ let displayNameUpdated = false;
165
+ let avatarUpdated = false;
166
+
167
+ if (desiredDisplayName && currentDisplayName !== desiredDisplayName) {
168
+ await params.client.setDisplayName(desiredDisplayName);
169
+ displayNameUpdated = true;
170
+ }
171
+ if (desiredAvatarUrl && currentAvatarUrl !== desiredAvatarUrl) {
172
+ await params.client.setAvatarUrl(desiredAvatarUrl);
173
+ avatarUpdated = true;
174
+ }
175
+
176
+ return {
177
+ skipped: false,
178
+ displayNameUpdated,
179
+ avatarUpdated,
180
+ resolvedAvatarUrl: desiredAvatarUrl,
181
+ uploadedAvatarSource: avatar.uploadedAvatarSource,
182
+ convertedAvatarFromHttp: avatar.convertedAvatarFromHttp,
183
+ };
184
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildMatrixReactionContent,
4
+ buildMatrixReactionRelationsPath,
5
+ extractMatrixReactionAnnotation,
6
+ selectOwnMatrixReactionEventIds,
7
+ summarizeMatrixReactionEvents,
8
+ } from "./reaction-common.js";
9
+
10
+ describe("matrix reaction helpers", () => {
11
+ it("builds trimmed reaction content and relation paths", () => {
12
+ expect(buildMatrixReactionContent(" $msg ", " 👍 ")).toEqual({
13
+ "m.relates_to": {
14
+ rel_type: "m.annotation",
15
+ event_id: "$msg",
16
+ key: "👍",
17
+ },
18
+ });
19
+ expect(buildMatrixReactionRelationsPath("!room:example.org", " $msg ")).toContain(
20
+ "/rooms/!room%3Aexample.org/relations/%24msg/m.annotation/m.reaction",
21
+ );
22
+ });
23
+
24
+ it("summarizes reactions by emoji and unique sender", () => {
25
+ expect(
26
+ summarizeMatrixReactionEvents([
27
+ { sender: "@alice:example.org", content: { "m.relates_to": { key: "👍" } } },
28
+ { sender: "@alice:example.org", content: { "m.relates_to": { key: "👍" } } },
29
+ { sender: "@bob:example.org", content: { "m.relates_to": { key: "👍" } } },
30
+ { sender: "@alice:example.org", content: { "m.relates_to": { key: "👎" } } },
31
+ { sender: "@ignored:example.org", content: {} },
32
+ ]),
33
+ ).toEqual([
34
+ {
35
+ key: "👍",
36
+ count: 3,
37
+ users: ["@alice:example.org", "@bob:example.org"],
38
+ },
39
+ {
40
+ key: "👎",
41
+ count: 1,
42
+ users: ["@alice:example.org"],
43
+ },
44
+ ]);
45
+ });
46
+
47
+ it("selects only matching reaction event ids for the current user", () => {
48
+ expect(
49
+ selectOwnMatrixReactionEventIds(
50
+ [
51
+ {
52
+ event_id: "$1",
53
+ sender: "@me:example.org",
54
+ content: { "m.relates_to": { key: "👍" } },
55
+ },
56
+ {
57
+ event_id: "$2",
58
+ sender: "@me:example.org",
59
+ content: { "m.relates_to": { key: "👎" } },
60
+ },
61
+ {
62
+ event_id: "$3",
63
+ sender: "@other:example.org",
64
+ content: { "m.relates_to": { key: "👍" } },
65
+ },
66
+ ],
67
+ "@me:example.org",
68
+ "👍",
69
+ ),
70
+ ).toEqual(["$1"]);
71
+ });
72
+
73
+ it("extracts annotations and ignores non-annotation relations", () => {
74
+ expect(
75
+ extractMatrixReactionAnnotation({
76
+ "m.relates_to": {
77
+ rel_type: "m.annotation",
78
+ event_id: " $msg ",
79
+ key: " 👍 ",
80
+ },
81
+ }),
82
+ ).toEqual({
83
+ eventId: "$msg",
84
+ key: "👍",
85
+ });
86
+ expect(
87
+ extractMatrixReactionAnnotation({
88
+ "m.relates_to": {
89
+ rel_type: "m.replace",
90
+ event_id: "$msg",
91
+ key: "👍",
92
+ },
93
+ }),
94
+ ).toBeUndefined();
95
+ });
96
+ });
@@ -0,0 +1,147 @@
1
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
2
+
3
+ export const MATRIX_ANNOTATION_RELATION_TYPE = "m.annotation";
4
+ export const MATRIX_REACTION_EVENT_TYPE = "m.reaction";
5
+
6
+ export type MatrixReactionEventContent = {
7
+ "m.relates_to": {
8
+ rel_type: typeof MATRIX_ANNOTATION_RELATION_TYPE;
9
+ event_id: string;
10
+ key: string;
11
+ };
12
+ };
13
+
14
+ export type MatrixReactionSummary = {
15
+ key: string;
16
+ count: number;
17
+ users: string[];
18
+ };
19
+
20
+ export type MatrixReactionAnnotation = {
21
+ key: string;
22
+ eventId?: string;
23
+ };
24
+
25
+ type MatrixReactionEventLike = {
26
+ content?: unknown;
27
+ sender?: string | null;
28
+ event_id?: string | null;
29
+ };
30
+
31
+ export function normalizeMatrixReactionMessageId(messageId: string): string {
32
+ const normalized = messageId.trim();
33
+ if (!normalized) {
34
+ throw new Error("Matrix reaction requires a messageId");
35
+ }
36
+ return normalized;
37
+ }
38
+
39
+ export function normalizeMatrixReactionEmoji(emoji: string): string {
40
+ const normalized = emoji.trim();
41
+ if (!normalized) {
42
+ throw new Error("Matrix reaction requires an emoji");
43
+ }
44
+ return normalized;
45
+ }
46
+
47
+ export function buildMatrixReactionContent(
48
+ messageId: string,
49
+ emoji: string,
50
+ ): MatrixReactionEventContent {
51
+ return {
52
+ "m.relates_to": {
53
+ rel_type: MATRIX_ANNOTATION_RELATION_TYPE,
54
+ event_id: normalizeMatrixReactionMessageId(messageId),
55
+ key: normalizeMatrixReactionEmoji(emoji),
56
+ },
57
+ };
58
+ }
59
+
60
+ export function buildMatrixReactionRelationsPath(roomId: string, messageId: string): string {
61
+ return `/_lobi/client/v1/rooms/${encodeURIComponent(roomId)}/relations/${encodeURIComponent(normalizeMatrixReactionMessageId(messageId))}/${MATRIX_ANNOTATION_RELATION_TYPE}/${MATRIX_REACTION_EVENT_TYPE}`;
62
+ }
63
+
64
+ export function extractMatrixReactionAnnotation(
65
+ content: unknown,
66
+ ): MatrixReactionAnnotation | undefined {
67
+ if (!content || typeof content !== "object") {
68
+ return undefined;
69
+ }
70
+ const relatesTo = (
71
+ content as {
72
+ "m.relates_to"?: {
73
+ rel_type?: unknown;
74
+ event_id?: unknown;
75
+ key?: unknown;
76
+ };
77
+ }
78
+ )["m.relates_to"];
79
+ if (!relatesTo || typeof relatesTo !== "object") {
80
+ return undefined;
81
+ }
82
+ if (
83
+ typeof relatesTo.rel_type === "string" &&
84
+ relatesTo.rel_type !== MATRIX_ANNOTATION_RELATION_TYPE
85
+ ) {
86
+ return undefined;
87
+ }
88
+ const key = normalizeOptionalString(relatesTo.key) ?? "";
89
+ if (!key) {
90
+ return undefined;
91
+ }
92
+ const eventId = normalizeOptionalString(relatesTo.event_id) ?? "";
93
+ return {
94
+ key,
95
+ eventId: eventId || undefined,
96
+ };
97
+ }
98
+
99
+ export function extractMatrixReactionKey(content: unknown): string | undefined {
100
+ return extractMatrixReactionAnnotation(content)?.key;
101
+ }
102
+
103
+ export function summarizeMatrixReactionEvents(
104
+ events: Iterable<Pick<MatrixReactionEventLike, "content" | "sender">>,
105
+ ): MatrixReactionSummary[] {
106
+ const summaries = new Map<string, MatrixReactionSummary>();
107
+ for (const event of events) {
108
+ const key = extractMatrixReactionKey(event.content);
109
+ if (!key) {
110
+ continue;
111
+ }
112
+ const sender = normalizeOptionalString(event.sender) ?? "";
113
+ const entry = summaries.get(key) ?? { key, count: 0, users: [] };
114
+ entry.count += 1;
115
+ if (sender && !entry.users.includes(sender)) {
116
+ entry.users.push(sender);
117
+ }
118
+ summaries.set(key, entry);
119
+ }
120
+ return Array.from(summaries.values());
121
+ }
122
+
123
+ export function selectOwnMatrixReactionEventIds(
124
+ events: Iterable<Pick<MatrixReactionEventLike, "content" | "event_id" | "sender">>,
125
+ userId: string,
126
+ emoji?: string,
127
+ ): string[] {
128
+ const senderId = normalizeOptionalString(userId) ?? "";
129
+ if (!senderId) {
130
+ return [];
131
+ }
132
+ const targetEmoji = normalizeOptionalString(emoji);
133
+ const ids: string[] = [];
134
+ for (const event of events) {
135
+ if ((normalizeOptionalString(event.sender) ?? "") !== senderId) {
136
+ continue;
137
+ }
138
+ if (targetEmoji && extractMatrixReactionKey(event.content) !== targetEmoji) {
139
+ continue;
140
+ }
141
+ const eventId = normalizeOptionalString(event.event_id);
142
+ if (eventId) {
143
+ ids.push(eventId);
144
+ }
145
+ }
146
+ return ids;
147
+ }