mij-discord 1.0.0
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 +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
|