mij-discord 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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