async-matrix 0.1.3 → 1.1.0

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