mij-discord 1.0.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE +21 -0
  6. data/README.md +35 -0
  7. data/Rakefile +10 -0
  8. data/bin/console +7 -0
  9. data/bin/setup +6 -0
  10. data/lib/mij-discord.rb +56 -0
  11. data/lib/mij-discord/bot.rb +579 -0
  12. data/lib/mij-discord/cache.rb +298 -0
  13. data/lib/mij-discord/core/api.rb +228 -0
  14. data/lib/mij-discord/core/api/channel.rb +416 -0
  15. data/lib/mij-discord/core/api/invite.rb +43 -0
  16. data/lib/mij-discord/core/api/server.rb +465 -0
  17. data/lib/mij-discord/core/api/user.rb +144 -0
  18. data/lib/mij-discord/core/errors.rb +106 -0
  19. data/lib/mij-discord/core/gateway.rb +505 -0
  20. data/lib/mij-discord/data.rb +65 -0
  21. data/lib/mij-discord/data/application.rb +38 -0
  22. data/lib/mij-discord/data/channel.rb +404 -0
  23. data/lib/mij-discord/data/embed.rb +115 -0
  24. data/lib/mij-discord/data/emoji.rb +62 -0
  25. data/lib/mij-discord/data/invite.rb +87 -0
  26. data/lib/mij-discord/data/member.rb +174 -0
  27. data/lib/mij-discord/data/message.rb +206 -0
  28. data/lib/mij-discord/data/permissions.rb +121 -0
  29. data/lib/mij-discord/data/role.rb +99 -0
  30. data/lib/mij-discord/data/server.rb +359 -0
  31. data/lib/mij-discord/data/user.rb +173 -0
  32. data/lib/mij-discord/data/voice.rb +68 -0
  33. data/lib/mij-discord/events.rb +133 -0
  34. data/lib/mij-discord/events/basic.rb +80 -0
  35. data/lib/mij-discord/events/channel.rb +50 -0
  36. data/lib/mij-discord/events/member.rb +66 -0
  37. data/lib/mij-discord/events/message.rb +150 -0
  38. data/lib/mij-discord/events/server.rb +102 -0
  39. data/lib/mij-discord/logger.rb +20 -0
  40. data/lib/mij-discord/version.rb +5 -0
  41. data/mij-discord.gemspec +31 -0
  42. metadata +154 -0
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MijDiscord::Cache
4
+ class BotCache
5
+ def initialize(bot)
6
+ @bot = bot
7
+
8
+ reset
9
+ end
10
+
11
+ def reset
12
+ @servers, @channels, @users = {}, {}, {}
13
+ @pm_channels, @restricted_channels = {}, {}
14
+ end
15
+
16
+ def list_servers
17
+ @servers.values
18
+ end
19
+
20
+ def list_channels
21
+ @channels.values
22
+ end
23
+
24
+ def list_users
25
+ @users.values
26
+ end
27
+
28
+ def get_server(key, local: false)
29
+ id = key&.to_id
30
+ return @servers[id] if @servers.has_key?(id)
31
+ return nil if local
32
+
33
+ begin
34
+ response = MijDiscord::Core::API::Server.resolve(@bot.token, id)
35
+ rescue RestClient::ResourceNotFound
36
+ return nil
37
+ end
38
+
39
+ @servers[id] = MijDiscord::Data::Server.new(JSON.parse(response), @bot)
40
+ end
41
+
42
+ def get_channel(key, server, local: false)
43
+ id = key&.to_id
44
+ return @channels[id] if @channels.has_key?(id)
45
+ raise MijDiscord::Errors::NoPermission if @restricted_channels[id]
46
+ return nil if local
47
+
48
+ begin
49
+ response = MijDiscord::Core::API::Channel.resolve(@bot.token, id)
50
+ rescue RestClient::ResourceNotFound
51
+ return nil
52
+ rescue MijDiscord::Errors::NoPermission
53
+ @restricted_channels[id] = true
54
+ raise
55
+ end
56
+
57
+ channel = @channels[id] = MijDiscord::Data::Channel.create(JSON.parse(response), @bot, server)
58
+ @pm_channels[channel.recipient.id] = channel if channel.pm?
59
+
60
+ if (server = channel.server)
61
+ server.cache.put_channel!(channel)
62
+ end
63
+
64
+ channel
65
+ end
66
+
67
+ def get_pm_channel(key, local: false)
68
+ id = key&.to_id
69
+ return @pm_channels[id] if @pm_channels.has_key?(id)
70
+ return nil if local
71
+
72
+ response = MijDiscord::Core::API::User.create_pm(@bot.token, id)
73
+ channel = MijDiscord::Data::Channel.create(JSON.parse(response), @bot, nil)
74
+
75
+ @channels[channel.id] = @pm_channels[id] = channel
76
+ end
77
+
78
+ def get_user(key, local: false)
79
+ id = key&.to_id
80
+ return @users[id] if @users.has_key?(id)
81
+ return nil if local
82
+
83
+ begin
84
+ response = MijDiscord::Core::API::User.resolve(@bot.token, id)
85
+ rescue RestClient::ResourceNotFound
86
+ return nil
87
+ end
88
+
89
+ @users[id] = MijDiscord::Data::User.new(JSON.parse(response), @bot)
90
+ end
91
+
92
+ def put_server(data, update: false)
93
+ id = data['id'].to_i
94
+ if @servers.has_key?(id)
95
+ @servers[id].update_data(data) if update
96
+ return @servers[id]
97
+ end
98
+
99
+ @servers[id] = MijDiscord::Data::Server.new(data, @bot)
100
+ end
101
+
102
+ def put_channel(data, server, update: false)
103
+ id = data['id'].to_i
104
+ if @channels.has_key?(id)
105
+ @channels[id].update_data(data) if update
106
+ return @channels[id]
107
+ end
108
+
109
+ channel = @channels[id] = MijDiscord::Data::Channel.create(data, @bot, server)
110
+ @pm_channels[channel.recipient.id] = channel if channel.pm?
111
+
112
+ if (server = channel.server)
113
+ server.cache.put_channel!(channel)
114
+ end
115
+
116
+ channel
117
+ end
118
+
119
+ def put_user(data, update: false)
120
+ id = data['id'].to_i
121
+ if @users.has_key?(id)
122
+ @users[id].update_data(data) if update
123
+ return @users[id]
124
+ end
125
+
126
+ @users[id] = MijDiscord::Data::User.new(data, @bot)
127
+ end
128
+
129
+ def remove_server(key)
130
+ @servers.delete(key&.to_id)
131
+ end
132
+
133
+ def remove_channel(key)
134
+ channel = @channels.delete(key&.to_id)
135
+ @pm_channels.delete(channel.recipient.id) if channel&.pm?
136
+
137
+ if (server = channel&.server)
138
+ server.cache.remove_channel(key)
139
+ end
140
+
141
+ channel
142
+ end
143
+
144
+ def remove_user(key)
145
+ @users.delete(key&.to_id)
146
+ end
147
+ end
148
+
149
+ class ServerCache
150
+ def initialize(server, bot)
151
+ @server, @bot = server, bot
152
+
153
+ reset
154
+ end
155
+
156
+ def reset
157
+ @channels, @members, @roles = {}, {}, {}
158
+ end
159
+
160
+ def list_members
161
+ @members.values
162
+ end
163
+
164
+ def list_roles
165
+ @roles.values
166
+ end
167
+
168
+ def list_channels
169
+ @channels.values
170
+ end
171
+
172
+ def get_member(key, local: false)
173
+ id = key&.to_id
174
+ return @members[id] if @members.has_key?(id)
175
+ return nil if local
176
+
177
+ begin
178
+ response = MijDiscord::Core::API::Server.resolve_member(@bot.token, @server.id, id)
179
+ rescue RestClient::ResourceNotFound
180
+ return nil
181
+ end
182
+
183
+ @members[id] = MijDiscord::Data::Member.new(JSON.parse(response), @server, @bot)
184
+ end
185
+
186
+ def get_role(key, local: false)
187
+ id = key&.to_id
188
+ return @roles[id] if @roles.has_key?(id)
189
+ return nil if local
190
+
191
+ # No API to get individual role
192
+ nil
193
+ end
194
+
195
+ def get_channel(key, local: false)
196
+ id = key&.to_id
197
+ return @channels[id] if @channels.has_key?(id)
198
+
199
+ channel = @bot.cache.get_channel(key, local: local)
200
+ return nil unless channel&.server == @server
201
+
202
+ @channels[channel.id] = channel
203
+ end
204
+
205
+ def put_member(data, update: false)
206
+ id = data['user']['id'].to_i
207
+ if @members.has_key?(id)
208
+ @members[id].update_data(data) if update
209
+ return @members[id]
210
+ end
211
+
212
+ @members[id] = MijDiscord::Data::Member.new(data, @server, @bot)
213
+ end
214
+
215
+ def put_role(data, update: false)
216
+ id = data['id'].to_i
217
+ if @roles.has_key?(id)
218
+ @roles[id].update_data(data) if update
219
+ return @roles[id]
220
+ end
221
+
222
+ @roles[id] = MijDiscord::Data::Role.new(data, @server, @bot)
223
+ end
224
+
225
+ def put_channel(data, update: false)
226
+ channel = @bot.cache.put_channel(data, @server, update: update)
227
+ @channels[channel.id] = channel
228
+ end
229
+
230
+ def put_channel!(channel)
231
+ @channels[channel.id] = channel
232
+ end
233
+
234
+ def remove_member(key)
235
+ @members.delete(key&.to_id)
236
+ end
237
+
238
+ def remove_role(key)
239
+ @roles.delete(key&.to_id)
240
+ end
241
+
242
+ def remove_channel(key)
243
+ channel = @channels.delete(key&.to_id)
244
+ @bot.cache.remove_channel(key) if channel
245
+
246
+ channel
247
+ end
248
+ end
249
+
250
+ class ChannelCache
251
+ MAX_MESSAGES = 200
252
+
253
+ def initialize(channel, bot, max_messages: MAX_MESSAGES)
254
+ @channel, @bot = channel, bot
255
+ @max_messages = max_messages
256
+
257
+ reset
258
+ end
259
+
260
+ def reset
261
+ @messages = {}
262
+ end
263
+
264
+ def get_message(key, local: false)
265
+ id = key&.to_id
266
+ return @messages[id] if @messages.has_key?(id)
267
+ return nil if local
268
+
269
+ begin
270
+ response = MijDiscord::Core::API::Channel.message(@bot.token, @channel.id, key)
271
+ rescue RestClient::ResourceNotFound
272
+ return nil
273
+ end
274
+
275
+ message = @messages.store(id, MijDiscord::Data::Message.new(JSON.parse(response), @bot))
276
+ @messages.shift while @messages.length > @max_messages
277
+
278
+ message
279
+ end
280
+
281
+ def put_message(data, update: false)
282
+ id = data['id'].to_i
283
+ if @messages.has_key?(id)
284
+ @messages[id].update_data(data) if update
285
+ return @messages[id]
286
+ end
287
+
288
+ message = @messages.store(id, MijDiscord::Data::Message.new(data, @bot))
289
+ @messages.shift while @messages.length > @max_messages
290
+
291
+ message
292
+ end
293
+
294
+ def remove_message(key)
295
+ @messages.delete(key&.to_id)
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MijDiscord::Core::API
4
+ APIBASE_URL = 'https://discordapp.com/api/v6'
5
+
6
+ CDN_URL = 'https://cdn.discordapp.com'
7
+
8
+ class << self
9
+ attr_accessor :bot_name
10
+
11
+ def user_agent
12
+ bot_name = @bot_name || 'generic'
13
+ ua_base = "DiscordBot (https://github.com/Mijyuoon/mij-discord, v#{MijDiscord::VERSION})"
14
+
15
+ "#{ua_base} mij-discord/#{MijDiscord::VERSION} #{bot_name}"
16
+ end
17
+
18
+ # Make an icon URL from server and icon IDs
19
+ def icon_url(server_id, icon_id, format = :webp)
20
+ "#{CDN_URL}/icons/#{server_id}/#{icon_id}.#{format}"
21
+ end
22
+
23
+ # Make an icon URL from application and icon IDs
24
+ def app_icon_url(app_id, icon_id, format = :webp)
25
+ "#{CDN_URL}/app-icons/#{app_id}/#{icon_id}.#{format}"
26
+ end
27
+
28
+ # Make a widget picture URL from server ID
29
+ def widget_url(server_id, style = 'shield')
30
+ "#{APIBASE_URL}/guilds/#{server_id}/widget.png?style=#{style}"
31
+ end
32
+
33
+ # Make a splash URL from server and splash IDs
34
+ def splash_url(server_id, splash_id)
35
+ "#{CDN_URL}{/splashes/#{server_id}/#{splash_id}.jpg"
36
+ end
37
+
38
+ # Make an emoji icon URL from emoji ID
39
+ def emoji_icon_url(emoji_id, format = :webp)
40
+ "#{CDN_URL}/emojis/#{emoji_id}.#{format}"
41
+ end
42
+
43
+ # Login to the server
44
+ def login(email, password)
45
+ request(
46
+ :auth_login,
47
+ nil,
48
+ :post,
49
+ "#{APIBASE_URL}/auth/login",
50
+ email: email,
51
+ password: password
52
+ )
53
+ end
54
+
55
+ # Logout from the server
56
+ def logout(token)
57
+ request(
58
+ :auth_logout,
59
+ nil,
60
+ :post,
61
+ "#{APIBASE_URL}/auth/logout",
62
+ nil,
63
+ Authorization: token
64
+ )
65
+ end
66
+
67
+ # Create an OAuth application
68
+ def create_oauth_application(token, name, redirect_uris)
69
+ request(
70
+ :oauth2_applications,
71
+ nil,
72
+ :post,
73
+ "#{APIBASE_URL}/oauth2/applications",
74
+ { name: name, redirect_uris: redirect_uris }.to_json,
75
+ Authorization: token,
76
+ content_type: :json
77
+ )
78
+ end
79
+
80
+ # Change an OAuth application's properties
81
+ def update_oauth_application(token, name, redirect_uris, description = '', icon = nil)
82
+ request(
83
+ :oauth2_applications,
84
+ nil,
85
+ :put,
86
+ "#{APIBASE_URL}/oauth2/applications",
87
+ { name: name, redirect_uris: redirect_uris, description: description, icon: icon }.to_json,
88
+ Authorization: token,
89
+ content_type: :json
90
+ )
91
+ end
92
+
93
+ # Get the bot's OAuth application's information
94
+ def oauth_application(token)
95
+ request(
96
+ :oauth2_applications_me,
97
+ nil,
98
+ :get,
99
+ "#{APIBASE_URL}/oauth2/applications/@me",
100
+ Authorization: token
101
+ )
102
+ end
103
+
104
+ # Acknowledge that a message has been received
105
+ # The last acknowledged message will be sent in the ready packet,
106
+ # so this is an easy way to catch up on messages
107
+ def acknowledge_message(token, channel_id, message_id)
108
+ request(
109
+ :channels_cid_messages_mid_ack,
110
+ nil, # This endpoint is unavailable for bot accounts and thus isn't subject to its rate limit requirements.
111
+ :post,
112
+ "#{APIBASE_URL}/channels/#{channel_id}/messages/#{message_id}/ack",
113
+ nil,
114
+ Authorization: token
115
+ )
116
+ end
117
+
118
+ # Get the gateway to be used
119
+ def gateway(token)
120
+ request(
121
+ :gateway,
122
+ nil,
123
+ :get,
124
+ "#{APIBASE_URL}/gateway",
125
+ Authorization: token
126
+ )
127
+ end
128
+
129
+ # Validate a token (this request will fail if the token is invalid)
130
+ def validate_token(token)
131
+ request(
132
+ :auth_login,
133
+ nil,
134
+ :post,
135
+ "#{APIBASE_URL}/auth/login",
136
+ {}.to_json,
137
+ Authorization: token,
138
+ content_type: :json
139
+ )
140
+ end
141
+
142
+ # Get a list of available voice regions
143
+ def voice_regions(token)
144
+ request(
145
+ :voice_regions,
146
+ nil,
147
+ :get,
148
+ "#{APIBASE_URL}/voice/regions",
149
+ Authorization: token,
150
+ content_type: :json
151
+ )
152
+ end
153
+
154
+ def raw_request(type, attributes)
155
+ RestClient.send(type, *attributes)
156
+ rescue RestClient::Forbidden
157
+ raise MijDiscord::Core::Errors::NoPermission
158
+ rescue RestClient::BadGateway
159
+ MijDiscord::LOGGER.warn('HTTP') { 'Received 502 Bad Gateway during API request' }
160
+ retry
161
+ end
162
+
163
+ def request(key, major_param, type, *attributes)
164
+ ratelimit_delta, response = nil, nil
165
+
166
+ if (params = attributes.last).is_a?(Hash)
167
+ params[:user_agent] = user_agent
168
+ ratelimit_delta = params.delete(:header_bypass_delay)
169
+ end
170
+
171
+ key = [key, major_param].freeze
172
+ key_mutex = (@rate_limit_mutex[key] ||= Mutex.new)
173
+ global_mutex = @rate_limit_mutex[:global]
174
+
175
+ begin
176
+ mutex_wait(key_mutex)
177
+ mutex_wait(global_mutex) if global_mutex.locked?
178
+
179
+ response = raw_request(type, attributes)
180
+ rescue RestClient::TooManyRequests => e
181
+ response = e.response
182
+
183
+ is_global = response.headers[:x_ratelimit_global]
184
+ mutex = is_global == 'true' ? global_mutex : key_mutex
185
+
186
+ unless mutex.locked?
187
+ response = JSON.parse(e.response)
188
+ retry_after = response['retry_after'].to_i / 1000.0
189
+
190
+ MijDiscord::LOGGER.info('HTTP') { "Hit Discord rate limit on <#{key}>, waiting for #{retry_after} seconds" }
191
+ sync_wait(retry_after, mutex)
192
+ end
193
+
194
+ retry
195
+ rescue RestClient::Exception => e
196
+ response = e.response
197
+ raise
198
+ ensure
199
+ headers = response&.headers
200
+ if headers && headers[:x_ratelimit_remaining] == '0' && !key_mutex.locked?
201
+ unless ratelimit_delta
202
+ now = Time.rfc2822(headers[:date])
203
+ reset = Time.at(headers[:x_ratelimit_reset].to_i)
204
+ ratelimit_delta = reset - now
205
+ end
206
+
207
+ sync_wait(ratelimit_delta, key_mutex)
208
+ end
209
+ end
210
+
211
+ response
212
+ end
213
+
214
+ private
215
+
216
+ def sync_wait(time, mutex)
217
+ mutex.synchronize { sleep(time) }
218
+ end
219
+
220
+ def mutex_wait(mutex)
221
+ mutex.lock
222
+ mutex.unlock
223
+ end
224
+ end
225
+
226
+ # Initialize rate limit mutexes
227
+ @rate_limit_mutex = { global: Mutex.new }
228
+ end