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.
@@ -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