async-matrix 3.0.0-arm-linux-musl

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 (333) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +200 -0
  3. data/README.md +108 -0
  4. data/data/discord-api-spec/openapi.json +40404 -0
  5. data/data/matrix-spec/api/client-server/account-data.yaml +389 -0
  6. data/data/matrix-spec/api/client-server/account_deactivation.yaml +149 -0
  7. data/data/matrix-spec/api/client-server/admin.yaml +512 -0
  8. data/data/matrix-spec/api/client-server/administrative_contact.yaml +620 -0
  9. data/data/matrix-spec/api/client-server/appservice_ping.yaml +183 -0
  10. data/data/matrix-spec/api/client-server/appservice_room_directory.yaml +104 -0
  11. data/data/matrix-spec/api/client-server/authed-content-repo.yaml +590 -0
  12. data/data/matrix-spec/api/client-server/banning.yaml +177 -0
  13. data/data/matrix-spec/api/client-server/capabilities.yaml +235 -0
  14. data/data/matrix-spec/api/client-server/content-repo.yaml +887 -0
  15. data/data/matrix-spec/api/client-server/create_room.yaml +306 -0
  16. data/data/matrix-spec/api/client-server/cross_signing.yaml +300 -0
  17. data/data/matrix-spec/api/client-server/definitions/auth_data.yaml +34 -0
  18. data/data/matrix-spec/api/client-server/definitions/auth_response.yaml +62 -0
  19. data/data/matrix-spec/api/client-server/definitions/client_device.yaml +44 -0
  20. data/data/matrix-spec/api/client-server/definitions/client_event.yaml +49 -0
  21. data/data/matrix-spec/api/client-server/definitions/client_event_without_room_id.yaml +147 -0
  22. data/data/matrix-spec/api/client-server/definitions/cross_signing_key.yaml +57 -0
  23. data/data/matrix-spec/api/client-server/definitions/device_keys.yaml +70 -0
  24. data/data/matrix-spec/api/client-server/definitions/errors/error.yaml +26 -0
  25. data/data/matrix-spec/api/client-server/definitions/errors/rate_limited.yaml +34 -0
  26. data/data/matrix-spec/api/client-server/definitions/event_batch.yaml +22 -0
  27. data/data/matrix-spec/api/client-server/definitions/event_filter.yaml +55 -0
  28. data/data/matrix-spec/api/client-server/definitions/invite_3pid.yaml +45 -0
  29. data/data/matrix-spec/api/client-server/definitions/key_backup_auth_data.yaml +36 -0
  30. data/data/matrix-spec/api/client-server/definitions/key_backup_data.yaml +50 -0
  31. data/data/matrix-spec/api/client-server/definitions/key_backup_session_data.yaml +57 -0
  32. data/data/matrix-spec/api/client-server/definitions/m.login.terms_params.yaml +83 -0
  33. data/data/matrix-spec/api/client-server/definitions/m.mentions.yaml +33 -0
  34. data/data/matrix-spec/api/client-server/definitions/m.oauth_params.yaml +30 -0
  35. data/data/matrix-spec/api/client-server/definitions/m.relates_to.yaml +45 -0
  36. data/data/matrix-spec/api/client-server/definitions/megolm_export_session_data.yaml +39 -0
  37. data/data/matrix-spec/api/client-server/definitions/olm_payload.yaml +88 -0
  38. data/data/matrix-spec/api/client-server/definitions/one_time_keys.yaml +27 -0
  39. data/data/matrix-spec/api/client-server/definitions/openid_token.yaml +37 -0
  40. data/data/matrix-spec/api/client-server/definitions/protocol.yaml +44 -0
  41. data/data/matrix-spec/api/client-server/definitions/public_rooms_chunk.yaml +74 -0
  42. data/data/matrix-spec/api/client-server/definitions/public_rooms_response.yaml +59 -0
  43. data/data/matrix-spec/api/client-server/definitions/push_condition.yaml +53 -0
  44. data/data/matrix-spec/api/client-server/definitions/push_rule.yaml +53 -0
  45. data/data/matrix-spec/api/client-server/definitions/push_ruleset.yaml +206 -0
  46. data/data/matrix-spec/api/client-server/definitions/recent_emoji.yaml +29 -0
  47. data/data/matrix-spec/api/client-server/definitions/request_email_validation.yaml +36 -0
  48. data/data/matrix-spec/api/client-server/definitions/request_msisdn_validation.yaml +36 -0
  49. data/data/matrix-spec/api/client-server/definitions/request_token_response.yaml +46 -0
  50. data/data/matrix-spec/api/client-server/definitions/room_event_filter.yaml +60 -0
  51. data/data/matrix-spec/api/client-server/definitions/room_key_backup.yaml +37 -0
  52. data/data/matrix-spec/api/client-server/definitions/room_summary.yaml +44 -0
  53. data/data/matrix-spec/api/client-server/definitions/security.yaml +47 -0
  54. data/data/matrix-spec/api/client-server/definitions/sso_login_flow.yaml +98 -0
  55. data/data/matrix-spec/api/client-server/definitions/state_event_batch.yaml +21 -0
  56. data/data/matrix-spec/api/client-server/definitions/sync_filter.yaml +86 -0
  57. data/data/matrix-spec/api/client-server/definitions/tag.yaml +24 -0
  58. data/data/matrix-spec/api/client-server/definitions/third_party_signed.yaml +45 -0
  59. data/data/matrix-spec/api/client-server/definitions/timeline_batch.yaml +36 -0
  60. data/data/matrix-spec/api/client-server/definitions/user_identifier.yaml +27 -0
  61. data/data/matrix-spec/api/client-server/definitions/wellknown/full.yaml +38 -0
  62. data/data/matrix-spec/api/client-server/definitions/wellknown/homeserver.yaml +25 -0
  63. data/data/matrix-spec/api/client-server/definitions/wellknown/identity_server.yaml +25 -0
  64. data/data/matrix-spec/api/client-server/device_management.yaml +314 -0
  65. data/data/matrix-spec/api/client-server/directory.yaml +318 -0
  66. data/data/matrix-spec/api/client-server/event_context.yaml +161 -0
  67. data/data/matrix-spec/api/client-server/filter.yaml +224 -0
  68. data/data/matrix-spec/api/client-server/inviting.yaml +149 -0
  69. data/data/matrix-spec/api/client-server/joining.yaml +238 -0
  70. data/data/matrix-spec/api/client-server/key_backup.yaml +993 -0
  71. data/data/matrix-spec/api/client-server/keys.yaml +482 -0
  72. data/data/matrix-spec/api/client-server/kicking.yaml +110 -0
  73. data/data/matrix-spec/api/client-server/knocking.yaml +152 -0
  74. data/data/matrix-spec/api/client-server/leaving.yaml +163 -0
  75. data/data/matrix-spec/api/client-server/list_joined_rooms.yaml +68 -0
  76. data/data/matrix-spec/api/client-server/list_public_rooms.yaml +280 -0
  77. data/data/matrix-spec/api/client-server/login.yaml +321 -0
  78. data/data/matrix-spec/api/client-server/login_token.yaml +138 -0
  79. data/data/matrix-spec/api/client-server/logout.yaml +86 -0
  80. data/data/matrix-spec/api/client-server/message_pagination.yaml +194 -0
  81. data/data/matrix-spec/api/client-server/notifications.yaml +152 -0
  82. data/data/matrix-spec/api/client-server/oauth_server_metadata.yaml +238 -0
  83. data/data/matrix-spec/api/client-server/old_sync.yaml +379 -0
  84. data/data/matrix-spec/api/client-server/openid.yaml +98 -0
  85. data/data/matrix-spec/api/client-server/password_management.yaml +246 -0
  86. data/data/matrix-spec/api/client-server/peeking_events.yaml +122 -0
  87. data/data/matrix-spec/api/client-server/policy_server.yaml +82 -0
  88. data/data/matrix-spec/api/client-server/presence.yaml +169 -0
  89. data/data/matrix-spec/api/client-server/profile.yaml +366 -0
  90. data/data/matrix-spec/api/client-server/pusher.yaml +303 -0
  91. data/data/matrix-spec/api/client-server/pushrules.yaml +633 -0
  92. data/data/matrix-spec/api/client-server/read_markers.yaml +103 -0
  93. data/data/matrix-spec/api/client-server/receipts.yaml +139 -0
  94. data/data/matrix-spec/api/client-server/redaction.yaml +116 -0
  95. data/data/matrix-spec/api/client-server/refresh.yaml +119 -0
  96. data/data/matrix-spec/api/client-server/registration.yaml +488 -0
  97. data/data/matrix-spec/api/client-server/registration_tokens.yaml +93 -0
  98. data/data/matrix-spec/api/client-server/relations.yaml +382 -0
  99. data/data/matrix-spec/api/client-server/report_content.yaml +283 -0
  100. data/data/matrix-spec/api/client-server/room_event_by_timestamp.yaml +147 -0
  101. data/data/matrix-spec/api/client-server/room_initial_sync.yaml +188 -0
  102. data/data/matrix-spec/api/client-server/room_send.yaml +130 -0
  103. data/data/matrix-spec/api/client-server/room_state.yaml +159 -0
  104. data/data/matrix-spec/api/client-server/room_summary.yaml +138 -0
  105. data/data/matrix-spec/api/client-server/room_upgrades.yaml +130 -0
  106. data/data/matrix-spec/api/client-server/rooms.yaml +380 -0
  107. data/data/matrix-spec/api/client-server/search.yaml +385 -0
  108. data/data/matrix-spec/api/client-server/space_hierarchy.yaml +237 -0
  109. data/data/matrix-spec/api/client-server/sso_login_redirect.yaml +135 -0
  110. data/data/matrix-spec/api/client-server/support.yaml +142 -0
  111. data/data/matrix-spec/api/client-server/sync.yaml +692 -0
  112. data/data/matrix-spec/api/client-server/tags.yaml +183 -0
  113. data/data/matrix-spec/api/client-server/third_party_lookup.yaml +324 -0
  114. data/data/matrix-spec/api/client-server/third_party_membership.yaml +139 -0
  115. data/data/matrix-spec/api/client-server/threads_list.yaml +167 -0
  116. data/data/matrix-spec/api/client-server/to_device.yaml +104 -0
  117. data/data/matrix-spec/api/client-server/typing.yaml +103 -0
  118. data/data/matrix-spec/api/client-server/users.yaml +136 -0
  119. data/data/matrix-spec/api/client-server/versions.yaml +108 -0
  120. data/data/matrix-spec/api/client-server/voip.yaml +93 -0
  121. data/data/matrix-spec/api/client-server/wellknown.yaml +60 -0
  122. data/data/matrix-spec/api/client-server/whoami.yaml +121 -0
  123. data/data/matrix-spec/event-schemas/examples/core/event.json +6 -0
  124. data/data/matrix-spec/event-schemas/examples/core/room_edu.json +3 -0
  125. data/data/matrix-spec/event-schemas/examples/core/room_event.json +11 -0
  126. data/data/matrix-spec/event-schemas/examples/core/state_event.json +4 -0
  127. data/data/matrix-spec/event-schemas/examples/invite_room_state.json +18 -0
  128. data/data/matrix-spec/event-schemas/examples/knock_room_state.json +18 -0
  129. data/data/matrix-spec/event-schemas/examples/m.accepted_terms.yaml +10 -0
  130. data/data/matrix-spec/event-schemas/examples/m.call.answer.yaml +21 -0
  131. data/data/matrix-spec/event-schemas/examples/m.call.candidates.yaml +16 -0
  132. data/data/matrix-spec/event-schemas/examples/m.call.hangup.yaml +10 -0
  133. data/data/matrix-spec/event-schemas/examples/m.call.invite.yaml +22 -0
  134. data/data/matrix-spec/event-schemas/examples/m.call.negotiate.yaml +22 -0
  135. data/data/matrix-spec/event-schemas/examples/m.call.reject.yaml +9 -0
  136. data/data/matrix-spec/event-schemas/examples/m.call.sdp_stream_metadata_changed.yaml +16 -0
  137. data/data/matrix-spec/event-schemas/examples/m.call.select_answer.yaml +10 -0
  138. data/data/matrix-spec/event-schemas/examples/m.direct.yaml +10 -0
  139. data/data/matrix-spec/event-schemas/examples/m.dummy.yaml +4 -0
  140. data/data/matrix-spec/event-schemas/examples/m.forwarded_room_key.yaml +14 -0
  141. data/data/matrix-spec/event-schemas/examples/m.fully_read.yaml +7 -0
  142. data/data/matrix-spec/event-schemas/examples/m.identity_server.yaml +7 -0
  143. data/data/matrix-spec/event-schemas/examples/m.ignored_user_list.yaml +9 -0
  144. data/data/matrix-spec/event-schemas/examples/m.invite_permission_config.yaml +7 -0
  145. data/data/matrix-spec/event-schemas/examples/m.key.verification.accept.yaml +12 -0
  146. data/data/matrix-spec/event-schemas/examples/m.key.verification.cancel.yaml +8 -0
  147. data/data/matrix-spec/event-schemas/examples/m.key.verification.done.yaml +6 -0
  148. data/data/matrix-spec/event-schemas/examples/m.key.verification.key.yaml +7 -0
  149. data/data/matrix-spec/event-schemas/examples/m.key.verification.mac.yaml +10 -0
  150. data/data/matrix-spec/event-schemas/examples/m.key.verification.ready.yaml +10 -0
  151. data/data/matrix-spec/event-schemas/examples/m.key.verification.request.yaml +11 -0
  152. data/data/matrix-spec/event-schemas/examples/m.key.verification.start$m.sas.v1.yaml +12 -0
  153. data/data/matrix-spec/event-schemas/examples/m.key.verification.start.yaml +8 -0
  154. data/data/matrix-spec/event-schemas/examples/m.key_backup.yaml +7 -0
  155. data/data/matrix-spec/event-schemas/examples/m.marked_unread.yaml +7 -0
  156. data/data/matrix-spec/event-schemas/examples/m.policy.rule.room.yaml +10 -0
  157. data/data/matrix-spec/event-schemas/examples/m.policy.rule.server.yaml +10 -0
  158. data/data/matrix-spec/event-schemas/examples/m.policy.rule.user.yaml +10 -0
  159. data/data/matrix-spec/event-schemas/examples/m.presence.yaml +12 -0
  160. data/data/matrix-spec/event-schemas/examples/m.push_rules.yaml +177 -0
  161. data/data/matrix-spec/event-schemas/examples/m.reaction.yaml +11 -0
  162. data/data/matrix-spec/event-schemas/examples/m.receipt.yaml +18 -0
  163. data/data/matrix-spec/event-schemas/examples/m.recent_emoji.yaml +16 -0
  164. data/data/matrix-spec/event-schemas/examples/m.room.avatar.yaml +14 -0
  165. data/data/matrix-spec/event-schemas/examples/m.room.canonical_alias.yaml +12 -0
  166. data/data/matrix-spec/event-schemas/examples/m.room.create.yaml +13 -0
  167. data/data/matrix-spec/event-schemas/examples/m.room.encrypted$megolm.yaml +11 -0
  168. data/data/matrix-spec/event-schemas/examples/m.room.encrypted$olm.yaml +14 -0
  169. data/data/matrix-spec/event-schemas/examples/m.room.encryption.yaml +10 -0
  170. data/data/matrix-spec/event-schemas/examples/m.room.guest_access.yaml +8 -0
  171. data/data/matrix-spec/event-schemas/examples/m.room.history_visibility.yaml +8 -0
  172. data/data/matrix-spec/event-schemas/examples/m.room.join_rules$restricted.yaml +18 -0
  173. data/data/matrix-spec/event-schemas/examples/m.room.join_rules.yaml +8 -0
  174. data/data/matrix-spec/event-schemas/examples/m.room.member$invite_room_state.yaml +15 -0
  175. data/data/matrix-spec/event-schemas/examples/m.room.member$join_authorised_via_users_server.yaml +12 -0
  176. data/data/matrix-spec/event-schemas/examples/m.room.member$knock_room_state.yaml +15 -0
  177. data/data/matrix-spec/event-schemas/examples/m.room.member$third_party_invite.yaml +20 -0
  178. data/data/matrix-spec/event-schemas/examples/m.room.member.yaml +12 -0
  179. data/data/matrix-spec/event-schemas/examples/m.room.message$m.audio.yaml +14 -0
  180. data/data/matrix-spec/event-schemas/examples/m.room.message$m.emote.yaml +10 -0
  181. data/data/matrix-spec/event-schemas/examples/m.room.message$m.file.yaml +14 -0
  182. data/data/matrix-spec/event-schemas/examples/m.room.message$m.image.yaml +16 -0
  183. data/data/matrix-spec/event-schemas/examples/m.room.message$m.key.verification.request.yaml +19 -0
  184. data/data/matrix-spec/event-schemas/examples/m.room.message$m.location.yaml +18 -0
  185. data/data/matrix-spec/event-schemas/examples/m.room.message$m.notice.yaml +10 -0
  186. data/data/matrix-spec/event-schemas/examples/m.room.message$m.server_notice.yaml +11 -0
  187. data/data/matrix-spec/event-schemas/examples/m.room.message$m.text.yaml +10 -0
  188. data/data/matrix-spec/event-schemas/examples/m.room.message$m.video.yaml +23 -0
  189. data/data/matrix-spec/event-schemas/examples/m.room.name.yaml +8 -0
  190. data/data/matrix-spec/event-schemas/examples/m.room.pinned_events.yaml +8 -0
  191. data/data/matrix-spec/event-schemas/examples/m.room.policy.yaml +11 -0
  192. data/data/matrix-spec/event-schemas/examples/m.room.power_levels.yaml +24 -0
  193. data/data/matrix-spec/event-schemas/examples/m.room.redaction.yaml +8 -0
  194. data/data/matrix-spec/event-schemas/examples/m.room.server_acl.yaml +10 -0
  195. data/data/matrix-spec/event-schemas/examples/m.room.third_party_invite.yaml +14 -0
  196. data/data/matrix-spec/event-schemas/examples/m.room.tombstone.yaml +9 -0
  197. data/data/matrix-spec/event-schemas/examples/m.room.topic.yaml +16 -0
  198. data/data/matrix-spec/event-schemas/examples/m.room_key.withheld.yaml +12 -0
  199. data/data/matrix-spec/event-schemas/examples/m.room_key.yaml +10 -0
  200. data/data/matrix-spec/event-schemas/examples/m.room_key_request$cancel_request.yaml +8 -0
  201. data/data/matrix-spec/event-schemas/examples/m.room_key_request$request.yaml +14 -0
  202. data/data/matrix-spec/event-schemas/examples/m.secret.request.yaml +9 -0
  203. data/data/matrix-spec/event-schemas/examples/m.secret.send.yaml +7 -0
  204. data/data/matrix-spec/event-schemas/examples/m.space.child.yaml +10 -0
  205. data/data/matrix-spec/event-schemas/examples/m.space.parent.yaml +9 -0
  206. data/data/matrix-spec/event-schemas/examples/m.sticker.yaml +22 -0
  207. data/data/matrix-spec/event-schemas/examples/m.tag.yaml +9 -0
  208. data/data/matrix-spec/event-schemas/examples/m.typing.yaml +7 -0
  209. data/data/matrix-spec/event-schemas/moderation_policy_rule.yaml +32 -0
  210. data/data/matrix-spec/event-schemas/schema/components/m_text_content_block.yaml +28 -0
  211. data/data/matrix-spec/event-schemas/schema/components/sdp_stream_metadata.yaml +43 -0
  212. data/data/matrix-spec/event-schemas/schema/components/signed_third_party_invite.yaml +48 -0
  213. data/data/matrix-spec/event-schemas/schema/core-event-schema/call_event.yaml +27 -0
  214. data/data/matrix-spec/event-schemas/schema/core-event-schema/event.yaml +16 -0
  215. data/data/matrix-spec/event-schemas/schema/core-event-schema/msgtype_infos/avatar_info.yaml +30 -0
  216. data/data/matrix-spec/event-schemas/schema/core-event-schema/msgtype_infos/image_info.yaml +52 -0
  217. data/data/matrix-spec/event-schemas/schema/core-event-schema/msgtype_infos/thumbnail_info.yaml +22 -0
  218. data/data/matrix-spec/event-schemas/schema/core-event-schema/room_event.yaml +17 -0
  219. data/data/matrix-spec/event-schemas/schema/core-event-schema/state_event.yaml +8 -0
  220. data/data/matrix-spec/event-schemas/schema/core-event-schema/stripped_state.yaml +48 -0
  221. data/data/matrix-spec/event-schemas/schema/core-event-schema/sync_room_event.yaml +49 -0
  222. data/data/matrix-spec/event-schemas/schema/core-event-schema/sync_state_event.yaml +36 -0
  223. data/data/matrix-spec/event-schemas/schema/core-event-schema/unsigned_prop.yaml +23 -0
  224. data/data/matrix-spec/event-schemas/schema/m.accepted_terms.yaml +25 -0
  225. data/data/matrix-spec/event-schemas/schema/m.call.answer.yaml +44 -0
  226. data/data/matrix-spec/event-schemas/schema/m.call.candidates.yaml +52 -0
  227. data/data/matrix-spec/event-schemas/schema/m.call.hangup.yaml +57 -0
  228. data/data/matrix-spec/event-schemas/schema/m.call.invite.yaml +53 -0
  229. data/data/matrix-spec/event-schemas/schema/m.call.negotiate.yaml +78 -0
  230. data/data/matrix-spec/event-schemas/schema/m.call.reject.yaml +29 -0
  231. data/data/matrix-spec/event-schemas/schema/m.call.sdp_stream_metadata_changed.yaml +24 -0
  232. data/data/matrix-spec/event-schemas/schema/m.call.select_answer.yaml +29 -0
  233. data/data/matrix-spec/event-schemas/schema/m.direct.yaml +29 -0
  234. data/data/matrix-spec/event-schemas/schema/m.dummy.yaml +25 -0
  235. data/data/matrix-spec/event-schemas/schema/m.forwarded_room_key.yaml +70 -0
  236. data/data/matrix-spec/event-schemas/schema/m.fully_read.yaml +26 -0
  237. data/data/matrix-spec/event-schemas/schema/m.identity_server.yaml +28 -0
  238. data/data/matrix-spec/event-schemas/schema/m.ignored_user_list.yaml +30 -0
  239. data/data/matrix-spec/event-schemas/schema/m.invite_permission_config.yaml +21 -0
  240. data/data/matrix-spec/event-schemas/schema/m.key.verification.accept.yaml +63 -0
  241. data/data/matrix-spec/event-schemas/schema/m.key.verification.cancel.yaml +72 -0
  242. data/data/matrix-spec/event-schemas/schema/m.key.verification.done.yaml +24 -0
  243. data/data/matrix-spec/event-schemas/schema/m.key.verification.key.yaml +31 -0
  244. data/data/matrix-spec/event-schemas/schema/m.key.verification.m.relates_to.yaml +23 -0
  245. data/data/matrix-spec/event-schemas/schema/m.key.verification.mac.yaml +43 -0
  246. data/data/matrix-spec/event-schemas/schema/m.key.verification.ready.yaml +41 -0
  247. data/data/matrix-spec/event-schemas/schema/m.key.verification.request.yaml +47 -0
  248. data/data/matrix-spec/event-schemas/schema/m.key.verification.start$m.reciprocate.v1.yaml +45 -0
  249. data/data/matrix-spec/event-schemas/schema/m.key.verification.start$m.sas.v1.yaml +76 -0
  250. data/data/matrix-spec/event-schemas/schema/m.key.verification.start.yaml +46 -0
  251. data/data/matrix-spec/event-schemas/schema/m.key_backup.yaml +24 -0
  252. data/data/matrix-spec/event-schemas/schema/m.marked_unread.yaml +26 -0
  253. data/data/matrix-spec/event-schemas/schema/m.policy.rule.room.yaml +17 -0
  254. data/data/matrix-spec/event-schemas/schema/m.policy.rule.server.yaml +17 -0
  255. data/data/matrix-spec/event-schemas/schema/m.policy.rule.user.yaml +17 -0
  256. data/data/matrix-spec/event-schemas/schema/m.presence.yaml +50 -0
  257. data/data/matrix-spec/event-schemas/schema/m.push_rules.yaml +23 -0
  258. data/data/matrix-spec/event-schemas/schema/m.reaction.yaml +45 -0
  259. data/data/matrix-spec/event-schemas/schema/m.receipt.yaml +65 -0
  260. data/data/matrix-spec/event-schemas/schema/m.recent_emoji.yaml +29 -0
  261. data/data/matrix-spec/event-schemas/schema/m.room.avatar.yaml +29 -0
  262. data/data/matrix-spec/event-schemas/schema/m.room.canonical_alias.yaml +40 -0
  263. data/data/matrix-spec/event-schemas/schema/m.room.create.yaml +86 -0
  264. data/data/matrix-spec/event-schemas/schema/m.room.encrypted.yaml +91 -0
  265. data/data/matrix-spec/event-schemas/schema/m.room.encryption.yaml +38 -0
  266. data/data/matrix-spec/event-schemas/schema/m.room.guest_access.yaml +28 -0
  267. data/data/matrix-spec/event-schemas/schema/m.room.history_visibility.yaml +30 -0
  268. data/data/matrix-spec/event-schemas/schema/m.room.join_rules.yaml +78 -0
  269. data/data/matrix-spec/event-schemas/schema/m.room.member.yaml +174 -0
  270. data/data/matrix-spec/event-schemas/schema/m.room.message$m.audio.yaml +73 -0
  271. data/data/matrix-spec/event-schemas/schema/m.room.message$m.emote.yaml +36 -0
  272. data/data/matrix-spec/event-schemas/schema/m.room.message$m.file.yaml +85 -0
  273. data/data/matrix-spec/event-schemas/schema/m.room.message$m.image.yaml +63 -0
  274. data/data/matrix-spec/event-schemas/schema/m.room.message$m.key.verification.request.yaml +70 -0
  275. data/data/matrix-spec/event-schemas/schema/m.room.message$m.location.yaml +50 -0
  276. data/data/matrix-spec/event-schemas/schema/m.room.message$m.notice.yaml +36 -0
  277. data/data/matrix-spec/event-schemas/schema/m.room.message$m.server_notice.yaml +41 -0
  278. data/data/matrix-spec/event-schemas/schema/m.room.message$m.text.yaml +36 -0
  279. data/data/matrix-spec/event-schemas/schema/m.room.message$m.video.yaml +95 -0
  280. data/data/matrix-spec/event-schemas/schema/m.room.message.yaml +25 -0
  281. data/data/matrix-spec/event-schemas/schema/m.room.name.yaml +35 -0
  282. data/data/matrix-spec/event-schemas/schema/m.room.pinned_events.yaml +27 -0
  283. data/data/matrix-spec/event-schemas/schema/m.room.policy.yaml +41 -0
  284. data/data/matrix-spec/event-schemas/schema/m.room.power_levels.yaml +139 -0
  285. data/data/matrix-spec/event-schemas/schema/m.room.redaction.yaml +29 -0
  286. data/data/matrix-spec/event-schemas/schema/m.room.server_acl.yaml +88 -0
  287. data/data/matrix-spec/event-schemas/schema/m.room.third_party_invite.yaml +79 -0
  288. data/data/matrix-spec/event-schemas/schema/m.room.tombstone.yaml +29 -0
  289. data/data/matrix-spec/event-schemas/schema/m.room.topic.yaml +53 -0
  290. data/data/matrix-spec/event-schemas/schema/m.room_key.withheld.yaml +88 -0
  291. data/data/matrix-spec/event-schemas/schema/m.room_key.yaml +38 -0
  292. data/data/matrix-spec/event-schemas/schema/m.room_key_request.yaml +73 -0
  293. data/data/matrix-spec/event-schemas/schema/m.secret.request.yaml +42 -0
  294. data/data/matrix-spec/event-schemas/schema/m.secret.send.yaml +35 -0
  295. data/data/matrix-spec/event-schemas/schema/m.space.child.yaml +50 -0
  296. data/data/matrix-spec/event-schemas/schema/m.space.parent.yaml +36 -0
  297. data/data/matrix-spec/event-schemas/schema/m.sticker.yaml +36 -0
  298. data/data/matrix-spec/event-schemas/schema/m.tag.yaml +28 -0
  299. data/data/matrix-spec/event-schemas/schema/m.typing.yaml +29 -0
  300. data/lib/async/matrix/3.3/async_matrix_e2ee.so +0 -0
  301. data/lib/async/matrix/3.4/async_matrix_e2ee.so +0 -0
  302. data/lib/async/matrix/4.0/async_matrix_e2ee.so +0 -0
  303. data/lib/async/matrix/api/chain.rb +604 -0
  304. data/lib/async/matrix/api/concat.rb +107 -0
  305. data/lib/async/matrix/api/path_tree.rb +232 -0
  306. data/lib/async/matrix/api.rb +205 -0
  307. data/lib/async/matrix/auth_error.rb +20 -0
  308. data/lib/async/matrix/bad_json_error.rb +20 -0
  309. data/lib/async/matrix/client.rb +966 -0
  310. data/lib/async/matrix/config/vivify.rb +107 -0
  311. data/lib/async/matrix/config.rb +184 -0
  312. data/lib/async/matrix/connection.rb +19 -0
  313. data/lib/async/matrix/content.rb +106 -0
  314. data/lib/async/matrix/double_puppet_client.rb +82 -0
  315. data/lib/async/matrix/e2ee.rb +129 -0
  316. data/lib/async/matrix/endpoint.rb +57 -0
  317. data/lib/async/matrix/error.rb +40 -0
  318. data/lib/async/matrix/error_response.rb +37 -0
  319. data/lib/async/matrix/event.rb +197 -0
  320. data/lib/async/matrix/homeserver_error.rb +20 -0
  321. data/lib/async/matrix/invalid_endpoint_error.rb +20 -0
  322. data/lib/async/matrix/media_client.rb +181 -0
  323. data/lib/async/matrix/not_found_error.rb +20 -0
  324. data/lib/async/matrix/notifier.rb +108 -0
  325. data/lib/async/matrix/response_too_large_error.rb +20 -0
  326. data/lib/async/matrix/schema/registry.rb +381 -0
  327. data/lib/async/matrix/schema/validation_error.rb +232 -0
  328. data/lib/async/matrix/schema.rb +170 -0
  329. data/lib/async/matrix/server.rb +18 -0
  330. data/lib/async/matrix/stream.rb +18 -0
  331. data/lib/async/matrix/version.rb +10 -0
  332. data/lib/async/matrix.rb +15 -0
  333. metadata +494 -0
@@ -0,0 +1,966 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require "async/http/internet"
7
+ require "json"
8
+ require "erb"
9
+ require "console"
10
+ require "securerandom"
11
+ require "time"
12
+
13
+ module Async
14
+ module Matrix
15
+ # Async HTTP client for the Matrix Client-Server API.
16
+ #
17
+ # Every outbound request is authenticated with the appservice `as_token`.
18
+ # All methods are fiber-safe and run naturally inside Falcon's async reactor.
19
+ #
20
+ # client = Async::Matrix::Client.new(config)
21
+ # client.send_text("!room:example.com", "Hello world")
22
+ # client.join_room("!room:example.com")
23
+ #
24
+ class Client
25
+ CLIENT_PREFIX = "/_matrix/client/v3"
26
+
27
+ # Retry defaults
28
+ DEFAULT_MAX_RETRIES = 3 # max retry attempts (0 disables)
29
+ DEFAULT_RETRY_BASE = 0.5 # initial backoff in seconds
30
+ DEFAULT_MAX_RETRY_DELAY = 30 # cap on any single delay in seconds
31
+
32
+ # Status codes eligible for retry
33
+ RATE_LIMIT_STATUS = 429
34
+ GATEWAY_ERROR_STATUSES = [502, 503, 504].freeze
35
+
36
+ # Response size limits (bytes)
37
+ DEFAULT_RESPONSE_SIZE_LIMIT = 50 * 1024 * 1024 # 50 MiB for JSON API responses
38
+ DEFAULT_ERROR_RESPONSE_SIZE_LIMIT = 512 * 1024 # 512 KiB for error bodies
39
+
40
+ attr_reader :config
41
+
42
+ def initialize(config, max_retries: DEFAULT_MAX_RETRIES,
43
+ retry_base_delay: DEFAULT_RETRY_BASE,
44
+ max_retry_delay: DEFAULT_MAX_RETRY_DELAY,
45
+ ignore_rate_limit: false,
46
+ response_size_limit: DEFAULT_RESPONSE_SIZE_LIMIT,
47
+ error_response_size_limit: DEFAULT_ERROR_RESPONSE_SIZE_LIMIT)
48
+ @config = config
49
+ @base = config.homeserver.address
50
+ @max_retries = max_retries
51
+ @retry_base_delay = retry_base_delay
52
+ @max_retry_delay = max_retry_delay
53
+ @ignore_rate_limit = ignore_rate_limit
54
+ @response_size_limit = response_size_limit
55
+ @error_response_size_limit = error_response_size_limit
56
+ @headers = [
57
+ ["authorization", "Bearer #{config.appservice.as_token}"],
58
+ ["content-type", "application/json"],
59
+ ["user-agent", "AsyncMatrix/#{Async::Matrix::VERSION}"]
60
+ ]
61
+ end
62
+
63
+ # ── Messaging ──────────────────────────────────────────────
64
+
65
+ def send_text(room_id, text)
66
+ content = {msgtype: "m.text", body: text}
67
+ send_message_event(room_id, "m.room.message", content)
68
+ end
69
+
70
+ def send_html(room_id, html, plaintext = nil)
71
+ content = {
72
+ msgtype: "m.text",
73
+ body: plaintext || html.gsub(/<[^>]+>/, ""),
74
+ format: "org.matrix.custom.html",
75
+ formatted_body: html,
76
+ }
77
+ send_message_event(room_id, "m.room.message", content)
78
+ end
79
+
80
+ def send_notice(room_id, text)
81
+ content = {msgtype: "m.notice", body: text}
82
+ send_message_event(room_id, "m.room.message", content)
83
+ end
84
+
85
+ # ── Room actions ───────────────────────────────────────────
86
+
87
+ def join_room(room_id)
88
+ post("#{CLIENT_PREFIX}/join/#{encode(room_id)}")
89
+ end
90
+
91
+ def leave_room(room_id)
92
+ post("#{CLIENT_PREFIX}/rooms/#{encode(room_id)}/leave")
93
+ end
94
+
95
+ # ── Profile ────────────────────────────────────────────────
96
+
97
+ def set_display_name(name, user_id = nil)
98
+ uid = user_id || @config.bot_mxid
99
+ put(
100
+ "#{CLIENT_PREFIX}/profile/#{encode(uid)}/displayname",
101
+ {displayname: name},
102
+ )
103
+ end
104
+
105
+ # ── Verification ───────────────────────────────────────────
106
+
107
+ def whoami
108
+ get("#{CLIENT_PREFIX}/account/whoami")
109
+ end
110
+
111
+ # ── Full API (runtime-generated from OpenAPI schemas) ─────
112
+
113
+ # Returns a Gateway that provides method-chained access to every
114
+ # Matrix Client-Server API endpoint. Chains are validated against
115
+ # the official OpenAPI path tree and terminated by .get(), .post(),
116
+ # .put(), or .delete().
117
+ #
118
+ # client.api.account.whoami.get
119
+ # client.api.createRoom.post(name: "Pub")
120
+ # client.api.rooms("!room:ex.com").ban.post(user_id: "@bad:ex.com")
121
+ # client.api.rooms("!room:ex.com").messages.get(dir: "b", limit: 10)
122
+ #
123
+ def api
124
+ Api::Gateway.new(self)
125
+ end
126
+
127
+ # Returns a Gateway rooted at /_matrix/media/v3 for media operations.
128
+ # Binary routes (upload/download/thumbnail) are automatically detected
129
+ # by the Chain and dispatched to the MediaClient.
130
+ #
131
+ # client.media.upload.post(bytes, content_type: "image/png")
132
+ # client.media.download("example.com", "abc123").get
133
+ # client.media.thumbnail("example.com", "abc123").get(width: 64, height: 64)
134
+ #
135
+ def media
136
+ Api::Gateway.new(self, prefix: %w[_matrix media v3])
137
+ end
138
+
139
+ # Returns the binary media client used for upload/download operations.
140
+ # Lazily initialized, shares the same config as this client.
141
+ def media_client
142
+ @media_client ||= MediaClient.new(@config)
143
+ end
144
+
145
+ # ── Low-level HTTP ─────────────────────────────────────────
146
+
147
+ def send_message_event(room_id, event_type, content)
148
+ txn_id = SecureRandom.uuid
149
+ path = "#{CLIENT_PREFIX}/rooms/#{encode(room_id)}/send/#{encode(event_type)}/#{txn_id}"
150
+ put(path, content)
151
+ end
152
+
153
+ def get(path, max_retries: nil)
154
+ request(
155
+ "GET",
156
+ path,
157
+ nil,
158
+ max_retries: max_retries,
159
+ )
160
+ end
161
+
162
+ def put(path, body = {}, max_retries: nil)
163
+ request(
164
+ "PUT",
165
+ path,
166
+ body,
167
+ max_retries: max_retries,
168
+ )
169
+ end
170
+
171
+ def post(path, body = {}, max_retries: nil)
172
+ request(
173
+ "POST",
174
+ path,
175
+ body,
176
+ max_retries: max_retries,
177
+ )
178
+ end
179
+
180
+ def close
181
+ @internet&.close
182
+ @internet = nil
183
+ @media_client&.close
184
+ @media_client = nil
185
+ end
186
+
187
+ private
188
+
189
+ def internet
190
+ @internet ||= Async::HTTP::Internet.new
191
+ end
192
+
193
+ def request(method, path, body = nil, max_retries: nil)
194
+ url = "#{@base}#{path}"
195
+ if body
196
+ json_body = JSON.generate(body)
197
+ else
198
+ json_body = nil
199
+ end
200
+ effective_max_retries = max_retries || @max_retries
201
+
202
+ Console.debug(self) { "#{method} #{path}" }
203
+
204
+ attempt = 0
205
+ loop do
206
+ response = internet.call(
207
+ method,
208
+ url,
209
+ @headers,
210
+ json_body,
211
+ )
212
+ status = response.status
213
+
214
+ if (200..299).cover?(status)
215
+ payload = read_limited(response, @response_size_limit)
216
+
217
+ if payload && !payload.empty?
218
+ break JSON.parse(payload)
219
+ else
220
+ break {}
221
+ end
222
+ end
223
+
224
+ attempt += 1
225
+
226
+ if attempt <= effective_max_retries && retryable_status?(status)
227
+ delay = compute_retry_delay(status, response, attempt)
228
+ Console.warn(self) {
229
+ "#{method} #{path} returned #{status}, retry #{attempt}/#{effective_max_retries} in #{delay.round(2)}s"
230
+ }
231
+ if response.respond_to?(:close)
232
+ response.close
233
+ end
234
+ sleep(delay)
235
+ next
236
+ end
237
+
238
+ payload = read_limited(response, @error_response_size_limit)
239
+ parsed = ErrorResponse.new(
240
+ begin; JSON.parse(payload); rescue; {} end,
241
+ )
242
+ Console.error(self) { "Matrix API #{status}: #{parsed.errcode} — #{parsed.error}" }
243
+ raise HomeserverError.new(
244
+ parsed.errcode || "UNKNOWN",
245
+ parsed.error || payload.to_s[0..200],
246
+ status: status,
247
+ )
248
+ end
249
+ end
250
+
251
+ # ── Response size limiting ──────────────────────────────────
252
+
253
+ # Read the response body with a size limit. Raises ResponseTooLargeError
254
+ # if the body exceeds the limit. Checks Content-Length first (fast path),
255
+ # then enforces during streaming read (safe path).
256
+ #
257
+ # @param response [Protocol::HTTP::Response] the HTTP response
258
+ # @param limit [Integer] maximum allowed body size in bytes
259
+ # @return [String, nil] the response body, or nil if empty
260
+ def read_limited(response, limit)
261
+ body = response.body
262
+ if body
263
+ if body.respond_to?(:length) && body.length && body.length > limit
264
+ body.close
265
+ raise ResponseTooLargeError.new(
266
+ "M_TOO_LARGE",
267
+ "Response Content-Length #{body.length} bytes exceeds limit of #{limit} bytes",
268
+ )
269
+ end
270
+ buffer = String.new(encoding: Encoding::BINARY)
271
+ body.each do |chunk|
272
+ buffer << chunk
273
+ if buffer.bytesize > limit
274
+ body.close
275
+ raise ResponseTooLargeError.new(
276
+ "M_TOO_LARGE",
277
+ "Response body exceeds limit of #{limit} bytes",
278
+ )
279
+ end
280
+ end
281
+ buffer.empty? ? nil : buffer
282
+ else
283
+ nil
284
+ end
285
+ end
286
+
287
+ # ── Retry logic ─────────────────────────────────────────────
288
+
289
+ # Whether the given HTTP status code should trigger a retry.
290
+ # 429 is only retried if @ignore_rate_limit is false.
291
+ # 502/503/504 are always retried.
292
+ def retryable_status?(status)
293
+ if status == RATE_LIMIT_STATUS
294
+ !@ignore_rate_limit
295
+ else
296
+ GATEWAY_ERROR_STATUSES.include?(status)
297
+ end
298
+ end
299
+
300
+ # Compute the delay before the next retry attempt.
301
+ #
302
+ # For 429 (rate-limited): use the server's Retry-After header if present,
303
+ # falling back to exponential backoff. The value is capped but not jittered
304
+ # — the server is telling us exactly when to come back.
305
+ #
306
+ # For 502/503/504 (gateway errors): exponential backoff with full jitter.
307
+ # Full jitter means rand(0..calculated), which is the AWS-recommended
308
+ # approach to avoid thundering herd on shared homeservers.
309
+ def compute_retry_delay(status, response, attempt)
310
+ if status == RATE_LIMIT_STATUS
311
+ server_delay = parse_retry_after(response)
312
+ delay = server_delay || exponential_delay(attempt)
313
+ [delay, @max_retry_delay].min
314
+ else
315
+ calculated = exponential_delay(attempt)
316
+ rand(0.0..[calculated, @max_retry_delay].min)
317
+ end
318
+ end
319
+
320
+ # Base * 2^(attempt-1): 0.5, 1.0, 2.0, 4.0, ...
321
+ def exponential_delay(attempt)
322
+ @retry_base_delay * (2 ** (attempt - 1))
323
+ end
324
+
325
+ # Parse the Retry-After header. Supports both delta-seconds ("120")
326
+ # and HTTP-date ("Fri, 31 Dec 2026 23:59:59 GMT") formats per RFC 9110.
327
+ # Returns seconds to wait as a Float, or nil if absent/unparseable.
328
+ def parse_retry_after(response)
329
+ value = response.headers["retry-after"]
330
+ if value
331
+ value = value.strip
332
+ if value.match?(/\A\d+\z/)
333
+ value.to_f
334
+ else
335
+ # HTTP-date format
336
+ begin
337
+ target = Time.httpdate(value)
338
+ delay = target - Time.now
339
+ delay > 0 ? delay : 0.0
340
+ rescue ArgumentError
341
+ nil
342
+ end
343
+ end
344
+ else
345
+ nil
346
+ end
347
+ end
348
+
349
+ def encode(value)
350
+ ERB::Util.url_encode(value)
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ __END__
357
+ describe "Async::Matrix::Client" do
358
+ def make_config
359
+ Async::Matrix::Config.new({
360
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
361
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
362
+ })
363
+ end
364
+
365
+ it "sets authorization header from config" do
366
+ client = Async::Matrix::Client.new(make_config)
367
+ client.config.appservice.as_token.should == "test_token"
368
+ end
369
+
370
+ it "responds to messaging methods" do
371
+ client = Async::Matrix::Client.new(make_config)
372
+ client.should.respond_to :send_text
373
+ client.should.respond_to :send_html
374
+ client.should.respond_to :send_notice
375
+ end
376
+
377
+ it "responds to room action methods" do
378
+ client = Async::Matrix::Client.new(make_config)
379
+ client.should.respond_to :join_room
380
+ client.should.respond_to :leave_room
381
+ end
382
+
383
+ it "responds to profile and verification methods" do
384
+ client = Async::Matrix::Client.new(make_config)
385
+ client.should.respond_to :set_display_name
386
+ client.should.respond_to :whoami
387
+ end
388
+
389
+ it "responds to media methods" do
390
+ client = Async::Matrix::Client.new(make_config)
391
+ client.should.respond_to :media
392
+ client.should.respond_to :media_client
393
+ end
394
+
395
+ it "returns a MediaClient from media_client" do
396
+ client = Async::Matrix::Client.new(make_config)
397
+ client.media_client.should.be.kind_of Async::Matrix::MediaClient
398
+ end
399
+
400
+ it "returns a Gateway from media with media prefix" do
401
+ client = Async::Matrix::Client.new(make_config)
402
+ client.media.inspect.should.include "/_matrix/media/v3"
403
+ end
404
+
405
+ it "can be closed without error" do
406
+ client = Async::Matrix::Client.new(make_config)
407
+ client.media_client # force lazy init
408
+ lambda { client.close }.should.not.raise
409
+ end
410
+
411
+ it "has retry defaults" do
412
+ Async::Matrix::Client::DEFAULT_MAX_RETRIES.should == 3
413
+ Async::Matrix::Client::DEFAULT_RETRY_BASE.should == 0.5
414
+ Async::Matrix::Client::DEFAULT_MAX_RETRY_DELAY.should == 30
415
+ Async::Matrix::Client::RATE_LIMIT_STATUS.should == 429
416
+ Async::Matrix::Client::GATEWAY_ERROR_STATUSES.should == [502, 503, 504]
417
+ end
418
+
419
+ it "has response size limit defaults" do
420
+ Async::Matrix::Client::DEFAULT_RESPONSE_SIZE_LIMIT.should == 50 * 1024 * 1024
421
+ Async::Matrix::Client::DEFAULT_ERROR_RESPONSE_SIZE_LIMIT.should == 512 * 1024
422
+ end
423
+
424
+ it "accepts custom retry configuration" do
425
+ client = Async::Matrix::Client.new(make_config, max_retries: 5, retry_base_delay: 1.0, max_retry_delay: 60)
426
+ client.config.appservice.as_token.should == "test_token"
427
+ end
428
+
429
+ it "accepts max_retries: 0 to disable retries" do
430
+ client = Async::Matrix::Client.new(make_config, max_retries: 0)
431
+ client.config.appservice.as_token.should == "test_token"
432
+ end
433
+
434
+ it "accepts ignore_rate_limit option" do
435
+ client = Async::Matrix::Client.new(make_config, ignore_rate_limit: true)
436
+ client.config.appservice.as_token.should == "test_token"
437
+ end
438
+
439
+ it "accepts custom response size limits" do
440
+ client = Async::Matrix::Client.new(make_config, response_size_limit: 1024, error_response_size_limit: 256)
441
+ client.config.appservice.as_token.should == "test_token"
442
+ end
443
+ end
444
+
445
+ # ── Shared test infrastructure ─────────────────────────────────────
446
+ #
447
+ # FakeBody and FakeResponse simulate async-http responses for testing.
448
+ # FakeInternet replaces Async::HTTP::Internet to avoid network calls.
449
+
450
+ # Simulates a response body that supports .each (streaming), .length,
451
+ # and .close — matching the Protocol::HTTP::Body interface.
452
+ FakeBody = Struct.new(:data, :content_length, :closed) do
453
+ def initialize(data, content_length: nil)
454
+ super(data, content_length, false)
455
+ end
456
+
457
+ def each(&block)
458
+ data.each_char.each_slice(64) { |chars| block.call(chars.join) } if data
459
+ end
460
+
461
+ def length
462
+ content_length
463
+ end
464
+
465
+ def close
466
+ self.closed = true
467
+ end
468
+ end
469
+
470
+ # Minimal response stub with status, body, and optional headers.
471
+ FakeResponse = Struct.new(:status, :body, :header_hash) do
472
+ def headers
473
+ header_hash || {}
474
+ end
475
+
476
+ def close
477
+ body&.close
478
+ end
479
+ end
480
+
481
+ # A controllable replacement for Async::HTTP::Internet.
482
+ FakeInternet = Struct.new(:responses, :call_count) do
483
+ def initialize(responses)
484
+ super(responses, 0)
485
+ end
486
+
487
+ def call(method, url, headers, body_data)
488
+ resp = responses[call_count] || responses.last
489
+ self.call_count += 1
490
+ resp
491
+ end
492
+
493
+ def close; end
494
+ end
495
+
496
+ # ── Retry behaviour ───────────────────────────────────────────────
497
+
498
+ describe "Client retry logic" do
499
+ def make_config
500
+ Async::Matrix::Config.new({
501
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
502
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
503
+ })
504
+ end
505
+
506
+ def make_client_with_responses(responses, max_retries: 3, retry_base_delay: 0.01,
507
+ max_retry_delay: 1.0, ignore_rate_limit: false,
508
+ response_size_limit: 50 * 1024 * 1024,
509
+ error_response_size_limit: 512 * 1024)
510
+ client = Async::Matrix::Client.new(
511
+ make_config,
512
+ max_retries: max_retries,
513
+ retry_base_delay: retry_base_delay,
514
+ max_retry_delay: max_retry_delay,
515
+ ignore_rate_limit: ignore_rate_limit,
516
+ response_size_limit: response_size_limit,
517
+ error_response_size_limit: error_response_size_limit
518
+ )
519
+ fake = FakeInternet.new(responses)
520
+ client.instance_variable_set(:@internet, fake)
521
+ client.define_singleton_method(:sleep) { |_n| }
522
+ [client, fake]
523
+ end
524
+
525
+ def ok_response(body_str = '{"ok":true}')
526
+ FakeResponse.new(200, FakeBody.new(body_str), {})
527
+ end
528
+
529
+ def error_response(status, body_str = '{"errcode":"M_UNKNOWN","error":"fail"}', headers = {})
530
+ FakeResponse.new(status, FakeBody.new(body_str), headers)
531
+ end
532
+
533
+ # ── Basic retry tests ──────────────────────────────────────────
534
+
535
+ it "returns immediately on 200" do
536
+ client, fake = make_client_with_responses([ok_response])
537
+ result = client.get("/_matrix/client/v3/account/whoami")
538
+ result["ok"].should == true
539
+ fake.call_count.should == 1
540
+ end
541
+
542
+ it "retries on 429 and succeeds" do
543
+ client, fake = make_client_with_responses([error_response(429), ok_response])
544
+ result = client.get("/_matrix/client/v3/account/whoami")
545
+ result["ok"].should == true
546
+ fake.call_count.should == 2
547
+ end
548
+
549
+ it "retries on 502 and succeeds" do
550
+ client, fake = make_client_with_responses([error_response(502, "Bad Gateway"), ok_response])
551
+ result = client.get("/_matrix/client/v3/account/whoami")
552
+ result["ok"].should == true
553
+ fake.call_count.should == 2
554
+ end
555
+
556
+ it "retries on 503 and succeeds" do
557
+ client, fake = make_client_with_responses([error_response(503), ok_response])
558
+ result = client.get("/_matrix/client/v3/account/whoami")
559
+ result["ok"].should == true
560
+ fake.call_count.should == 2
561
+ end
562
+
563
+ it "retries on 504 and succeeds" do
564
+ client, fake = make_client_with_responses([error_response(504), ok_response])
565
+ result = client.get("/_matrix/client/v3/account/whoami")
566
+ result["ok"].should == true
567
+ fake.call_count.should == 2
568
+ end
569
+
570
+ it "retries up to max_retries times then raises" do
571
+ client, fake = make_client_with_responses(
572
+ [error_response(502, "Bad Gateway")] * 4, max_retries: 3
573
+ )
574
+ lambda {
575
+ client.get("/_matrix/client/v3/account/whoami")
576
+ }.should.raise(Async::Matrix::HomeserverError)
577
+ fake.call_count.should == 4
578
+ end
579
+
580
+ it "does not retry on 400" do
581
+ client, fake = make_client_with_responses([error_response(400)])
582
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
583
+ fake.call_count.should == 1
584
+ end
585
+
586
+ it "does not retry on 403" do
587
+ client, fake = make_client_with_responses([error_response(403)])
588
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
589
+ fake.call_count.should == 1
590
+ end
591
+
592
+ it "does not retry on 404" do
593
+ client, fake = make_client_with_responses([error_response(404)])
594
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
595
+ fake.call_count.should == 1
596
+ end
597
+
598
+ it "does not retry on 500" do
599
+ client, fake = make_client_with_responses([error_response(500)])
600
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
601
+ fake.call_count.should == 1
602
+ end
603
+
604
+ it "does not retry when max_retries is 0" do
605
+ client, fake = make_client_with_responses([error_response(429), ok_response], max_retries: 0)
606
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
607
+ fake.call_count.should == 1
608
+ end
609
+
610
+ it "retries POST on 429" do
611
+ client, fake = make_client_with_responses([error_response(429), ok_response])
612
+ result = client.post("/_matrix/client/v3/createRoom", {name: "test"})
613
+ result["ok"].should == true
614
+ fake.call_count.should == 2
615
+ end
616
+
617
+ it "retries POST on 502" do
618
+ client, fake = make_client_with_responses([error_response(502, "Bad Gateway"), ok_response])
619
+ result = client.post("/_matrix/client/v3/createRoom", {name: "test"})
620
+ result["ok"].should == true
621
+ fake.call_count.should == 2
622
+ end
623
+
624
+ it "recovers after multiple retries" do
625
+ client, fake = make_client_with_responses([
626
+ error_response(503), error_response(503), error_response(503),
627
+ ok_response('{"recovered":true}')
628
+ ], max_retries: 3)
629
+ result = client.get("/_matrix/client/v3/account/whoami")
630
+ result["recovered"].should == true
631
+ fake.call_count.should == 4
632
+ end
633
+
634
+ it "raises HomeserverError with correct status after exhausting retries" do
635
+ client, _fake = make_client_with_responses([error_response(429)] * 4, max_retries: 3)
636
+ begin
637
+ client.get("/_matrix/client/v3/account/whoami")
638
+ raise "should have raised"
639
+ rescue Async::Matrix::HomeserverError => e
640
+ e.status.should == 429
641
+ e.errcode.should == "M_UNKNOWN"
642
+ end
643
+ end
644
+
645
+ # ── Retry-After header parsing ──────────────────────────────────
646
+
647
+ it "respects Retry-After header with delta-seconds" do
648
+ delays = []
649
+ client, _fake = make_client_with_responses([
650
+ error_response(429, '{"errcode":"M_LIMIT_EXCEEDED","error":"rate limited"}', {"retry-after" => "2"}),
651
+ ok_response
652
+ ], max_retry_delay: 30.0)
653
+ client.define_singleton_method(:sleep) { |n| delays << n }
654
+ client.get("/_matrix/client/v3/account/whoami")
655
+ delays.length.should == 1
656
+ delays.first.should == 2.0
657
+ end
658
+
659
+ it "caps Retry-After at max_retry_delay" do
660
+ delays = []
661
+ client, _fake = make_client_with_responses([
662
+ error_response(429, '{"errcode":"M_LIMIT_EXCEEDED","error":"rate limited"}', {"retry-after" => "9999"}),
663
+ ok_response
664
+ ], max_retry_delay: 5.0)
665
+ client.define_singleton_method(:sleep) { |n| delays << n }
666
+ client.get("/_matrix/client/v3/account/whoami")
667
+ delays.first.should == 5.0
668
+ end
669
+
670
+ it "falls back to exponential backoff when Retry-After is absent on 429" do
671
+ delays = []
672
+ client, _fake = make_client_with_responses([error_response(429), ok_response], retry_base_delay: 0.25)
673
+ client.define_singleton_method(:sleep) { |n| delays << n }
674
+ client.get("/_matrix/client/v3/account/whoami")
675
+ delays.length.should == 1
676
+ delays.first.should == 0.25
677
+ end
678
+
679
+ it "uses jittered backoff for 502/503/504 (delay within expected range)" do
680
+ delays = []
681
+ client, _fake = make_client_with_responses([
682
+ error_response(502, "Bad Gateway"), ok_response
683
+ ], retry_base_delay: 1.0, max_retry_delay: 10.0)
684
+ client.define_singleton_method(:sleep) { |n| delays << n }
685
+ client.get("/_matrix/client/v3/account/whoami")
686
+ delays.length.should == 1
687
+ (delays.first >= 0.0).should == true
688
+ (delays.first <= 1.0).should == true
689
+ end
690
+
691
+ # ── Unit tests for helpers ──────────────────────────────────────
692
+
693
+ it "exponential_delay doubles each attempt" do
694
+ client = Async::Matrix::Client.new(make_config, retry_base_delay: 0.5)
695
+ client.send(:exponential_delay, 1).should == 0.5
696
+ client.send(:exponential_delay, 2).should == 1.0
697
+ client.send(:exponential_delay, 3).should == 2.0
698
+ client.send(:exponential_delay, 4).should == 4.0
699
+ end
700
+
701
+ it "parse_retry_after returns nil when header is absent" do
702
+ client = Async::Matrix::Client.new(make_config)
703
+ resp = FakeResponse.new(429, nil, {})
704
+ client.send(:parse_retry_after, resp).should.be.nil
705
+ end
706
+
707
+ it "parse_retry_after parses integer seconds" do
708
+ client = Async::Matrix::Client.new(make_config)
709
+ resp = FakeResponse.new(429, nil, {"retry-after" => "120"})
710
+ client.send(:parse_retry_after, resp).should == 120.0
711
+ end
712
+
713
+ it "parse_retry_after returns nil for garbage" do
714
+ client = Async::Matrix::Client.new(make_config)
715
+ resp = FakeResponse.new(429, nil, {"retry-after" => "not-a-date"})
716
+ client.send(:parse_retry_after, resp).should.be.nil
717
+ end
718
+ end
719
+
720
+ # ── ignore_rate_limit ────────────────────────────────────────────
721
+
722
+ describe "Client ignore_rate_limit" do
723
+ def make_config
724
+ Async::Matrix::Config.new({
725
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
726
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
727
+ })
728
+ end
729
+
730
+ def make_client_with_responses(responses, **opts)
731
+ client = Async::Matrix::Client.new(make_config, **opts)
732
+ fake = FakeInternet.new(responses)
733
+ client.instance_variable_set(:@internet, fake)
734
+ client.define_singleton_method(:sleep) { |_n| }
735
+ [client, fake]
736
+ end
737
+
738
+ def ok_response(body_str = '{"ok":true}')
739
+ FakeResponse.new(200, FakeBody.new(body_str), {})
740
+ end
741
+
742
+ def error_response(status, body_str = '{"errcode":"M_UNKNOWN","error":"fail"}', headers = {})
743
+ FakeResponse.new(status, FakeBody.new(body_str), headers)
744
+ end
745
+
746
+ it "does not retry 429 when ignore_rate_limit is true" do
747
+ client, fake = make_client_with_responses(
748
+ [error_response(429), ok_response],
749
+ ignore_rate_limit: true, max_retries: 3
750
+ )
751
+ lambda {
752
+ client.get("/_matrix/client/v3/account/whoami")
753
+ }.should.raise(Async::Matrix::HomeserverError)
754
+ fake.call_count.should == 1
755
+ end
756
+
757
+ it "still retries 502 when ignore_rate_limit is true" do
758
+ client, fake = make_client_with_responses(
759
+ [error_response(502, "Bad Gateway"), ok_response],
760
+ ignore_rate_limit: true, max_retries: 3, retry_base_delay: 0.01
761
+ )
762
+ result = client.get("/_matrix/client/v3/account/whoami")
763
+ result["ok"].should == true
764
+ fake.call_count.should == 2
765
+ end
766
+
767
+ it "still retries 503 when ignore_rate_limit is true" do
768
+ client, fake = make_client_with_responses(
769
+ [error_response(503), ok_response],
770
+ ignore_rate_limit: true, max_retries: 3, retry_base_delay: 0.01
771
+ )
772
+ result = client.get("/_matrix/client/v3/account/whoami")
773
+ result["ok"].should == true
774
+ fake.call_count.should == 2
775
+ end
776
+
777
+ it "retries 429 normally when ignore_rate_limit is false (default)" do
778
+ client, fake = make_client_with_responses(
779
+ [error_response(429), ok_response],
780
+ ignore_rate_limit: false, max_retries: 3, retry_base_delay: 0.01
781
+ )
782
+ result = client.get("/_matrix/client/v3/account/whoami")
783
+ result["ok"].should == true
784
+ fake.call_count.should == 2
785
+ end
786
+ end
787
+
788
+ # ── Per-request max_retries override ─────────────────────────────
789
+
790
+ describe "Client per-request max_retries" do
791
+ def make_config
792
+ Async::Matrix::Config.new({
793
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
794
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
795
+ })
796
+ end
797
+
798
+ def make_client_with_responses(responses, **opts)
799
+ client = Async::Matrix::Client.new(make_config, **opts)
800
+ fake = FakeInternet.new(responses)
801
+ client.instance_variable_set(:@internet, fake)
802
+ client.define_singleton_method(:sleep) { |_n| }
803
+ [client, fake]
804
+ end
805
+
806
+ def ok_response(body_str = '{"ok":true}')
807
+ FakeResponse.new(200, FakeBody.new(body_str), {})
808
+ end
809
+
810
+ def error_response(status, body_str = '{"errcode":"M_UNKNOWN","error":"fail"}', headers = {})
811
+ FakeResponse.new(status, FakeBody.new(body_str), headers)
812
+ end
813
+
814
+ it "per-request max_retries: 0 disables retries for that call" do
815
+ client, fake = make_client_with_responses(
816
+ [error_response(429), ok_response],
817
+ max_retries: 3, retry_base_delay: 0.01
818
+ )
819
+ lambda {
820
+ client.get("/_matrix/client/v3/account/whoami", max_retries: 0)
821
+ }.should.raise(Async::Matrix::HomeserverError)
822
+ fake.call_count.should == 1
823
+ end
824
+
825
+ it "per-request max_retries: 1 allows exactly one retry" do
826
+ client, fake = make_client_with_responses(
827
+ [error_response(502, "Bad Gateway"), error_response(502, "Bad Gateway"), ok_response],
828
+ max_retries: 3, retry_base_delay: 0.01
829
+ )
830
+ lambda {
831
+ client.get("/_matrix/client/v3/account/whoami", max_retries: 1)
832
+ }.should.raise(Async::Matrix::HomeserverError)
833
+ fake.call_count.should == 2 # 1 initial + 1 retry
834
+ end
835
+
836
+ it "per-request max_retries: 1 succeeds on second attempt" do
837
+ client, fake = make_client_with_responses(
838
+ [error_response(502, "Bad Gateway"), ok_response],
839
+ max_retries: 3, retry_base_delay: 0.01
840
+ )
841
+ result = client.get("/_matrix/client/v3/account/whoami", max_retries: 1)
842
+ result["ok"].should == true
843
+ fake.call_count.should == 2
844
+ end
845
+
846
+ it "nil max_retries falls back to client default" do
847
+ client, fake = make_client_with_responses(
848
+ [error_response(503), error_response(503), ok_response],
849
+ max_retries: 2, retry_base_delay: 0.01
850
+ )
851
+ result = client.get("/_matrix/client/v3/account/whoami")
852
+ result["ok"].should == true
853
+ fake.call_count.should == 3
854
+ end
855
+
856
+ it "per-request max_retries works on post" do
857
+ client, fake = make_client_with_responses(
858
+ [error_response(429), ok_response],
859
+ max_retries: 3, retry_base_delay: 0.01
860
+ )
861
+ lambda {
862
+ client.post("/_matrix/client/v3/createRoom", {name: "test"}, max_retries: 0)
863
+ }.should.raise(Async::Matrix::HomeserverError)
864
+ fake.call_count.should == 1
865
+ end
866
+
867
+ it "per-request max_retries works on put" do
868
+ client, fake = make_client_with_responses(
869
+ [error_response(429), ok_response],
870
+ max_retries: 3, retry_base_delay: 0.01
871
+ )
872
+ lambda {
873
+ client.put("/_matrix/client/v3/some/path", {data: true}, max_retries: 0)
874
+ }.should.raise(Async::Matrix::HomeserverError)
875
+ fake.call_count.should == 1
876
+ end
877
+ end
878
+
879
+ # ── Response size limiting ───────────────────────────────────────
880
+
881
+ describe "Client response size limiting" do
882
+ def make_config
883
+ Async::Matrix::Config.new({
884
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
885
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
886
+ })
887
+ end
888
+
889
+ def make_client_with_responses(responses, **opts)
890
+ client = Async::Matrix::Client.new(make_config, **opts)
891
+ fake = FakeInternet.new(responses)
892
+ client.instance_variable_set(:@internet, fake)
893
+ client.define_singleton_method(:sleep) { |_n| }
894
+ [client, fake]
895
+ end
896
+
897
+ it "accepts responses within the size limit" do
898
+ body = '{"ok":true}'
899
+ resp = FakeResponse.new(200, FakeBody.new(body), {})
900
+ client, _fake = make_client_with_responses([resp], response_size_limit: 1024)
901
+ result = client.get("/_matrix/client/v3/account/whoami")
902
+ result["ok"].should == true
903
+ end
904
+
905
+ it "raises ResponseTooLargeError when response body exceeds limit (streaming)" do
906
+ big_body = "x" * 200
907
+ resp = FakeResponse.new(200, FakeBody.new(big_body), {})
908
+ client, _fake = make_client_with_responses([resp], response_size_limit: 100)
909
+ lambda {
910
+ client.get("/_matrix/client/v3/account/whoami")
911
+ }.should.raise(Async::Matrix::ResponseTooLargeError)
912
+ end
913
+
914
+ it "raises ResponseTooLargeError via Content-Length pre-check" do
915
+ body = FakeBody.new("small", content_length: 999_999_999)
916
+ resp = FakeResponse.new(200, body, {})
917
+ client, _fake = make_client_with_responses([resp], response_size_limit: 1024)
918
+ lambda {
919
+ client.get("/_matrix/client/v3/account/whoami")
920
+ }.should.raise(Async::Matrix::ResponseTooLargeError)
921
+ end
922
+
923
+ it "closes the body on Content-Length rejection" do
924
+ body = FakeBody.new("small", content_length: 999_999_999)
925
+ resp = FakeResponse.new(200, body, {})
926
+ client, _fake = make_client_with_responses([resp], response_size_limit: 1024)
927
+ begin
928
+ client.get("/_matrix/client/v3/account/whoami")
929
+ rescue Async::Matrix::ResponseTooLargeError
930
+ end
931
+ body.closed.should == true
932
+ end
933
+
934
+ it "enforces error_response_size_limit on error responses" do
935
+ big_error = "e" * 2000
936
+ resp = FakeResponse.new(400, FakeBody.new(big_error), {})
937
+ client, _fake = make_client_with_responses([resp], error_response_size_limit: 100)
938
+ lambda {
939
+ client.get("/_matrix/client/v3/account/whoami")
940
+ }.should.raise(Async::Matrix::ResponseTooLargeError)
941
+ end
942
+
943
+ it "allows error responses within the error size limit" do
944
+ error_body = '{"errcode":"M_UNKNOWN","error":"fail"}'
945
+ resp = FakeResponse.new(400, FakeBody.new(error_body), {})
946
+ client, _fake = make_client_with_responses([resp], error_response_size_limit: 1024)
947
+ lambda {
948
+ client.get("/_matrix/client/v3/account/whoami")
949
+ }.should.raise(Async::Matrix::HomeserverError)
950
+ end
951
+
952
+ it "handles nil body gracefully" do
953
+ resp = FakeResponse.new(200, nil, {})
954
+ client, _fake = make_client_with_responses([resp], response_size_limit: 1024)
955
+ result = client.get("/_matrix/client/v3/account/whoami")
956
+ result.should == {}
957
+ end
958
+
959
+ it "read_limited returns nil for empty body" do
960
+ body = FakeBody.new("")
961
+ resp = FakeResponse.new(200, body, {})
962
+ client = Async::Matrix::Client.new(make_config)
963
+ result = client.send(:read_limited, resp, 1024)
964
+ result.should.be.nil
965
+ end
966
+ end