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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE +21 -0
  6. data/README.md +35 -0
  7. data/Rakefile +10 -0
  8. data/bin/console +7 -0
  9. data/bin/setup +6 -0
  10. data/lib/mij-discord.rb +56 -0
  11. data/lib/mij-discord/bot.rb +579 -0
  12. data/lib/mij-discord/cache.rb +298 -0
  13. data/lib/mij-discord/core/api.rb +228 -0
  14. data/lib/mij-discord/core/api/channel.rb +416 -0
  15. data/lib/mij-discord/core/api/invite.rb +43 -0
  16. data/lib/mij-discord/core/api/server.rb +465 -0
  17. data/lib/mij-discord/core/api/user.rb +144 -0
  18. data/lib/mij-discord/core/errors.rb +106 -0
  19. data/lib/mij-discord/core/gateway.rb +505 -0
  20. data/lib/mij-discord/data.rb +65 -0
  21. data/lib/mij-discord/data/application.rb +38 -0
  22. data/lib/mij-discord/data/channel.rb +404 -0
  23. data/lib/mij-discord/data/embed.rb +115 -0
  24. data/lib/mij-discord/data/emoji.rb +62 -0
  25. data/lib/mij-discord/data/invite.rb +87 -0
  26. data/lib/mij-discord/data/member.rb +174 -0
  27. data/lib/mij-discord/data/message.rb +206 -0
  28. data/lib/mij-discord/data/permissions.rb +121 -0
  29. data/lib/mij-discord/data/role.rb +99 -0
  30. data/lib/mij-discord/data/server.rb +359 -0
  31. data/lib/mij-discord/data/user.rb +173 -0
  32. data/lib/mij-discord/data/voice.rb +68 -0
  33. data/lib/mij-discord/events.rb +133 -0
  34. data/lib/mij-discord/events/basic.rb +80 -0
  35. data/lib/mij-discord/events/channel.rb +50 -0
  36. data/lib/mij-discord/events/member.rb +66 -0
  37. data/lib/mij-discord/events/message.rb +150 -0
  38. data/lib/mij-discord/events/server.rb +102 -0
  39. data/lib/mij-discord/logger.rb +20 -0
  40. data/lib/mij-discord/version.rb +5 -0
  41. data/mij-discord.gemspec +31 -0
  42. metadata +154 -0
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MijDiscord::Data
4
+ class Permissions
5
+ FLAGS = {
6
+ # (1 << Bit) => Permission # Value
7
+ (1 << 0) => :create_instant_invite, # 1
8
+ (1 << 1) => :kick_members, # 2
9
+ (1 << 2) => :ban_members, # 4
10
+ (1 << 3) => :administrator, # 8
11
+ (1 << 4) => :manage_channels, # 16
12
+ (1 << 5) => :manage_server, # 32
13
+ (1 << 6) => :add_reactions, # 64
14
+ (1 << 7) => :view_audit_log, # 128
15
+ # 1 << 8 # 256
16
+ # 1 << 9 # 512
17
+ (1 << 10) => :read_messages, # 1024
18
+ (1 << 11) => :send_messages, # 2048
19
+ (1 << 12) => :send_tts_messages, # 4096
20
+ (1 << 13) => :manage_messages, # 8192
21
+ (1 << 14) => :embed_links, # 16384
22
+ (1 << 15) => :attach_files, # 32768
23
+ (1 << 16) => :read_message_history, # 65536
24
+ (1 << 17) => :mention_everyone, # 131072
25
+ (1 << 18) => :use_external_emoji, # 262144
26
+ # 1 << 19 # 524288
27
+ (1 << 20) => :connect, # 1048576
28
+ (1 << 21) => :speak, # 2097152
29
+ (1 << 22) => :mute_members, # 4194304
30
+ (1 << 23) => :deafen_members, # 8388608
31
+ (1 << 24) => :move_members, # 16777216
32
+ (1 << 25) => :use_voice_activity, # 33554432
33
+ (1 << 26) => :change_nickname, # 67108864
34
+ (1 << 27) => :manage_nicknames, # 134217728
35
+ (1 << 28) => :manage_roles, # 268435456
36
+ (1 << 29) => :manage_webhooks, # 536870912
37
+ (1 << 30) => :manage_emojis, # 1073741824
38
+ }.freeze
39
+
40
+ def self.bits(list)
41
+ FLAGS.map {|bit, name| list.include?(name) ? bit : 0 }.reduce(&:|)
42
+ end
43
+
44
+ FLAGS.each do |bit, name|
45
+ attr_reader name
46
+ alias_method :"#{name}?", name
47
+
48
+ define_method(:"can_#{name}=") do |flag|
49
+ set_bits(flag ? (@bits | bit) : (@bits & ~bit))
50
+ end
51
+ end
52
+
53
+ alias_method :can_administrate=, :can_administrator=
54
+ alias_method :administrate, :administrator
55
+ alias_method :administrate?, :administrator?
56
+
57
+ attr_reader :bits
58
+ alias_method :to_i, :bits
59
+
60
+ def initialize(bits = 0)
61
+ set_bits(bits)
62
+ end
63
+
64
+ def set_bits(bits)
65
+ @bits = bits.is_a?(Array) ? Permissions.bits(bits) : bits
66
+
67
+ FLAGS.each do |bit, name|
68
+ flag = !(@bits & bit).zero?
69
+ instance_variable_set "@#{name}", flag
70
+ end
71
+ end
72
+
73
+ alias_method :bits=, :set_bits
74
+ end
75
+
76
+ class Overwrite
77
+ include IDObject
78
+
79
+ attr_accessor :type
80
+
81
+ attr_accessor :allow
82
+
83
+ attr_accessor :deny
84
+
85
+ def initialize(object, type: nil, allow: 0, deny: 0)
86
+ case type
87
+ when nil, :member, :role
88
+ # Do nothing
89
+ else
90
+ raise ArgumentError, 'Overwrite type must be :member or :role'
91
+ end
92
+
93
+ @id = object.to_id
94
+
95
+ @type = case object
96
+ when User, Member, Recipient, Profile
97
+ :member
98
+ when Role
99
+ :role
100
+ else
101
+ type
102
+ end
103
+
104
+ @allow = allow.is_a?(Permissions) ? allow : Permissions.new(allow)
105
+ @deny = deny.is_a?(Permissions) ? deny : Permissions.new(deny)
106
+ end
107
+
108
+ def self.from_hash(data)
109
+ Overwrite.new(
110
+ data['id'].to_i,
111
+ type: data['type'].to_sym,
112
+ allow: Permissions.new(data['allow']),
113
+ deny: Permissions.new(data['deny'])
114
+ )
115
+ end
116
+
117
+ def to_hash
118
+ { id: @id, type: @type, allow: @allow.bits, deny: @deny.bits }
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MijDiscord::Data
4
+ class Role
5
+ include IDObject
6
+
7
+ attr_reader :permissions
8
+
9
+ attr_reader :name
10
+
11
+ attr_reader :hoist
12
+
13
+ attr_reader :managed
14
+ alias_method :managed?, :managed
15
+
16
+ attr_reader :mentionable
17
+ alias_method :mentionable?, :mentionable
18
+
19
+ attr_reader :color
20
+
21
+ attr_reader :position
22
+
23
+ def initialize(data, server, bot)
24
+ @bot, @server = bot, server
25
+
26
+ @permissions = Permissions.new
27
+ @color = ColorRGB.new
28
+
29
+ @id = data['id'].to_i
30
+ update_data(data)
31
+ end
32
+
33
+ def update_data(data)
34
+ @name = data.fetch('name', @name)
35
+ @position = data.fetch('position', @position)
36
+ @hoist = data.fetch('hoist', @hoist)
37
+ @mentionable = data.fetch('mentionable', @mentionable)
38
+ @managed = data.fetch('managed', @managed)
39
+
40
+ @color.value = data.fetch('color', @color.value)
41
+
42
+ if (bits = data['permissions'])
43
+ @permissions.bits = bits.to_i
44
+ end
45
+ end
46
+
47
+ def mention
48
+ "<@&#{@id}>"
49
+ end
50
+
51
+ alias_method :to_s, :mention
52
+
53
+ def members
54
+ @server.members.select {|x| x.role?(self) }
55
+ end
56
+
57
+ alias_method :users, :members
58
+
59
+ def set_name(name)
60
+ set_options(name: name)
61
+ end
62
+
63
+ alias_method :name=, :set_name
64
+
65
+ def set_hoist(flag)
66
+ set_options(hoist: flag)
67
+ end
68
+
69
+ alias_method :hoist=, :set_hoist
70
+
71
+ def set_mentionable(flag)
72
+ set_options(mentionable: flag)
73
+ end
74
+
75
+ alias_method :mentionable=, :set_mentionable
76
+
77
+ def set_color(color)
78
+ set_options(color: color)
79
+ end
80
+
81
+ alias_method :color=, :set_color
82
+
83
+ def delete(reason = nil)
84
+ MijDiscord::Core::API::Server.delete_role(@bot.token, @server.id, @id, reason)
85
+ end
86
+
87
+ def set_options(name: nil, color: nil, hoist: nil, mentionable: nil, permissions: nil)
88
+ response = MijDiscord::Core::API::Server.update_role(@bot.token, @server.id, @id,
89
+ name, color&.to_i, hoist, mentionable, permissions&.to_i)
90
+ @server.cache.put_role(JSON.parse(response), update: true)
91
+ end
92
+
93
+ def inspect
94
+ %(<Role id=#{@id} name=#{@name} server=#{@server.inspect}>)
95
+ end
96
+
97
+ private
98
+ end
99
+ end
@@ -0,0 +1,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MijDiscord::Data
4
+ class Server
5
+ VERIFICATION_LEVEL = {
6
+ 0 => :none,
7
+ 1 => :low,
8
+ 2 => :medium,
9
+ 3 => :high,
10
+ 4 => :very_high,
11
+ }.freeze
12
+
13
+ CONTENT_FILTER_LEVEL = {
14
+ 0 => :none,
15
+ 1 => :no_roles,
16
+ 2 => :all,
17
+ }.freeze
18
+
19
+ DEFAULT_NOTIFICATIONS = {
20
+ 0 => :all,
21
+ 1 => :mentions,
22
+ }.freeze
23
+
24
+ include IDObject
25
+
26
+ attr_reader :bot
27
+
28
+ attr_reader :name
29
+
30
+ attr_reader :icon_id
31
+
32
+ attr_reader :owner
33
+
34
+ attr_reader :large
35
+ alias_method :large?, :large
36
+
37
+ attr_reader :features
38
+
39
+ attr_reader :embed_enabled
40
+ alias_method :embed_enabled?, :embed_enabled
41
+ alias_method :has_embed?, :embed_enabled
42
+
43
+ # attr_reader :member_count
44
+
45
+ attr_reader :verification_level
46
+
47
+ attr_reader :content_filter_level
48
+ alias_method :content_filter, :content_filter_level
49
+
50
+ attr_reader :default_notifications
51
+
52
+ attr_reader :afk_timeout
53
+
54
+ attr_reader :afk_channel
55
+
56
+ attr_reader :cache
57
+
58
+ def initialize(data, bot, invalid = false)
59
+ @bot = bot
60
+
61
+ @id = data['id'].to_i
62
+ @large = data['large']
63
+ @members_init = data['member_count']
64
+ @members_chunked = 0
65
+
66
+ @cache = MijDiscord::Cache::ServerCache.new(self, @bot)
67
+
68
+ data['channels']&.each {|ch| @cache.put_channel(ch) }
69
+
70
+ data['roles']&.each {|ro| @cache.put_role(ro) }
71
+
72
+ data['members']&.each {|mb| @cache.put_member(mb) }
73
+
74
+ data['presences']&.each do |pr|
75
+ next unless pr['user']
76
+
77
+ user_id = pr['user']['id'].to_i
78
+ user = @cache.get_member(user_id, local: true)
79
+ user.update_presence(pr) if user
80
+ end
81
+
82
+ @voice_states = {}
83
+ data['voice_states']&.each {|vs| update_voice_state(vs) }
84
+
85
+ update_emojis(data)
86
+ update_data(data)
87
+
88
+ @owner = member(data['owner_id'])
89
+ end
90
+
91
+ def update_data(data)
92
+ @name = data.fetch('name', @name)
93
+ @region_id = data.fetch('region', @region_id)
94
+ @icon_id = data.fetch('icon', @icon_id)
95
+ @afk_timeout = data.fetch('afk_timeout', @afk_timeout)
96
+ @embed_enabled = data.fetch('embed_enabled', @embed_enabled)
97
+ @verification_level = VERIFICATION_LEVEL.fetch(data['verification_level'], @verification_level)
98
+ @content_filter_level = CONTENT_FILTER_LEVEL.fetch(data['explicit_content_filter'], @content_filter_level)
99
+ @default_notifications = DEFAULT_NOTIFICATIONS.fetch(data['default_message_notifications'], @default_notifications)
100
+
101
+ begin
102
+ if data.has_key?('afk_channel_id')
103
+ id = data['afk_channel_id'].to_i
104
+ @afk_channel = @bot.channel(id, self)
105
+ end
106
+ rescue MijDiscord::Errors::NoPermission
107
+ @afk_channel = nil
108
+ end
109
+ end
110
+
111
+ def update_emojis(data)
112
+ @emojis = {}
113
+ data['emojis'].each do |em|
114
+ emoji = MijDiscord::Data::Emoji.new(em, @bot, self)
115
+ @emojis[emoji.id] = emoji
116
+ end
117
+ end
118
+
119
+ def update_voice_state(data)
120
+ user = @cache.get_member(data['user_id'])
121
+
122
+ if (chan_id = data['channel_id']&.to_i)
123
+ state = (@voice_states[user.id] ||= VoiceState.new(user))
124
+ channel = @cache.get_channel(chan_id)
125
+
126
+ state.update_data(channel, data) if channel
127
+ state
128
+ else
129
+ state = @voice_states.delete(user.id)
130
+ state ||= VoiceState.new(user)
131
+
132
+ state.update_data(nil, data)
133
+ state
134
+ end
135
+ end
136
+
137
+ def update_members_chunk(data)
138
+ return if @members_chunked.nil?
139
+
140
+ @members_chunked += data.length
141
+ data.each {|mb| @cache.put_member(mb) }
142
+
143
+ @members_chunked = nil if @members_chunked == @members_init
144
+ end
145
+
146
+ def channels
147
+ @cache.list_channels
148
+ end
149
+
150
+ def channel(id)
151
+ @cache.get_channel(id)
152
+ end
153
+
154
+ def roles
155
+ @cache.list_roles
156
+ end
157
+
158
+ def role(id)
159
+ @cache.get_role(id)
160
+ end
161
+
162
+ def members
163
+ unless @members_chunked.nil?
164
+ @bot.gateway.send_request_members(@id, '', 0)
165
+ sleep(0.05) while @members_chunked
166
+ end
167
+
168
+ @cache.list_members
169
+ end
170
+
171
+ def member(id)
172
+ @cache.get_member(id)
173
+ end
174
+
175
+ def emojis
176
+ @emojis.values
177
+ end
178
+
179
+ def emoji(id)
180
+ @emojis[id&.to_id]
181
+ end
182
+
183
+ def everyone_role
184
+ role(@id)
185
+ end
186
+
187
+ def voice_states
188
+ @voice_states.values
189
+ end
190
+
191
+ def voice_state(id)
192
+ @voice_states[id&.to_id]
193
+ end
194
+
195
+ def default_channel
196
+ text_channels.sort_by {|ch| [ch.position, ch.id] }.find do |ch|
197
+ if (overwrite = ch.permission_overwrites[id])
198
+ overwrite.allow.read_messages? || !overwrite.deny.read_messages?
199
+ else
200
+ everyone_role.permissions.read_messages
201
+ end
202
+ end
203
+ end
204
+
205
+ alias_method :general_channel, :default_channel
206
+
207
+ def online_members(idle: true, bots: false)
208
+ members.select! do |m|
209
+ ((idle ? m.idle? : false) || m.online?) && (bots ? true : !m.bot_account?)
210
+ end
211
+ end
212
+
213
+ def invites
214
+ response = MijDiscord::Core::API::Server.invites(@bot.token, @id)
215
+ JSON.parse(response).map {|x| Invite.new(x, @bot) }
216
+ end
217
+
218
+ def prune_count(days)
219
+ raise ArgumentError, 'Days must be between 1 and 30' unless days.between?(1, 30)
220
+
221
+ response = MijDiscord::Core::API::Server.prune_count(@bot.token, @id, days)
222
+ JSON.parse(response)['pruned']
223
+ end
224
+
225
+ def prune(days, reason = nil)
226
+ raise ArgumentError, 'Days must be between 1 and 30' unless days.between?(1, 30)
227
+
228
+ response = MijDiscord::Core::API::Server.begin_prune(@bot.token, @id, days, reason)
229
+ JSON.parse(response)['pruned']
230
+ end
231
+
232
+ def text_channels
233
+ channels.select!(&:text?)
234
+ end
235
+
236
+ def voice_channels
237
+ channels.select!(&:voice?)
238
+ end
239
+
240
+ def create_channel(name, reason = nil, voice: false, bitrate: nil, user_limit: nil, permissions: [], nsfw: false)
241
+ permissions = permissions.map {|x| x.is_a(Overwrite) ? x.to_hash : x }
242
+ response = MijDiscord::Core::API::Server.create_channel(@bot.token, @id,
243
+ name, voice ? 2 : 0, bitrate, user_limit, permissions, nsfw, reason)
244
+ @cache.put_channel(JSON.parse(response))
245
+ end
246
+
247
+ def create_role(name, reason = nil, color: 0, hoist: false, mentionable: false, permissions: 104_324_161)
248
+ response = MijDiscord::Core::API::Server.create_role(@bot.token, @id,
249
+ name, color.to_i, hoist, mentionable, permissions.to_i, reason)
250
+ @cache.put_role(JSON.parse(response))
251
+ end
252
+
253
+ def bans
254
+ response = MijDiscord::Core::API::Server.bans(@bot.token, @id)
255
+ JSON.parse(response).map {|x| @bot.cache.put_user(x['user']) }
256
+ end
257
+
258
+ def ban(user, message_days = 0, reason = nil)
259
+ MijDiscord::Core::API::Server.ban_user(@bot.token, @id, user.to_id, message_days, reason)
260
+ nil
261
+ end
262
+
263
+ def unban(user, reason = nil)
264
+ MijDiscord::Core::API::Server.unban_user(@bot.token, @id, user.to_id, reason)
265
+ nil
266
+ end
267
+
268
+ def kick(user, reason = nil)
269
+ MijDiscord::Core::API::Server.remove_member(@bot.token, @id, user.to_id, reason)
270
+ nil
271
+ end
272
+
273
+ def leave
274
+ MijDiscord::Core::API::User.leave_server(@bot.token, @id)
275
+ nil
276
+ end
277
+
278
+ def delete
279
+ MijDiscord::Core::API::Server.delete(@bot.token, @id)
280
+ nil
281
+ end
282
+
283
+ def set_owner(user, reason = nil)
284
+ MijDiscord::Core::API::Server.transfer_ownership(@bot.token, @id, user.to_id, reason)
285
+ end
286
+
287
+ alias_method :owner=, :set_owner
288
+
289
+ def set_options(reason = nil, name: nil, region: nil, icon: nil, afk_channel: nil, afk_timeout: nil)
290
+ response = MijDiscord::Core::API::Server.update(@bot.token, @id,
291
+ name, region, icon, afk_channel&.to_id, afk_timeout, reason)
292
+ @bot.cache.put_server(JSON.parse(response), update: true)
293
+ end
294
+
295
+ def set_name(name, reason = nil)
296
+ set_options(reason, name: name)
297
+ end
298
+
299
+ alias_method :name=, :set_name
300
+
301
+ def available_regions
302
+ return @voice_regions if @voice_regions
303
+
304
+ response = MijDiscord::Core::API::Server.regions(@bot.token, @id)
305
+ @voice_regions = JSON.parse(response).map {|x| VoiceRegion.new(x) }
306
+ end
307
+
308
+ def region
309
+ available_regions.find {|x| x.id == @region_id }
310
+ end
311
+
312
+ def set_region(region, reason = nil)
313
+ region = region.to_s
314
+ raise ArgumentError, 'Invalid region' unless available_regions.find {|x| x.id == region }
315
+
316
+ set_options(reason, region: region)
317
+ end
318
+
319
+ alias_method :region=, :set_region
320
+
321
+ def set_icon(icon, reason = nil)
322
+ if icon.respond_to?(:read)
323
+ buffer = Base64.strict_encode64(icon.read)
324
+ icon = "data:image/jpg;base64,#{buffer}"
325
+ set_options(reason, icon: icon)
326
+ else
327
+ set_options(reason, icon: icon.to_s)
328
+ end
329
+ end
330
+
331
+ alias_method :icon=, :set_icon
332
+
333
+ def set_afk_channel(channel, reason = nil)
334
+ set_options(reason, afk_channel: channel.to_id)
335
+ end
336
+
337
+ alias_method :afk_channel=, :set_afk_channel
338
+
339
+ def set_afk_timeout(timeout, reason = nil)
340
+ set_options(reason, afk_timeout: timeout)
341
+ end
342
+
343
+ alias_method :afk_timeout=, :set_afk_timeout
344
+
345
+ def icon_url(format = :webp)
346
+ return nil unless @icon_id
347
+ MijDiscord::Core::API.icon_url(@id, @icon_id, format)
348
+ end
349
+
350
+ def embed_url(style = :shield)
351
+ return nil unless @embed_enabled
352
+ MijDiscord::Core::API.widget_url(@id, style)
353
+ end
354
+
355
+ def inspect
356
+ %(<Server id=#{@id} name="#{@name}" large=#{@large} region=#{@region_id} owner=#{@owner.id}>)
357
+ end
358
+ end
359
+ end