discordrb 3.1.1 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of discordrb might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.circleci/config.yml +126 -0
- data/.codeclimate.yml +16 -0
- data/.github/CONTRIBUTING.md +13 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
- data/.github/pull_request_template.md +37 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +39 -33
- data/.travis.yml +27 -2
- data/.yardopts +1 -1
- data/CHANGELOG.md +808 -208
- data/Gemfile +4 -1
- data/LICENSE.txt +1 -1
- data/README.md +108 -53
- data/Rakefile +14 -1
- data/bin/console +1 -0
- data/bin/travis_build_docs.sh +17 -0
- data/discordrb-webhooks.gemspec +26 -0
- data/discordrb.gemspec +24 -15
- data/lib/discordrb.rb +75 -2
- data/lib/discordrb/allowed_mentions.rb +36 -0
- data/lib/discordrb/api.rb +126 -27
- data/lib/discordrb/api/channel.rb +165 -43
- data/lib/discordrb/api/invite.rb +10 -7
- data/lib/discordrb/api/server.rb +240 -61
- data/lib/discordrb/api/user.rb +26 -24
- data/lib/discordrb/api/webhook.rb +83 -0
- data/lib/discordrb/await.rb +1 -2
- data/lib/discordrb/bot.rb +417 -149
- data/lib/discordrb/cache.rb +42 -10
- data/lib/discordrb/colour_rgb.rb +43 -0
- data/lib/discordrb/commands/command_bot.rb +186 -31
- data/lib/discordrb/commands/container.rb +30 -16
- data/lib/discordrb/commands/parser.rb +102 -47
- data/lib/discordrb/commands/rate_limiter.rb +18 -17
- data/lib/discordrb/container.rb +245 -41
- data/lib/discordrb/data.rb +27 -2511
- data/lib/discordrb/data/activity.rb +264 -0
- data/lib/discordrb/data/application.rb +50 -0
- data/lib/discordrb/data/attachment.rb +56 -0
- data/lib/discordrb/data/audit_logs.rb +345 -0
- data/lib/discordrb/data/channel.rb +849 -0
- data/lib/discordrb/data/embed.rb +251 -0
- data/lib/discordrb/data/emoji.rb +82 -0
- data/lib/discordrb/data/integration.rb +83 -0
- data/lib/discordrb/data/invite.rb +137 -0
- data/lib/discordrb/data/member.rb +297 -0
- data/lib/discordrb/data/message.rb +334 -0
- data/lib/discordrb/data/overwrite.rb +102 -0
- data/lib/discordrb/data/profile.rb +91 -0
- data/lib/discordrb/data/reaction.rb +33 -0
- data/lib/discordrb/data/recipient.rb +34 -0
- data/lib/discordrb/data/role.rb +191 -0
- data/lib/discordrb/data/server.rb +1002 -0
- data/lib/discordrb/data/user.rb +204 -0
- data/lib/discordrb/data/voice_region.rb +45 -0
- data/lib/discordrb/data/voice_state.rb +41 -0
- data/lib/discordrb/data/webhook.rb +145 -0
- data/lib/discordrb/errors.rb +36 -2
- data/lib/discordrb/events/bans.rb +7 -5
- data/lib/discordrb/events/channels.rb +2 -0
- data/lib/discordrb/events/generic.rb +19 -3
- data/lib/discordrb/events/guilds.rb +129 -6
- data/lib/discordrb/events/invites.rb +125 -0
- data/lib/discordrb/events/members.rb +6 -2
- data/lib/discordrb/events/message.rb +86 -36
- data/lib/discordrb/events/presence.rb +23 -16
- data/lib/discordrb/events/raw.rb +47 -0
- data/lib/discordrb/events/reactions.rb +159 -0
- data/lib/discordrb/events/roles.rb +7 -6
- data/lib/discordrb/events/typing.rb +9 -5
- data/lib/discordrb/events/voice_server_update.rb +47 -0
- data/lib/discordrb/events/voice_state_update.rb +29 -9
- data/lib/discordrb/events/webhooks.rb +64 -0
- data/lib/discordrb/gateway.rb +219 -88
- data/lib/discordrb/id_object.rb +39 -0
- data/lib/discordrb/light.rb +1 -1
- data/lib/discordrb/light/integrations.rb +1 -1
- data/lib/discordrb/light/light_bot.rb +1 -1
- data/lib/discordrb/logger.rb +12 -11
- data/lib/discordrb/paginator.rb +57 -0
- data/lib/discordrb/permissions.rb +148 -14
- data/lib/discordrb/version.rb +1 -1
- data/lib/discordrb/voice/encoder.rb +14 -15
- data/lib/discordrb/voice/network.rb +86 -45
- data/lib/discordrb/voice/sodium.rb +96 -0
- data/lib/discordrb/voice/voice_bot.rb +52 -40
- data/lib/discordrb/webhooks.rb +12 -0
- data/lib/discordrb/websocket.rb +2 -2
- metadata +137 -34
data/lib/discordrb/api/user.rb
CHANGED
@@ -1,21 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# API calls for User object
|
2
4
|
module Discordrb::API::User
|
3
5
|
module_function
|
4
6
|
|
5
|
-
# Returns users based on a query
|
6
|
-
# https://discordapp.com/developers/docs/resources/user#query-users
|
7
|
-
def query(token, query, limit = nil)
|
8
|
-
Discordrb::API.request(
|
9
|
-
:users,
|
10
|
-
nil,
|
11
|
-
:get,
|
12
|
-
"#{Discordrb::API.api_base}/users?q=#{query}#{"&limit=#{limit}" if limit}",
|
13
|
-
Authorization: token
|
14
|
-
)
|
15
|
-
end
|
16
|
-
|
17
7
|
# Get user data
|
18
|
-
# https://
|
8
|
+
# https://discord.com/developers/docs/resources/user#get-user
|
19
9
|
def resolve(token, user_id)
|
20
10
|
Discordrb::API.request(
|
21
11
|
:users_uid,
|
@@ -27,7 +17,7 @@ module Discordrb::API::User
|
|
27
17
|
end
|
28
18
|
|
29
19
|
# Get profile data
|
30
|
-
# https://
|
20
|
+
# https://discord.com/developers/docs/resources/user#get-current-user
|
31
21
|
def profile(token)
|
32
22
|
Discordrb::API.request(
|
33
23
|
:users_me,
|
@@ -39,7 +29,7 @@ module Discordrb::API::User
|
|
39
29
|
end
|
40
30
|
|
41
31
|
# Change the current bot's nickname on a server
|
42
|
-
def change_own_nickname(token, server_id, nick)
|
32
|
+
def change_own_nickname(token, server_id, nick, reason = nil)
|
43
33
|
Discordrb::API.request(
|
44
34
|
:guilds_sid_members_me_nick,
|
45
35
|
server_id, # This is technically a guild endpoint
|
@@ -47,12 +37,13 @@ module Discordrb::API::User
|
|
47
37
|
"#{Discordrb::API.api_base}/guilds/#{server_id}/members/@me/nick",
|
48
38
|
{ nick: nick }.to_json,
|
49
39
|
Authorization: token,
|
50
|
-
content_type: :json
|
40
|
+
content_type: :json,
|
41
|
+
'X-Audit-Log-Reason': reason
|
51
42
|
)
|
52
43
|
end
|
53
44
|
|
54
45
|
# Update user data
|
55
|
-
# https://
|
46
|
+
# https://discord.com/developers/docs/resources/user#modify-current-user
|
56
47
|
def update_profile(token, email, password, new_username, avatar, new_password = nil)
|
57
48
|
Discordrb::API.request(
|
58
49
|
:users_me,
|
@@ -66,7 +57,7 @@ module Discordrb::API::User
|
|
66
57
|
end
|
67
58
|
|
68
59
|
# Get the servers a user is connected to
|
69
|
-
# https://
|
60
|
+
# https://discord.com/developers/docs/resources/user#get-current-user-guilds
|
70
61
|
def servers(token)
|
71
62
|
Discordrb::API.request(
|
72
63
|
:users_me_guilds,
|
@@ -78,7 +69,7 @@ module Discordrb::API::User
|
|
78
69
|
end
|
79
70
|
|
80
71
|
# Leave a server
|
81
|
-
# https://
|
72
|
+
# https://discord.com/developers/docs/resources/user#leave-guild
|
82
73
|
def leave_server(token, server_id)
|
83
74
|
Discordrb::API.request(
|
84
75
|
:users_me_guilds_sid,
|
@@ -90,7 +81,7 @@ module Discordrb::API::User
|
|
90
81
|
end
|
91
82
|
|
92
83
|
# Get the DMs for the current user
|
93
|
-
# https://
|
84
|
+
# https://discord.com/developers/docs/resources/user#get-user-dms
|
94
85
|
def user_dms(token)
|
95
86
|
Discordrb::API.request(
|
96
87
|
:users_me_channels,
|
@@ -102,7 +93,7 @@ module Discordrb::API::User
|
|
102
93
|
end
|
103
94
|
|
104
95
|
# Create a DM to another user
|
105
|
-
# https://
|
96
|
+
# https://discord.com/developers/docs/resources/user#create-dm
|
106
97
|
def create_pm(token, recipient_id)
|
107
98
|
Discordrb::API.request(
|
108
99
|
:users_me_channels,
|
@@ -116,7 +107,7 @@ module Discordrb::API::User
|
|
116
107
|
end
|
117
108
|
|
118
109
|
# Get information about a user's connections
|
119
|
-
# https://
|
110
|
+
# https://discord.com/developers/docs/resources/user#get-users-connections
|
120
111
|
def connections(token)
|
121
112
|
Discordrb::API.request(
|
122
113
|
:users_me_connections,
|
@@ -140,8 +131,19 @@ module Discordrb::API::User
|
|
140
131
|
)
|
141
132
|
end
|
142
133
|
|
134
|
+
# Returns one of the "default" discord avatars from the CDN given a discriminator
|
135
|
+
def default_avatar(discrim = 0)
|
136
|
+
index = discrim.to_i % 5
|
137
|
+
"#{Discordrb::API.cdn_url}/embed/avatars/#{index}.png"
|
138
|
+
end
|
139
|
+
|
143
140
|
# Make an avatar URL from the user and avatar IDs
|
144
|
-
def avatar_url(user_id, avatar_id)
|
145
|
-
|
141
|
+
def avatar_url(user_id, avatar_id, format = nil)
|
142
|
+
format ||= if avatar_id.start_with?('a_')
|
143
|
+
'gif'
|
144
|
+
else
|
145
|
+
'webp'
|
146
|
+
end
|
147
|
+
"#{Discordrb::API.cdn_url}/avatars/#{user_id}/#{avatar_id}.#{format}"
|
146
148
|
end
|
147
149
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# API calls for Webhook object
|
4
|
+
module Discordrb::API::Webhook
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Get a webhook
|
8
|
+
# https://discord.com/developers/docs/resources/webhook#get-webhook
|
9
|
+
def webhook(token, webhook_id)
|
10
|
+
Discordrb::API.request(
|
11
|
+
:webhooks_wid,
|
12
|
+
nil,
|
13
|
+
:get,
|
14
|
+
"#{Discordrb::API.api_base}/webhooks/#{webhook_id}",
|
15
|
+
Authorization: token
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get a webhook via webhook token
|
20
|
+
# https://discord.com/developers/docs/resources/webhook#get-webhook-with-token
|
21
|
+
def token_webhook(webhook_token, webhook_id)
|
22
|
+
Discordrb::API.request(
|
23
|
+
:webhooks_wid,
|
24
|
+
nil,
|
25
|
+
:get,
|
26
|
+
"#{Discordrb::API.api_base}/webhooks/#{webhook_id}/#{webhook_token}"
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Update a webhook
|
31
|
+
# https://discord.com/developers/docs/resources/webhook#modify-webhook
|
32
|
+
def update_webhook(token, webhook_id, data, reason = nil)
|
33
|
+
Discordrb::API.request(
|
34
|
+
:webhooks_wid,
|
35
|
+
webhook_id,
|
36
|
+
:patch,
|
37
|
+
"#{Discordrb::API.api_base}/webhooks/#{webhook_id}",
|
38
|
+
data.to_json,
|
39
|
+
Authorization: token,
|
40
|
+
content_type: :json,
|
41
|
+
'X-Audit-Log-Reason': reason
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Update a webhook via webhook token
|
46
|
+
# https://discord.com/developers/docs/resources/webhook#modify-webhook-with-token
|
47
|
+
def token_update_webhook(webhook_token, webhook_id, data, reason = nil)
|
48
|
+
Discordrb::API.request(
|
49
|
+
:webhooks_wid,
|
50
|
+
webhook_id,
|
51
|
+
:patch,
|
52
|
+
"#{Discordrb::API.api_base}/webhooks/#{webhook_id}/#{webhook_token}",
|
53
|
+
data.to_json,
|
54
|
+
content_type: :json,
|
55
|
+
'X-Audit-Log-Reason': reason
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Deletes a webhook
|
60
|
+
# https://discord.com/developers/docs/resources/webhook#delete-webhook
|
61
|
+
def delete_webhook(token, webhook_id, reason = nil)
|
62
|
+
Discordrb::API.request(
|
63
|
+
:webhooks_wid,
|
64
|
+
webhook_id,
|
65
|
+
:delete,
|
66
|
+
"#{Discordrb::API.api_base}/webhooks/#{webhook_id}",
|
67
|
+
Authorization: token,
|
68
|
+
'X-Audit-Log-Reason': reason
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Deletes a webhook via webhook token
|
73
|
+
# https://discord.com/developers/docs/resources/webhook#delete-webhook-with-token
|
74
|
+
def token_delete_webhook(webhook_token, webhook_id, reason = nil)
|
75
|
+
Discordrb::API.request(
|
76
|
+
:webhooks_wid,
|
77
|
+
webhook_id,
|
78
|
+
:delete,
|
79
|
+
"#{Discordrb::API.api_base}/webhooks/#{webhook_id}/#{webhook_token}",
|
80
|
+
'X-Audit-Log-Reason': reason
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
data/lib/discordrb/await.rb
CHANGED
@@ -41,9 +41,8 @@ module Discordrb
|
|
41
41
|
# @return [Array] This await's key and whether or not it should be deleted. If there was no match, both are nil.
|
42
42
|
def match(event)
|
43
43
|
dummy_handler = EventContainer.handler_class(@type).new(@attributes, @bot)
|
44
|
-
return [nil, nil] unless dummy_handler.matches?(event)
|
44
|
+
return [nil, nil] unless event.instance_of?(@type) && dummy_handler.matches?(event)
|
45
45
|
|
46
|
-
should_delete = nil
|
47
46
|
should_delete = true if (@block && @block.call(event) != false) || !@block
|
48
47
|
|
49
48
|
[@key, should_delete]
|
data/lib/discordrb/bot.rb
CHANGED
@@ -15,6 +15,10 @@ require 'discordrb/events/roles'
|
|
15
15
|
require 'discordrb/events/guilds'
|
16
16
|
require 'discordrb/events/await'
|
17
17
|
require 'discordrb/events/bans'
|
18
|
+
require 'discordrb/events/raw'
|
19
|
+
require 'discordrb/events/reactions'
|
20
|
+
require 'discordrb/events/webhooks'
|
21
|
+
require 'discordrb/events/invites'
|
18
22
|
|
19
23
|
require 'discordrb/api'
|
20
24
|
require 'discordrb/api/channel'
|
@@ -40,11 +44,12 @@ module Discordrb
|
|
40
44
|
# @return [Array<Thread>] The threads.
|
41
45
|
attr_reader :event_threads
|
42
46
|
|
43
|
-
#
|
47
|
+
# @return [true, false] whether or not the bot should parse its own messages. Off by default.
|
44
48
|
attr_accessor :should_parse_self
|
45
49
|
|
46
50
|
# The bot's name which discordrb sends to Discord when making any request, so Discord can identify bots with the
|
47
51
|
# same codebase. Not required but I recommend setting it anyway.
|
52
|
+
# @return [String] The bot's name.
|
48
53
|
attr_accessor :name
|
49
54
|
|
50
55
|
# @return [Array(Integer, Integer)] the current shard key
|
@@ -74,7 +79,8 @@ module Discordrb
|
|
74
79
|
# @param token [String] The token that should be used to log in. If your bot is a bot account, you have to specify
|
75
80
|
# this. If you're logging in as a user, make sure to also set the account type to :user so discordrb doesn't think
|
76
81
|
# you're trying to log in as a bot.
|
77
|
-
# @param client_id [Integer] If you're logging in as a bot, the bot's client ID.
|
82
|
+
# @param client_id [Integer] If you're logging in as a bot, the bot's client ID. This is optional, and may be fetched
|
83
|
+
# from the API by calling {Bot#bot_application} (see {Application}).
|
78
84
|
# @param type [Symbol] This parameter lets you manually overwrite the account type. This needs to be set when
|
79
85
|
# logging in as a user, otherwise discordrb will treat you as a bot account. Valid values are `:user` and `:bot`.
|
80
86
|
# @param name [String] Your bot's name. This will be sent to Discord with any API requests, who will use this to
|
@@ -87,31 +93,29 @@ module Discordrb
|
|
87
93
|
# @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
|
88
94
|
# unless you really need this so you don't inadvertently create infinite loops.
|
89
95
|
# @param shard_id [Integer] The number of the shard this bot should handle. See
|
90
|
-
# https://github.com/
|
96
|
+
# https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
|
91
97
|
# @param num_shards [Integer] The total number of shards that should be running. See
|
92
|
-
# https://github.com/
|
98
|
+
# https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
|
93
99
|
# @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
|
100
|
+
# @param ignore_bots [true, false] Whether the bot should ignore bot accounts or not. Default is false.
|
101
|
+
# @param compress_mode [:none, :large, :stream] Sets which compression mode should be used when connecting
|
102
|
+
# to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
|
103
|
+
# production bots). `:large` will request that large payloads are received compressed. `:stream` will request
|
104
|
+
# that all data be received in a continuous compressed stream.
|
105
|
+
# @param intents [:all, Array<Symbol>, nil] Intents that this bot requires. See {Discordrb::INTENTS}. If `nil`, no intents
|
106
|
+
# field will be passed.
|
94
107
|
def initialize(
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
108
|
+
log_mode: :normal,
|
109
|
+
token: nil, client_id: nil,
|
110
|
+
type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
|
111
|
+
shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
|
112
|
+
compress_mode: :large, intents: nil
|
99
113
|
)
|
100
|
-
|
101
|
-
LOGGER.mode = if log_mode.is_a? TrueClass # Specifically check for `true` because people might not have updated yet
|
102
|
-
:debug
|
103
|
-
else
|
104
|
-
log_mode
|
105
|
-
end
|
106
|
-
|
114
|
+
LOGGER.mode = log_mode
|
107
115
|
LOGGER.token = token if redact_token
|
108
116
|
|
109
117
|
@should_parse_self = parse_self
|
110
118
|
|
111
|
-
if application_id
|
112
|
-
raise ArgumentError, 'Starting with discordrb 3.0.0, the application_id parameter has been renamed to client_id! Make sure to change this in your bot. This check will be removed in 3.1.0.'
|
113
|
-
end
|
114
|
-
|
115
119
|
@client_id = client_id
|
116
120
|
|
117
121
|
@type = type || :bot
|
@@ -122,8 +126,14 @@ module Discordrb
|
|
122
126
|
LOGGER.fancy = fancy_log
|
123
127
|
@prevent_ready = suppress_ready
|
124
128
|
|
129
|
+
@compress_mode = compress_mode
|
130
|
+
|
131
|
+
raise 'Token string is empty or nil' if token.nil? || token.empty?
|
132
|
+
|
133
|
+
@intents = intents == :all ? INTENTS.values.reduce(&:|) : calculate_intents(intents) if intents
|
134
|
+
|
125
135
|
@token = process_token(@type, token)
|
126
|
-
@gateway = Gateway.new(self, @token)
|
136
|
+
@gateway = Gateway.new(self, @token, @shard_key, @compress_mode, @intents)
|
127
137
|
|
128
138
|
init_cache
|
129
139
|
|
@@ -131,6 +141,7 @@ module Discordrb
|
|
131
141
|
@should_connect_to_voice = {}
|
132
142
|
|
133
143
|
@ignored_ids = Set.new
|
144
|
+
@ignore_bots = ignore_bots
|
134
145
|
|
135
146
|
@event_threads = []
|
136
147
|
@current_thread = 0
|
@@ -142,6 +153,7 @@ module Discordrb
|
|
142
153
|
# @return [Hash<Integer => User>] The users by ID.
|
143
154
|
def users
|
144
155
|
gateway_check
|
156
|
+
unavailable_servers_check
|
145
157
|
@users
|
146
158
|
end
|
147
159
|
|
@@ -149,28 +161,24 @@ module Discordrb
|
|
149
161
|
# @return [Hash<Integer => Server>] The servers by ID.
|
150
162
|
def servers
|
151
163
|
gateway_check
|
164
|
+
unavailable_servers_check
|
152
165
|
@servers
|
153
166
|
end
|
154
167
|
|
155
168
|
# @overload emoji(id)
|
156
169
|
# Return an emoji by its ID
|
157
|
-
# @param id [Integer] The emoji's ID.
|
158
|
-
# @return
|
170
|
+
# @param id [String, Integer] The emoji's ID.
|
171
|
+
# @return [Emoji, nil] the emoji object. `nil` if the emoji was not found.
|
159
172
|
# @overload emoji
|
160
173
|
# The list of emoji the bot can use.
|
161
|
-
# @return [Array<
|
174
|
+
# @return [Array<Emoji>] the emoji available.
|
162
175
|
def emoji(id = nil)
|
163
|
-
|
176
|
+
emoji_hash = servers.values.map(&:emoji).reduce(&:merge)
|
164
177
|
if id
|
165
|
-
|
178
|
+
id = id.resolve_id
|
179
|
+
emoji_hash[id]
|
166
180
|
else
|
167
|
-
|
168
|
-
@servers.each do |_, server|
|
169
|
-
server.emoji.values.each do |element|
|
170
|
-
emoji[element.name] = GlobalEmoji.new(element, self)
|
171
|
-
end
|
172
|
-
end
|
173
|
-
emoji.values
|
181
|
+
emoji_hash.values
|
174
182
|
end
|
175
183
|
end
|
176
184
|
|
@@ -198,8 +206,8 @@ module Discordrb
|
|
198
206
|
# The bot's OAuth application.
|
199
207
|
# @return [Application, nil] The bot's application info. Returns `nil` if bot is not a bot account.
|
200
208
|
def bot_application
|
201
|
-
|
202
|
-
|
209
|
+
return unless @type == :bot
|
210
|
+
|
203
211
|
response = API.oauth_application(token)
|
204
212
|
Application.new(JSON.parse(response), self)
|
205
213
|
end
|
@@ -214,37 +222,46 @@ module Discordrb
|
|
214
222
|
@token
|
215
223
|
end
|
216
224
|
|
217
|
-
# @return the raw token, without any prefix
|
225
|
+
# @return [String] the raw token, without any prefix
|
218
226
|
# @see #token
|
219
227
|
def raw_token
|
220
228
|
@token.split(' ').last
|
221
229
|
end
|
222
230
|
|
223
|
-
# Runs the bot, which logs into Discord and connects the WebSocket. This
|
224
|
-
#
|
225
|
-
#
|
226
|
-
#
|
227
|
-
#
|
228
|
-
#
|
229
|
-
|
231
|
+
# Runs the bot, which logs into Discord and connects the WebSocket. This
|
232
|
+
# prevents all further execution unless it is executed with
|
233
|
+
# `background` = `true`.
|
234
|
+
# @param background [true, false] If it is `true`, then the bot will run in
|
235
|
+
# another thread to allow further execution. If it is `false`, this method
|
236
|
+
# will block until {#stop} is called. If the bot is run with `true`, make
|
237
|
+
# sure to eventually call {#join} so the script doesn't stop prematurely.
|
238
|
+
# @note Running the bot in the background means that you can call some
|
239
|
+
# methods that require a gateway connection *before* that connection is
|
240
|
+
# established. In most cases an exception will be raised if you try to do
|
241
|
+
# this. If you need a way to safely run code after the bot is fully
|
242
|
+
# connected, use a {#ready} event handler instead.
|
243
|
+
def run(background = false)
|
230
244
|
@gateway.run_async
|
231
|
-
return if
|
245
|
+
return if background
|
232
246
|
|
233
247
|
debug('Oh wait! Not exiting yet as run was run synchronously.')
|
234
248
|
@gateway.sync
|
235
249
|
end
|
236
250
|
|
237
|
-
#
|
238
|
-
#
|
239
|
-
|
251
|
+
# Joins the bot's connection thread with the current thread.
|
252
|
+
# This blocks execution until the websocket stops, which should only happen
|
253
|
+
# manually triggered. or due to an error. This is necessary to have a
|
254
|
+
# continuously running bot.
|
255
|
+
def join
|
240
256
|
@gateway.sync
|
241
257
|
end
|
258
|
+
alias_method :sync, :join
|
242
259
|
|
243
260
|
# Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
|
244
261
|
# Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
|
245
|
-
# @
|
246
|
-
def stop(
|
247
|
-
@gateway.stop
|
262
|
+
# @note This method no longer takes an argument as of 3.4.0
|
263
|
+
def stop(_no_sync = nil)
|
264
|
+
@gateway.stop
|
248
265
|
end
|
249
266
|
|
250
267
|
# @return [true, false] whether or not the bot is currently connected to Discord.
|
@@ -254,22 +271,21 @@ module Discordrb
|
|
254
271
|
|
255
272
|
# Makes the bot join an invite to a server.
|
256
273
|
# @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
|
257
|
-
def
|
274
|
+
def accept_invite(invite)
|
258
275
|
resolved = invite(invite).code
|
259
276
|
API::Invite.accept(token, resolved)
|
260
277
|
end
|
261
278
|
|
262
279
|
# Creates an OAuth invite URL that can be used to invite this bot to a particular server.
|
263
|
-
# Requires the application ID to have been set during initialization.
|
264
280
|
# @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
|
265
|
-
# @param permission_bits [
|
281
|
+
# @param permission_bits [String, Integer] Permission bits that should be appended to invite url.
|
266
282
|
# @return [String] the OAuth invite URL.
|
267
283
|
def invite_url(server: nil, permission_bits: nil)
|
268
|
-
|
284
|
+
@client_id ||= bot_application.id
|
269
285
|
|
270
286
|
server_id_str = server ? "&guild_id=#{server.id}" : ''
|
271
287
|
permission_bits_str = permission_bits ? "&permissions=#{permission_bits}" : ''
|
272
|
-
"https://
|
288
|
+
"https://discord.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
|
273
289
|
end
|
274
290
|
|
275
291
|
# @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
|
@@ -278,7 +294,7 @@ module Discordrb
|
|
278
294
|
# Gets the voice bot for a particular server or channel. You can connect to a new channel using the {#voice_connect}
|
279
295
|
# method.
|
280
296
|
# @param thing [Channel, Server, Integer] the server or channel you want to get the voice bot for, or its ID.
|
281
|
-
# @return [VoiceBot, nil] the VoiceBot for the thing you specified, or nil if there is no connection yet
|
297
|
+
# @return [Voice::VoiceBot, nil] the VoiceBot for the thing you specified, or nil if there is no connection yet
|
282
298
|
def voice(thing)
|
283
299
|
id = thing.resolve_id
|
284
300
|
return @voices[id] if @voices[id]
|
@@ -288,22 +304,21 @@ module Discordrb
|
|
288
304
|
|
289
305
|
server_id = channel.server.id
|
290
306
|
return @voices[server_id] if @voices[server_id]
|
291
|
-
|
292
|
-
nil
|
293
307
|
end
|
294
308
|
|
295
309
|
# Connects to a voice channel, initializes network connections and returns the {Voice::VoiceBot} over which audio
|
296
310
|
# data can then be sent. After connecting, the bot can also be accessed using {#voice}. If the bot is already
|
297
311
|
# connected to voice, the existing connection will be terminated - you don't have to call
|
298
312
|
# {Discordrb::Voice::VoiceBot#destroy} before calling this method.
|
299
|
-
# @param chan [Channel] The voice channel to connect to.
|
300
|
-
# @param encrypted [true, false] Whether voice communication should be encrypted using
|
313
|
+
# @param chan [Channel, String, Integer] The voice channel, or its ID, to connect to.
|
314
|
+
# @param encrypted [true, false] Whether voice communication should be encrypted using
|
301
315
|
# (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)
|
302
316
|
# @return [Voice::VoiceBot] the initialized bot over which audio data can then be sent.
|
303
317
|
def voice_connect(chan, encrypted = true)
|
318
|
+
raise ArgumentError, 'Unencrypted voice connections are no longer supported.' unless encrypted
|
319
|
+
|
304
320
|
chan = channel(chan.resolve_id)
|
305
321
|
server_id = chan.server.id
|
306
|
-
@should_encrypt_voice = encrypted
|
307
322
|
|
308
323
|
if @voices[chan.id]
|
309
324
|
debug('Voice bot exists already! Destroying it')
|
@@ -325,13 +340,14 @@ module Discordrb
|
|
325
340
|
|
326
341
|
# Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use
|
327
342
|
# {Discordrb::Voice::VoiceBot#destroy} rather than this.
|
328
|
-
# @param
|
343
|
+
# @param server [Server, String, Integer] The server, or server ID, the voice connection is on.
|
329
344
|
# @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
|
330
345
|
# directly, you should leave it as true.
|
331
|
-
def voice_destroy(
|
332
|
-
|
333
|
-
@
|
334
|
-
@voices.
|
346
|
+
def voice_destroy(server, destroy_vws = true)
|
347
|
+
server = server.resolve_id
|
348
|
+
@gateway.send_voice_state_update(server.to_s, nil, false, false)
|
349
|
+
@voices[server].destroy if @voices[server] && destroy_vws
|
350
|
+
@voices.delete(server)
|
335
351
|
end
|
336
352
|
|
337
353
|
# Revokes an invite to a server. Will fail unless you have the *Manage Server* permission.
|
@@ -343,32 +359,38 @@ module Discordrb
|
|
343
359
|
end
|
344
360
|
|
345
361
|
# Sends a text message to a channel given its ID and the message's content.
|
346
|
-
# @param
|
362
|
+
# @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
|
347
363
|
# @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
|
348
364
|
# @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
|
349
|
-
# @param
|
365
|
+
# @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
|
366
|
+
# @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
|
367
|
+
# @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
|
350
368
|
# @return [Message] The message that was sent.
|
351
|
-
def send_message(
|
352
|
-
|
353
|
-
debug("Sending message to #{
|
369
|
+
def send_message(channel, content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
|
370
|
+
channel = channel.resolve_id
|
371
|
+
debug("Sending message to #{channel} with content '#{content}'")
|
372
|
+
allowed_mentions = { parse: [] } if allowed_mentions == false
|
373
|
+
message_reference = { message_id: message_reference.id } if message_reference
|
354
374
|
|
355
|
-
response = API::Channel.create_message(token,
|
375
|
+
response = API::Channel.create_message(token, channel, content, tts, embed&.to_hash, nil, attachments, allowed_mentions&.to_hash, message_reference)
|
356
376
|
Message.new(JSON.parse(response), self)
|
357
377
|
end
|
358
378
|
|
359
379
|
# Sends a text message to a channel given its ID and the message's content,
|
360
380
|
# then deletes it after the specified timeout in seconds.
|
361
|
-
# @param
|
381
|
+
# @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
|
362
382
|
# @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
|
363
383
|
# @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
|
364
384
|
# @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
|
365
|
-
# @param
|
366
|
-
|
385
|
+
# @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
|
386
|
+
# @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
|
387
|
+
# @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
|
388
|
+
def send_temporary_message(channel, content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil)
|
367
389
|
Thread.new do
|
368
|
-
|
390
|
+
Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
|
369
391
|
|
392
|
+
message = send_message(channel, content, tts, embed, attachments, allowed_mentions)
|
370
393
|
sleep(timeout)
|
371
|
-
|
372
394
|
message.delete
|
373
395
|
end
|
374
396
|
|
@@ -377,12 +399,26 @@ module Discordrb
|
|
377
399
|
|
378
400
|
# Sends a file to a channel. If it is an image, it will automatically be embedded.
|
379
401
|
# @note This executes in a blocking way, so if you're sending long files, be wary of delays.
|
380
|
-
# @param
|
402
|
+
# @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
|
381
403
|
# @param file [File] The file that should be sent.
|
382
404
|
# @param caption [string] The caption for the file.
|
383
405
|
# @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
|
384
|
-
|
385
|
-
|
406
|
+
# @param filename [String] Overrides the filename of the uploaded file
|
407
|
+
# @param spoiler [true, false] Whether or not this file should appear as a spoiler.
|
408
|
+
# @example Send a file from disk
|
409
|
+
# bot.send_file(83281822225530880, File.open('rubytaco.png', 'r'))
|
410
|
+
def send_file(channel, file, caption: nil, tts: false, filename: nil, spoiler: nil)
|
411
|
+
if file.respond_to?(:read)
|
412
|
+
if spoiler
|
413
|
+
filename ||= File.basename(file.path)
|
414
|
+
filename = "SPOILER_#{filename}" unless filename.start_with? 'SPOILER_'
|
415
|
+
end
|
416
|
+
# https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
|
417
|
+
file.define_singleton_method(:original_filename) { filename } if filename
|
418
|
+
end
|
419
|
+
|
420
|
+
channel = channel.resolve_id
|
421
|
+
response = API::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
|
386
422
|
Message.new(JSON.parse(response), self)
|
387
423
|
end
|
388
424
|
|
@@ -390,29 +426,18 @@ module Discordrb
|
|
390
426
|
# @note Discord's API doesn't directly return the server when creating it, so this method
|
391
427
|
# waits until the data has been received via the websocket. This may make the execution take a while.
|
392
428
|
# @param name [String] The name the new server should have. Doesn't have to be alphanumeric.
|
393
|
-
# @param region [Symbol] The region where the server should be created
|
394
|
-
#
|
395
|
-
# * `:london`
|
396
|
-
# * `:amsterdam`
|
397
|
-
# * `:frankfurt`
|
398
|
-
# * `:us-east`
|
399
|
-
# * `:us-west`
|
400
|
-
# * `:us-south`
|
401
|
-
# * `:us-central`
|
402
|
-
# * `:singapore`
|
403
|
-
# * `:sydney`
|
429
|
+
# @param region [Symbol] The region where the server should be created, for example 'eu-central' or 'hongkong'.
|
404
430
|
# @return [Server] The server that was created.
|
405
|
-
def create_server(name, region = :
|
431
|
+
def create_server(name, region = :'eu-central')
|
406
432
|
response = API::Server.create(token, name, region)
|
407
433
|
id = JSON.parse(response)['id'].to_i
|
408
|
-
sleep 0.1 until @servers[id]
|
409
|
-
server = @servers[id]
|
434
|
+
sleep 0.1 until (server = @servers[id])
|
410
435
|
debug "Successfully created server #{server.id} with name #{server.name}"
|
411
436
|
server
|
412
437
|
end
|
413
438
|
|
414
439
|
# Creates a new application to do OAuth authorization with. This allows you to use OAuth to authorize users using
|
415
|
-
# Discord. For information how to use this, see the docs: https://
|
440
|
+
# Discord. For information how to use this, see the docs: https://discord.com/developers/docs/topics/oauth2
|
416
441
|
# @param name [String] What your application should be called.
|
417
442
|
# @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
|
418
443
|
# @return [Array(String, String)] your applications' client ID and client secret to be used in OAuth authorization.
|
@@ -431,42 +456,69 @@ module Discordrb
|
|
431
456
|
API.update_oauth_application(@token, name, redirect_uris, description, icon)
|
432
457
|
end
|
433
458
|
|
434
|
-
# Gets the
|
435
|
-
# @param
|
436
|
-
# @param server [Server, nil] The server of the associated
|
437
|
-
# @return [User, Role, Emoji] The
|
438
|
-
def
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
459
|
+
# Gets the users, channels, roles and emoji from a string.
|
460
|
+
# @param mentions [String] The mentions, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
|
461
|
+
# @param server [Server, nil] The server of the associated mentions. (recommended for role parsing, to speed things up)
|
462
|
+
# @return [Array<User, Channel, Role, Emoji>] The array of users, channels, roles and emoji identified by the mentions, or `nil` if none exists.
|
463
|
+
def parse_mentions(mentions, server = nil)
|
464
|
+
array_to_return = []
|
465
|
+
# While possible mentions may be in message
|
466
|
+
while mentions.include?('<') && mentions.include?('>')
|
467
|
+
# Removing all content before the next possible mention
|
468
|
+
mentions = mentions.split('<', 2)[1]
|
469
|
+
# Locate the first valid mention enclosed in `<...>`, otherwise advance to the next open `<`
|
470
|
+
next unless mentions.split('>', 2).first.length < mentions.split('<', 2).first.length
|
471
|
+
|
472
|
+
# Store the possible mention value to be validated with RegEx
|
473
|
+
mention = mentions.split('>', 2).first
|
474
|
+
if /@!?(?<id>\d+)/ =~ mention
|
475
|
+
array_to_return << user(id) unless user(id).nil?
|
476
|
+
elsif /#(?<id>\d+)/ =~ mention
|
477
|
+
array_to_return << channel(id, server) unless channel(id, server).nil?
|
478
|
+
elsif /@&(?<id>\d+)/ =~ mention
|
479
|
+
if server
|
480
|
+
array_to_return << server.role(id) unless server.role(id).nil?
|
481
|
+
else
|
482
|
+
@servers.each_value do |element|
|
483
|
+
array_to_return << element.role(id) unless element.role(id).nil?
|
484
|
+
end
|
485
|
+
end
|
486
|
+
elsif /(?<animated>^a|^${0}):(?<name>\w+):(?<id>\d+)/ =~ mention
|
487
|
+
array_to_return << (emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil))
|
447
488
|
end
|
448
|
-
elsif /<:(\w+):(?<id>\d+)>?/ =~ mention
|
449
|
-
emoji.find { |element| element.id.to_i == id.to_i }
|
450
489
|
end
|
490
|
+
array_to_return
|
491
|
+
end
|
492
|
+
|
493
|
+
# Gets the user, channel, role or emoji from a string.
|
494
|
+
# @param mention [String] The mention, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
|
495
|
+
# @param server [Server, nil] The server of the associated mention. (recommended for role parsing, to speed things up)
|
496
|
+
# @return [User, Channel, Role, Emoji] The user, channel, role or emoji identified by the mention, or `nil` if none exists.
|
497
|
+
def parse_mention(mention, server = nil)
|
498
|
+
parse_mentions(mention, server).first
|
451
499
|
end
|
452
500
|
|
453
501
|
# Updates presence status.
|
454
|
-
# @param status [String] The status the bot should show up as.
|
455
|
-
# @param
|
502
|
+
# @param status [String] The status the bot should show up as. Can be `online`, `dnd`, `idle`, or `invisible`
|
503
|
+
# @param activity [String, nil] The name of the activity to be played/watched/listened to/stream name on the stream.
|
456
504
|
# @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
|
457
505
|
# @param since [Integer] When this status was set.
|
458
506
|
# @param afk [true, false] Whether the bot is AFK.
|
507
|
+
# @param activity_type [Integer] The type of activity status to display. Can be 0 (Playing), 1 (Streaming), 2 (Listening), 3 (Watching)
|
459
508
|
# @see Gateway#send_status_update
|
460
|
-
def update_status(status,
|
509
|
+
def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
|
461
510
|
gateway_check
|
462
511
|
|
463
|
-
@
|
512
|
+
@activity = activity
|
464
513
|
@status = status
|
465
514
|
@streamurl = url
|
466
|
-
type = url ? 1 :
|
515
|
+
type = url ? 1 : activity_type
|
516
|
+
|
517
|
+
activity_obj = activity || url ? { 'name' => activity, 'url' => url, 'type' => type } : nil
|
518
|
+
@gateway.send_status_update(status, since, activity_obj, afk)
|
467
519
|
|
468
|
-
|
469
|
-
|
520
|
+
# Update the status in the cache
|
521
|
+
profile.update_presence('status' => status.to_s, 'activities' => [activity_obj].compact)
|
470
522
|
end
|
471
523
|
|
472
524
|
# Sets the currently playing game to the specified game.
|
@@ -475,7 +527,24 @@ module Discordrb
|
|
475
527
|
def game=(name)
|
476
528
|
gateway_check
|
477
529
|
update_status(@status, name, nil)
|
478
|
-
|
530
|
+
end
|
531
|
+
|
532
|
+
alias_method :playing=, :game=
|
533
|
+
|
534
|
+
# Sets the current listening status to the specified name.
|
535
|
+
# @param name [String] The thing to be listened to.
|
536
|
+
# @return [String] The thing that is now being listened to.
|
537
|
+
def listening=(name)
|
538
|
+
gateway_check
|
539
|
+
update_status(@status, name, nil, nil, nil, 2)
|
540
|
+
end
|
541
|
+
|
542
|
+
# Sets the current watching status to the specified name.
|
543
|
+
# @param name [String] The thing to be watched.
|
544
|
+
# @return [String] The thing that is now being watched.
|
545
|
+
def watching=(name)
|
546
|
+
gateway_check
|
547
|
+
update_status(@status, name, nil, nil, nil, 3)
|
479
548
|
end
|
480
549
|
|
481
550
|
# Sets the currently online stream to the specified name and Twitch URL.
|
@@ -491,7 +560,7 @@ module Discordrb
|
|
491
560
|
# Sets status to online.
|
492
561
|
def online
|
493
562
|
gateway_check
|
494
|
-
update_status(:online, @
|
563
|
+
update_status(:online, @activity, @streamurl)
|
495
564
|
end
|
496
565
|
|
497
566
|
alias_method :on, :online
|
@@ -499,7 +568,7 @@ module Discordrb
|
|
499
568
|
# Sets status to idle.
|
500
569
|
def idle
|
501
570
|
gateway_check
|
502
|
-
update_status(:idle, @
|
571
|
+
update_status(:idle, @activity, nil)
|
503
572
|
end
|
504
573
|
|
505
574
|
alias_method :away, :idle
|
@@ -507,13 +576,13 @@ module Discordrb
|
|
507
576
|
# Sets the bot's status to DnD (red icon).
|
508
577
|
def dnd
|
509
578
|
gateway_check
|
510
|
-
update_status(:dnd, @
|
579
|
+
update_status(:dnd, @activity, nil)
|
511
580
|
end
|
512
581
|
|
513
582
|
# Sets the bot's status to invisible (appears offline).
|
514
583
|
def invisible
|
515
584
|
gateway_check
|
516
|
-
update_status(:invisible, @
|
585
|
+
update_status(:invisible, @activity, nil)
|
517
586
|
end
|
518
587
|
|
519
588
|
# Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
|
@@ -539,29 +608,77 @@ module Discordrb
|
|
539
608
|
# @yield Is executed when the await is triggered.
|
540
609
|
# @yieldparam event [Event] The event object that was triggered.
|
541
610
|
# @return [Await] The await that was created.
|
611
|
+
# @deprecated Will be changed to blocking behavior in v4.0. Use {#add_await!} instead.
|
542
612
|
def add_await(key, type, attributes = {}, &block)
|
543
613
|
raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
|
614
|
+
|
544
615
|
await = Await.new(self, key, type, attributes, block)
|
545
616
|
@awaits ||= {}
|
546
617
|
@awaits[key] = await
|
547
618
|
end
|
548
619
|
|
620
|
+
# Awaits an event, blocking the current thread until a response is received.
|
621
|
+
# @param type [Class] The event class that should be listened for.
|
622
|
+
# @option attributes [Numeric] :timeout the amount of time (in seconds) to wait for a response before returning `nil`. Waits forever if omitted.
|
623
|
+
# @yield Executed when a matching event is received.
|
624
|
+
# @yieldparam event [Event] The event object that was triggered.
|
625
|
+
# @yieldreturn [true, false] Whether the event matches extra await criteria described by the block
|
626
|
+
# @return [Event, nil] The event object that was triggered, or `nil` if a `timeout` was set and no event was raised in time.
|
627
|
+
# @raise [ArgumentError] if `timeout` is given and is not a positive numeric value
|
628
|
+
def add_await!(type, attributes = {})
|
629
|
+
raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
|
630
|
+
|
631
|
+
timeout = attributes[:timeout]
|
632
|
+
raise ArgumentError, 'Timeout must be a number > 0' if timeout.is_a?(Numeric) && !timeout.positive?
|
633
|
+
|
634
|
+
mutex = Mutex.new
|
635
|
+
cv = ConditionVariable.new
|
636
|
+
response = nil
|
637
|
+
block = lambda do |event|
|
638
|
+
mutex.synchronize do
|
639
|
+
response = event
|
640
|
+
if block_given?
|
641
|
+
result = yield(event)
|
642
|
+
cv.signal if result.is_a?(TrueClass)
|
643
|
+
else
|
644
|
+
cv.signal
|
645
|
+
end
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
handler = register_event(type, attributes, block)
|
650
|
+
|
651
|
+
if timeout
|
652
|
+
Thread.new do
|
653
|
+
sleep timeout
|
654
|
+
mutex.synchronize { cv.signal }
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
mutex.synchronize { cv.wait(mutex) }
|
659
|
+
|
660
|
+
remove_handler(handler)
|
661
|
+
raise 'ConditionVariable was signaled without returning an event!' if response.nil? && timeout.nil?
|
662
|
+
|
663
|
+
response
|
664
|
+
end
|
665
|
+
|
549
666
|
# Add a user to the list of ignored users. Those users will be ignored in message events at event processing level.
|
550
667
|
# @note Ignoring a user only prevents any message events (including mentions, commands etc.) from them! Typing and
|
551
668
|
# presence and any other events will still be received.
|
552
|
-
# @param user [User,
|
669
|
+
# @param user [User, String, Integer] The user, or its ID, to be ignored.
|
553
670
|
def ignore_user(user)
|
554
671
|
@ignored_ids << user.resolve_id
|
555
672
|
end
|
556
673
|
|
557
674
|
# Remove a user from the ignore list.
|
558
|
-
# @param user [User,
|
675
|
+
# @param user [User, String, Integer] The user, or its ID, to be unignored.
|
559
676
|
def unignore_user(user)
|
560
677
|
@ignored_ids.delete(user.resolve_id)
|
561
678
|
end
|
562
679
|
|
563
680
|
# Checks whether a user is being ignored.
|
564
|
-
# @param user [User,
|
681
|
+
# @param user [User, String, Integer] The user, or its ID, to check.
|
565
682
|
# @return [true, false] whether or not the user is ignored.
|
566
683
|
def ignored?(user)
|
567
684
|
@ignored_ids.include?(user.resolve_id)
|
@@ -587,6 +704,7 @@ module Discordrb
|
|
587
704
|
raise_event(HeartbeatEvent.new(self))
|
588
705
|
end
|
589
706
|
|
707
|
+
# Makes the bot leave any groups with no recipients remaining
|
590
708
|
def prune_empty_groups
|
591
709
|
@channels.each_value do |channel|
|
592
710
|
channel.leave_group if channel.group? && channel.recipients.empty?
|
@@ -595,11 +713,19 @@ module Discordrb
|
|
595
713
|
|
596
714
|
private
|
597
715
|
|
598
|
-
# Throws a useful exception if there's currently no gateway connection
|
716
|
+
# Throws a useful exception if there's currently no gateway connection.
|
599
717
|
def gateway_check
|
600
|
-
|
718
|
+
raise "A gateway connection is necessary to call this method! You'll have to do it inside any event (e.g. `ready`) or after `bot.run :async`." unless connected?
|
719
|
+
end
|
720
|
+
|
721
|
+
# Logs a warning if there are servers which are still unavailable.
|
722
|
+
# e.g. due to a Discord outage or because the servers are large and taking a while to load.
|
723
|
+
def unavailable_servers_check
|
724
|
+
# Return unless there are servers that are unavailable.
|
725
|
+
return unless @unavailable_servers&.positive?
|
601
726
|
|
602
|
-
|
727
|
+
LOGGER.warn("#{@unavailable_servers} servers haven't been cached yet.")
|
728
|
+
LOGGER.warn('Servers may be unavailable due to an outage, or your bot is on very large servers that are taking a while to load.')
|
603
729
|
end
|
604
730
|
|
605
731
|
### ## ## ######## ######## ######## ## ## ### ## ######
|
@@ -640,21 +766,39 @@ module Discordrb
|
|
640
766
|
member.update_username(username)
|
641
767
|
end
|
642
768
|
|
643
|
-
member.
|
644
|
-
|
769
|
+
member.update_presence(data)
|
770
|
+
|
771
|
+
member.avatar_id = data['user']['avatar'] if data['user']['avatar']
|
645
772
|
|
646
773
|
server.cache_member(member)
|
647
774
|
end
|
648
775
|
|
649
|
-
# Internal handler for
|
776
|
+
# Internal handler for VOICE_STATE_UPDATE
|
650
777
|
def update_voice_state(data)
|
778
|
+
@session_id = data['session_id']
|
779
|
+
|
651
780
|
server_id = data['guild_id'].to_i
|
652
781
|
server = server(server_id)
|
653
782
|
return unless server
|
654
783
|
|
784
|
+
user_id = data['user_id'].to_i
|
785
|
+
old_voice_state = server.voice_states[user_id]
|
786
|
+
old_channel_id = old_voice_state.voice_channel&.id if old_voice_state
|
787
|
+
|
655
788
|
server.update_voice_state(data)
|
656
789
|
|
657
|
-
|
790
|
+
existing_voice = @voices[server_id]
|
791
|
+
if user_id == @profile.id && existing_voice
|
792
|
+
new_channel_id = data['channel_id']
|
793
|
+
if new_channel_id
|
794
|
+
new_channel = channel(new_channel_id)
|
795
|
+
existing_voice.channel = new_channel
|
796
|
+
else
|
797
|
+
voice_destroy(server_id)
|
798
|
+
end
|
799
|
+
end
|
800
|
+
|
801
|
+
old_channel_id
|
658
802
|
end
|
659
803
|
|
660
804
|
# Internal handler for VOICE_SERVER_UPDATE
|
@@ -664,6 +808,7 @@ module Discordrb
|
|
664
808
|
|
665
809
|
debug("Voice server update received! chan: #{channel.inspect}")
|
666
810
|
return unless channel
|
811
|
+
|
667
812
|
@should_connect_to_voice.delete(server_id)
|
668
813
|
debug('Updating voice server!')
|
669
814
|
|
@@ -676,7 +821,7 @@ module Discordrb
|
|
676
821
|
end
|
677
822
|
|
678
823
|
debug('Got data, now creating the bot.')
|
679
|
-
@voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint
|
824
|
+
@voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint)
|
680
825
|
end
|
681
826
|
|
682
827
|
# Internal handler for CHANNEL_CREATE
|
@@ -686,7 +831,7 @@ module Discordrb
|
|
686
831
|
|
687
832
|
# Handle normal and private channels separately
|
688
833
|
if server
|
689
|
-
server.
|
834
|
+
server.add_channel(channel)
|
690
835
|
@channels[channel.id] = channel
|
691
836
|
elsif channel.pm?
|
692
837
|
@pm_channels[channel.recipient.id] = channel
|
@@ -700,6 +845,7 @@ module Discordrb
|
|
700
845
|
channel = Channel.new(data, self)
|
701
846
|
old_channel = @channels[channel.id]
|
702
847
|
return unless old_channel
|
848
|
+
|
703
849
|
old_channel.update_from(channel)
|
704
850
|
end
|
705
851
|
|
@@ -711,7 +857,7 @@ module Discordrb
|
|
711
857
|
# Handle normal and private channels separately
|
712
858
|
if server
|
713
859
|
@channels.delete(channel.id)
|
714
|
-
server.
|
860
|
+
server.delete_channel(channel.id)
|
715
861
|
elsif channel.pm?
|
716
862
|
@pm_channels.delete(channel.recipient.id)
|
717
863
|
elsif channel.group?
|
@@ -756,12 +902,14 @@ module Discordrb
|
|
756
902
|
member = server.member(data['user']['id'].to_i)
|
757
903
|
member.update_roles(data['roles'])
|
758
904
|
member.update_nick(data['nick'])
|
905
|
+
member.update_boosting_since(data['premium_since'])
|
759
906
|
end
|
760
907
|
|
761
908
|
# Internal handler for GUILD_MEMBER_DELETE
|
762
909
|
def delete_guild_member(data)
|
763
910
|
server_id = data['guild_id'].to_i
|
764
911
|
server = self.server(server_id)
|
912
|
+
return unless server
|
765
913
|
|
766
914
|
user_id = data['user']['id'].to_i
|
767
915
|
server.delete_member(user_id)
|
@@ -802,7 +950,12 @@ module Discordrb
|
|
802
950
|
server_id = data['guild_id'].to_i
|
803
951
|
server = @servers[server_id]
|
804
952
|
new_role = Role.new(role_data, self, server)
|
805
|
-
server.
|
953
|
+
existing_role = server.role(new_role.id)
|
954
|
+
if existing_role
|
955
|
+
existing_role.update_from(new_role)
|
956
|
+
else
|
957
|
+
server.add_role(new_role)
|
958
|
+
end
|
806
959
|
end
|
807
960
|
|
808
961
|
# Internal handler for GUILD_ROLE_DELETE
|
@@ -813,6 +966,13 @@ module Discordrb
|
|
813
966
|
server.delete_role(role_id)
|
814
967
|
end
|
815
968
|
|
969
|
+
# Internal handler for GUILD_EMOJIS_UPDATE
|
970
|
+
def update_guild_emoji(data)
|
971
|
+
server_id = data['guild_id'].to_i
|
972
|
+
server = @servers[server_id]
|
973
|
+
server.update_emoji_data(data)
|
974
|
+
end
|
975
|
+
|
816
976
|
# Internal handler for MESSAGE_CREATE
|
817
977
|
def create_message(data); end
|
818
978
|
|
@@ -825,6 +985,15 @@ module Discordrb
|
|
825
985
|
# Internal handler for MESSAGE_DELETE
|
826
986
|
def delete_message(data); end
|
827
987
|
|
988
|
+
# Internal handler for MESSAGE_REACTION_ADD
|
989
|
+
def add_message_reaction(data); end
|
990
|
+
|
991
|
+
# Internal handler for MESSAGE_REACTION_REMOVE
|
992
|
+
def remove_message_reaction(data); end
|
993
|
+
|
994
|
+
# Internal handler for MESSAGE_REACTION_REMOVE_ALL
|
995
|
+
def remove_all_message_reactions(data); end
|
996
|
+
|
828
997
|
# Internal handler for GUILD_BAN_ADD
|
829
998
|
def add_user_ban(data); end
|
830
999
|
|
@@ -843,16 +1012,16 @@ module Discordrb
|
|
843
1012
|
# Remove the "Bot " prefix if it exists
|
844
1013
|
token = token[4..-1] if token.start_with? 'Bot '
|
845
1014
|
|
846
|
-
token =
|
1015
|
+
token = "Bot #{token}" unless type == :user
|
847
1016
|
token
|
848
1017
|
end
|
849
1018
|
|
850
1019
|
def handle_dispatch(type, data)
|
851
1020
|
# Check whether there are still unavailable servers and there have been more than 10 seconds since READY
|
852
|
-
if @unavailable_servers &&
|
1021
|
+
if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !((@intents || 0) & INTENTS[:servers]).zero?
|
853
1022
|
# The server streaming timed out!
|
854
|
-
LOGGER.
|
855
|
-
LOGGER.
|
1023
|
+
LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
|
1024
|
+
LOGGER.debug('Calling ready now because server loading is taking a long time. Servers may be unavailable due to an outage, or your bot is on very large servers.')
|
856
1025
|
|
857
1026
|
# Unset the unavailable server count so this doesn't get triggered again
|
858
1027
|
@unavailable_servers = 0
|
@@ -911,12 +1080,22 @@ module Discordrb
|
|
911
1080
|
id = data['guild_id'].to_i
|
912
1081
|
server = server(id)
|
913
1082
|
server.process_chunk(data['members'])
|
1083
|
+
when :INVITE_CREATE
|
1084
|
+
invite = Invite.new(data, self)
|
1085
|
+
raise_event(InviteCreateEvent.new(data, invite, self))
|
1086
|
+
when :INVITE_DELETE
|
1087
|
+
raise_event(InviteDeleteEvent.new(data, self))
|
914
1088
|
when :MESSAGE_CREATE
|
915
1089
|
if ignored?(data['author']['id'].to_i)
|
916
1090
|
debug("Ignored author with ID #{data['author']['id']}")
|
917
1091
|
return
|
918
1092
|
end
|
919
1093
|
|
1094
|
+
if @ignore_bots && data['author']['bot']
|
1095
|
+
debug("Ignored Bot account with ID #{data['author']['id']}")
|
1096
|
+
return
|
1097
|
+
end
|
1098
|
+
|
920
1099
|
# If create_message is overwritten with a method that returns the parsed message, use that instead, so we don't
|
921
1100
|
# parse the message twice (which is just thrown away performance)
|
922
1101
|
message = create_message(data)
|
@@ -940,6 +1119,10 @@ module Discordrb
|
|
940
1119
|
update_message(data)
|
941
1120
|
|
942
1121
|
message = Message.new(data, self)
|
1122
|
+
|
1123
|
+
event = MessageUpdateEvent.new(message, self)
|
1124
|
+
raise_event(event)
|
1125
|
+
|
943
1126
|
return if message.from_bot? && !should_parse_self
|
944
1127
|
|
945
1128
|
unless message.author
|
@@ -979,31 +1162,51 @@ module Discordrb
|
|
979
1162
|
rescue Discordrb::Errors::NoPermission
|
980
1163
|
debug 'Typing started in channel the bot has no access to, ignoring'
|
981
1164
|
end
|
1165
|
+
when :MESSAGE_REACTION_ADD
|
1166
|
+
add_message_reaction(data)
|
1167
|
+
|
1168
|
+
return if profile.id == data['user_id'].to_i && !should_parse_self
|
1169
|
+
|
1170
|
+
event = ReactionAddEvent.new(data, self)
|
1171
|
+
raise_event(event)
|
1172
|
+
when :MESSAGE_REACTION_REMOVE
|
1173
|
+
remove_message_reaction(data)
|
1174
|
+
|
1175
|
+
return if profile.id == data['user_id'].to_i && !should_parse_self
|
1176
|
+
|
1177
|
+
event = ReactionRemoveEvent.new(data, self)
|
1178
|
+
raise_event(event)
|
1179
|
+
when :MESSAGE_REACTION_REMOVE_ALL
|
1180
|
+
remove_all_message_reactions(data)
|
1181
|
+
|
1182
|
+
event = ReactionRemoveAllEvent.new(data, self)
|
1183
|
+
raise_event(event)
|
982
1184
|
when :PRESENCE_UPDATE
|
983
1185
|
# Ignore friends list presences
|
984
1186
|
return unless data['guild_id']
|
985
1187
|
|
986
|
-
now_playing = data['game']
|
1188
|
+
now_playing = data['game'].nil? ? nil : data['game']['name']
|
987
1189
|
presence_user = @users[data['user']['id'].to_i]
|
988
1190
|
played_before = presence_user.nil? ? nil : presence_user.game
|
989
1191
|
update_presence(data)
|
990
1192
|
|
991
|
-
event = if now_playing
|
992
|
-
PlayingEvent.new(data, self)
|
993
|
-
else
|
1193
|
+
event = if now_playing == played_before
|
994
1194
|
PresenceEvent.new(data, self)
|
1195
|
+
else
|
1196
|
+
PlayingEvent.new(data, self)
|
995
1197
|
end
|
996
1198
|
|
997
1199
|
raise_event(event)
|
998
1200
|
when :VOICE_STATE_UPDATE
|
999
|
-
update_voice_state(data)
|
1201
|
+
old_channel_id = update_voice_state(data)
|
1000
1202
|
|
1001
|
-
event = VoiceStateUpdateEvent.new(data, self)
|
1203
|
+
event = VoiceStateUpdateEvent.new(data, old_channel_id, self)
|
1002
1204
|
raise_event(event)
|
1003
1205
|
when :VOICE_SERVER_UPDATE
|
1004
1206
|
update_voice_server(data)
|
1005
1207
|
|
1006
|
-
|
1208
|
+
event = VoiceServerUpdateEvent.new(data, self)
|
1209
|
+
raise_event(event)
|
1007
1210
|
when :CHANNEL_CREATE
|
1008
1211
|
create_channel(data)
|
1009
1212
|
|
@@ -1100,9 +1303,52 @@ module Discordrb
|
|
1100
1303
|
|
1101
1304
|
event = ServerDeleteEvent.new(data, self)
|
1102
1305
|
raise_event(event)
|
1306
|
+
when :GUILD_EMOJIS_UPDATE
|
1307
|
+
server_id = data['guild_id'].to_i
|
1308
|
+
server = @servers[server_id]
|
1309
|
+
old_emoji_data = server.emoji.clone
|
1310
|
+
update_guild_emoji(data)
|
1311
|
+
new_emoji_data = server.emoji
|
1312
|
+
|
1313
|
+
created_ids = new_emoji_data.keys - old_emoji_data.keys
|
1314
|
+
deleted_ids = old_emoji_data.keys - new_emoji_data.keys
|
1315
|
+
updated_ids = old_emoji_data.select do |k, v|
|
1316
|
+
new_emoji_data[k] && (v.name != new_emoji_data[k].name || v.roles != new_emoji_data[k].roles)
|
1317
|
+
end.keys
|
1318
|
+
|
1319
|
+
event = ServerEmojiChangeEvent.new(server, data, self)
|
1320
|
+
raise_event(event)
|
1321
|
+
|
1322
|
+
created_ids.each do |e|
|
1323
|
+
event = ServerEmojiCreateEvent.new(server, new_emoji_data[e], self)
|
1324
|
+
raise_event(event)
|
1325
|
+
end
|
1326
|
+
|
1327
|
+
deleted_ids.each do |e|
|
1328
|
+
event = ServerEmojiDeleteEvent.new(server, old_emoji_data[e], self)
|
1329
|
+
raise_event(event)
|
1330
|
+
end
|
1331
|
+
|
1332
|
+
updated_ids.each do |e|
|
1333
|
+
event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
|
1334
|
+
raise_event(event)
|
1335
|
+
end
|
1336
|
+
when :WEBHOOKS_UPDATE
|
1337
|
+
event = WebhookUpdateEvent.new(data, self)
|
1338
|
+
raise_event(event)
|
1103
1339
|
else
|
1104
1340
|
# another event that we don't support yet
|
1105
|
-
debug "Event #{type} has been received but is unsupported
|
1341
|
+
debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"
|
1342
|
+
|
1343
|
+
event = UnknownEvent.new(type, data, self)
|
1344
|
+
raise_event(event)
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
# The existence of this array is checked before for performance reasons, since this has to be done for *every*
|
1348
|
+
# dispatch.
|
1349
|
+
if @event_handlers && @event_handlers[RawEvent]
|
1350
|
+
event = RawEvent.new(type, data, self)
|
1351
|
+
raise_event(event)
|
1106
1352
|
end
|
1107
1353
|
rescue Exception => e
|
1108
1354
|
LOGGER.error('Gateway message error!')
|
@@ -1124,7 +1370,9 @@ module Discordrb
|
|
1124
1370
|
|
1125
1371
|
@event_handlers ||= {}
|
1126
1372
|
handlers = @event_handlers[event.class]
|
1127
|
-
|
1373
|
+
return unless handlers
|
1374
|
+
|
1375
|
+
handlers.dup.each do |handler|
|
1128
1376
|
call_event(handler, event) if handler.matches?(event)
|
1129
1377
|
end
|
1130
1378
|
end
|
@@ -1139,7 +1387,7 @@ module Discordrb
|
|
1139
1387
|
begin
|
1140
1388
|
handler.call(event)
|
1141
1389
|
handler.after_call(event)
|
1142
|
-
rescue => e
|
1390
|
+
rescue StandardError => e
|
1143
1391
|
log_exception(e)
|
1144
1392
|
ensure
|
1145
1393
|
@event_threads.delete(t)
|
@@ -1152,6 +1400,7 @@ module Discordrb
|
|
1152
1400
|
@awaits.each do |_, await|
|
1153
1401
|
key, should_delete = await.match(event)
|
1154
1402
|
next unless key
|
1403
|
+
|
1155
1404
|
debug("should_delete: #{should_delete}")
|
1156
1405
|
@awaits.delete(await.key) if should_delete
|
1157
1406
|
|
@@ -1159,5 +1408,24 @@ module Discordrb
|
|
1159
1408
|
raise_event(await_event)
|
1160
1409
|
end
|
1161
1410
|
end
|
1411
|
+
|
1412
|
+
def calculate_intents(intents)
|
1413
|
+
intents.reduce(0) do |sum, intent|
|
1414
|
+
case intent
|
1415
|
+
when Symbol
|
1416
|
+
if INTENTS[intent]
|
1417
|
+
sum | INTENTS[intent]
|
1418
|
+
else
|
1419
|
+
LOGGER.warn("Unknown intent: #{intent}")
|
1420
|
+
sum
|
1421
|
+
end
|
1422
|
+
when Integer
|
1423
|
+
sum | intent
|
1424
|
+
else
|
1425
|
+
LOGGER.warn("Invalid intent: #{intent}")
|
1426
|
+
sum
|
1427
|
+
end
|
1428
|
+
end
|
1429
|
+
end
|
1162
1430
|
end
|
1163
1431
|
end
|