onyxcord 2.0.13 → 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/{errors.rb → core/errors.rb} +18 -3
  27. data/lib/onyxcord/{logger.rb → core/logger.rb} +5 -0
  28. data/lib/onyxcord/core/version.rb +5 -0
  29. data/lib/onyxcord/events/await/base.rb +48 -0
  30. data/lib/onyxcord/events/await.rb +1 -46
  31. data/lib/onyxcord/events/ban/base.rb +54 -0
  32. data/lib/onyxcord/events/ban/remove.rb +11 -0
  33. data/lib/onyxcord/events/bans.rb +2 -58
  34. data/lib/onyxcord/events/channels/base.rb +91 -0
  35. data/lib/onyxcord/events/channels/delete.rb +73 -0
  36. data/lib/onyxcord/events/channels/pins.rb +44 -0
  37. data/lib/onyxcord/events/channels/recipients.rb +77 -0
  38. data/lib/onyxcord/events/channels.rb +4 -239
  39. data/lib/onyxcord/events/guilds/audit_logs.rb +79 -0
  40. data/lib/onyxcord/events/guilds/base.rb +97 -0
  41. data/lib/onyxcord/events/guilds/emoji.rb +147 -0
  42. data/lib/onyxcord/events/guilds.rb +3 -267
  43. data/lib/onyxcord/events/handlers/channels.rb +133 -0
  44. data/lib/onyxcord/events/handlers/core.rb +146 -0
  45. data/lib/onyxcord/events/handlers/guilds.rb +375 -0
  46. data/lib/onyxcord/events/handlers/interactions.rb +238 -0
  47. data/lib/onyxcord/events/handlers/lifetime.rb +71 -0
  48. data/lib/onyxcord/events/handlers/messages.rb +218 -0
  49. data/lib/onyxcord/events/handlers/presence.rb +56 -0
  50. data/lib/onyxcord/events/handlers/raw.rb +39 -0
  51. data/lib/onyxcord/events/handlers/reactions.rb +98 -0
  52. data/lib/onyxcord/events/handlers/scheduled_events.rb +243 -0
  53. data/lib/onyxcord/events/handlers/voice.rb +46 -0
  54. data/lib/onyxcord/events/integration/base.rb +45 -0
  55. data/lib/onyxcord/events/integration/create.rb +11 -0
  56. data/lib/onyxcord/events/integration/delete.rb +48 -0
  57. data/lib/onyxcord/events/integration/update.rb +11 -0
  58. data/lib/onyxcord/events/integrations.rb +4 -98
  59. data/lib/onyxcord/events/interactions/application_commands.rb +185 -0
  60. data/lib/onyxcord/events/interactions/autocomplete.rb +62 -0
  61. data/lib/onyxcord/events/interactions/base.rb +175 -0
  62. data/lib/onyxcord/events/interactions/components.rb +221 -0
  63. data/lib/onyxcord/events/interactions/permissions.rb +53 -0
  64. data/lib/onyxcord/events/interactions.rb +5 -622
  65. data/lib/onyxcord/events/invite/create.rb +79 -0
  66. data/lib/onyxcord/events/invite/delete.rb +55 -0
  67. data/lib/onyxcord/events/invites.rb +2 -125
  68. data/lib/onyxcord/events/lifetime/base.rb +31 -0
  69. data/lib/onyxcord/events/lifetime.rb +1 -29
  70. data/lib/onyxcord/events/member/add.rb +12 -0
  71. data/lib/onyxcord/events/member/base.rb +67 -0
  72. data/lib/onyxcord/events/member/delete.rb +21 -0
  73. data/lib/onyxcord/events/member/update.rb +25 -0
  74. data/lib/onyxcord/events/members.rb +4 -108
  75. data/lib/onyxcord/events/message/base.rb +295 -0
  76. data/lib/onyxcord/events/message/ids.rb +71 -0
  77. data/lib/onyxcord/events/message/mentions.rb +55 -0
  78. data/lib/onyxcord/events/message/updates.rb +27 -0
  79. data/lib/onyxcord/events/message.rb +4 -397
  80. data/lib/onyxcord/events/poll/add.rb +11 -0
  81. data/lib/onyxcord/events/poll/base.rb +106 -0
  82. data/lib/onyxcord/events/poll/remove.rb +11 -0
  83. data/lib/onyxcord/events/polls.rb +3 -116
  84. data/lib/onyxcord/events/presence/playing.rb +77 -0
  85. data/lib/onyxcord/events/presence/status.rb +60 -0
  86. data/lib/onyxcord/events/presence.rb +2 -129
  87. data/lib/onyxcord/events/raw/base.rb +74 -0
  88. data/lib/onyxcord/events/raw.rb +1 -72
  89. data/lib/onyxcord/events/reactions/base.rb +143 -0
  90. data/lib/onyxcord/events/reactions/clear.rb +99 -0
  91. data/lib/onyxcord/events/reactions.rb +2 -216
  92. data/lib/onyxcord/events/role/create.rb +46 -0
  93. data/lib/onyxcord/events/role/delete.rb +40 -0
  94. data/lib/onyxcord/events/role/update.rb +11 -0
  95. data/lib/onyxcord/events/roles.rb +3 -85
  96. data/lib/onyxcord/events/scheduled_events/events.rb +112 -0
  97. data/lib/onyxcord/events/scheduled_events/users.rb +91 -0
  98. data/lib/onyxcord/events/scheduled_events.rb +2 -169
  99. data/lib/onyxcord/events/thread/base.rb +49 -0
  100. data/lib/onyxcord/events/thread/members.rb +44 -0
  101. data/lib/onyxcord/events/thread/update.rb +18 -0
  102. data/lib/onyxcord/events/threads.rb +3 -98
  103. data/lib/onyxcord/events/typing/base.rb +73 -0
  104. data/lib/onyxcord/events/typing.rb +1 -71
  105. data/lib/onyxcord/events/voice/server_update.rb +48 -0
  106. data/lib/onyxcord/events/voice/state_update.rb +137 -0
  107. data/lib/onyxcord/events/voice_server_update.rb +2 -46
  108. data/lib/onyxcord/events/voice_state_update.rb +2 -135
  109. data/lib/onyxcord/events/webhook/update.rb +65 -0
  110. data/lib/onyxcord/events/webhooks.rb +1 -63
  111. data/lib/onyxcord/gateway/client.rb +460 -0
  112. data/lib/onyxcord/interactions/command.rb +120 -0
  113. data/lib/onyxcord/interactions/context.rb +109 -0
  114. data/lib/onyxcord/interactions/internal/application_command.rb +203 -0
  115. data/lib/onyxcord/interactions/internal/message.rb +152 -0
  116. data/lib/onyxcord/interactions/internal/metadata.rb +109 -0
  117. data/lib/onyxcord/interactions/internal/option_builder.rb +230 -0
  118. data/lib/onyxcord/interactions/internal/permission_builder.rb +90 -0
  119. data/lib/onyxcord/interactions/option.rb +88 -0
  120. data/lib/onyxcord/interactions/registry.rb +50 -0
  121. data/lib/onyxcord/interactions.rb +16 -0
  122. data/lib/onyxcord/internal/async_runtime.rb +32 -0
  123. data/lib/onyxcord/internal/event_bus.rb +591 -0
  124. data/lib/onyxcord/internal/event_executor.rb +155 -0
  125. data/lib/onyxcord/internal/gateway/opcodes.rb +22 -0
  126. data/lib/onyxcord/internal/gateway/session.rb +45 -0
  127. data/lib/onyxcord/internal/http.rb +189 -0
  128. data/lib/onyxcord/internal/json.rb +51 -0
  129. data/lib/onyxcord/internal/message_payload.rb +78 -0
  130. data/lib/onyxcord/internal/rate_limiter/async_rest.rb +151 -0
  131. data/lib/onyxcord/internal/rate_limiter/gateway.rb +44 -0
  132. data/lib/onyxcord/internal/rate_limiter/rest.rb +144 -0
  133. data/lib/onyxcord/internal/upload.rb +39 -0
  134. data/lib/onyxcord/internal/websocket.rb +79 -0
  135. data/lib/onyxcord/light/data.rb +1 -1
  136. data/lib/onyxcord/light/integrations.rb +1 -1
  137. data/lib/onyxcord/light/light_bot.rb +7 -7
  138. data/lib/onyxcord/{data → models}/activity.rb +2 -2
  139. data/lib/onyxcord/{data → models}/application.rb +3 -3
  140. data/lib/onyxcord/{data → models}/avatar_decoration.rb +1 -1
  141. data/lib/onyxcord/{data → models}/channel.rb +23 -23
  142. data/lib/onyxcord/{data → models}/collectibles.rb +1 -1
  143. data/lib/onyxcord/{data → models}/emoji.rb +1 -1
  144. data/lib/onyxcord/models/interaction.rb +450 -0
  145. data/lib/onyxcord/{data → models}/invite.rb +1 -1
  146. data/lib/onyxcord/{data → models}/member.rb +11 -11
  147. data/lib/onyxcord/{data → models}/message.rb +22 -14
  148. data/lib/onyxcord/{data → models}/poll.rb +2 -2
  149. data/lib/onyxcord/{data → models}/primary_server.rb +1 -1
  150. data/lib/onyxcord/{data → models}/profile.rb +4 -4
  151. data/lib/onyxcord/{data → models}/role.rb +6 -12
  152. data/lib/onyxcord/{data → models}/scheduled_event.rb +5 -5
  153. data/lib/onyxcord/{data → models}/server.rb +41 -41
  154. data/lib/onyxcord/{data → models}/server_preview.rb +2 -2
  155. data/lib/onyxcord/{data → models}/team.rb +1 -1
  156. data/lib/onyxcord/{data → models}/user.rb +5 -5
  157. data/lib/onyxcord/{data → models}/webhook.rb +13 -11
  158. data/lib/onyxcord/models.rb +57 -0
  159. data/lib/onyxcord/{api.rb → rest/client.rb} +83 -27
  160. data/lib/onyxcord/{api → rest/routes}/application.rb +79 -57
  161. data/lib/onyxcord/rest/routes/channel/base.rb +73 -0
  162. data/lib/onyxcord/rest/routes/channel/groups.rb +78 -0
  163. data/lib/onyxcord/rest/routes/channel/messages.rb +130 -0
  164. data/lib/onyxcord/rest/routes/channel/permissions.rb +73 -0
  165. data/lib/onyxcord/rest/routes/channel/pins.rb +45 -0
  166. data/lib/onyxcord/rest/routes/channel/polls.rb +32 -0
  167. data/lib/onyxcord/rest/routes/channel/reactions.rb +86 -0
  168. data/lib/onyxcord/rest/routes/channel/threads.rb +176 -0
  169. data/lib/onyxcord/rest/routes/channel/webhooks.rb +47 -0
  170. data/lib/onyxcord/rest/routes/channel.rb +12 -0
  171. data/lib/onyxcord/{api/interaction.rb → rest/routes/interaction/base.rb} +10 -33
  172. data/lib/onyxcord/rest/routes/interaction/response.rb +24 -0
  173. data/lib/onyxcord/rest/routes/interaction.rb +4 -0
  174. data/lib/onyxcord/{api → rest/routes}/invite.rb +7 -7
  175. data/lib/onyxcord/rest/routes/server/admin.rb +174 -0
  176. data/lib/onyxcord/rest/routes/server/assets.rb +25 -0
  177. data/lib/onyxcord/rest/routes/server/bans.rb +67 -0
  178. data/lib/onyxcord/rest/routes/server/base.rb +77 -0
  179. data/lib/onyxcord/rest/routes/server/channels.rb +58 -0
  180. data/lib/onyxcord/rest/routes/server/emoji.rb +48 -0
  181. data/lib/onyxcord/rest/routes/server/members.rb +109 -0
  182. data/lib/onyxcord/rest/routes/server/messages.rb +19 -0
  183. data/lib/onyxcord/rest/routes/server/roles.rb +156 -0
  184. data/lib/onyxcord/rest/routes/server/scheduled_events.rb +86 -0
  185. data/lib/onyxcord/rest/routes/server.rb +12 -0
  186. data/lib/onyxcord/{api → rest/routes}/user.rb +23 -23
  187. data/lib/onyxcord/{api → rest/routes}/webhook.rb +30 -32
  188. data/lib/onyxcord/{allowed_mentions.rb → utils/allowed_mentions.rb} +9 -1
  189. data/lib/onyxcord/utils/timestamp.rb +69 -0
  190. data/lib/onyxcord/voice/{voice_bot.rb → client.rb} +4 -4
  191. data/lib/onyxcord/voice/encoder.rb +1 -1
  192. data/lib/onyxcord/voice/network/udp.rb +135 -0
  193. data/lib/onyxcord/voice/network/websocket.rb +225 -0
  194. data/lib/onyxcord/voice/network.rb +5 -357
  195. data/lib/onyxcord/webhooks/builder.rb +1 -1
  196. data/lib/onyxcord/webhooks/client.rb +13 -13
  197. data/lib/onyxcord/webhooks/modal/group_builder.rb +55 -0
  198. data/lib/onyxcord/webhooks/modal/label_builder.rb +183 -0
  199. data/lib/onyxcord/webhooks/modal.rb +2 -239
  200. data/lib/onyxcord/webhooks/version.rb +3 -3
  201. data/lib/onyxcord/webhooks/view/container_builder.rb +80 -0
  202. data/lib/onyxcord/webhooks/view/file_builder.rb +24 -0
  203. data/lib/onyxcord/webhooks/view/media_gallery_builder.rb +50 -0
  204. data/lib/onyxcord/webhooks/view/row_builder.rb +120 -0
  205. data/lib/onyxcord/webhooks/view/section_builder.rb +61 -0
  206. data/lib/onyxcord/webhooks/view/select_menu_builder.rb +77 -0
  207. data/lib/onyxcord/webhooks/view/separator_builder.rb +22 -0
  208. data/lib/onyxcord/webhooks/view/text_display_builder.rb +20 -0
  209. data/lib/onyxcord/webhooks/view.rb +9 -425
  210. data/lib/onyxcord.rb +106 -40
  211. metadata +208 -91
  212. data/.github/CONTRIBUTING.md +0 -13
  213. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  214. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -24
  215. data/.github/pull_request_template.md +0 -37
  216. data/.github/workflows/ci.yml +0 -78
  217. data/.github/workflows/codeql.yml +0 -65
  218. data/.github/workflows/deploy.yml +0 -54
  219. data/.github/workflows/release.yml +0 -51
  220. data/.gitignore +0 -17
  221. data/.markdownlint.json +0 -4
  222. data/.overcommit.yml +0 -7
  223. data/.rspec +0 -2
  224. data/.rubocop.yml +0 -129
  225. data/.yardopts +0 -1
  226. data/CHANGELOG.md +0 -202
  227. data/Gemfile +0 -6
  228. data/LICENSE.txt +0 -21
  229. data/README.md +0 -448
  230. data/Rakefile +0 -13
  231. data/lib/onyxcord/api/channel.rb +0 -705
  232. data/lib/onyxcord/api/server.rb +0 -775
  233. data/lib/onyxcord/async/runtime.rb +0 -30
  234. data/lib/onyxcord/commands/command_bot.rb +0 -505
  235. data/lib/onyxcord/data/interaction.rb +0 -1143
  236. data/lib/onyxcord/data.rb +0 -57
  237. data/lib/onyxcord/event_executor.rb +0 -153
  238. data/lib/onyxcord/gateway.rb +0 -500
  239. data/lib/onyxcord/http.rb +0 -115
  240. data/lib/onyxcord/json.rb +0 -49
  241. data/lib/onyxcord/rate_limiter/async_rest.rb +0 -149
  242. data/lib/onyxcord/rate_limiter/gateway.rb +0 -42
  243. data/lib/onyxcord/rate_limiter/rest.rb +0 -142
  244. data/lib/onyxcord/version.rb +0 -7
  245. data/lib/onyxcord/websocket.rb +0 -77
  246. data/onyxcord.gemspec +0 -63
  247. /data/lib/onyxcord/{configuration.rb → core/configuration.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,460 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+ require 'async/http/endpoint'
5
+ require 'async/http/protocol/http11'
6
+ require 'async/websocket/client'
7
+ require_relative '../internal/async_runtime'
8
+ require_relative '../internal/rate_limiter/gateway'
9
+ require 'protocol/websocket'
10
+
11
+ module OnyxCord
12
+ module Gateway
13
+ # Client for the Discord gateway protocol — fully async.
14
+ class Client
15
+ LARGE_THRESHOLD = 100
16
+ GATEWAY_VERSION = ENV.fetch('DISCORD_GATEWAY_VERSION', '10').to_i
17
+ FATAL_CLOSE_CODES = [4003, 4004, 4011, 4014].freeze
18
+
19
+ attr_accessor :check_heartbeat_acks
20
+ attr_reader :intents
21
+
22
+ def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = ALL_INTENTS)
23
+ @token = token
24
+ @bot = bot
25
+ @shard_key = shard_key
26
+ @ws_success = false
27
+ @check_heartbeat_acks = true
28
+ @compress_mode = compress_mode
29
+ @intents = intents
30
+ @send_limiter = Internal::RateLimiter::Gateway.new
31
+ @connection = nil
32
+ @closed = true
33
+ @pipe_broken = false
34
+ @missed_heartbeat_acks = 0
35
+ end
36
+
37
+ # Connect to the gateway server inside an Async reactor.
38
+ def run_async
39
+ @task = Internal::AsyncRuntime.async { run }
40
+
41
+ LOGGER.debug('Gateway task created! Waiting for confirmation...')
42
+ loop do
43
+ Internal::AsyncRuntime.sleep(0.5)
44
+ break if @ws_success
45
+ break if @should_reconnect == false
46
+ end
47
+ end
48
+
49
+ def run
50
+ @reactor_task = Async::Task.current
51
+ connect_loop
52
+ LOGGER.warn('The gateway loop exited!')
53
+ end
54
+
55
+ def sync
56
+ @task&.wait
57
+ end
58
+
59
+ def open?
60
+ !@closed && @connection
61
+ end
62
+
63
+ def stop
64
+ @should_reconnect = false
65
+ close
66
+ nil
67
+ end
68
+
69
+ def kill
70
+ @task&.stop
71
+ end
72
+
73
+ def notify_ready
74
+ @ws_success = true
75
+ end
76
+
77
+ def inject_reconnect(url = nil)
78
+ data = url ? { url: url } : nil
79
+ handle_message({ op: Internal::Gateway::Opcodes::RECONNECT, d: data }.to_json)
80
+ end
81
+
82
+ def inject_resume(seq)
83
+ send_resume(raw_token, @session_id, seq || @sequence)
84
+ end
85
+
86
+ def inject_error(e)
87
+ handle_internal_close(e)
88
+ end
89
+
90
+ def heartbeat
91
+ if check_heartbeat_acks
92
+ if @last_heartbeat_acked
93
+ @missed_heartbeat_acks = 0
94
+ else
95
+ @missed_heartbeat_acks += 1
96
+ if @missed_heartbeat_acks >= 2
97
+ LOGGER.warn('Last heartbeats were not acked — zombie connection! Reconnecting')
98
+ @pipe_broken = true
99
+ reconnect
100
+ return
101
+ end
102
+
103
+ LOGGER.warn('Last heartbeat was not acked — waiting one more interval before reconnecting')
104
+ end
105
+ @last_heartbeat_acked = false
106
+ end
107
+ send_heartbeat(@session ? @session.sequence : 0)
108
+ end
109
+
110
+ def send_heartbeat(sequence)
111
+ send_packet(Internal::Gateway::Opcodes::HEARTBEAT, sequence)
112
+ end
113
+
114
+ def identify
115
+ compress = @compress_mode == :large
116
+ send_identify(@token, {
117
+ os: RUBY_PLATFORM,
118
+ browser: 'onyxcord',
119
+ device: 'onyxcord'
120
+ }, compress, LARGE_THRESHOLD, @shard_key, @intents)
121
+ end
122
+
123
+ def send_identify(token, properties, compress, large_threshold, shard_key = nil, intents = ALL_INTENTS)
124
+ data = {
125
+ token: token,
126
+ properties: properties,
127
+ compress: compress,
128
+ large_threshold: large_threshold,
129
+ intents: intents
130
+ }
131
+ data[:shard] = shard_key if shard_key
132
+ send_packet(Internal::Gateway::Opcodes::IDENTIFY, data)
133
+ end
134
+
135
+ def send_status_update(status, since, game, afk)
136
+ send_packet(Internal::Gateway::Opcodes::PRESENCE, { status: status, since: since, game: game, afk: afk })
137
+ end
138
+
139
+ def send_voice_state_update(server_id, channel_id, self_mute, self_deaf)
140
+ send_packet(Internal::Gateway::Opcodes::VOICE_STATE, {
141
+ guild_id: server_id, channel_id: channel_id,
142
+ self_mute: self_mute, self_deaf: self_deaf
143
+ })
144
+ end
145
+
146
+ def resume
147
+ send_resume(@token, @session.session_id, @session.sequence)
148
+ end
149
+
150
+ def reconnect(attempt_resume = true)
151
+ @session.suspend if @session && attempt_resume
152
+ @session.invalidate if attempt_resume && @session && !@received_hello
153
+ @instant_reconnect = true
154
+ @should_reconnect = true
155
+ close(4000)
156
+ end
157
+
158
+ def send_resume(token, session_id, seq)
159
+ send_packet(Internal::Gateway::Opcodes::RESUME, { token: token, session_id: session_id, seq: seq })
160
+ end
161
+
162
+ def send_request_members(server_id, query, limit)
163
+ send_packet(Internal::Gateway::Opcodes::REQUEST_MEMBERS, { guild_id: server_id, query: query, limit: limit })
164
+ end
165
+
166
+ def send_packet(opcode, packet)
167
+ send({ op: opcode, d: packet }.to_json)
168
+ end
169
+
170
+ def send_raw(data, _type = :text)
171
+ send(data)
172
+ end
173
+
174
+ private
175
+
176
+ def setup_heartbeats(interval)
177
+ @last_heartbeat_acked = true
178
+ @missed_heartbeat_acks = 0
179
+ return if @heartbeat_task
180
+
181
+ @heartbeat_interval = interval
182
+ @heartbeat_task = @reactor_task&.async do
183
+ loop do
184
+ if (@session && !@session.suspended?) || !@session
185
+ Internal::AsyncRuntime.sleep(@heartbeat_interval)
186
+ if !@closed && @connection
187
+ @bot.raise_heartbeat_event
188
+ heartbeat
189
+ else
190
+ LOGGER.debug('Tried to heartbeat without connection — skipping.')
191
+ end
192
+ else
193
+ Internal::AsyncRuntime.sleep(1)
194
+ end
195
+ rescue StandardError => e
196
+ LOGGER.error('Error while heartbeating!')
197
+ LOGGER.log_exception(e)
198
+ end
199
+ end
200
+ end
201
+
202
+ def connect_loop
203
+ @falloff = 1.0
204
+ @should_reconnect = true
205
+
206
+ loop do
207
+ connect
208
+ break unless @should_reconnect
209
+
210
+ if @instant_reconnect
211
+ LOGGER.info('Instant reconnection — reconnecting now')
212
+ @instant_reconnect = false
213
+ else
214
+ wait_for_reconnect
215
+ end
216
+ end
217
+ end
218
+
219
+ def wait_for_reconnect
220
+ LOGGER.debug("Reconnecting in #{@falloff} seconds.")
221
+ Internal::AsyncRuntime.sleep(@falloff)
222
+ @falloff *= 1.5
223
+ @falloff = 115 + (rand * 10) if @falloff > 120
224
+ end
225
+
226
+ def find_gateway
227
+ response = REST.gateway(@token)
228
+ JSON.parse(response)['url']
229
+ end
230
+
231
+ def process_gateway
232
+ raw_url = @session&.resume_gateway_url || find_gateway
233
+ raw_url += '/' unless raw_url.end_with?('/')
234
+
235
+ query = if @compress_mode == :stream
236
+ "?encoding=json&v=#{GATEWAY_VERSION}&compress=zlib-stream"
237
+ else
238
+ "?encoding=json&v=#{GATEWAY_VERSION}"
239
+ end
240
+
241
+ raw_url + query
242
+ end
243
+
244
+ def connect
245
+ LOGGER.debug('Connecting')
246
+
247
+ url = process_gateway
248
+ LOGGER.debug("Gateway URL: #{url}")
249
+
250
+ @zlib_reader = Zlib::Inflate.new
251
+ @pipe_broken = false
252
+ @received_hello = false
253
+ @closed = false
254
+
255
+ endpoint = websocket_endpoint(url)
256
+
257
+ Async::WebSocket::Client.connect(endpoint, extensions: nil) do |connection|
258
+ @connection = connection
259
+ LOGGER.debug('WebSocket connected')
260
+
261
+ handle_open
262
+
263
+ while (message = connection.read)
264
+ handle_message(message.to_str)
265
+ end
266
+ end
267
+ rescue Protocol::WebSocket::ClosedError, EOFError => e
268
+ LOGGER.warn("Gateway websocket closed (#{e.class}); reconnecting.")
269
+ begin
270
+ @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
271
+ rescue StandardError
272
+ nil
273
+ end
274
+ rescue StandardError => e
275
+ @session&.invalidate unless @received_hello
276
+ LOGGER.error('Error connecting to gateway!')
277
+ LOGGER.log_exception(e)
278
+ ensure
279
+ @session&.invalidate unless @received_hello
280
+ @closed = true
281
+ @connection = nil
282
+ end
283
+
284
+ def websocket_endpoint(url)
285
+ Async::HTTP::Endpoint.parse(
286
+ url,
287
+ protocol: Async::HTTP::Protocol::HTTP11,
288
+ alpn_protocols: ['http/1.1']
289
+ )
290
+ end
291
+
292
+ def handle_open; end
293
+
294
+ def handle_error(e)
295
+ LOGGER.error('Error in gateway loop!')
296
+ LOGGER.log_exception(e)
297
+ end
298
+
299
+ ZLIB_SUFFIX = "\x00\x00\xFF\xFF".b.freeze
300
+ private_constant :ZLIB_SUFFIX
301
+
302
+ def handle_message(msg)
303
+ case @compress_mode
304
+ when :large
305
+ msg = Zlib::Inflate.inflate(msg) if msg.byteslice(0) == 'x'
306
+ when :stream
307
+ @zlib_reader << msg
308
+ return if msg.bytesize < 4 || msg.byteslice(-4, 4) != ZLIB_SUFFIX
309
+
310
+ msg = @zlib_reader.inflate('')
311
+ end
312
+
313
+ packet = JSON.parse(msg)
314
+ op = packet['op'].to_i
315
+
316
+ LOGGER.in(packet)
317
+
318
+ @session.sequence = packet['s'] if packet['s'] && @session
319
+
320
+ case op
321
+ when Internal::Gateway::Opcodes::DISPATCH
322
+ handle_dispatch(packet)
323
+ when Internal::Gateway::Opcodes::HELLO
324
+ handle_hello(packet)
325
+ when Internal::Gateway::Opcodes::RECONNECT
326
+ handle_reconnect
327
+ when Internal::Gateway::Opcodes::INVALIDATE_SESSION
328
+ handle_invalidate_session(packet)
329
+ when Internal::Gateway::Opcodes::HEARTBEAT_ACK
330
+ handle_heartbeat_ack(packet)
331
+ when Internal::Gateway::Opcodes::HEARTBEAT
332
+ handle_heartbeat(packet)
333
+ else
334
+ LOGGER.warn("Invalid opcode #{op}: #{msg}")
335
+ end
336
+ end
337
+
338
+ # Op 0
339
+ def handle_dispatch(packet)
340
+ data = packet['d']
341
+ type = packet['t'].intern
342
+
343
+ case type
344
+ when :READY
345
+ LOGGER.info("Discord gateway v#{data['v']}, requested: #{GATEWAY_VERSION}")
346
+ @session = Internal::Gateway::Session.new(data['session_id'], data['resume_gateway_url'])
347
+ @session.sequence = 0
348
+ @bot.__send__(:notify_ready) if @intents && @intents.nobits?(INTENTS[:servers])
349
+ when :RESUMED
350
+ LOGGER.info 'Resumed'
351
+ return
352
+ end
353
+
354
+ @bot.dispatch(packet)
355
+ end
356
+
357
+ # Op 1
358
+ def handle_heartbeat(packet)
359
+ send_heartbeat(packet['s'])
360
+ end
361
+
362
+ # Op 7
363
+ def handle_reconnect
364
+ LOGGER.debug('Received op 7, reconnecting')
365
+ reconnect
366
+ end
367
+
368
+ # Op 9
369
+ def handle_invalidate_session(packet)
370
+ LOGGER.debug('Received op 9, invalidating session')
371
+ if @session
372
+ if packet['d'] == true
373
+ reconnect
374
+ else
375
+ @session.invalidate
376
+ end
377
+ else
378
+ LOGGER.warn('Op 9 without session!')
379
+ end
380
+ identify
381
+ end
382
+
383
+ # Op 10
384
+ def handle_hello(packet)
385
+ @received_hello = true
386
+ LOGGER.debug('Hello!')
387
+ interval = packet['d']['heartbeat_interval'].to_f / 1000.0
388
+ setup_heartbeats(interval)
389
+ LOGGER.debug("Trace: #{packet['d']['_trace']}")
390
+ LOGGER.debug("Session: #{@session.inspect}")
391
+
392
+ if @session&.should_resume?
393
+ @session.resume
394
+ resume
395
+ else
396
+ identify
397
+ end
398
+ end
399
+
400
+ # Op 11
401
+ def handle_heartbeat_ack(packet)
402
+ LOGGER.debug("Heartbeat ACK: #{packet.inspect}")
403
+ return unless @check_heartbeat_acks
404
+
405
+ @last_heartbeat_acked = true
406
+ @missed_heartbeat_acks = 0
407
+ end
408
+
409
+ def handle_internal_close(e)
410
+ close
411
+ handle_close(e)
412
+ end
413
+
414
+ def handle_close(e)
415
+ @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
416
+
417
+ if e.respond_to?(:code)
418
+ LOGGER.error("WebSocket close frame! Code: #{e.code}")
419
+ LOGGER.error('Privileged intents not authorized. Enable them in the Discord developer portal.') if e.code == 4014
420
+ @should_reconnect = false if FATAL_CLOSE_CODES.include?(e.code)
421
+ elsif e.is_a?(Exception)
422
+ LOGGER.error('WebSocket closed due to error!')
423
+ LOGGER.log_exception(e)
424
+ else
425
+ LOGGER.error("WebSocket closed: #{e&.inspect || '(no info)'}")
426
+ end
427
+ end
428
+
429
+ def send(data, _type = :text, _code = nil)
430
+ LOGGER.out(data)
431
+
432
+ raise 'Tried to send to websocket while not connected!' unless @connection && !@closed
433
+
434
+ @send_limiter.wait
435
+
436
+ @connection.write(data)
437
+ @connection.flush
438
+ rescue StandardError => e
439
+ @pipe_broken = true
440
+ handle_internal_close(e)
441
+ end
442
+
443
+ def close(_code = 1000)
444
+ return if @closed
445
+
446
+ @session&.suspend
447
+ @closed = true
448
+
449
+ begin
450
+ @connection&.close
451
+ rescue StandardError
452
+ # Ignore close errors
453
+ end
454
+
455
+ @connection = nil
456
+ handle_close(nil)
457
+ end
458
+ end
459
+ end
460
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module Interactions
5
+ class Command
6
+ attr_reader :name, :description, :type, :attributes, :options, :block
7
+
8
+ TYPES = {
9
+ chat_input: 1,
10
+ user: 2,
11
+ message: 3,
12
+ primary_entry_point: 4
13
+ }.freeze
14
+
15
+ DESCRIPTION_TYPES = %i[chat_input primary_entry_point].freeze
16
+
17
+ def self.chat_input(name, description:, **attributes, &block)
18
+ new(name, description: description, type: :chat_input, **attributes, &block)
19
+ end
20
+
21
+ def self.user(name, **attributes, &block)
22
+ new(name, description: '', type: :user, **attributes, &block)
23
+ end
24
+
25
+ def self.message(name, **attributes, &block)
26
+ new(name, description: '', type: :message, **attributes, &block)
27
+ end
28
+
29
+ def self.primary_entry_point(name, description:, **attributes, &block)
30
+ new(name, description: description, type: :primary_entry_point, **attributes, &block)
31
+ end
32
+
33
+ def initialize(name, description: '', type: :chat_input, **attributes, &block)
34
+ @name = name.to_s
35
+ @description = description
36
+ @type = type
37
+ @attributes = attributes
38
+ @options = []
39
+ @block = block
40
+ @executor = nil
41
+ @default_member_permissions = attributes[:default_member_permissions]
42
+ @nsfw = attributes[:nsfw]
43
+ @contexts = attributes[:contexts]
44
+ @dm_permission = attributes.fetch(:dm_permission, true)
45
+ @name_localizations = attributes[:name_localizations]
46
+ @description_localizations = attributes[:description_localizations]
47
+ @integration_types = attributes[:integration_types]
48
+ end
49
+
50
+ def parse(&block)
51
+ instance_eval(&block) if block
52
+ self
53
+ end
54
+
55
+ def execute(&block)
56
+ @executor = block
57
+ end
58
+
59
+ def call(context)
60
+ return unless @executor
61
+
62
+ @executor.call(context)
63
+ end
64
+
65
+ def to_h
66
+ data = {
67
+ name: @name,
68
+ type: TYPES[@type] || @type
69
+ }
70
+
71
+ if DESCRIPTION_TYPES.include?(@type)
72
+ data[:description] = @description
73
+ data[:description_localizations] = @description_localizations if @description_localizations
74
+ end
75
+
76
+ data[:name_localizations] = @name_localizations if @name_localizations
77
+ data[:options] = @options.map(&:to_h) unless @options.empty?
78
+ data[:default_member_permissions] = @default_member_permissions.to_s if @default_member_permissions
79
+ data[:dm_permission] = @dm_permission
80
+ data[:nsfw] = @nsfw if @nsfw
81
+ data[:contexts] = @contexts if @contexts
82
+ data[:integration_types] = @integration_types if @integration_types
83
+
84
+ data
85
+ end
86
+
87
+ Option::OPTION_METHODS.each do |method_name, option_type|
88
+ define_method(method_name) do |name, description = '', **attrs, &blk|
89
+ opt = Option.new(name, description, option_type, **attrs, &blk)
90
+ @options << opt
91
+ opt
92
+ end
93
+ end
94
+
95
+ def subcommand(name, description, &block)
96
+ sub = Option.new(name, description, :subcommand, &block)
97
+ @options << sub
98
+ sub
99
+ end
100
+
101
+ def subcommand_group(name, description, &block)
102
+ group = Option.new(name, description, :subcommand_group, &block)
103
+ @options << group
104
+ group
105
+ end
106
+
107
+ def method_missing(method_name, *args, **kwargs, &block)
108
+ if @block && @block.arity.positive?
109
+ @block.call(Context::Proxy.new(self, method_name, args, kwargs, block))
110
+ else
111
+ super
112
+ end
113
+ end
114
+
115
+ def respond_to_missing?(_method_name, _include_private = false)
116
+ true
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module Interactions
5
+ class Context
6
+ attr_reader :event, :command
7
+
8
+ def initialize(event, command)
9
+ @event = event
10
+ @command = command
11
+ end
12
+
13
+ def bot
14
+ event.bot
15
+ end
16
+
17
+ def user
18
+ event.user
19
+ end
20
+
21
+ def member
22
+ return unless event.respond_to?(:interaction) && event.interaction
23
+
24
+ event.interaction.user if event.interaction.user.is_a?(OnyxCord::Member)
25
+ end
26
+
27
+ def guild
28
+ event.server
29
+ end
30
+
31
+ def guild_id
32
+ event.server_id
33
+ end
34
+
35
+ def channel
36
+ event.channel
37
+ end
38
+
39
+ def channel_id
40
+ event.channel_id
41
+ end
42
+
43
+ def server
44
+ event.server
45
+ end
46
+
47
+ def server_id
48
+ event.server_id
49
+ end
50
+
51
+ def locale
52
+ event.user_locale
53
+ end
54
+
55
+ def guild_locale
56
+ event.server_locale
57
+ end
58
+
59
+ def options
60
+ return {} unless event.data
61
+
62
+ if event.data['options']
63
+ result = {}
64
+ event.data['options'].each do |opt|
65
+ key = opt['name'].to_sym
66
+ result[key] = opt['value']
67
+ end
68
+ result
69
+ else
70
+ {}
71
+ end
72
+ end
73
+
74
+ def respond(...)
75
+ event.respond(...)
76
+ end
77
+
78
+ def defer(...)
79
+ event.defer(...)
80
+ end
81
+
82
+ def edit_original(...)
83
+ event.edit_response(...)
84
+ end
85
+
86
+ def delete_original
87
+ event.delete_response
88
+ end
89
+
90
+ def followup(...)
91
+ event.send_message(...)
92
+ end
93
+
94
+ class Proxy
95
+ def initialize(command, method_name, args, kwargs, block)
96
+ @command = command
97
+ @method_name = method_name
98
+ @args = args
99
+ @kwargs = kwargs
100
+ @block = block
101
+ end
102
+
103
+ def to_h
104
+ {}
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end