mij-discord 1.0.9 → 1.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- when :user
17
- 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0'
18
- end
19
- end
20
-
21
- # Make an icon URL from server and icon IDs
22
- def icon_url(server_id, icon_id, format = :webp)
23
- "#{CDN_URL}/icons/#{server_id}/#{icon_id}.#{format}"
24
- end
25
-
26
- # Make an icon URL from application and icon IDs
27
- def app_icon_url(app_id, icon_id, format = :webp)
28
- "#{CDN_URL}/app-icons/#{app_id}/#{icon_id}.#{format}"
29
- end
30
-
31
- # Make a widget picture URL from server ID
32
- def widget_url(server_id, style = 'shield')
33
- "#{APIBASE_URL}/guilds/#{server_id}/widget.png?style=#{style}"
34
- end
35
-
36
- # Make a splash URL from server and splash IDs
37
- def splash_url(server_id, splash_id)
38
- "#{CDN_URL}{/splashes/#{server_id}/#{splash_id}.jpg"
39
- end
40
-
41
- # Make an emoji icon URL from emoji ID
42
- def emoji_icon_url(emoji_id, format = :webp)
43
- "#{CDN_URL}/emojis/#{emoji_id}.#{format}"
44
- end
45
-
46
- # Login to the server
47
- def login(email, password)
48
- request(
49
- :auth_login,
50
- nil,
51
- :post,
52
- "#{APIBASE_URL}/auth/login",
53
- email: email,
54
- password: password
55
- )
56
- end
57
-
58
- # Logout from the server
59
- def logout(auth)
60
- request(
61
- :auth_logout,
62
- nil,
63
- :post,
64
- "#{APIBASE_URL}/auth/logout",
65
- nil,
66
- Authorization: auth
67
- )
68
- end
69
-
70
- # Create an OAuth application
71
- def create_oauth_application(auth, name, redirect_uris)
72
- request(
73
- :oauth2_applications,
74
- nil,
75
- :post,
76
- "#{APIBASE_URL}/oauth2/applications",
77
- { name: name, redirect_uris: redirect_uris }.to_json,
78
- Authorization: auth,
79
- content_type: :json
80
- )
81
- end
82
-
83
- # Change an OAuth application's properties
84
- def update_oauth_application(auth, name, redirect_uris, description = '', icon = nil)
85
- request(
86
- :oauth2_applications,
87
- nil,
88
- :put,
89
- "#{APIBASE_URL}/oauth2/applications",
90
- { name: name, redirect_uris: redirect_uris, description: description, icon: icon }.to_json,
91
- Authorization: auth,
92
- content_type: :json
93
- )
94
- end
95
-
96
- # Get the bot's OAuth application's information
97
- def oauth_application(auth)
98
- request(
99
- :oauth2_applications_me,
100
- nil,
101
- :get,
102
- "#{APIBASE_URL}/oauth2/applications/@me",
103
- Authorization: auth
104
- )
105
- end
106
-
107
- # Acknowledge that a message has been received
108
- # The last acknowledged message will be sent in the ready packet,
109
- # so this is an easy way to catch up on messages
110
- def acknowledge_message(auth, channel_id, message_id)
111
- request(
112
- :channels_cid_messages_mid_ack,
113
- nil, # This endpoint is unavailable for bot accounts and thus isn't subject to its rate limit requirements.
114
- :post,
115
- "#{APIBASE_URL}/channels/#{channel_id}/messages/#{message_id}/ack",
116
- nil,
117
- Authorization: auth
118
- )
119
- end
120
-
121
- # Get the gateway to be used
122
- def gateway(auth)
123
- request(
124
- :gateway,
125
- nil,
126
- :get,
127
- "#{APIBASE_URL}/gateway",
128
- Authorization: auth,
129
- )
130
- end
131
-
132
- # Validate a token (this request will fail if the token is invalid)
133
- def validate_token(auth)
134
- request(
135
- :auth_login,
136
- nil,
137
- :post,
138
- "#{APIBASE_URL}/auth/login",
139
- {}.to_json,
140
- Authorization: auth,
141
- content_type: :json
142
- )
143
- end
144
-
145
- # Get a list of available voice regions
146
- def voice_regions(auth)
147
- request(
148
- :voice_regions,
149
- nil,
150
- :get,
151
- "#{APIBASE_URL}/voice/regions",
152
- Authorization: auth,
153
- content_type: :json
154
- )
155
- end
156
-
157
- def raw_request(type, *attributes)
158
- RestClient.send(type, *attributes)
159
- rescue RestClient::RequestFailed => e
160
- # Holy fuck, Discord…
161
- if (klazz = MijDiscord::Errors::HTTP_ERRORS[e.http_code])
162
- data = JSON.parse(e.response)
163
- if data['message'] || data['code']
164
- raise klazz.new(data['code'], data['message'], e.response)
165
- elsif (error = (data['content'] || data['embed']).join)
166
- if MijDiscord::Errors::MessageTooLong.match_pattern?(error)
167
- raise MijDiscord::Errors::MessageTooLong.new(error, e.response)
168
- end
169
- end
170
- end
171
- raise
172
- rescue RestClient::BadGateway
173
- MijDiscord::LOGGER.warn('HTTP') { 'Received 502 Bad Gateway during API request' }
174
- retry
175
- end
176
-
177
- def request(key, major_param, type, *attributes)
178
- ratelimit_delta, response = nil, nil
179
-
180
- if (params = attributes.last).is_a?(Hash)
181
- params[:user_agent] = user_agent(params[:Authorization])
182
- ratelimit_delta = params.delete(:header_bypass_delay)
183
- end
184
-
185
- key = [key, major_param].freeze
186
- key_mutex = (@rate_limit_mutex[key] ||= Mutex.new)
187
- global_mutex = @rate_limit_mutex[:global]
188
-
189
- begin
190
- mutex_wait(key_mutex)
191
- mutex_wait(global_mutex) if global_mutex.locked?
192
-
193
- response = raw_request(type, *attributes)
194
- rescue RestClient::TooManyRequests => e
195
- response = e.response
196
-
197
- is_global = response.headers[:x_ratelimit_global]
198
- mutex = is_global == 'true' ? global_mutex : key_mutex
199
-
200
- unless mutex.locked?
201
- response = JSON.parse(e.response)
202
- retry_after = response['retry_after'].to_i / 1000.0
203
-
204
- MijDiscord::LOGGER.info('HTTP') { "Hit Discord rate limit on <#{key}>, waiting for #{retry_after} seconds" }
205
- sync_wait(retry_after, mutex)
206
- end
207
-
208
- retry
209
- rescue RestClient::Exception => e
210
- response = e.response
211
- raise
212
- ensure
213
- headers = response&.headers
214
- if headers && headers[:x_ratelimit_remaining] == '0' && !key_mutex.locked?
215
- unless ratelimit_delta
216
- now = Time.rfc2822(headers[:date])
217
- reset = Time.at(headers[:x_ratelimit_reset].to_i)
218
- ratelimit_delta = reset - now
219
- end
220
-
221
- sync_wait(ratelimit_delta, key_mutex)
222
- end
223
- end
224
-
225
- response
226
- end
227
-
228
- private
229
-
230
- def sync_wait(time, mutex)
231
- mutex.synchronize { sleep(time) }
232
- end
233
-
234
- def mutex_wait(mutex)
235
- mutex.lock
236
- mutex.unlock
237
- end
238
- end
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