mij-discord 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/mij-discord.rb +56 -0
- data/lib/mij-discord/bot.rb +579 -0
- data/lib/mij-discord/cache.rb +298 -0
- data/lib/mij-discord/core/api.rb +228 -0
- data/lib/mij-discord/core/api/channel.rb +416 -0
- data/lib/mij-discord/core/api/invite.rb +43 -0
- data/lib/mij-discord/core/api/server.rb +465 -0
- data/lib/mij-discord/core/api/user.rb +144 -0
- data/lib/mij-discord/core/errors.rb +106 -0
- data/lib/mij-discord/core/gateway.rb +505 -0
- data/lib/mij-discord/data.rb +65 -0
- data/lib/mij-discord/data/application.rb +38 -0
- data/lib/mij-discord/data/channel.rb +404 -0
- data/lib/mij-discord/data/embed.rb +115 -0
- data/lib/mij-discord/data/emoji.rb +62 -0
- data/lib/mij-discord/data/invite.rb +87 -0
- data/lib/mij-discord/data/member.rb +174 -0
- data/lib/mij-discord/data/message.rb +206 -0
- data/lib/mij-discord/data/permissions.rb +121 -0
- data/lib/mij-discord/data/role.rb +99 -0
- data/lib/mij-discord/data/server.rb +359 -0
- data/lib/mij-discord/data/user.rb +173 -0
- data/lib/mij-discord/data/voice.rb +68 -0
- data/lib/mij-discord/events.rb +133 -0
- data/lib/mij-discord/events/basic.rb +80 -0
- data/lib/mij-discord/events/channel.rb +50 -0
- data/lib/mij-discord/events/member.rb +66 -0
- data/lib/mij-discord/events/message.rb +150 -0
- data/lib/mij-discord/events/server.rb +102 -0
- data/lib/mij-discord/logger.rb +20 -0
- data/lib/mij-discord/version.rb +5 -0
- data/mij-discord.gemspec +31 -0
- 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
|