@gakr-gakr/matrix 0.1.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 (205) hide show
  1. package/CHANGELOG.md +285 -0
  2. package/SPEC-SUPPORT.md +116 -0
  3. package/api.ts +38 -0
  4. package/auth-presence.ts +56 -0
  5. package/autobot.plugin.json +28 -0
  6. package/channel-plugin-api.ts +3 -0
  7. package/cli-metadata.ts +11 -0
  8. package/contract-api.ts +17 -0
  9. package/doctor-contract-api.ts +1 -0
  10. package/helper-api.ts +3 -0
  11. package/index.ts +55 -0
  12. package/package.json +101 -0
  13. package/plugin-entry.handlers.runtime.ts +1 -0
  14. package/runtime-api.ts +72 -0
  15. package/runtime-heavy-api.ts +1 -0
  16. package/runtime-setter-api.ts +3 -0
  17. package/secret-contract-api.ts +5 -0
  18. package/setup-entry.ts +17 -0
  19. package/setup-plugin-api.ts +3 -0
  20. package/src/account-selection.ts +223 -0
  21. package/src/actions.ts +346 -0
  22. package/src/approval-auth.ts +25 -0
  23. package/src/approval-handler.runtime.ts +595 -0
  24. package/src/approval-ids.ts +6 -0
  25. package/src/approval-native.ts +348 -0
  26. package/src/approval-reaction-auth.ts +45 -0
  27. package/src/approval-reactions.ts +313 -0
  28. package/src/auth-precedence.ts +61 -0
  29. package/src/channel-account-paths.ts +97 -0
  30. package/src/channel.runtime.ts +17 -0
  31. package/src/channel.setup.ts +48 -0
  32. package/src/channel.ts +667 -0
  33. package/src/cli-metadata.ts +19 -0
  34. package/src/cli.ts +2298 -0
  35. package/src/config-adapter.ts +41 -0
  36. package/src/config-schema.ts +159 -0
  37. package/src/config-ui-hints.ts +56 -0
  38. package/src/directory-live.ts +238 -0
  39. package/src/doctor-contract.ts +287 -0
  40. package/src/doctor.ts +262 -0
  41. package/src/env-vars.ts +92 -0
  42. package/src/exec-approval-resolver.ts +23 -0
  43. package/src/exec-approvals.ts +293 -0
  44. package/src/group-mentions.ts +41 -0
  45. package/src/legacy-crypto-inspector-availability.ts +60 -0
  46. package/src/legacy-crypto.ts +531 -0
  47. package/src/legacy-state.ts +156 -0
  48. package/src/matrix/account-config.ts +175 -0
  49. package/src/matrix/accounts.ts +194 -0
  50. package/src/matrix/actions/client.ts +31 -0
  51. package/src/matrix/actions/devices.ts +34 -0
  52. package/src/matrix/actions/limits.ts +6 -0
  53. package/src/matrix/actions/messages.ts +129 -0
  54. package/src/matrix/actions/pins.ts +63 -0
  55. package/src/matrix/actions/polls.ts +109 -0
  56. package/src/matrix/actions/profile.ts +37 -0
  57. package/src/matrix/actions/reactions.ts +59 -0
  58. package/src/matrix/actions/room.ts +71 -0
  59. package/src/matrix/actions/summary.ts +88 -0
  60. package/src/matrix/actions/types.ts +63 -0
  61. package/src/matrix/actions/verification.ts +589 -0
  62. package/src/matrix/actions.ts +37 -0
  63. package/src/matrix/active-client.ts +26 -0
  64. package/src/matrix/async-lock.ts +18 -0
  65. package/src/matrix/backup-health.ts +124 -0
  66. package/src/matrix/client/config-runtime-api.ts +9 -0
  67. package/src/matrix/client/config-secret-input.runtime.ts +1 -0
  68. package/src/matrix/client/config.ts +853 -0
  69. package/src/matrix/client/create-client.ts +105 -0
  70. package/src/matrix/client/env-auth.ts +95 -0
  71. package/src/matrix/client/file-sync-store.ts +289 -0
  72. package/src/matrix/client/logging.ts +140 -0
  73. package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
  74. package/src/matrix/client/private-network-host.ts +1 -0
  75. package/src/matrix/client/runtime.ts +4 -0
  76. package/src/matrix/client/shared.ts +316 -0
  77. package/src/matrix/client/storage.ts +543 -0
  78. package/src/matrix/client/types.ts +50 -0
  79. package/src/matrix/client/url-validation.ts +76 -0
  80. package/src/matrix/client-bootstrap.ts +173 -0
  81. package/src/matrix/client.ts +23 -0
  82. package/src/matrix/config-paths.ts +31 -0
  83. package/src/matrix/config-update.ts +292 -0
  84. package/src/matrix/credentials-read.ts +207 -0
  85. package/src/matrix/credentials-write.runtime.ts +35 -0
  86. package/src/matrix/credentials.ts +95 -0
  87. package/src/matrix/deps.ts +309 -0
  88. package/src/matrix/device-health.ts +31 -0
  89. package/src/matrix/direct-management.ts +349 -0
  90. package/src/matrix/direct-room.ts +128 -0
  91. package/src/matrix/draft-stream.ts +225 -0
  92. package/src/matrix/encryption-guidance.ts +24 -0
  93. package/src/matrix/errors.ts +21 -0
  94. package/src/matrix/format.ts +426 -0
  95. package/src/matrix/legacy-crypto-inspector.ts +95 -0
  96. package/src/matrix/media-errors.ts +20 -0
  97. package/src/matrix/media-text.ts +162 -0
  98. package/src/matrix/monitor/access-state.ts +145 -0
  99. package/src/matrix/monitor/ack-config.ts +27 -0
  100. package/src/matrix/monitor/allowlist.ts +92 -0
  101. package/src/matrix/monitor/auto-join.ts +86 -0
  102. package/src/matrix/monitor/config.ts +569 -0
  103. package/src/matrix/monitor/context-summary.ts +43 -0
  104. package/src/matrix/monitor/direct.ts +296 -0
  105. package/src/matrix/monitor/events.ts +397 -0
  106. package/src/matrix/monitor/handler.ts +2271 -0
  107. package/src/matrix/monitor/inbound-dedupe.ts +267 -0
  108. package/src/matrix/monitor/index.ts +540 -0
  109. package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
  110. package/src/matrix/monitor/location.ts +108 -0
  111. package/src/matrix/monitor/media.ts +119 -0
  112. package/src/matrix/monitor/mentions.ts +256 -0
  113. package/src/matrix/monitor/reaction-events.ts +197 -0
  114. package/src/matrix/monitor/recent-invite.ts +30 -0
  115. package/src/matrix/monitor/replies.ts +136 -0
  116. package/src/matrix/monitor/reply-context.ts +92 -0
  117. package/src/matrix/monitor/room-history.ts +301 -0
  118. package/src/matrix/monitor/room-info.ts +126 -0
  119. package/src/matrix/monitor/rooms.ts +52 -0
  120. package/src/matrix/monitor/route.ts +179 -0
  121. package/src/matrix/monitor/runtime-api.ts +28 -0
  122. package/src/matrix/monitor/startup-verification.ts +237 -0
  123. package/src/matrix/monitor/startup.ts +218 -0
  124. package/src/matrix/monitor/status.ts +120 -0
  125. package/src/matrix/monitor/sync-lifecycle.ts +91 -0
  126. package/src/matrix/monitor/task-runner.ts +38 -0
  127. package/src/matrix/monitor/test-events.ts +21 -0
  128. package/src/matrix/monitor/thread-context.ts +108 -0
  129. package/src/matrix/monitor/threads.ts +85 -0
  130. package/src/matrix/monitor/types.ts +30 -0
  131. package/src/matrix/monitor/verification-events.ts +643 -0
  132. package/src/matrix/monitor/verification-utils.ts +46 -0
  133. package/src/matrix/outbound-media-runtime.ts +1 -0
  134. package/src/matrix/poll-summary.ts +110 -0
  135. package/src/matrix/poll-types.ts +429 -0
  136. package/src/matrix/probe.runtime.ts +4 -0
  137. package/src/matrix/probe.ts +97 -0
  138. package/src/matrix/profile.ts +184 -0
  139. package/src/matrix/reaction-common.ts +147 -0
  140. package/src/matrix/sdk/crypto-bootstrap.ts +438 -0
  141. package/src/matrix/sdk/crypto-facade.ts +242 -0
  142. package/src/matrix/sdk/crypto-node.runtime.ts +17 -0
  143. package/src/matrix/sdk/crypto-runtime.ts +14 -0
  144. package/src/matrix/sdk/decrypt-bridge.ts +410 -0
  145. package/src/matrix/sdk/event-helpers.ts +83 -0
  146. package/src/matrix/sdk/http-client.ts +87 -0
  147. package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
  148. package/src/matrix/sdk/idb-persistence.ts +286 -0
  149. package/src/matrix/sdk/logger.ts +108 -0
  150. package/src/matrix/sdk/read-response-with-limit.ts +19 -0
  151. package/src/matrix/sdk/recovery-key-store.ts +453 -0
  152. package/src/matrix/sdk/timeout-abort-signal.ts +1 -0
  153. package/src/matrix/sdk/transport-runtime-api.ts +18 -0
  154. package/src/matrix/sdk/transport.ts +352 -0
  155. package/src/matrix/sdk/types.ts +245 -0
  156. package/src/matrix/sdk/verification-manager.ts +795 -0
  157. package/src/matrix/sdk/verification-status.ts +23 -0
  158. package/src/matrix/sdk.ts +2152 -0
  159. package/src/matrix/send/client.ts +93 -0
  160. package/src/matrix/send/formatting.ts +189 -0
  161. package/src/matrix/send/media.ts +244 -0
  162. package/src/matrix/send/targets.ts +104 -0
  163. package/src/matrix/send/types.ts +131 -0
  164. package/src/matrix/send.ts +660 -0
  165. package/src/matrix/session-store-metadata.ts +108 -0
  166. package/src/matrix/startup-abort.ts +44 -0
  167. package/src/matrix/subagent-hooks.ts +308 -0
  168. package/src/matrix/sync-state.ts +27 -0
  169. package/src/matrix/target-ids.ts +79 -0
  170. package/src/matrix/thread-bindings-shared.ts +206 -0
  171. package/src/matrix/thread-bindings.ts +580 -0
  172. package/src/matrix-migration.runtime.ts +9 -0
  173. package/src/migration-config.ts +243 -0
  174. package/src/migration-snapshot-backup.ts +116 -0
  175. package/src/migration-snapshot.ts +53 -0
  176. package/src/onboarding.ts +775 -0
  177. package/src/outbound.ts +248 -0
  178. package/src/plugin-entry.runtime.js +115 -0
  179. package/src/plugin-entry.runtime.ts +70 -0
  180. package/src/profile-update.ts +71 -0
  181. package/src/record-shared.ts +3 -0
  182. package/src/resolve-targets.ts +175 -0
  183. package/src/resolver.runtime.ts +5 -0
  184. package/src/resolver.ts +21 -0
  185. package/src/runtime-api.ts +106 -0
  186. package/src/runtime.ts +13 -0
  187. package/src/secret-contract.ts +174 -0
  188. package/src/session-route.ts +126 -0
  189. package/src/setup-bootstrap.ts +102 -0
  190. package/src/setup-config.ts +222 -0
  191. package/src/setup-contract.ts +90 -0
  192. package/src/setup-core.ts +146 -0
  193. package/src/setup-dm-policy.ts +15 -0
  194. package/src/setup-surface.ts +4 -0
  195. package/src/startup-maintenance.ts +114 -0
  196. package/src/storage-paths.ts +92 -0
  197. package/src/thread-binding-api.ts +23 -0
  198. package/src/tool-actions.runtime.ts +1 -0
  199. package/src/tool-actions.ts +498 -0
  200. package/src/types.ts +257 -0
  201. package/subagent-hooks-api.ts +31 -0
  202. package/test-api.ts +21 -0
  203. package/thread-binding-api.ts +4 -0
  204. package/thread-bindings-runtime.ts +4 -0
  205. package/tsconfig.json +16 -0
@@ -0,0 +1,110 @@
1
+ import type { MatrixMessageSummary } from "./actions/types.js";
2
+ import {
3
+ buildPollResultsSummary,
4
+ formatPollAsText,
5
+ formatPollResultsAsText,
6
+ isPollEventType,
7
+ isPollStartType,
8
+ parsePollStartContent,
9
+ resolvePollReferenceEventId,
10
+ type PollStartContent,
11
+ } from "./poll-types.js";
12
+ import type { MatrixClient, MatrixRawEvent } from "./sdk.js";
13
+
14
+ export type MatrixPollSnapshot = {
15
+ pollEventId: string;
16
+ triggerEvent: MatrixRawEvent;
17
+ rootEvent: MatrixRawEvent;
18
+ text: string;
19
+ };
20
+
21
+ export function resolveMatrixPollRootEventId(
22
+ event: Pick<MatrixRawEvent, "event_id" | "type" | "content">,
23
+ ): string | null {
24
+ if (isPollStartType(event.type)) {
25
+ const eventId = event.event_id?.trim();
26
+ return eventId ? eventId : null;
27
+ }
28
+ return resolvePollReferenceEventId(event.content);
29
+ }
30
+
31
+ async function readAllPollRelations(
32
+ client: MatrixClient,
33
+ roomId: string,
34
+ pollEventId: string,
35
+ ): Promise<MatrixRawEvent[]> {
36
+ const relationEvents: MatrixRawEvent[] = [];
37
+ let nextBatch: string | undefined;
38
+ do {
39
+ const page = await client.getRelations(roomId, pollEventId, "m.reference", undefined, {
40
+ from: nextBatch,
41
+ });
42
+ relationEvents.push(...page.events);
43
+ nextBatch = page.nextBatch ?? undefined;
44
+ } while (nextBatch);
45
+ return relationEvents;
46
+ }
47
+
48
+ export async function fetchMatrixPollSnapshot(
49
+ client: MatrixClient,
50
+ roomId: string,
51
+ event: MatrixRawEvent,
52
+ ): Promise<MatrixPollSnapshot | null> {
53
+ if (!isPollEventType(event.type)) {
54
+ return null;
55
+ }
56
+
57
+ const pollEventId = resolveMatrixPollRootEventId(event);
58
+ if (!pollEventId) {
59
+ return null;
60
+ }
61
+
62
+ const rootEvent = isPollStartType(event.type)
63
+ ? event
64
+ : ((await client.getEvent(roomId, pollEventId)) as MatrixRawEvent);
65
+ if (!isPollStartType(rootEvent.type)) {
66
+ return null;
67
+ }
68
+
69
+ const pollStartContent = rootEvent.content as PollStartContent;
70
+ const pollSummary = parsePollStartContent(pollStartContent);
71
+ if (!pollSummary) {
72
+ return null;
73
+ }
74
+
75
+ const relationEvents = await readAllPollRelations(client, roomId, pollEventId);
76
+ const pollResults = buildPollResultsSummary({
77
+ pollEventId,
78
+ roomId,
79
+ sender: rootEvent.sender,
80
+ senderName: rootEvent.sender,
81
+ content: pollStartContent,
82
+ relationEvents,
83
+ });
84
+
85
+ return {
86
+ pollEventId,
87
+ triggerEvent: event,
88
+ rootEvent,
89
+ text: pollResults ? formatPollResultsAsText(pollResults) : formatPollAsText(pollSummary),
90
+ };
91
+ }
92
+
93
+ export async function fetchMatrixPollMessageSummary(
94
+ client: MatrixClient,
95
+ roomId: string,
96
+ event: MatrixRawEvent,
97
+ ): Promise<MatrixMessageSummary | null> {
98
+ const snapshot = await fetchMatrixPollSnapshot(client, roomId, event);
99
+ if (!snapshot) {
100
+ return null;
101
+ }
102
+
103
+ return {
104
+ eventId: snapshot.pollEventId,
105
+ sender: snapshot.rootEvent.sender,
106
+ body: snapshot.text,
107
+ msgtype: "m.text",
108
+ timestamp: snapshot.triggerEvent.origin_server_ts || snapshot.rootEvent.origin_server_ts,
109
+ };
110
+ }
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Matrix Poll Types (MSC3381)
3
+ *
4
+ * Defines types for Matrix poll events:
5
+ * - m.poll.start - Creates a new poll
6
+ * - m.poll.response - Records a vote
7
+ * - m.poll.end - Closes a poll
8
+ */
9
+
10
+ import { normalizePollInput, type PollInput } from "autobot/plugin-sdk/poll-runtime";
11
+ import { normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
12
+
13
+ export const M_POLL_START = "m.poll.start" as const;
14
+ const M_POLL_RESPONSE = "m.poll.response" as const;
15
+ const M_POLL_END = "m.poll.end" as const;
16
+
17
+ const ORG_POLL_START = "org.matrix.msc3381.poll.start" as const;
18
+ const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response" as const;
19
+ const ORG_POLL_END = "org.matrix.msc3381.poll.end" as const;
20
+
21
+ const POLL_EVENT_TYPES = [
22
+ M_POLL_START,
23
+ M_POLL_RESPONSE,
24
+ M_POLL_END,
25
+ ORG_POLL_START,
26
+ ORG_POLL_RESPONSE,
27
+ ORG_POLL_END,
28
+ ];
29
+
30
+ const POLL_START_TYPES = [M_POLL_START, ORG_POLL_START];
31
+ const POLL_RESPONSE_TYPES = [M_POLL_RESPONSE, ORG_POLL_RESPONSE];
32
+ const POLL_END_TYPES = [M_POLL_END, ORG_POLL_END];
33
+
34
+ type PollKind = "m.poll.disclosed" | "m.poll.undisclosed";
35
+
36
+ type TextContent = {
37
+ "m.text"?: string;
38
+ "org.matrix.msc1767.text"?: string;
39
+ body?: string;
40
+ };
41
+
42
+ type PollAnswer = {
43
+ id: string;
44
+ } & TextContent;
45
+
46
+ type PollParsedAnswer = {
47
+ id: string;
48
+ text: string;
49
+ };
50
+
51
+ type PollStartSubtype = {
52
+ question: TextContent;
53
+ kind?: PollKind;
54
+ max_selections?: number;
55
+ answers: PollAnswer[];
56
+ };
57
+
58
+ export type PollStartContent = {
59
+ [M_POLL_START]?: PollStartSubtype;
60
+ [ORG_POLL_START]?: PollStartSubtype;
61
+ "m.poll"?: PollStartSubtype;
62
+ "m.text"?: string;
63
+ "org.matrix.msc1767.text"?: string;
64
+ };
65
+
66
+ type PollSummary = {
67
+ eventId: string;
68
+ roomId: string;
69
+ sender: string;
70
+ senderName: string;
71
+ question: string;
72
+ answers: string[];
73
+ kind: PollKind;
74
+ maxSelections: number;
75
+ };
76
+
77
+ type PollResultsSummary = PollSummary & {
78
+ entries: Array<{
79
+ id: string;
80
+ text: string;
81
+ votes: number;
82
+ }>;
83
+ totalVotes: number;
84
+ closed: boolean;
85
+ };
86
+
87
+ type ParsedPollStart = {
88
+ question: string;
89
+ answers: PollParsedAnswer[];
90
+ kind: PollKind;
91
+ maxSelections: number;
92
+ };
93
+
94
+ type PollResponseSubtype = {
95
+ answers: string[];
96
+ };
97
+
98
+ type PollResponseContent = {
99
+ [M_POLL_RESPONSE]?: PollResponseSubtype;
100
+ [ORG_POLL_RESPONSE]?: PollResponseSubtype;
101
+ "m.relates_to": {
102
+ rel_type: "m.reference";
103
+ event_id: string;
104
+ };
105
+ };
106
+
107
+ export function isPollStartType(eventType: string): boolean {
108
+ return (POLL_START_TYPES as readonly string[]).includes(eventType);
109
+ }
110
+
111
+ function isPollResponseType(eventType: string): boolean {
112
+ return (POLL_RESPONSE_TYPES as readonly string[]).includes(eventType);
113
+ }
114
+
115
+ function isPollEndType(eventType: string): boolean {
116
+ return (POLL_END_TYPES as readonly string[]).includes(eventType);
117
+ }
118
+
119
+ export function isPollEventType(eventType: string): boolean {
120
+ return (POLL_EVENT_TYPES as readonly string[]).includes(eventType);
121
+ }
122
+
123
+ function getTextContent(text?: TextContent): string {
124
+ if (!text) {
125
+ return "";
126
+ }
127
+ return text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body ?? "";
128
+ }
129
+
130
+ export function parsePollStart(content: PollStartContent): ParsedPollStart | null {
131
+ const poll =
132
+ (content as Record<string, PollStartSubtype | undefined>)[M_POLL_START] ??
133
+ (content as Record<string, PollStartSubtype | undefined>)[ORG_POLL_START] ??
134
+ (content as Record<string, PollStartSubtype | undefined>)["m.poll"];
135
+ if (!poll) {
136
+ return null;
137
+ }
138
+
139
+ const question = getTextContent(poll.question).trim();
140
+ if (!question) {
141
+ return null;
142
+ }
143
+
144
+ const answers = poll.answers
145
+ .map((answer) => ({
146
+ id: answer.id,
147
+ text: getTextContent(answer).trim(),
148
+ }))
149
+ .filter((answer) => answer.id.trim().length > 0 && answer.text.length > 0);
150
+ if (answers.length === 0) {
151
+ return null;
152
+ }
153
+
154
+ const maxSelectionsRaw = poll.max_selections;
155
+ const maxSelections =
156
+ typeof maxSelectionsRaw === "number" && Number.isFinite(maxSelectionsRaw)
157
+ ? Math.floor(maxSelectionsRaw)
158
+ : 1;
159
+
160
+ return {
161
+ question,
162
+ answers,
163
+ kind: poll.kind ?? "m.poll.disclosed",
164
+ maxSelections: Math.min(Math.max(maxSelections, 1), answers.length),
165
+ };
166
+ }
167
+
168
+ export function parsePollStartContent(content: PollStartContent): PollSummary | null {
169
+ const parsed = parsePollStart(content);
170
+ if (!parsed) {
171
+ return null;
172
+ }
173
+
174
+ return {
175
+ eventId: "",
176
+ roomId: "",
177
+ sender: "",
178
+ senderName: "",
179
+ question: parsed.question,
180
+ answers: parsed.answers.map((answer) => answer.text),
181
+ kind: parsed.kind,
182
+ maxSelections: parsed.maxSelections,
183
+ };
184
+ }
185
+
186
+ export function formatPollAsText(summary: PollSummary): string {
187
+ const lines = [
188
+ "[Poll]",
189
+ summary.question,
190
+ "",
191
+ ...summary.answers.map((answer, idx) => `${idx + 1}. ${answer}`),
192
+ ];
193
+ return lines.join("\n");
194
+ }
195
+
196
+ export function resolvePollReferenceEventId(content: unknown): string | null {
197
+ if (!content || typeof content !== "object") {
198
+ return null;
199
+ }
200
+ const relates = (content as { "m.relates_to"?: { event_id?: unknown } })["m.relates_to"];
201
+ if (!relates || typeof relates.event_id !== "string") {
202
+ return null;
203
+ }
204
+ const eventId = relates.event_id.trim();
205
+ return eventId.length > 0 ? eventId : null;
206
+ }
207
+
208
+ export function parsePollResponseAnswerIds(content: unknown): string[] | null {
209
+ if (!content || typeof content !== "object") {
210
+ return null;
211
+ }
212
+ const response =
213
+ (content as Record<string, PollResponseSubtype | undefined>)[M_POLL_RESPONSE] ??
214
+ (content as Record<string, PollResponseSubtype | undefined>)[ORG_POLL_RESPONSE];
215
+ if (!response || !Array.isArray(response.answers)) {
216
+ return null;
217
+ }
218
+ return response.answers.filter((answer): answer is string => typeof answer === "string");
219
+ }
220
+
221
+ export function buildPollResultsSummary(params: {
222
+ pollEventId: string;
223
+ roomId: string;
224
+ sender: string;
225
+ senderName: string;
226
+ content: PollStartContent;
227
+ relationEvents: Array<{
228
+ event_id?: string;
229
+ sender?: string;
230
+ type?: string;
231
+ origin_server_ts?: number;
232
+ content?: Record<string, unknown>;
233
+ unsigned?: {
234
+ redacted_because?: unknown;
235
+ };
236
+ }>;
237
+ }): PollResultsSummary | null {
238
+ const parsed = parsePollStart(params.content);
239
+ if (!parsed) {
240
+ return null;
241
+ }
242
+
243
+ let pollClosedAt = Number.POSITIVE_INFINITY;
244
+ for (const event of params.relationEvents) {
245
+ if (event.unsigned?.redacted_because) {
246
+ continue;
247
+ }
248
+ if (!isPollEndType(typeof event.type === "string" ? event.type : "")) {
249
+ continue;
250
+ }
251
+ if (event.sender !== params.sender) {
252
+ continue;
253
+ }
254
+ const ts =
255
+ typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts)
256
+ ? event.origin_server_ts
257
+ : Number.POSITIVE_INFINITY;
258
+ if (ts < pollClosedAt) {
259
+ pollClosedAt = ts;
260
+ }
261
+ }
262
+
263
+ const answerIds = new Set(parsed.answers.map((answer) => answer.id));
264
+ const latestVoteBySender = new Map<
265
+ string,
266
+ {
267
+ ts: number;
268
+ eventId: string;
269
+ answerIds: string[];
270
+ }
271
+ >();
272
+
273
+ const orderedRelationEvents = [...params.relationEvents].toSorted((left, right) => {
274
+ const leftTs =
275
+ typeof left.origin_server_ts === "number" && Number.isFinite(left.origin_server_ts)
276
+ ? left.origin_server_ts
277
+ : Number.POSITIVE_INFINITY;
278
+ const rightTs =
279
+ typeof right.origin_server_ts === "number" && Number.isFinite(right.origin_server_ts)
280
+ ? right.origin_server_ts
281
+ : Number.POSITIVE_INFINITY;
282
+ if (leftTs !== rightTs) {
283
+ return leftTs - rightTs;
284
+ }
285
+ return (left.event_id ?? "").localeCompare(right.event_id ?? "");
286
+ });
287
+
288
+ for (const event of orderedRelationEvents) {
289
+ if (event.unsigned?.redacted_because) {
290
+ continue;
291
+ }
292
+ if (!isPollResponseType(typeof event.type === "string" ? event.type : "")) {
293
+ continue;
294
+ }
295
+ const senderId = normalizeOptionalString(event.sender) ?? "";
296
+ if (!senderId) {
297
+ continue;
298
+ }
299
+ const eventTs =
300
+ typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts)
301
+ ? event.origin_server_ts
302
+ : Number.POSITIVE_INFINITY;
303
+ if (eventTs > pollClosedAt) {
304
+ continue;
305
+ }
306
+ const rawAnswers = parsePollResponseAnswerIds(event.content) ?? [];
307
+ const normalizedAnswers = Array.from(
308
+ new Set(
309
+ rawAnswers
310
+ .map((answerId) => normalizeOptionalString(answerId) ?? "")
311
+ .filter((answerId) => answerIds.has(answerId))
312
+ .slice(0, parsed.maxSelections),
313
+ ),
314
+ );
315
+ latestVoteBySender.set(senderId, {
316
+ ts: eventTs,
317
+ eventId: typeof event.event_id === "string" ? event.event_id : "",
318
+ answerIds: normalizedAnswers,
319
+ });
320
+ }
321
+
322
+ const voteCounts = new Map<string, number>(
323
+ parsed.answers.map((answer): [string, number] => [answer.id, 0]),
324
+ );
325
+ let totalVotes = 0;
326
+ for (const latestVote of latestVoteBySender.values()) {
327
+ if (latestVote.answerIds.length === 0) {
328
+ continue;
329
+ }
330
+ totalVotes += 1;
331
+ for (const answerId of latestVote.answerIds) {
332
+ voteCounts.set(answerId, (voteCounts.get(answerId) ?? 0) + 1);
333
+ }
334
+ }
335
+
336
+ return {
337
+ eventId: params.pollEventId,
338
+ roomId: params.roomId,
339
+ sender: params.sender,
340
+ senderName: params.senderName,
341
+ question: parsed.question,
342
+ answers: parsed.answers.map((answer) => answer.text),
343
+ kind: parsed.kind,
344
+ maxSelections: parsed.maxSelections,
345
+ entries: parsed.answers.map((answer) => ({
346
+ id: answer.id,
347
+ text: answer.text,
348
+ votes: voteCounts.get(answer.id) ?? 0,
349
+ })),
350
+ totalVotes,
351
+ closed: Number.isFinite(pollClosedAt),
352
+ };
353
+ }
354
+
355
+ export function formatPollResultsAsText(summary: PollResultsSummary): string {
356
+ const lines = [summary.closed ? "[Poll closed]" : "[Poll]", summary.question, ""];
357
+ const revealResults = summary.kind === "m.poll.disclosed" || summary.closed;
358
+ for (const [index, entry] of summary.entries.entries()) {
359
+ if (!revealResults) {
360
+ lines.push(`${index + 1}. ${entry.text}`);
361
+ continue;
362
+ }
363
+ lines.push(`${index + 1}. ${entry.text} (${entry.votes} vote${entry.votes === 1 ? "" : "s"})`);
364
+ }
365
+ lines.push("");
366
+ if (!revealResults) {
367
+ lines.push("Responses are hidden until the poll closes.");
368
+ } else {
369
+ lines.push(`Total voters: ${summary.totalVotes}`);
370
+ }
371
+ return lines.join("\n");
372
+ }
373
+
374
+ function buildTextContent(body: string): TextContent {
375
+ return {
376
+ "m.text": body,
377
+ "org.matrix.msc1767.text": body,
378
+ };
379
+ }
380
+
381
+ function buildPollFallbackText(question: string, answers: string[]): string {
382
+ if (answers.length === 0) {
383
+ return question;
384
+ }
385
+ return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`;
386
+ }
387
+
388
+ export function buildPollStartContent(poll: PollInput): PollStartContent {
389
+ const normalized = normalizePollInput(poll);
390
+ const answers = normalized.options.map((option, idx) => ({
391
+ id: `answer${idx + 1}`,
392
+ ...buildTextContent(option),
393
+ }));
394
+
395
+ const isMultiple = normalized.maxSelections > 1;
396
+ const fallbackText = buildPollFallbackText(
397
+ normalized.question,
398
+ answers.map((answer) => getTextContent(answer)),
399
+ );
400
+
401
+ return {
402
+ [M_POLL_START]: {
403
+ question: buildTextContent(normalized.question),
404
+ kind: isMultiple ? "m.poll.undisclosed" : "m.poll.disclosed",
405
+ max_selections: normalized.maxSelections,
406
+ answers,
407
+ },
408
+ "m.text": fallbackText,
409
+ "org.matrix.msc1767.text": fallbackText,
410
+ };
411
+ }
412
+
413
+ export function buildPollResponseContent(
414
+ pollEventId: string,
415
+ answerIds: string[],
416
+ ): PollResponseContent {
417
+ return {
418
+ [M_POLL_RESPONSE]: {
419
+ answers: answerIds,
420
+ },
421
+ [ORG_POLL_RESPONSE]: {
422
+ answers: answerIds,
423
+ },
424
+ "m.relates_to": {
425
+ rel_type: "m.reference",
426
+ event_id: pollEventId,
427
+ },
428
+ };
429
+ }
@@ -0,0 +1,4 @@
1
+ import { createMatrixClient } from "./client.js";
2
+
3
+ // Keep probe's runtime seam narrow so tests can mock it without loading the full client barrel.
4
+ export { createMatrixClient };
@@ -0,0 +1,97 @@
1
+ import { formatErrorMessage } from "autobot/plugin-sdk/error-runtime";
2
+ import type { PinnedDispatcherPolicy } from "autobot/plugin-sdk/ssrf-dispatcher";
3
+ import { normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
4
+ import type { SsrFPolicy } from "../runtime-api.js";
5
+ import type { BaseProbeResult } from "../runtime-api.js";
6
+ import { isBunRuntime } from "./client/runtime.js";
7
+
8
+ type MatrixProbeRuntimeDeps = Pick<typeof import("./probe.runtime.js"), "createMatrixClient">;
9
+
10
+ let matrixProbeRuntimeDepsPromise: Promise<MatrixProbeRuntimeDeps> | undefined;
11
+
12
+ async function loadMatrixProbeRuntimeDeps(): Promise<MatrixProbeRuntimeDeps> {
13
+ matrixProbeRuntimeDepsPromise ??= import("./probe.runtime.js").then((runtimeModule) => ({
14
+ createMatrixClient: runtimeModule.createMatrixClient,
15
+ }));
16
+ return await matrixProbeRuntimeDepsPromise;
17
+ }
18
+
19
+ export type MatrixProbe = BaseProbeResult & {
20
+ status?: number | null;
21
+ elapsedMs: number;
22
+ userId?: string | null;
23
+ };
24
+
25
+ export async function probeMatrix(params: {
26
+ homeserver: string;
27
+ accessToken: string;
28
+ userId?: string;
29
+ deviceId?: string;
30
+ timeoutMs?: number;
31
+ accountId?: string | null;
32
+ allowPrivateNetwork?: boolean;
33
+ ssrfPolicy?: SsrFPolicy;
34
+ dispatcherPolicy?: PinnedDispatcherPolicy;
35
+ }): Promise<MatrixProbe> {
36
+ const started = Date.now();
37
+ const result: MatrixProbe = {
38
+ ok: false,
39
+ status: null,
40
+ error: null,
41
+ elapsedMs: 0,
42
+ };
43
+ if (isBunRuntime()) {
44
+ return {
45
+ ...result,
46
+ error: "Matrix probe requires Node (bun runtime not supported)",
47
+ elapsedMs: Date.now() - started,
48
+ };
49
+ }
50
+ if (!params.homeserver?.trim()) {
51
+ return {
52
+ ...result,
53
+ error: "missing homeserver",
54
+ elapsedMs: Date.now() - started,
55
+ };
56
+ }
57
+ if (!params.accessToken?.trim()) {
58
+ return {
59
+ ...result,
60
+ error: "missing access token",
61
+ elapsedMs: Date.now() - started,
62
+ };
63
+ }
64
+ try {
65
+ const { createMatrixClient } = await loadMatrixProbeRuntimeDeps();
66
+ const inputUserId = normalizeOptionalString(params.userId);
67
+ const client = await createMatrixClient({
68
+ homeserver: params.homeserver,
69
+ userId: inputUserId,
70
+ accessToken: params.accessToken,
71
+ deviceId: params.deviceId,
72
+ persistStorage: false,
73
+ localTimeoutMs: params.timeoutMs,
74
+ accountId: params.accountId,
75
+ allowPrivateNetwork: params.allowPrivateNetwork,
76
+ ssrfPolicy: params.ssrfPolicy,
77
+ dispatcherPolicy: params.dispatcherPolicy,
78
+ });
79
+ // The client wrapper resolves user ID via whoami when needed.
80
+ const userId = await client.getUserId();
81
+ result.ok = true;
82
+ result.userId = userId ?? null;
83
+
84
+ result.elapsedMs = Date.now() - started;
85
+ return result;
86
+ } catch (err) {
87
+ return {
88
+ ...result,
89
+ status:
90
+ typeof err === "object" && err && "statusCode" in err
91
+ ? Number((err as { statusCode?: number }).statusCode)
92
+ : result.status,
93
+ error: formatErrorMessage(err),
94
+ elapsedMs: Date.now() - started,
95
+ };
96
+ }
97
+ }