async-matrix 2.0.1-x86_64-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 (372) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +200 -0
  3. data/README.md +133 -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/discord/api/path_tree.rb +127 -0
  301. data/lib/async/discord/api.rb +151 -0
  302. data/lib/async/discord/client.rb +283 -0
  303. data/lib/async/discord/error.rb +84 -0
  304. data/lib/async/discord/gateway.rb +359 -0
  305. data/lib/async/discord.rb +15 -0
  306. data/lib/async/matrix/3.3/async_matrix_e2ee.so +0 -0
  307. data/lib/async/matrix/3.4/async_matrix_e2ee.so +0 -0
  308. data/lib/async/matrix/4.0/async_matrix_e2ee.so +0 -0
  309. data/lib/async/matrix/api/chain.rb +578 -0
  310. data/lib/async/matrix/api/concat.rb +101 -0
  311. data/lib/async/matrix/api/path_tree.rb +205 -0
  312. data/lib/async/matrix/api.rb +200 -0
  313. data/lib/async/matrix/application_service/bot.rb +232 -0
  314. data/lib/async/matrix/application_service/config/schema/analytics.json +21 -0
  315. data/lib/async/matrix/application_service/config/schema/appservice.json +82 -0
  316. data/lib/async/matrix/application_service/config/schema/backfill.json +91 -0
  317. data/lib/async/matrix/application_service/config/schema/bridge.json +209 -0
  318. data/lib/async/matrix/application_service/config/schema/config.json +61 -0
  319. data/lib/async/matrix/application_service/config/schema/database.json +38 -0
  320. data/lib/async/matrix/application_service/config/schema/direct_media.json +35 -0
  321. data/lib/async/matrix/application_service/config/schema/double_puppet.json +24 -0
  322. data/lib/async/matrix/application_service/config/schema/encryption.json +164 -0
  323. data/lib/async/matrix/application_service/config/schema/homeserver.json +58 -0
  324. data/lib/async/matrix/application_service/config/schema/logging.json +50 -0
  325. data/lib/async/matrix/application_service/config/schema/management_room_texts.json +25 -0
  326. data/lib/async/matrix/application_service/config/schema/matrix.json +45 -0
  327. data/lib/async/matrix/application_service/config/schema/permissions.json +54 -0
  328. data/lib/async/matrix/application_service/config/schema/provisioning.json +23 -0
  329. data/lib/async/matrix/application_service/config/schema/public_media.json +39 -0
  330. data/lib/async/matrix/application_service/config/schema/relay.json +43 -0
  331. data/lib/async/matrix/application_service/config/vivify.rb +109 -0
  332. data/lib/async/matrix/application_service/config.rb +225 -0
  333. data/lib/async/matrix/application_service/dispatcher.rb +185 -0
  334. data/lib/async/matrix/application_service/error_response.rb +37 -0
  335. data/lib/async/matrix/application_service/event.rb +285 -0
  336. data/lib/async/matrix/application_service/server.rb +430 -0
  337. data/lib/async/matrix/application_service/transaction.rb +66 -0
  338. data/lib/async/matrix/application_service/transaction_handler.rb +185 -0
  339. data/lib/async/matrix/application_service/transaction_store.rb +80 -0
  340. data/lib/async/matrix/bridge/discord/db/connection.rb +141 -0
  341. data/lib/async/matrix/bridge/discord/db/file.rb +118 -0
  342. data/lib/async/matrix/bridge/discord/db/guild.rb +120 -0
  343. data/lib/async/matrix/bridge/discord/db/message.rb +160 -0
  344. data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +14 -0
  345. data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +14 -0
  346. data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +23 -0
  347. data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +19 -0
  348. data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +20 -0
  349. data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +19 -0
  350. data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +18 -0
  351. data/lib/async/matrix/bridge/discord/db/portal.rb +150 -0
  352. data/lib/async/matrix/bridge/discord/db/puppet.rb +128 -0
  353. data/lib/async/matrix/bridge/discord/db/reaction.rb +165 -0
  354. data/lib/async/matrix/bridge/discord/db/schema.rb +18 -0
  355. data/lib/async/matrix/bridge/discord/db/user.rb +112 -0
  356. data/lib/async/matrix/bridge/discord/db.rb +138 -0
  357. data/lib/async/matrix/client.rb +934 -0
  358. data/lib/async/matrix/connection.rb +19 -0
  359. data/lib/async/matrix/double_puppet_client.rb +82 -0
  360. data/lib/async/matrix/e2ee.rb +92 -0
  361. data/lib/async/matrix/endpoint.rb +57 -0
  362. data/lib/async/matrix/error.rb +67 -0
  363. data/lib/async/matrix/media_client.rb +170 -0
  364. data/lib/async/matrix/notifier.rb +104 -0
  365. data/lib/async/matrix/schema/registry.rb +354 -0
  366. data/lib/async/matrix/schema/validation_error.rb +225 -0
  367. data/lib/async/matrix/schema.rb +170 -0
  368. data/lib/async/matrix/server.rb +18 -0
  369. data/lib/async/matrix/stream.rb +18 -0
  370. data/lib/async/matrix/version.rb +10 -0
  371. data/lib/async/matrix.rb +22 -0
  372. metadata +588 -0
@@ -0,0 +1,934 @@
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("GET", path, nil, max_retries: max_retries)
155
+ end
156
+
157
+ def put(path, body = {}, max_retries: nil)
158
+ request("PUT", path, body, max_retries: max_retries)
159
+ end
160
+
161
+ def post(path, body = {}, max_retries: nil)
162
+ request("POST", path, body, max_retries: max_retries)
163
+ end
164
+
165
+ def close
166
+ @internet&.close
167
+ @internet = nil
168
+ @media_client&.close
169
+ @media_client = nil
170
+ end
171
+
172
+ private
173
+
174
+ def internet
175
+ @internet ||= Async::HTTP::Internet.new
176
+ end
177
+
178
+ def request(method, path, body = nil, max_retries: nil)
179
+ url = "#{@base}#{path}"
180
+ json_body = body ? JSON.generate(body) : nil
181
+ effective_max_retries = max_retries || @max_retries
182
+
183
+ Console.debug(self) { "#{method} #{path}" }
184
+
185
+ attempt = 0
186
+ loop do
187
+ response = internet.call(method, url, @headers, json_body)
188
+ status = response.status
189
+
190
+ if (200..299).cover?(status)
191
+ payload = read_limited(response, @response_size_limit)
192
+ return payload && !payload.empty? ? JSON.parse(payload) : {}
193
+ end
194
+
195
+ attempt += 1
196
+
197
+ if attempt <= effective_max_retries && retryable_status?(status)
198
+ delay = compute_retry_delay(status, response, attempt)
199
+ Console.warn(self) {
200
+ "#{method} #{path} returned #{status}, retry #{attempt}/#{effective_max_retries} in #{delay.round(2)}s"
201
+ }
202
+ response.close if response.respond_to?(:close)
203
+ sleep(delay)
204
+ next
205
+ end
206
+
207
+ payload = read_limited(response, @error_response_size_limit)
208
+ parsed = ApplicationService::ErrorResponse.new(
209
+ begin; JSON.parse(payload); rescue; {} end
210
+ )
211
+ Console.error(self) { "Matrix API #{status}: #{parsed.errcode} — #{parsed.error}" }
212
+ raise HomeserverError.new(
213
+ parsed.errcode || "UNKNOWN",
214
+ parsed.error || payload.to_s[0..200],
215
+ status: status
216
+ )
217
+ end
218
+ end
219
+
220
+ # ── Response size limiting ──────────────────────────────────
221
+
222
+ # Read the response body with a size limit. Raises ResponseTooLargeError
223
+ # if the body exceeds the limit. Checks Content-Length first (fast path),
224
+ # then enforces during streaming read (safe path).
225
+ #
226
+ # @param response [Protocol::HTTP::Response] the HTTP response
227
+ # @param limit [Integer] maximum allowed body size in bytes
228
+ # @return [String, nil] the response body, or nil if empty
229
+ def read_limited(response, limit)
230
+ body = response.body
231
+ return nil unless body
232
+
233
+ # Fast path: reject immediately if Content-Length exceeds limit
234
+ if body.respond_to?(:length) && body.length && body.length > limit
235
+ body.close
236
+ raise ResponseTooLargeError.new(
237
+ "M_TOO_LARGE",
238
+ "Response Content-Length #{body.length} bytes exceeds limit of #{limit} bytes"
239
+ )
240
+ end
241
+
242
+ # Streaming read with enforcement
243
+ buffer = String.new(encoding: Encoding::BINARY)
244
+ body.each do |chunk|
245
+ buffer << chunk
246
+ if buffer.bytesize > limit
247
+ body.close
248
+ raise ResponseTooLargeError.new(
249
+ "M_TOO_LARGE",
250
+ "Response body exceeds limit of #{limit} bytes"
251
+ )
252
+ end
253
+ end
254
+ buffer.empty? ? nil : buffer
255
+ end
256
+
257
+ # ── Retry logic ─────────────────────────────────────────────
258
+
259
+ # Whether the given HTTP status code should trigger a retry.
260
+ # 429 is only retried if @ignore_rate_limit is false.
261
+ # 502/503/504 are always retried.
262
+ def retryable_status?(status)
263
+ if status == RATE_LIMIT_STATUS
264
+ !@ignore_rate_limit
265
+ else
266
+ GATEWAY_ERROR_STATUSES.include?(status)
267
+ end
268
+ end
269
+
270
+ # Compute the delay before the next retry attempt.
271
+ #
272
+ # For 429 (rate-limited): use the server's Retry-After header if present,
273
+ # falling back to exponential backoff. The value is capped but not jittered
274
+ # — the server is telling us exactly when to come back.
275
+ #
276
+ # For 502/503/504 (gateway errors): exponential backoff with full jitter.
277
+ # Full jitter means rand(0..calculated), which is the AWS-recommended
278
+ # approach to avoid thundering herd on shared homeservers.
279
+ def compute_retry_delay(status, response, attempt)
280
+ if status == RATE_LIMIT_STATUS
281
+ server_delay = parse_retry_after(response)
282
+ delay = server_delay || exponential_delay(attempt)
283
+ [delay, @max_retry_delay].min
284
+ else
285
+ calculated = exponential_delay(attempt)
286
+ rand(0.0..[calculated, @max_retry_delay].min)
287
+ end
288
+ end
289
+
290
+ # Base * 2^(attempt-1): 0.5, 1.0, 2.0, 4.0, ...
291
+ def exponential_delay(attempt)
292
+ @retry_base_delay * (2 ** (attempt - 1))
293
+ end
294
+
295
+ # Parse the Retry-After header. Supports both delta-seconds ("120")
296
+ # and HTTP-date ("Fri, 31 Dec 2026 23:59:59 GMT") formats per RFC 9110.
297
+ # Returns seconds to wait as a Float, or nil if absent/unparseable.
298
+ def parse_retry_after(response)
299
+ value = response.headers["retry-after"]
300
+ return nil unless value
301
+
302
+ value = value.strip
303
+ if value.match?(/\A\d+\z/)
304
+ value.to_f
305
+ else
306
+ # HTTP-date format
307
+ begin
308
+ target = Time.httpdate(value)
309
+ delay = target - Time.now
310
+ delay > 0 ? delay : 0.0
311
+ rescue ArgumentError
312
+ nil
313
+ end
314
+ end
315
+ end
316
+
317
+ def encode(value)
318
+ ERB::Util.url_encode(value)
319
+ end
320
+ end
321
+ end
322
+ end
323
+
324
+ __END__
325
+ describe "Async::Matrix::Client" do
326
+ def make_config
327
+ Async::Matrix::ApplicationService::Config.new({
328
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
329
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
330
+ })
331
+ end
332
+
333
+ it "sets authorization header from config" do
334
+ client = Async::Matrix::Client.new(make_config)
335
+ client.config.appservice.as_token.should == "test_token"
336
+ end
337
+
338
+ it "responds to messaging methods" do
339
+ client = Async::Matrix::Client.new(make_config)
340
+ client.should.respond_to :send_text
341
+ client.should.respond_to :send_html
342
+ client.should.respond_to :send_notice
343
+ end
344
+
345
+ it "responds to room action methods" do
346
+ client = Async::Matrix::Client.new(make_config)
347
+ client.should.respond_to :join_room
348
+ client.should.respond_to :leave_room
349
+ end
350
+
351
+ it "responds to profile and verification methods" do
352
+ client = Async::Matrix::Client.new(make_config)
353
+ client.should.respond_to :set_display_name
354
+ client.should.respond_to :whoami
355
+ end
356
+
357
+ it "responds to media methods" do
358
+ client = Async::Matrix::Client.new(make_config)
359
+ client.should.respond_to :media
360
+ client.should.respond_to :media_client
361
+ end
362
+
363
+ it "returns a MediaClient from media_client" do
364
+ client = Async::Matrix::Client.new(make_config)
365
+ client.media_client.should.be.kind_of Async::Matrix::MediaClient
366
+ end
367
+
368
+ it "returns a Gateway from media with media prefix" do
369
+ client = Async::Matrix::Client.new(make_config)
370
+ client.media.inspect.should.include "/_matrix/media/v3"
371
+ end
372
+
373
+ it "can be closed without error" do
374
+ client = Async::Matrix::Client.new(make_config)
375
+ client.media_client # force lazy init
376
+ lambda { client.close }.should.not.raise
377
+ end
378
+
379
+ it "has retry defaults" do
380
+ Async::Matrix::Client::DEFAULT_MAX_RETRIES.should == 3
381
+ Async::Matrix::Client::DEFAULT_RETRY_BASE.should == 0.5
382
+ Async::Matrix::Client::DEFAULT_MAX_RETRY_DELAY.should == 30
383
+ Async::Matrix::Client::RATE_LIMIT_STATUS.should == 429
384
+ Async::Matrix::Client::GATEWAY_ERROR_STATUSES.should == [502, 503, 504]
385
+ end
386
+
387
+ it "has response size limit defaults" do
388
+ Async::Matrix::Client::DEFAULT_RESPONSE_SIZE_LIMIT.should == 50 * 1024 * 1024
389
+ Async::Matrix::Client::DEFAULT_ERROR_RESPONSE_SIZE_LIMIT.should == 512 * 1024
390
+ end
391
+
392
+ it "accepts custom retry configuration" do
393
+ client = Async::Matrix::Client.new(make_config, max_retries: 5, retry_base_delay: 1.0, max_retry_delay: 60)
394
+ client.config.appservice.as_token.should == "test_token"
395
+ end
396
+
397
+ it "accepts max_retries: 0 to disable retries" do
398
+ client = Async::Matrix::Client.new(make_config, max_retries: 0)
399
+ client.config.appservice.as_token.should == "test_token"
400
+ end
401
+
402
+ it "accepts ignore_rate_limit option" do
403
+ client = Async::Matrix::Client.new(make_config, ignore_rate_limit: true)
404
+ client.config.appservice.as_token.should == "test_token"
405
+ end
406
+
407
+ it "accepts custom response size limits" do
408
+ client = Async::Matrix::Client.new(make_config, response_size_limit: 1024, error_response_size_limit: 256)
409
+ client.config.appservice.as_token.should == "test_token"
410
+ end
411
+ end
412
+
413
+ # ── Shared test infrastructure ─────────────────────────────────────
414
+ #
415
+ # FakeBody and FakeResponse simulate async-http responses for testing.
416
+ # FakeInternet replaces Async::HTTP::Internet to avoid network calls.
417
+
418
+ # Simulates a response body that supports .each (streaming), .length,
419
+ # and .close — matching the Protocol::HTTP::Body interface.
420
+ FakeBody = Struct.new(:data, :content_length, :closed) do
421
+ def initialize(data, content_length: nil)
422
+ super(data, content_length, false)
423
+ end
424
+
425
+ def each(&block)
426
+ data.each_char.each_slice(64) { |chars| block.call(chars.join) } if data
427
+ end
428
+
429
+ def length
430
+ content_length
431
+ end
432
+
433
+ def close
434
+ self.closed = true
435
+ end
436
+ end
437
+
438
+ # Minimal response stub with status, body, and optional headers.
439
+ FakeResponse = Struct.new(:status, :body, :header_hash) do
440
+ def headers
441
+ header_hash || {}
442
+ end
443
+
444
+ def close
445
+ body&.close
446
+ end
447
+ end
448
+
449
+ # A controllable replacement for Async::HTTP::Internet.
450
+ FakeInternet = Struct.new(:responses, :call_count) do
451
+ def initialize(responses)
452
+ super(responses, 0)
453
+ end
454
+
455
+ def call(method, url, headers, body_data)
456
+ resp = responses[call_count] || responses.last
457
+ self.call_count += 1
458
+ resp
459
+ end
460
+
461
+ def close; end
462
+ end
463
+
464
+ # ── Retry behaviour ───────────────────────────────────────────────
465
+
466
+ describe "Client retry logic" do
467
+ def make_config
468
+ Async::Matrix::ApplicationService::Config.new({
469
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
470
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
471
+ })
472
+ end
473
+
474
+ def make_client_with_responses(responses, max_retries: 3, retry_base_delay: 0.01,
475
+ max_retry_delay: 1.0, ignore_rate_limit: false,
476
+ response_size_limit: 50 * 1024 * 1024,
477
+ error_response_size_limit: 512 * 1024)
478
+ client = Async::Matrix::Client.new(
479
+ make_config,
480
+ max_retries: max_retries,
481
+ retry_base_delay: retry_base_delay,
482
+ max_retry_delay: max_retry_delay,
483
+ ignore_rate_limit: ignore_rate_limit,
484
+ response_size_limit: response_size_limit,
485
+ error_response_size_limit: error_response_size_limit
486
+ )
487
+ fake = FakeInternet.new(responses)
488
+ client.instance_variable_set(:@internet, fake)
489
+ client.define_singleton_method(:sleep) { |_n| }
490
+ [client, fake]
491
+ end
492
+
493
+ def ok_response(body_str = '{"ok":true}')
494
+ FakeResponse.new(200, FakeBody.new(body_str), {})
495
+ end
496
+
497
+ def error_response(status, body_str = '{"errcode":"M_UNKNOWN","error":"fail"}', headers = {})
498
+ FakeResponse.new(status, FakeBody.new(body_str), headers)
499
+ end
500
+
501
+ # ── Basic retry tests ──────────────────────────────────────────
502
+
503
+ it "returns immediately on 200" do
504
+ client, fake = make_client_with_responses([ok_response])
505
+ result = client.get("/_matrix/client/v3/account/whoami")
506
+ result["ok"].should == true
507
+ fake.call_count.should == 1
508
+ end
509
+
510
+ it "retries on 429 and succeeds" do
511
+ client, fake = make_client_with_responses([error_response(429), ok_response])
512
+ result = client.get("/_matrix/client/v3/account/whoami")
513
+ result["ok"].should == true
514
+ fake.call_count.should == 2
515
+ end
516
+
517
+ it "retries on 502 and succeeds" do
518
+ client, fake = make_client_with_responses([error_response(502, "Bad Gateway"), ok_response])
519
+ result = client.get("/_matrix/client/v3/account/whoami")
520
+ result["ok"].should == true
521
+ fake.call_count.should == 2
522
+ end
523
+
524
+ it "retries on 503 and succeeds" do
525
+ client, fake = make_client_with_responses([error_response(503), ok_response])
526
+ result = client.get("/_matrix/client/v3/account/whoami")
527
+ result["ok"].should == true
528
+ fake.call_count.should == 2
529
+ end
530
+
531
+ it "retries on 504 and succeeds" do
532
+ client, fake = make_client_with_responses([error_response(504), ok_response])
533
+ result = client.get("/_matrix/client/v3/account/whoami")
534
+ result["ok"].should == true
535
+ fake.call_count.should == 2
536
+ end
537
+
538
+ it "retries up to max_retries times then raises" do
539
+ client, fake = make_client_with_responses(
540
+ [error_response(502, "Bad Gateway")] * 4, max_retries: 3
541
+ )
542
+ lambda {
543
+ client.get("/_matrix/client/v3/account/whoami")
544
+ }.should.raise(Async::Matrix::HomeserverError)
545
+ fake.call_count.should == 4
546
+ end
547
+
548
+ it "does not retry on 400" do
549
+ client, fake = make_client_with_responses([error_response(400)])
550
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
551
+ fake.call_count.should == 1
552
+ end
553
+
554
+ it "does not retry on 403" do
555
+ client, fake = make_client_with_responses([error_response(403)])
556
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
557
+ fake.call_count.should == 1
558
+ end
559
+
560
+ it "does not retry on 404" do
561
+ client, fake = make_client_with_responses([error_response(404)])
562
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
563
+ fake.call_count.should == 1
564
+ end
565
+
566
+ it "does not retry on 500" do
567
+ client, fake = make_client_with_responses([error_response(500)])
568
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
569
+ fake.call_count.should == 1
570
+ end
571
+
572
+ it "does not retry when max_retries is 0" do
573
+ client, fake = make_client_with_responses([error_response(429), ok_response], max_retries: 0)
574
+ lambda { client.get("/_matrix/client/v3/account/whoami") }.should.raise(Async::Matrix::HomeserverError)
575
+ fake.call_count.should == 1
576
+ end
577
+
578
+ it "retries POST on 429" do
579
+ client, fake = make_client_with_responses([error_response(429), ok_response])
580
+ result = client.post("/_matrix/client/v3/createRoom", {name: "test"})
581
+ result["ok"].should == true
582
+ fake.call_count.should == 2
583
+ end
584
+
585
+ it "retries POST on 502" do
586
+ client, fake = make_client_with_responses([error_response(502, "Bad Gateway"), ok_response])
587
+ result = client.post("/_matrix/client/v3/createRoom", {name: "test"})
588
+ result["ok"].should == true
589
+ fake.call_count.should == 2
590
+ end
591
+
592
+ it "recovers after multiple retries" do
593
+ client, fake = make_client_with_responses([
594
+ error_response(503), error_response(503), error_response(503),
595
+ ok_response('{"recovered":true}')
596
+ ], max_retries: 3)
597
+ result = client.get("/_matrix/client/v3/account/whoami")
598
+ result["recovered"].should == true
599
+ fake.call_count.should == 4
600
+ end
601
+
602
+ it "raises HomeserverError with correct status after exhausting retries" do
603
+ client, _fake = make_client_with_responses([error_response(429)] * 4, max_retries: 3)
604
+ begin
605
+ client.get("/_matrix/client/v3/account/whoami")
606
+ raise "should have raised"
607
+ rescue Async::Matrix::HomeserverError => e
608
+ e.status.should == 429
609
+ e.errcode.should == "M_UNKNOWN"
610
+ end
611
+ end
612
+
613
+ # ── Retry-After header parsing ──────────────────────────────────
614
+
615
+ it "respects Retry-After header with delta-seconds" do
616
+ delays = []
617
+ client, _fake = make_client_with_responses([
618
+ error_response(429, '{"errcode":"M_LIMIT_EXCEEDED","error":"rate limited"}', {"retry-after" => "2"}),
619
+ ok_response
620
+ ], max_retry_delay: 30.0)
621
+ client.define_singleton_method(:sleep) { |n| delays << n }
622
+ client.get("/_matrix/client/v3/account/whoami")
623
+ delays.length.should == 1
624
+ delays.first.should == 2.0
625
+ end
626
+
627
+ it "caps Retry-After at max_retry_delay" do
628
+ delays = []
629
+ client, _fake = make_client_with_responses([
630
+ error_response(429, '{"errcode":"M_LIMIT_EXCEEDED","error":"rate limited"}', {"retry-after" => "9999"}),
631
+ ok_response
632
+ ], max_retry_delay: 5.0)
633
+ client.define_singleton_method(:sleep) { |n| delays << n }
634
+ client.get("/_matrix/client/v3/account/whoami")
635
+ delays.first.should == 5.0
636
+ end
637
+
638
+ it "falls back to exponential backoff when Retry-After is absent on 429" do
639
+ delays = []
640
+ client, _fake = make_client_with_responses([error_response(429), ok_response], retry_base_delay: 0.25)
641
+ client.define_singleton_method(:sleep) { |n| delays << n }
642
+ client.get("/_matrix/client/v3/account/whoami")
643
+ delays.length.should == 1
644
+ delays.first.should == 0.25
645
+ end
646
+
647
+ it "uses jittered backoff for 502/503/504 (delay within expected range)" do
648
+ delays = []
649
+ client, _fake = make_client_with_responses([
650
+ error_response(502, "Bad Gateway"), ok_response
651
+ ], retry_base_delay: 1.0, max_retry_delay: 10.0)
652
+ client.define_singleton_method(:sleep) { |n| delays << n }
653
+ client.get("/_matrix/client/v3/account/whoami")
654
+ delays.length.should == 1
655
+ (delays.first >= 0.0).should == true
656
+ (delays.first <= 1.0).should == true
657
+ end
658
+
659
+ # ── Unit tests for helpers ──────────────────────────────────────
660
+
661
+ it "exponential_delay doubles each attempt" do
662
+ client = Async::Matrix::Client.new(make_config, retry_base_delay: 0.5)
663
+ client.send(:exponential_delay, 1).should == 0.5
664
+ client.send(:exponential_delay, 2).should == 1.0
665
+ client.send(:exponential_delay, 3).should == 2.0
666
+ client.send(:exponential_delay, 4).should == 4.0
667
+ end
668
+
669
+ it "parse_retry_after returns nil when header is absent" do
670
+ client = Async::Matrix::Client.new(make_config)
671
+ resp = FakeResponse.new(429, nil, {})
672
+ client.send(:parse_retry_after, resp).should.be.nil
673
+ end
674
+
675
+ it "parse_retry_after parses integer seconds" do
676
+ client = Async::Matrix::Client.new(make_config)
677
+ resp = FakeResponse.new(429, nil, {"retry-after" => "120"})
678
+ client.send(:parse_retry_after, resp).should == 120.0
679
+ end
680
+
681
+ it "parse_retry_after returns nil for garbage" do
682
+ client = Async::Matrix::Client.new(make_config)
683
+ resp = FakeResponse.new(429, nil, {"retry-after" => "not-a-date"})
684
+ client.send(:parse_retry_after, resp).should.be.nil
685
+ end
686
+ end
687
+
688
+ # ── ignore_rate_limit ────────────────────────────────────────────
689
+
690
+ describe "Client ignore_rate_limit" do
691
+ def make_config
692
+ Async::Matrix::ApplicationService::Config.new({
693
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
694
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
695
+ })
696
+ end
697
+
698
+ def make_client_with_responses(responses, **opts)
699
+ client = Async::Matrix::Client.new(make_config, **opts)
700
+ fake = FakeInternet.new(responses)
701
+ client.instance_variable_set(:@internet, fake)
702
+ client.define_singleton_method(:sleep) { |_n| }
703
+ [client, fake]
704
+ end
705
+
706
+ def ok_response(body_str = '{"ok":true}')
707
+ FakeResponse.new(200, FakeBody.new(body_str), {})
708
+ end
709
+
710
+ def error_response(status, body_str = '{"errcode":"M_UNKNOWN","error":"fail"}', headers = {})
711
+ FakeResponse.new(status, FakeBody.new(body_str), headers)
712
+ end
713
+
714
+ it "does not retry 429 when ignore_rate_limit is true" do
715
+ client, fake = make_client_with_responses(
716
+ [error_response(429), ok_response],
717
+ ignore_rate_limit: true, max_retries: 3
718
+ )
719
+ lambda {
720
+ client.get("/_matrix/client/v3/account/whoami")
721
+ }.should.raise(Async::Matrix::HomeserverError)
722
+ fake.call_count.should == 1
723
+ end
724
+
725
+ it "still retries 502 when ignore_rate_limit is true" do
726
+ client, fake = make_client_with_responses(
727
+ [error_response(502, "Bad Gateway"), ok_response],
728
+ ignore_rate_limit: true, max_retries: 3, retry_base_delay: 0.01
729
+ )
730
+ result = client.get("/_matrix/client/v3/account/whoami")
731
+ result["ok"].should == true
732
+ fake.call_count.should == 2
733
+ end
734
+
735
+ it "still retries 503 when ignore_rate_limit is true" do
736
+ client, fake = make_client_with_responses(
737
+ [error_response(503), ok_response],
738
+ ignore_rate_limit: true, max_retries: 3, retry_base_delay: 0.01
739
+ )
740
+ result = client.get("/_matrix/client/v3/account/whoami")
741
+ result["ok"].should == true
742
+ fake.call_count.should == 2
743
+ end
744
+
745
+ it "retries 429 normally when ignore_rate_limit is false (default)" do
746
+ client, fake = make_client_with_responses(
747
+ [error_response(429), ok_response],
748
+ ignore_rate_limit: false, max_retries: 3, retry_base_delay: 0.01
749
+ )
750
+ result = client.get("/_matrix/client/v3/account/whoami")
751
+ result["ok"].should == true
752
+ fake.call_count.should == 2
753
+ end
754
+ end
755
+
756
+ # ── Per-request max_retries override ─────────────────────────────
757
+
758
+ describe "Client per-request max_retries" do
759
+ def make_config
760
+ Async::Matrix::ApplicationService::Config.new({
761
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
762
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
763
+ })
764
+ end
765
+
766
+ def make_client_with_responses(responses, **opts)
767
+ client = Async::Matrix::Client.new(make_config, **opts)
768
+ fake = FakeInternet.new(responses)
769
+ client.instance_variable_set(:@internet, fake)
770
+ client.define_singleton_method(:sleep) { |_n| }
771
+ [client, fake]
772
+ end
773
+
774
+ def ok_response(body_str = '{"ok":true}')
775
+ FakeResponse.new(200, FakeBody.new(body_str), {})
776
+ end
777
+
778
+ def error_response(status, body_str = '{"errcode":"M_UNKNOWN","error":"fail"}', headers = {})
779
+ FakeResponse.new(status, FakeBody.new(body_str), headers)
780
+ end
781
+
782
+ it "per-request max_retries: 0 disables retries for that call" do
783
+ client, fake = make_client_with_responses(
784
+ [error_response(429), ok_response],
785
+ max_retries: 3, retry_base_delay: 0.01
786
+ )
787
+ lambda {
788
+ client.get("/_matrix/client/v3/account/whoami", max_retries: 0)
789
+ }.should.raise(Async::Matrix::HomeserverError)
790
+ fake.call_count.should == 1
791
+ end
792
+
793
+ it "per-request max_retries: 1 allows exactly one retry" do
794
+ client, fake = make_client_with_responses(
795
+ [error_response(502, "Bad Gateway"), error_response(502, "Bad Gateway"), ok_response],
796
+ max_retries: 3, retry_base_delay: 0.01
797
+ )
798
+ lambda {
799
+ client.get("/_matrix/client/v3/account/whoami", max_retries: 1)
800
+ }.should.raise(Async::Matrix::HomeserverError)
801
+ fake.call_count.should == 2 # 1 initial + 1 retry
802
+ end
803
+
804
+ it "per-request max_retries: 1 succeeds on second attempt" do
805
+ client, fake = make_client_with_responses(
806
+ [error_response(502, "Bad Gateway"), ok_response],
807
+ max_retries: 3, retry_base_delay: 0.01
808
+ )
809
+ result = client.get("/_matrix/client/v3/account/whoami", max_retries: 1)
810
+ result["ok"].should == true
811
+ fake.call_count.should == 2
812
+ end
813
+
814
+ it "nil max_retries falls back to client default" do
815
+ client, fake = make_client_with_responses(
816
+ [error_response(503), error_response(503), ok_response],
817
+ max_retries: 2, retry_base_delay: 0.01
818
+ )
819
+ result = client.get("/_matrix/client/v3/account/whoami")
820
+ result["ok"].should == true
821
+ fake.call_count.should == 3
822
+ end
823
+
824
+ it "per-request max_retries works on post" do
825
+ client, fake = make_client_with_responses(
826
+ [error_response(429), ok_response],
827
+ max_retries: 3, retry_base_delay: 0.01
828
+ )
829
+ lambda {
830
+ client.post("/_matrix/client/v3/createRoom", {name: "test"}, max_retries: 0)
831
+ }.should.raise(Async::Matrix::HomeserverError)
832
+ fake.call_count.should == 1
833
+ end
834
+
835
+ it "per-request max_retries works on put" do
836
+ client, fake = make_client_with_responses(
837
+ [error_response(429), ok_response],
838
+ max_retries: 3, retry_base_delay: 0.01
839
+ )
840
+ lambda {
841
+ client.put("/_matrix/client/v3/some/path", {data: true}, max_retries: 0)
842
+ }.should.raise(Async::Matrix::HomeserverError)
843
+ fake.call_count.should == 1
844
+ end
845
+ end
846
+
847
+ # ── Response size limiting ───────────────────────────────────────
848
+
849
+ describe "Client response size limiting" do
850
+ def make_config
851
+ Async::Matrix::ApplicationService::Config.new({
852
+ "homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
853
+ "appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
854
+ })
855
+ end
856
+
857
+ def make_client_with_responses(responses, **opts)
858
+ client = Async::Matrix::Client.new(make_config, **opts)
859
+ fake = FakeInternet.new(responses)
860
+ client.instance_variable_set(:@internet, fake)
861
+ client.define_singleton_method(:sleep) { |_n| }
862
+ [client, fake]
863
+ end
864
+
865
+ it "accepts responses within the size limit" do
866
+ body = '{"ok":true}'
867
+ resp = FakeResponse.new(200, FakeBody.new(body), {})
868
+ client, _fake = make_client_with_responses([resp], response_size_limit: 1024)
869
+ result = client.get("/_matrix/client/v3/account/whoami")
870
+ result["ok"].should == true
871
+ end
872
+
873
+ it "raises ResponseTooLargeError when response body exceeds limit (streaming)" do
874
+ big_body = "x" * 200
875
+ resp = FakeResponse.new(200, FakeBody.new(big_body), {})
876
+ client, _fake = make_client_with_responses([resp], response_size_limit: 100)
877
+ lambda {
878
+ client.get("/_matrix/client/v3/account/whoami")
879
+ }.should.raise(Async::Matrix::ResponseTooLargeError)
880
+ end
881
+
882
+ it "raises ResponseTooLargeError via Content-Length pre-check" do
883
+ body = FakeBody.new("small", content_length: 999_999_999)
884
+ resp = FakeResponse.new(200, body, {})
885
+ client, _fake = make_client_with_responses([resp], response_size_limit: 1024)
886
+ lambda {
887
+ client.get("/_matrix/client/v3/account/whoami")
888
+ }.should.raise(Async::Matrix::ResponseTooLargeError)
889
+ end
890
+
891
+ it "closes the body on Content-Length rejection" do
892
+ body = FakeBody.new("small", content_length: 999_999_999)
893
+ resp = FakeResponse.new(200, body, {})
894
+ client, _fake = make_client_with_responses([resp], response_size_limit: 1024)
895
+ begin
896
+ client.get("/_matrix/client/v3/account/whoami")
897
+ rescue Async::Matrix::ResponseTooLargeError
898
+ end
899
+ body.closed.should == true
900
+ end
901
+
902
+ it "enforces error_response_size_limit on error responses" do
903
+ big_error = "e" * 2000
904
+ resp = FakeResponse.new(400, FakeBody.new(big_error), {})
905
+ client, _fake = make_client_with_responses([resp], error_response_size_limit: 100)
906
+ lambda {
907
+ client.get("/_matrix/client/v3/account/whoami")
908
+ }.should.raise(Async::Matrix::ResponseTooLargeError)
909
+ end
910
+
911
+ it "allows error responses within the error size limit" do
912
+ error_body = '{"errcode":"M_UNKNOWN","error":"fail"}'
913
+ resp = FakeResponse.new(400, FakeBody.new(error_body), {})
914
+ client, _fake = make_client_with_responses([resp], error_response_size_limit: 1024)
915
+ lambda {
916
+ client.get("/_matrix/client/v3/account/whoami")
917
+ }.should.raise(Async::Matrix::HomeserverError)
918
+ end
919
+
920
+ it "handles nil body gracefully" do
921
+ resp = FakeResponse.new(200, nil, {})
922
+ client, _fake = make_client_with_responses([resp], response_size_limit: 1024)
923
+ result = client.get("/_matrix/client/v3/account/whoami")
924
+ result.should == {}
925
+ end
926
+
927
+ it "read_limited returns nil for empty body" do
928
+ body = FakeBody.new("")
929
+ resp = FakeResponse.new(200, body, {})
930
+ client = Async::Matrix::Client.new(make_config)
931
+ result = client.send(:read_limited, resp, 1024)
932
+ result.should.be.nil
933
+ end
934
+ end