mij-discord 1.0.9 → 1.0.10
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.
- checksums.yaml +4 -4
- data/lib/mij-discord/bot.rb +88 -36
- data/lib/mij-discord/cache.rb +309 -315
- data/lib/mij-discord/core/api.rb +238 -241
- data/lib/mij-discord/core/api/server.rb +464 -477
- data/lib/mij-discord/core/api/user.rb +142 -154
- data/lib/mij-discord/data/embed.rb +226 -226
- data/lib/mij-discord/data/member.rb +173 -175
- data/lib/mij-discord/data/server.rb +404 -467
- data/lib/mij-discord/data/user.rb +294 -296
- data/lib/mij-discord/events/basic.rb +14 -0
- data/lib/mij-discord/events/channel.rb +2 -0
- data/lib/mij-discord/events/server.rb +91 -101
- data/lib/mij-discord/version.rb +4 -4
- metadata +2 -2
data/lib/mij-discord/core/api.rb
CHANGED
@@ -1,242 +1,239 @@
|
|
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
|
-
def user_agent(auth)
|
10
|
-
case auth&.type
|
11
|
-
when :bot
|
12
|
-
bot_name = auth.name || 'generic'
|
13
|
-
ua_base = "DiscordBot (https://github.com/Mijyuoon/mij-discord, v#{MijDiscord::VERSION})"
|
14
|
-
"#{ua_base} mij-discord/#{MijDiscord::VERSION} #{bot_name}"
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
:
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
nil,
|
63
|
-
:
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
:
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
:
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
nil,
|
114
|
-
:
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
:
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
:
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
retry_after
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
# Initialize rate limit mutexes
|
241
|
-
@rate_limit_mutex = { global: Mutex.new }
|
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
|
+
def user_agent(auth)
|
10
|
+
case auth&.type
|
11
|
+
when :bot
|
12
|
+
bot_name = auth.name || 'generic'
|
13
|
+
ua_base = "DiscordBot (https://github.com/Mijyuoon/mij-discord, v#{MijDiscord::VERSION})"
|
14
|
+
"#{ua_base} mij-discord/#{MijDiscord::VERSION} #{bot_name}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Make an icon URL from server and icon IDs
|
19
|
+
def icon_url(server_id, icon_id, format = :png)
|
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 = :png)
|
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 = :png)
|
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(auth)
|
57
|
+
request(
|
58
|
+
:auth_logout,
|
59
|
+
nil,
|
60
|
+
:post,
|
61
|
+
"#{APIBASE_URL}/auth/logout",
|
62
|
+
nil,
|
63
|
+
Authorization: auth
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create an OAuth application
|
68
|
+
def create_oauth_application(auth, 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: auth,
|
76
|
+
content_type: :json
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Change an OAuth application's properties
|
81
|
+
def update_oauth_application(auth, 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: auth,
|
89
|
+
content_type: :json
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Get the bot's OAuth application's information
|
94
|
+
def oauth_application(auth)
|
95
|
+
request(
|
96
|
+
:oauth2_applications_me,
|
97
|
+
nil,
|
98
|
+
:get,
|
99
|
+
"#{APIBASE_URL}/oauth2/applications/@me",
|
100
|
+
Authorization: auth
|
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(auth, 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: auth
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get the gateway to be used
|
119
|
+
def gateway(auth)
|
120
|
+
request(
|
121
|
+
:gateway,
|
122
|
+
nil,
|
123
|
+
:get,
|
124
|
+
"#{APIBASE_URL}/gateway",
|
125
|
+
Authorization: auth,
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Validate a token (this request will fail if the token is invalid)
|
130
|
+
def validate_token(auth)
|
131
|
+
request(
|
132
|
+
:auth_login,
|
133
|
+
nil,
|
134
|
+
:post,
|
135
|
+
"#{APIBASE_URL}/auth/login",
|
136
|
+
{}.to_json,
|
137
|
+
Authorization: auth,
|
138
|
+
content_type: :json
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Get a list of available voice regions
|
143
|
+
def voice_regions(auth)
|
144
|
+
request(
|
145
|
+
:voice_regions,
|
146
|
+
nil,
|
147
|
+
:get,
|
148
|
+
"#{APIBASE_URL}/voice/regions",
|
149
|
+
Authorization: auth,
|
150
|
+
content_type: :json
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
def raw_request(type, *attributes)
|
155
|
+
RestClient.send(type, *attributes)
|
156
|
+
rescue RestClient::RequestFailed => e
|
157
|
+
# Holy fuck, Discord…
|
158
|
+
if (klazz = MijDiscord::Errors::HTTP_ERRORS[e.http_code])
|
159
|
+
data = JSON.parse(e.response)
|
160
|
+
if data['message'] || data['code']
|
161
|
+
raise klazz.new(data['code'], data['message'], e.response)
|
162
|
+
elsif (error = (data['content'] || data['embed']).join)
|
163
|
+
if MijDiscord::Errors::MessageTooLong.match_pattern?(error)
|
164
|
+
raise MijDiscord::Errors::MessageTooLong.new(error, e.response)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
raise
|
169
|
+
rescue RestClient::BadGateway
|
170
|
+
MijDiscord::LOGGER.warn('HTTP') { 'Received 502 Bad Gateway during API request' }
|
171
|
+
retry
|
172
|
+
end
|
173
|
+
|
174
|
+
def request(key, major_param, type, *attributes)
|
175
|
+
ratelimit_delta, response = nil, nil
|
176
|
+
|
177
|
+
if (params = attributes.last).is_a?(Hash)
|
178
|
+
params[:user_agent] = user_agent(params[:Authorization])
|
179
|
+
ratelimit_delta = params.delete(:header_bypass_delay)
|
180
|
+
end
|
181
|
+
|
182
|
+
key = [key, major_param].freeze
|
183
|
+
key_mutex = (@rate_limit_mutex[key] ||= Mutex.new)
|
184
|
+
global_mutex = @rate_limit_mutex[:global]
|
185
|
+
|
186
|
+
begin
|
187
|
+
mutex_wait(key_mutex)
|
188
|
+
mutex_wait(global_mutex) if global_mutex.locked?
|
189
|
+
|
190
|
+
response = raw_request(type, *attributes)
|
191
|
+
rescue RestClient::TooManyRequests => e
|
192
|
+
response = e.response
|
193
|
+
|
194
|
+
is_global = response.headers[:x_ratelimit_global]
|
195
|
+
mutex = is_global == 'true' ? global_mutex : key_mutex
|
196
|
+
|
197
|
+
unless mutex.locked?
|
198
|
+
response = JSON.parse(e.response)
|
199
|
+
retry_after = response['retry_after'].to_i / 1000.0
|
200
|
+
|
201
|
+
MijDiscord::LOGGER.info('HTTP') { "Hit Discord rate limit on <#{key}>, waiting for #{retry_after} seconds" }
|
202
|
+
sync_wait(retry_after, mutex)
|
203
|
+
end
|
204
|
+
|
205
|
+
retry
|
206
|
+
rescue RestClient::Exception => e
|
207
|
+
response = e.response
|
208
|
+
raise
|
209
|
+
ensure
|
210
|
+
headers = response&.headers
|
211
|
+
if headers && headers[:x_ratelimit_remaining] == '0' && !key_mutex.locked?
|
212
|
+
unless ratelimit_delta
|
213
|
+
now = Time.rfc2822(headers[:date])
|
214
|
+
reset = Time.at(headers[:x_ratelimit_reset].to_i)
|
215
|
+
ratelimit_delta = reset - now
|
216
|
+
end
|
217
|
+
|
218
|
+
sync_wait(ratelimit_delta, key_mutex)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
response
|
223
|
+
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
def sync_wait(time, mutex)
|
228
|
+
mutex.synchronize { sleep(time) }
|
229
|
+
end
|
230
|
+
|
231
|
+
def mutex_wait(mutex)
|
232
|
+
mutex.lock
|
233
|
+
mutex.unlock
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Initialize rate limit mutexes
|
238
|
+
@rate_limit_mutex = { global: Mutex.new }
|
242
239
|
end
|