onyxcord 2.1.0 → 3.2.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 (270) hide show
  1. checksums.yaml +4 -4
  2. data/lib/onyxcord/application_commands/command.rb +4 -97
  3. data/lib/onyxcord/application_commands/context.rb +4 -94
  4. data/lib/onyxcord/application_commands/option.rb +4 -75
  5. data/lib/onyxcord/application_commands/registry.rb +4 -45
  6. data/lib/onyxcord/application_commands.rb +8 -5
  7. data/lib/onyxcord/bot.rb +24 -1759
  8. data/lib/onyxcord/{cache.rb → cache/manager.rb} +16 -14
  9. data/lib/onyxcord/cache/stores/gateway.rb +272 -0
  10. data/lib/onyxcord/commands/bot/channels.rb +48 -0
  11. data/lib/onyxcord/commands/bot/execution.rb +174 -0
  12. data/lib/onyxcord/commands/bot/message_dispatch.rb +58 -0
  13. data/lib/onyxcord/commands/bot/permissions.rb +72 -0
  14. data/lib/onyxcord/commands/bot.rb +209 -0
  15. data/lib/onyxcord/commands/container.rb +21 -2
  16. data/lib/onyxcord/commands/parser.rb +30 -1
  17. data/lib/onyxcord/container.rb +16 -960
  18. data/lib/onyxcord/core/bot/application_commands.rb +210 -0
  19. data/lib/onyxcord/core/bot/awaits.rb +112 -0
  20. data/lib/onyxcord/core/bot/invites.rb +42 -0
  21. data/lib/onyxcord/core/bot/messaging.rb +161 -0
  22. data/lib/onyxcord/core/bot/oauth.rb +27 -0
  23. data/lib/onyxcord/core/bot/presence.rb +103 -0
  24. data/lib/onyxcord/core/bot/runtime.rb +183 -0
  25. data/lib/onyxcord/core/bot/voice.rb +69 -0
  26. data/lib/onyxcord/{logger.rb → core/logger.rb} +5 -0
  27. data/lib/onyxcord/core/version.rb +5 -0
  28. data/lib/onyxcord/events/await/base.rb +48 -0
  29. data/lib/onyxcord/events/await.rb +1 -46
  30. data/lib/onyxcord/events/ban/base.rb +54 -0
  31. data/lib/onyxcord/events/ban/remove.rb +11 -0
  32. data/lib/onyxcord/events/bans.rb +2 -58
  33. data/lib/onyxcord/events/channels/base.rb +91 -0
  34. data/lib/onyxcord/events/channels/delete.rb +73 -0
  35. data/lib/onyxcord/events/channels/pins.rb +44 -0
  36. data/lib/onyxcord/events/channels/recipients.rb +77 -0
  37. data/lib/onyxcord/events/channels.rb +4 -239
  38. data/lib/onyxcord/events/guilds/audit_logs.rb +79 -0
  39. data/lib/onyxcord/events/guilds/base.rb +97 -0
  40. data/lib/onyxcord/events/guilds/emoji.rb +147 -0
  41. data/lib/onyxcord/events/guilds.rb +3 -267
  42. data/lib/onyxcord/events/handlers/channels.rb +133 -0
  43. data/lib/onyxcord/events/handlers/core.rb +146 -0
  44. data/lib/onyxcord/events/handlers/guilds.rb +375 -0
  45. data/lib/onyxcord/events/handlers/interactions.rb +238 -0
  46. data/lib/onyxcord/events/handlers/lifetime.rb +71 -0
  47. data/lib/onyxcord/events/handlers/messages.rb +218 -0
  48. data/lib/onyxcord/events/handlers/presence.rb +56 -0
  49. data/lib/onyxcord/events/handlers/raw.rb +39 -0
  50. data/lib/onyxcord/events/handlers/reactions.rb +98 -0
  51. data/lib/onyxcord/events/handlers/scheduled_events.rb +243 -0
  52. data/lib/onyxcord/events/handlers/voice.rb +46 -0
  53. data/lib/onyxcord/events/integration/base.rb +45 -0
  54. data/lib/onyxcord/events/integration/create.rb +11 -0
  55. data/lib/onyxcord/events/integration/delete.rb +48 -0
  56. data/lib/onyxcord/events/integration/update.rb +11 -0
  57. data/lib/onyxcord/events/integrations.rb +4 -98
  58. data/lib/onyxcord/events/interactions/application_commands.rb +185 -0
  59. data/lib/onyxcord/events/interactions/autocomplete.rb +62 -0
  60. data/lib/onyxcord/events/interactions/base.rb +175 -0
  61. data/lib/onyxcord/events/interactions/components.rb +221 -0
  62. data/lib/onyxcord/events/interactions/permissions.rb +53 -0
  63. data/lib/onyxcord/events/interactions.rb +5 -622
  64. data/lib/onyxcord/events/invite/create.rb +79 -0
  65. data/lib/onyxcord/events/invite/delete.rb +55 -0
  66. data/lib/onyxcord/events/invites.rb +2 -125
  67. data/lib/onyxcord/events/lifetime/base.rb +31 -0
  68. data/lib/onyxcord/events/lifetime.rb +1 -29
  69. data/lib/onyxcord/events/member/add.rb +12 -0
  70. data/lib/onyxcord/events/member/base.rb +67 -0
  71. data/lib/onyxcord/events/member/delete.rb +21 -0
  72. data/lib/onyxcord/events/member/update.rb +25 -0
  73. data/lib/onyxcord/events/members.rb +4 -108
  74. data/lib/onyxcord/events/message/base.rb +295 -0
  75. data/lib/onyxcord/events/message/ids.rb +71 -0
  76. data/lib/onyxcord/events/message/mentions.rb +55 -0
  77. data/lib/onyxcord/events/message/updates.rb +27 -0
  78. data/lib/onyxcord/events/message.rb +4 -397
  79. data/lib/onyxcord/events/poll/add.rb +11 -0
  80. data/lib/onyxcord/events/poll/base.rb +106 -0
  81. data/lib/onyxcord/events/poll/remove.rb +11 -0
  82. data/lib/onyxcord/events/polls.rb +3 -116
  83. data/lib/onyxcord/events/presence/playing.rb +77 -0
  84. data/lib/onyxcord/events/presence/status.rb +60 -0
  85. data/lib/onyxcord/events/presence.rb +2 -129
  86. data/lib/onyxcord/events/raw/base.rb +74 -0
  87. data/lib/onyxcord/events/raw.rb +1 -72
  88. data/lib/onyxcord/events/reactions/base.rb +143 -0
  89. data/lib/onyxcord/events/reactions/clear.rb +99 -0
  90. data/lib/onyxcord/events/reactions.rb +2 -216
  91. data/lib/onyxcord/events/role/create.rb +46 -0
  92. data/lib/onyxcord/events/role/delete.rb +40 -0
  93. data/lib/onyxcord/events/role/update.rb +11 -0
  94. data/lib/onyxcord/events/roles.rb +3 -85
  95. data/lib/onyxcord/events/scheduled_events/events.rb +112 -0
  96. data/lib/onyxcord/events/scheduled_events/users.rb +91 -0
  97. data/lib/onyxcord/events/scheduled_events.rb +2 -169
  98. data/lib/onyxcord/events/thread/base.rb +49 -0
  99. data/lib/onyxcord/events/thread/members.rb +44 -0
  100. data/lib/onyxcord/events/thread/update.rb +18 -0
  101. data/lib/onyxcord/events/threads.rb +3 -98
  102. data/lib/onyxcord/events/typing/base.rb +73 -0
  103. data/lib/onyxcord/events/typing.rb +1 -71
  104. data/lib/onyxcord/events/voice/server_update.rb +48 -0
  105. data/lib/onyxcord/events/voice/state_update.rb +137 -0
  106. data/lib/onyxcord/events/voice_server_update.rb +2 -46
  107. data/lib/onyxcord/events/voice_state_update.rb +2 -135
  108. data/lib/onyxcord/events/webhook/update.rb +65 -0
  109. data/lib/onyxcord/events/webhooks.rb +1 -63
  110. data/lib/onyxcord/gateway/client.rb +460 -0
  111. data/lib/onyxcord/interactions/command.rb +120 -0
  112. data/lib/onyxcord/interactions/context.rb +109 -0
  113. data/lib/onyxcord/interactions/internal/application_command.rb +203 -0
  114. data/lib/onyxcord/interactions/internal/message.rb +152 -0
  115. data/lib/onyxcord/interactions/internal/metadata.rb +109 -0
  116. data/lib/onyxcord/interactions/internal/option_builder.rb +230 -0
  117. data/lib/onyxcord/interactions/internal/permission_builder.rb +90 -0
  118. data/lib/onyxcord/interactions/option.rb +88 -0
  119. data/lib/onyxcord/interactions/registry.rb +50 -0
  120. data/lib/onyxcord/interactions.rb +16 -0
  121. data/lib/onyxcord/internal/async_runtime.rb +32 -0
  122. data/lib/onyxcord/internal/event_bus.rb +591 -0
  123. data/lib/onyxcord/internal/event_executor.rb +155 -0
  124. data/lib/onyxcord/internal/gateway/opcodes.rb +22 -0
  125. data/lib/onyxcord/internal/gateway/session.rb +45 -0
  126. data/lib/onyxcord/internal/http.rb +189 -0
  127. data/lib/onyxcord/internal/json.rb +51 -0
  128. data/lib/onyxcord/{message_payload.rb → internal/message_payload.rb} +3 -3
  129. data/lib/onyxcord/internal/rate_limiter/async_rest.rb +151 -0
  130. data/lib/onyxcord/internal/rate_limiter/gateway.rb +44 -0
  131. data/lib/onyxcord/internal/rate_limiter/rest.rb +144 -0
  132. data/lib/onyxcord/internal/websocket.rb +79 -0
  133. data/lib/onyxcord/light/data.rb +1 -1
  134. data/lib/onyxcord/light/integrations.rb +1 -1
  135. data/lib/onyxcord/light/light_bot.rb +7 -7
  136. data/lib/onyxcord/{data → models}/activity.rb +2 -2
  137. data/lib/onyxcord/{data → models}/application.rb +3 -3
  138. data/lib/onyxcord/{data → models}/avatar_decoration.rb +1 -1
  139. data/lib/onyxcord/{data → models}/channel.rb +23 -23
  140. data/lib/onyxcord/{data → models}/collectibles.rb +1 -1
  141. data/lib/onyxcord/{data → models}/emoji.rb +1 -1
  142. data/lib/onyxcord/models/interaction.rb +450 -0
  143. data/lib/onyxcord/{data → models}/invite.rb +1 -1
  144. data/lib/onyxcord/{data → models}/member.rb +11 -11
  145. data/lib/onyxcord/{data → models}/message.rb +18 -14
  146. data/lib/onyxcord/{data → models}/poll.rb +2 -2
  147. data/lib/onyxcord/{data → models}/primary_server.rb +1 -1
  148. data/lib/onyxcord/{data → models}/profile.rb +4 -4
  149. data/lib/onyxcord/{data → models}/role.rb +6 -12
  150. data/lib/onyxcord/{data → models}/scheduled_event.rb +5 -5
  151. data/lib/onyxcord/{data → models}/server.rb +41 -41
  152. data/lib/onyxcord/{data → models}/server_preview.rb +2 -2
  153. data/lib/onyxcord/{data → models}/team.rb +1 -1
  154. data/lib/onyxcord/{data → models}/user.rb +5 -5
  155. data/lib/onyxcord/{data → models}/webhook.rb +10 -10
  156. data/lib/onyxcord/models.rb +57 -0
  157. data/lib/onyxcord/{api.rb → rest/client.rb} +23 -22
  158. data/lib/onyxcord/{api → rest/routes}/application.rb +79 -57
  159. data/lib/onyxcord/rest/routes/channel/base.rb +73 -0
  160. data/lib/onyxcord/rest/routes/channel/groups.rb +78 -0
  161. data/lib/onyxcord/rest/routes/channel/messages.rb +130 -0
  162. data/lib/onyxcord/rest/routes/channel/permissions.rb +73 -0
  163. data/lib/onyxcord/rest/routes/channel/pins.rb +45 -0
  164. data/lib/onyxcord/rest/routes/channel/polls.rb +32 -0
  165. data/lib/onyxcord/rest/routes/channel/reactions.rb +86 -0
  166. data/lib/onyxcord/rest/routes/channel/threads.rb +176 -0
  167. data/lib/onyxcord/rest/routes/channel/webhooks.rb +47 -0
  168. data/lib/onyxcord/rest/routes/channel.rb +12 -0
  169. data/lib/onyxcord/{api/interaction.rb → rest/routes/interaction/base.rb} +7 -25
  170. data/lib/onyxcord/rest/routes/interaction/response.rb +24 -0
  171. data/lib/onyxcord/rest/routes/interaction.rb +4 -0
  172. data/lib/onyxcord/{api → rest/routes}/invite.rb +7 -7
  173. data/lib/onyxcord/rest/routes/server/admin.rb +174 -0
  174. data/lib/onyxcord/rest/routes/server/assets.rb +25 -0
  175. data/lib/onyxcord/rest/routes/server/bans.rb +67 -0
  176. data/lib/onyxcord/rest/routes/server/base.rb +77 -0
  177. data/lib/onyxcord/rest/routes/server/channels.rb +58 -0
  178. data/lib/onyxcord/rest/routes/server/emoji.rb +48 -0
  179. data/lib/onyxcord/rest/routes/server/members.rb +109 -0
  180. data/lib/onyxcord/rest/routes/server/messages.rb +19 -0
  181. data/lib/onyxcord/rest/routes/server/roles.rb +156 -0
  182. data/lib/onyxcord/rest/routes/server/scheduled_events.rb +86 -0
  183. data/lib/onyxcord/rest/routes/server.rb +12 -0
  184. data/lib/onyxcord/{api → rest/routes}/user.rb +23 -23
  185. data/lib/onyxcord/{api → rest/routes}/webhook.rb +22 -22
  186. data/lib/onyxcord/{allowed_mentions.rb → utils/allowed_mentions.rb} +1 -1
  187. data/lib/onyxcord/utils/timestamp.rb +69 -0
  188. data/lib/onyxcord/voice/{voice_bot.rb → client.rb} +4 -4
  189. data/lib/onyxcord/voice/encoder.rb +1 -1
  190. data/lib/onyxcord/voice/network/udp.rb +135 -0
  191. data/lib/onyxcord/voice/network/websocket.rb +225 -0
  192. data/lib/onyxcord/voice/network.rb +5 -357
  193. data/lib/onyxcord/webhooks/builder.rb +1 -1
  194. data/lib/onyxcord/webhooks/client.rb +13 -13
  195. data/lib/onyxcord/webhooks/modal/group_builder.rb +55 -0
  196. data/lib/onyxcord/webhooks/modal/label_builder.rb +183 -0
  197. data/lib/onyxcord/webhooks/modal.rb +2 -239
  198. data/lib/onyxcord/webhooks/version.rb +3 -3
  199. data/lib/onyxcord/webhooks/view/container_builder.rb +80 -0
  200. data/lib/onyxcord/webhooks/view/file_builder.rb +24 -0
  201. data/lib/onyxcord/webhooks/view/media_gallery_builder.rb +50 -0
  202. data/lib/onyxcord/webhooks/view/row_builder.rb +120 -0
  203. data/lib/onyxcord/webhooks/view/section_builder.rb +61 -0
  204. data/lib/onyxcord/webhooks/view/select_menu_builder.rb +77 -0
  205. data/lib/onyxcord/webhooks/view/separator_builder.rb +22 -0
  206. data/lib/onyxcord/webhooks/view/text_display_builder.rb +20 -0
  207. data/lib/onyxcord/webhooks/view.rb +9 -425
  208. data/lib/onyxcord.rb +106 -42
  209. metadata +207 -92
  210. data/.github/CONTRIBUTING.md +0 -13
  211. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  212. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -24
  213. data/.github/pull_request_template.md +0 -37
  214. data/.github/workflows/ci.yml +0 -78
  215. data/.github/workflows/codeql.yml +0 -65
  216. data/.github/workflows/deploy.yml +0 -54
  217. data/.github/workflows/release.yml +0 -51
  218. data/.gitignore +0 -17
  219. data/.markdownlint.json +0 -4
  220. data/.overcommit.yml +0 -7
  221. data/.rspec +0 -2
  222. data/.rubocop.yml +0 -129
  223. data/.yardopts +0 -1
  224. data/CHANGELOG.md +0 -257
  225. data/Gemfile +0 -6
  226. data/LICENSE.txt +0 -21
  227. data/README.md +0 -448
  228. data/Rakefile +0 -13
  229. data/lib/onyxcord/api/channel.rb +0 -703
  230. data/lib/onyxcord/api/server.rb +0 -775
  231. data/lib/onyxcord/async/runtime.rb +0 -30
  232. data/lib/onyxcord/commands/command_bot.rb +0 -505
  233. data/lib/onyxcord/data/interaction.rb +0 -1151
  234. data/lib/onyxcord/data.rb +0 -57
  235. data/lib/onyxcord/event_executor.rb +0 -153
  236. data/lib/onyxcord/gateway.rb +0 -505
  237. data/lib/onyxcord/http.rb +0 -186
  238. data/lib/onyxcord/json.rb +0 -49
  239. data/lib/onyxcord/rate_limiter/async_rest.rb +0 -150
  240. data/lib/onyxcord/rate_limiter/gateway.rb +0 -42
  241. data/lib/onyxcord/rate_limiter/rest.rb +0 -142
  242. data/lib/onyxcord/version.rb +0 -7
  243. data/lib/onyxcord/websocket.rb +0 -77
  244. data/onyxcord.gemspec +0 -63
  245. /data/lib/onyxcord/{configuration.rb → core/configuration.rb} +0 -0
  246. /data/lib/onyxcord/{errors.rb → core/errors.rb} +0 -0
  247. /data/lib/onyxcord/{upload.rb → internal/upload.rb} +0 -0
  248. /data/lib/onyxcord/{data → models}/attachment.rb +0 -0
  249. /data/lib/onyxcord/{data → models}/audit_logs.rb +0 -0
  250. /data/lib/onyxcord/{data → models}/call.rb +0 -0
  251. /data/lib/onyxcord/{data → models}/channel_tag.rb +0 -0
  252. /data/lib/onyxcord/{data → models}/component.rb +0 -0
  253. /data/lib/onyxcord/{data → models}/embed.rb +0 -0
  254. /data/lib/onyxcord/{data → models}/install_params.rb +0 -0
  255. /data/lib/onyxcord/{data → models}/integration.rb +0 -0
  256. /data/lib/onyxcord/{data → models}/message_activity.rb +0 -0
  257. /data/lib/onyxcord/{data → models}/overwrite.rb +0 -0
  258. /data/lib/onyxcord/{data → models}/reaction.rb +0 -0
  259. /data/lib/onyxcord/{data → models}/recipient.rb +0 -0
  260. /data/lib/onyxcord/{data → models}/role_connection_data.rb +0 -0
  261. /data/lib/onyxcord/{data → models}/role_subscription.rb +0 -0
  262. /data/lib/onyxcord/{data → models}/snapshot.rb +0 -0
  263. /data/lib/onyxcord/{data → models}/timestamp.rb +0 -0
  264. /data/lib/onyxcord/{data → models}/voice_region.rb +0 -0
  265. /data/lib/onyxcord/{data → models}/voice_state.rb +0 -0
  266. /data/lib/onyxcord/{colour_rgb.rb → utils/colour_rgb.rb} +0 -0
  267. /data/lib/onyxcord/{id_object.rb → utils/id_object.rb} +0 -0
  268. /data/lib/onyxcord/{message_components.rb → utils/message_components.rb} +0 -0
  269. /data/lib/onyxcord/{paginator.rb → utils/paginator.rb} +0 -0
  270. /data/lib/onyxcord/{permissions.rb → utils/permissions.rb} +0 -0
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'async_runtime'
4
+
5
+ module OnyxCord
6
+ module Internal
7
+ module EventExecutor
8
+ STOP = Object.new.freeze
9
+
10
+ class Inline
11
+ def post
12
+ yield
13
+ end
14
+
15
+ def shutdown; end
16
+
17
+ def threads
18
+ []
19
+ end
20
+ end
21
+
22
+ class Pool
23
+ attr_reader :queue
24
+
25
+ def initialize(size:, queue_size: nil)
26
+ raise ArgumentError, 'Pool size must be greater than zero' unless size.positive?
27
+
28
+ @size = size
29
+ @queue = queue_size ? SizedQueue.new(queue_size) : Queue.new
30
+ @closed = false
31
+ @workers = []
32
+
33
+ start_workers
34
+ end
35
+
36
+ def post(&block)
37
+ raise ArgumentError, 'EventExecutor::Pool#post requires a block' unless block
38
+ raise 'Event executor has been shut down' if @closed
39
+
40
+ @queue << block
41
+ end
42
+
43
+ def queue_size
44
+ @queue.size
45
+ end
46
+
47
+ def threads
48
+ @workers
49
+ end
50
+
51
+ def shutdown
52
+ return if @closed
53
+
54
+ @closed = true
55
+ @size.times { @queue << STOP }
56
+ @workers.each do |w|
57
+ w.join unless w == Thread.current
58
+ rescue StandardError
59
+ nil
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def start_workers
66
+ @workers = Array.new(@size) do |index|
67
+ Thread.new do
68
+ Thread.current[:onyxcord_name] = "event-worker-#{index + 1}"
69
+ worker_loop
70
+ end
71
+ end
72
+ end
73
+
74
+ def worker_loop
75
+ loop do
76
+ job = @queue.pop
77
+ break if job.equal?(STOP)
78
+
79
+ job.call
80
+ rescue StandardError => e
81
+ OnyxCord::LOGGER.log_exception(e) if defined?(OnyxCord::LOGGER)
82
+ end
83
+ end
84
+ end
85
+
86
+ class AsyncPool
87
+ attr_reader :queue
88
+
89
+ def initialize(size:, queue_size: nil) # rubocop:disable Lint/UnusedMethodArgument
90
+ raise ArgumentError, 'Pool size must be greater than zero' unless size.positive?
91
+
92
+ @size = size
93
+ @queue = ::Async::Queue.new
94
+ @closed = false
95
+ @workers = []
96
+ start_workers
97
+ end
98
+
99
+ def post(&block)
100
+ raise ArgumentError, 'EventExecutor::AsyncPool#post requires a block' unless block
101
+ raise 'Event executor has been shut down' if @closed
102
+
103
+ @queue.enqueue(block)
104
+ end
105
+
106
+ def queue_size
107
+ @queue.size
108
+ end
109
+
110
+ def shutdown
111
+ return if @closed
112
+
113
+ @closed = true
114
+ @size.times { @queue.enqueue(STOP) }
115
+ end
116
+
117
+ def threads
118
+ []
119
+ end
120
+
121
+ private
122
+
123
+ def start_workers
124
+ @workers = Array.new(@size) do
125
+ AsyncRuntime.async do
126
+ loop do
127
+ job = @queue.dequeue
128
+ break if job.equal?(STOP)
129
+
130
+ job.call
131
+ rescue StandardError => e
132
+ OnyxCord::LOGGER.log_exception(e) if defined?(OnyxCord::LOGGER)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ module_function
140
+
141
+ def build(type, workers:, queue_size: nil)
142
+ case type
143
+ when :inline
144
+ Inline.new
145
+ when :pool
146
+ Pool.new(size: workers, queue_size: queue_size)
147
+ when :async_pool
148
+ AsyncPool.new(size: workers, queue_size: queue_size)
149
+ else
150
+ raise ArgumentError, "Unknown event executor: #{type.inspect}"
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module Internal
5
+ module Gateway
6
+ module Opcodes
7
+ DISPATCH = 0
8
+ HEARTBEAT = 1
9
+ IDENTIFY = 2
10
+ PRESENCE = 3
11
+ VOICE_STATE = 4
12
+ VOICE_PING = 5
13
+ RESUME = 6
14
+ RECONNECT = 7
15
+ REQUEST_MEMBERS = 8
16
+ INVALIDATE_SESSION = 9
17
+ HELLO = 10
18
+ HEARTBEAT_ACK = 11
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module Internal
5
+ module Gateway
6
+ class Session
7
+ attr_reader :session_id, :resume_gateway_url
8
+ attr_accessor :sequence
9
+
10
+ def initialize(session_id, resume_gateway_url)
11
+ @session_id = session_id
12
+ @sequence = 0
13
+ @suspended = false
14
+ @invalid = false
15
+ @resume_gateway_url = resume_gateway_url
16
+ end
17
+
18
+ def suspend
19
+ @suspended = true
20
+ end
21
+
22
+ def suspended?
23
+ @suspended
24
+ end
25
+
26
+ def resume
27
+ @suspended = false
28
+ end
29
+
30
+ def invalidate
31
+ @invalid = true
32
+ @resume_gateway_url = nil
33
+ end
34
+
35
+ def invalid?
36
+ @invalid
37
+ end
38
+
39
+ def should_resume?
40
+ suspended? && !invalid?
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httpx'
4
+ require 'net/http'
5
+ require_relative 'json'
6
+ require_relative 'upload'
7
+ require 'securerandom'
8
+ require 'uri'
9
+
10
+ module OnyxCord
11
+ module Internal
12
+ # Modern HTTP adapter wrapping HTTPX with persistent connections, automatic
13
+ # retries on 502, and a response interface compatible with the rest of OnyxCord.
14
+ module HTTP
15
+ # Lightweight response wrapper so call-sites that relied on RestClient's
16
+ # `.body`, `.headers`, `.code` interface keep working unchanged.
17
+ class Response
18
+ # @return [String] the response body
19
+ attr_reader :body
20
+
21
+ # @return [Integer] the HTTP status code
22
+ attr_reader :code
23
+
24
+ # @return [Hash] the response headers (symbol keys, underscored)
25
+ attr_reader :headers
26
+
27
+ def initialize(httpx_response)
28
+ @raw = httpx_response
29
+ @body = httpx_response.body.to_s
30
+ @code = httpx_response.respond_to?(:status) ? httpx_response.status : httpx_response.code.to_i
31
+ response_headers = httpx_response.respond_to?(:headers) ? httpx_response.headers : httpx_response.to_hash
32
+ @headers = normalize_headers(response_headers)
33
+ end
34
+
35
+ def to_s
36
+ @body
37
+ end
38
+
39
+ # RestClient compatibility — some code calls `response` directly as a
40
+ # string (implicit to_s).
41
+ alias_method :to_str, :to_s
42
+
43
+ private
44
+
45
+ def normalize_headers(headers)
46
+ transformed = headers.to_h.transform_keys do |key|
47
+ key.to_s.tr('-', '_').downcase.to_sym
48
+ end
49
+ transformed.transform_values do |value|
50
+ value.is_a?(Array) && value.size == 1 ? value.first : value
51
+ end
52
+ end
53
+ end
54
+
55
+ module_function
56
+
57
+ # The shared HTTPX session with persistent connections for the current thread.
58
+ def session
59
+ Thread.current[:onyxcord_http_session] ||= HTTPX.plugin(:persistent)
60
+ .plugin(:follow_redirects)
61
+ .with(
62
+ fallback_protocol: 'http/1.1',
63
+ ssl: { alpn_protocols: ['http/1.1'] }
64
+ )
65
+ end
66
+
67
+ # Reset the HTTP session (useful for tests).
68
+ def reset!
69
+ Thread.current[:onyxcord_http_session] = nil
70
+ end
71
+
72
+ # Perform a raw HTTP request and return a {Response}.
73
+ # @param type [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
74
+ # @param url [String] The full URL.
75
+ # @param body [String, Hash, nil] The request body.
76
+ # @param headers [Hash] Request headers.
77
+ # @return [Response]
78
+ def request(type, url, body = nil, **headers)
79
+ http = session.with(headers: headers)
80
+
81
+ raw = case type
82
+ when :get
83
+ http.get(url)
84
+ when :post
85
+ if multipart?(body)
86
+ # Multipart upload
87
+ post_multipart(url, body, headers)
88
+ else
89
+ http.post(url, body: body)
90
+ end
91
+ when :put
92
+ http.put(url, body: body)
93
+ when :patch
94
+ http.patch(url, body: body)
95
+ when :delete
96
+ http.delete(url)
97
+ else
98
+ raise ArgumentError, "Unknown HTTP method: #{type}"
99
+ end
100
+
101
+ # HTTPX returns an error response object on network failures
102
+ raise raw.error if raw.is_a?(HTTPX::ErrorResponse)
103
+
104
+ Response.new(raw)
105
+ end
106
+
107
+ def post_multipart(url, body, headers)
108
+ uri = URI(url)
109
+ boundary = "----OnyxCord#{SecureRandom.hex(12)}"
110
+ request = Net::HTTP::Post.new(uri)
111
+ headers.each { |key, value| request[key.to_s] = value }
112
+ request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
113
+ request.body = multipart_body(body, boundary)
114
+
115
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
116
+ http.request(request)
117
+ end
118
+ end
119
+
120
+ def multipart_body(body, boundary)
121
+ output = (+'').b
122
+
123
+ multipart_parts(body).each do |part|
124
+ key = part[:name]
125
+ value = part[:value]
126
+ output << "--#{boundary}\r\n"
127
+ if part[:filename]
128
+ value.rewind if value.respond_to?(:rewind)
129
+ filename = part[:filename]
130
+ output << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{filename}\"\r\n"
131
+ output << "Content-Type: #{part[:content_type] || multipart_content_type(filename)}\r\n\r\n"
132
+ output << value.read.to_s
133
+ else
134
+ output << "Content-Disposition: form-data; name=\"#{key}\"\r\n"
135
+ output << "\r\n"
136
+ output << value.to_s
137
+ end
138
+ output << "\r\n"
139
+ end
140
+
141
+ output << "--#{boundary}--\r\n"
142
+ end
143
+
144
+ def multipart_content_type(filename)
145
+ File.extname(filename).casecmp('.txt').zero? ? 'text/plain' : 'application/octet-stream'
146
+ end
147
+
148
+ def multipart?(body)
149
+ return true if body.is_a?(Array) && body.all? { |part| part.is_a?(Hash) && part[:name] && part.key?(:value) }
150
+
151
+ body.is_a?(Hash) && body.any? { |_, value| value.respond_to?(:read) || value.respond_to?(:path) }
152
+ end
153
+
154
+ def multipart_parts(body)
155
+ return body if body.is_a?(Array)
156
+
157
+ body.map do |key, value|
158
+ if value.respond_to?(:read) || value.respond_to?(:path)
159
+ upload = Upload.wrap(value)
160
+ { name: key, value: upload, filename: upload.filename, content_type: upload.content_type }
161
+ else
162
+ { name: key, value: value }
163
+ end
164
+ end
165
+ end
166
+
167
+ # Convenience wrappers
168
+ def get(url, **headers)
169
+ request(:get, url, nil, **headers)
170
+ end
171
+
172
+ def post(url, body = nil, **headers)
173
+ request(:post, url, body, **headers)
174
+ end
175
+
176
+ def put(url, body = nil, **headers)
177
+ request(:put, url, body, **headers)
178
+ end
179
+
180
+ def patch(url, body = nil, **headers)
181
+ request(:patch, url, body, **headers)
182
+ end
183
+
184
+ def delete(url, **headers)
185
+ request(:delete, url, nil, **headers)
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oj'
4
+
5
+ # Configure Oj in compat mode so that all stdlib JSON.parse / .to_json calls
6
+ # are transparently accelerated. This is loaded early by the main onyxcord.rb
7
+ # entry point so every module benefits automatically.
8
+ module OnyxCord
9
+ module Internal
10
+ # Fast Oj-backed JSON wrapper providing compatibility with stdlib JSON methods.
11
+ module JSON
12
+ Oj.default_options = { mode: :compat, symbol_keys: false }
13
+
14
+ module_function
15
+
16
+ # Fast JSON decode using Oj via stdlib JSON compatibility.
17
+ # @param data [String] The JSON string to decode.
18
+ # @return [Hash, Array, String, Numeric, nil]
19
+ def decode(data, *args)
20
+ ::JSON.parse(data, *args)
21
+ end
22
+
23
+ # Fast JSON encode using Oj via stdlib JSON compatibility.
24
+ # @param data [Object] The object to encode as JSON.
25
+ # @return [String]
26
+ def encode(data, *args)
27
+ ::JSON.generate(data, *args)
28
+ end
29
+
30
+ # Alias parse to decode for standard library compatibility
31
+ def parse(data, *args)
32
+ ::JSON.parse(data, *args)
33
+ end
34
+
35
+ # Alias generate to encode for standard library compatibility
36
+ def generate(data, *args)
37
+ ::JSON.generate(data, *args)
38
+ end
39
+
40
+ # Alias load to decode for Oj/JSON compatibility
41
+ def load(data, *args)
42
+ ::JSON.parse(data, *args)
43
+ end
44
+
45
+ # Alias dump to encode for Oj/JSON compatibility
46
+ def dump(data, *args)
47
+ ::JSON.generate(data, *args)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'onyxcord/message_components'
4
- require 'onyxcord/upload'
3
+ require_relative '../utils/message_components'
4
+ require_relative 'upload'
5
5
 
6
6
  module OnyxCord
7
7
  module MessagePayload
@@ -64,7 +64,7 @@ module OnyxCord
64
64
 
65
65
  def components_v2?(components, flags)
66
66
  OnyxCord::MessageComponents.components_v2?(components) ||
67
- (OnyxCord::MessageComponents.flag_value(flags) & OnyxCord::MessageComponents::IS_COMPONENTS_V2).positive?
67
+ OnyxCord::MessageComponents.flag_value(flags).anybits?(OnyxCord::MessageComponents::IS_COMPONENTS_V2)
68
68
  end
69
69
 
70
70
  def present?(value)
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../async_runtime'
4
+
5
+ module OnyxCord
6
+ module Internal
7
+ module RateLimiter
8
+ class AsyncRest
9
+ DEFAULT_ENTRY_TTL = 3600
10
+ DEFAULT_PRUNE_INTERVAL = 100
11
+
12
+ def initialize(clock: -> { Time.now }, entry_ttl: DEFAULT_ENTRY_TTL, prune_interval: DEFAULT_PRUNE_INTERVAL)
13
+ @route_buckets = {}
14
+ @bucket_locks = {}
15
+ @bucket_last_used = {}
16
+ @global_lock = Mutex.new
17
+ @clock = clock
18
+ @entry_ttl = entry_ttl
19
+ @prune_interval = prune_interval
20
+ @requests_since_prune = 0
21
+ end
22
+
23
+ def before_request(route, major_parameter)
24
+ wait_for(@global_lock)
25
+ wait_for(mutex_for(route, major_parameter))
26
+ end
27
+
28
+ def record_response(route, major_parameter, headers)
29
+ headers = normalize_headers(headers)
30
+ bucket = headers[:x_ratelimit_bucket]
31
+ key = route_key(route, major_parameter)
32
+ touch(key)
33
+
34
+ if bucket
35
+ bucket = bucket_key(bucket, major_parameter)
36
+ @route_buckets[key] = bucket
37
+ touch(bucket)
38
+ end
39
+
40
+ return unless headers[:x_ratelimit_remaining] == '0'
41
+
42
+ wait_seconds = headers[:x_ratelimit_reset_after].to_f
43
+ return unless wait_seconds.positive?
44
+
45
+ async_wait(wait_seconds, mutex_for(route, major_parameter))
46
+ end
47
+
48
+ def handle_rate_limit(route, major_parameter, response)
49
+ headers = normalize_headers(response.headers)
50
+ wait_seconds = retry_after(response, headers)
51
+
52
+ return unless wait_seconds.positive?
53
+
54
+ if headers[:x_ratelimit_global] == 'true' || headers[:x_ratelimit_scope] == 'global'
55
+ global_wait(wait_seconds)
56
+ else
57
+ async_wait(wait_seconds, mutex_for(route, major_parameter))
58
+ end
59
+ end
60
+
61
+ def stats
62
+ {
63
+ route_buckets: @route_buckets.size,
64
+ bucket_locks: @bucket_locks.size,
65
+ tracked_keys: @bucket_last_used.size
66
+ }
67
+ end
68
+
69
+ def prune!
70
+ return 0 unless @entry_ttl
71
+
72
+ cutoff = @clock.call - @entry_ttl
73
+ stale_keys = @bucket_last_used.select { |_, last_used| last_used < cutoff }.keys
74
+
75
+ stale_keys.each do |key|
76
+ @bucket_locks.delete(key)
77
+ @bucket_last_used.delete(key)
78
+ @route_buckets.delete(key)
79
+ @route_buckets.delete_if { |_, bucket_key| bucket_key == key }
80
+ end
81
+
82
+ @requests_since_prune = 0
83
+ stale_keys.length
84
+ end
85
+
86
+ private
87
+
88
+ def mutex_for(route, major_parameter)
89
+ key = resolved_key(route, major_parameter)
90
+ touch(key)
91
+ @bucket_locks[key] ||= Mutex.new
92
+ end
93
+
94
+ def resolved_key(route, major_parameter)
95
+ @route_buckets[route_key(route, major_parameter)] || route_key(route, major_parameter)
96
+ end
97
+
98
+ def route_key(route, major_parameter)
99
+ [route, major_parameter].freeze
100
+ end
101
+
102
+ def bucket_key(bucket, major_parameter)
103
+ [:bucket, bucket, major_parameter].freeze
104
+ end
105
+
106
+ def retry_after(response, headers)
107
+ body = response.respond_to?(:body) ? response.body : response.to_s
108
+ if body && !body.empty?
109
+ data = JSON.parse(body)
110
+ return data['retry_after'].to_f if data['retry_after']
111
+ end
112
+
113
+ (headers[:retry_after] || 0).to_f
114
+ rescue JSON::ParserError
115
+ (headers[:retry_after] || 0).to_f
116
+ end
117
+
118
+ def normalize_headers(headers)
119
+ headers.each_with_object({}) do |(key, value), memo|
120
+ memo[key.to_s.tr('-', '_').downcase.to_sym] = value.to_s
121
+ end
122
+ end
123
+
124
+ def touch(key)
125
+ @bucket_last_used[key] = @clock.call
126
+ prune_if_needed
127
+ end
128
+
129
+ def prune_if_needed
130
+ return unless @prune_interval
131
+
132
+ @requests_since_prune += 1
133
+ prune! if @requests_since_prune >= @prune_interval
134
+ end
135
+
136
+ def wait_for(mutex)
137
+ mutex.lock
138
+ mutex.unlock
139
+ end
140
+
141
+ def async_wait(time, mutex)
142
+ mutex.synchronize { AsyncRuntime.sleep(time) }
143
+ end
144
+
145
+ def global_wait(time)
146
+ async_wait(time, @global_lock)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module Internal
5
+ module RateLimiter
6
+ # Sliding-window limiter for Gateway sends.
7
+ class Gateway
8
+ DEFAULT_LIMIT = 120
9
+ DEFAULT_INTERVAL = 60
10
+
11
+ def initialize(limit: DEFAULT_LIMIT, interval: DEFAULT_INTERVAL, clock: -> { Time.now }, sleeper: ->(duration) { sleep(duration) })
12
+ @limit = limit
13
+ @interval = interval
14
+ @clock = clock
15
+ @sleeper = sleeper
16
+ @sent_at = []
17
+ @mutex = Mutex.new
18
+ end
19
+
20
+ def wait
21
+ @mutex.synchronize do
22
+ now = @clock.call
23
+ prune(now)
24
+
25
+ if @sent_at.length >= @limit
26
+ sleep_for = @interval - (now - @sent_at.first)
27
+ @sleeper.call(sleep_for) if sleep_for.positive?
28
+ now = @clock.call
29
+ prune(now)
30
+ end
31
+
32
+ @sent_at << now
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def prune(now)
39
+ @sent_at.shift while @sent_at.any? && (now - @sent_at.first) >= @interval
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end