rubycord 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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/lib/rubycord/allowed_mentions.rb +34 -0
  3. data/lib/rubycord/api/application.rb +200 -0
  4. data/lib/rubycord/api/channel.rb +597 -0
  5. data/lib/rubycord/api/interaction.rb +52 -0
  6. data/lib/rubycord/api/invite.rb +42 -0
  7. data/lib/rubycord/api/server.rb +557 -0
  8. data/lib/rubycord/api/user.rb +153 -0
  9. data/lib/rubycord/api/webhook.rb +138 -0
  10. data/lib/rubycord/api.rb +356 -0
  11. data/lib/rubycord/await.rb +49 -0
  12. data/lib/rubycord/bot.rb +1757 -0
  13. data/lib/rubycord/cache.rb +259 -0
  14. data/lib/rubycord/colour_rgb.rb +41 -0
  15. data/lib/rubycord/commands/command_bot.rb +519 -0
  16. data/lib/rubycord/commands/container.rb +110 -0
  17. data/lib/rubycord/commands/events.rb +9 -0
  18. data/lib/rubycord/commands/parser.rb +325 -0
  19. data/lib/rubycord/commands/rate_limiter.rb +142 -0
  20. data/lib/rubycord/container.rb +753 -0
  21. data/lib/rubycord/data/activity.rb +269 -0
  22. data/lib/rubycord/data/application.rb +48 -0
  23. data/lib/rubycord/data/attachment.rb +109 -0
  24. data/lib/rubycord/data/audit_logs.rb +343 -0
  25. data/lib/rubycord/data/channel.rb +996 -0
  26. data/lib/rubycord/data/component.rb +227 -0
  27. data/lib/rubycord/data/embed.rb +249 -0
  28. data/lib/rubycord/data/emoji.rb +80 -0
  29. data/lib/rubycord/data/integration.rb +120 -0
  30. data/lib/rubycord/data/interaction.rb +798 -0
  31. data/lib/rubycord/data/invite.rb +135 -0
  32. data/lib/rubycord/data/member.rb +370 -0
  33. data/lib/rubycord/data/message.rb +412 -0
  34. data/lib/rubycord/data/overwrite.rb +106 -0
  35. data/lib/rubycord/data/profile.rb +89 -0
  36. data/lib/rubycord/data/reaction.rb +31 -0
  37. data/lib/rubycord/data/recipient.rb +32 -0
  38. data/lib/rubycord/data/role.rb +246 -0
  39. data/lib/rubycord/data/server.rb +1002 -0
  40. data/lib/rubycord/data/user.rb +261 -0
  41. data/lib/rubycord/data/voice_region.rb +43 -0
  42. data/lib/rubycord/data/voice_state.rb +39 -0
  43. data/lib/rubycord/data/webhook.rb +232 -0
  44. data/lib/rubycord/data.rb +40 -0
  45. data/lib/rubycord/errors.rb +737 -0
  46. data/lib/rubycord/events/await.rb +46 -0
  47. data/lib/rubycord/events/bans.rb +58 -0
  48. data/lib/rubycord/events/channels.rb +186 -0
  49. data/lib/rubycord/events/generic.rb +126 -0
  50. data/lib/rubycord/events/guilds.rb +191 -0
  51. data/lib/rubycord/events/interactions.rb +480 -0
  52. data/lib/rubycord/events/invites.rb +123 -0
  53. data/lib/rubycord/events/lifetime.rb +29 -0
  54. data/lib/rubycord/events/members.rb +91 -0
  55. data/lib/rubycord/events/message.rb +337 -0
  56. data/lib/rubycord/events/presence.rb +127 -0
  57. data/lib/rubycord/events/raw.rb +45 -0
  58. data/lib/rubycord/events/reactions.rb +156 -0
  59. data/lib/rubycord/events/roles.rb +86 -0
  60. data/lib/rubycord/events/threads.rb +94 -0
  61. data/lib/rubycord/events/typing.rb +70 -0
  62. data/lib/rubycord/events/voice_server_update.rb +45 -0
  63. data/lib/rubycord/events/voice_state_update.rb +103 -0
  64. data/lib/rubycord/events/webhooks.rb +62 -0
  65. data/lib/rubycord/gateway.rb +867 -0
  66. data/lib/rubycord/id_object.rb +37 -0
  67. data/lib/rubycord/light/data.rb +60 -0
  68. data/lib/rubycord/light/integrations.rb +71 -0
  69. data/lib/rubycord/light/light_bot.rb +56 -0
  70. data/lib/rubycord/light.rb +6 -0
  71. data/lib/rubycord/logger.rb +118 -0
  72. data/lib/rubycord/paginator.rb +55 -0
  73. data/lib/rubycord/permissions.rb +251 -0
  74. data/lib/rubycord/version.rb +5 -0
  75. data/lib/rubycord/voice/encoder.rb +113 -0
  76. data/lib/rubycord/voice/network.rb +366 -0
  77. data/lib/rubycord/voice/sodium.rb +96 -0
  78. data/lib/rubycord/voice/voice_bot.rb +408 -0
  79. data/lib/rubycord/webhooks/builder.rb +100 -0
  80. data/lib/rubycord/webhooks/client.rb +132 -0
  81. data/lib/rubycord/webhooks/embeds.rb +248 -0
  82. data/lib/rubycord/webhooks/modal.rb +78 -0
  83. data/lib/rubycord/webhooks/version.rb +7 -0
  84. data/lib/rubycord/webhooks/view.rb +192 -0
  85. data/lib/rubycord/webhooks.rb +12 -0
  86. data/lib/rubycord/websocket.rb +70 -0
  87. data/lib/rubycord.rb +140 -0
  88. metadata +231 -0
@@ -0,0 +1,135 @@
1
+ module Rubycord
2
+ # A channel referenced by an invite. It has less data than regular channels, so it's a separate class
3
+ class InviteChannel
4
+ include IDObject
5
+
6
+ # @return [String] this channel's name.
7
+ attr_reader :name
8
+
9
+ # @return [Integer] this channel's type (0: text, 1: private, 2: voice, 3: group).
10
+ attr_reader :type
11
+
12
+ # @!visibility private
13
+ def initialize(data, bot)
14
+ @bot = bot
15
+
16
+ @id = data["id"].to_i
17
+ @name = data["name"]
18
+ @type = data["type"]
19
+ end
20
+ end
21
+
22
+ # A server referenced to by an invite
23
+ class InviteServer
24
+ include IDObject
25
+
26
+ # @return [String] this server's name.
27
+ attr_reader :name
28
+
29
+ # @return [String, nil] the hash of the server's invite splash screen (for partnered servers) or nil if none is
30
+ # present
31
+ attr_reader :splash_hash
32
+
33
+ # @!visibility private
34
+ def initialize(data, bot)
35
+ @bot = bot
36
+
37
+ @id = data["id"].to_i
38
+ @name = data["name"]
39
+ @splash_hash = data["splash_hash"]
40
+ end
41
+ end
42
+
43
+ # A Discord invite to a channel
44
+ class Invite
45
+ # @return [InviteChannel, Channel] the channel this invite references.
46
+ attr_reader :channel
47
+
48
+ # @return [InviteServer, Server] the server this invite references.
49
+ attr_reader :server
50
+
51
+ # @return [Integer] the amount of uses left on this invite.
52
+ attr_reader :uses
53
+ alias_method :max_uses, :uses
54
+
55
+ # @return [User, nil] the user that made this invite. May also be nil if the user can't be determined.
56
+ attr_reader :inviter
57
+ alias_method :user, :inviter
58
+
59
+ # @return [true, false] whether or not this invite grants temporary membership. If someone joins a server with this invite, they will be removed from the server when they go offline unless they've received a role.
60
+ attr_reader :temporary
61
+ alias_method :temporary?, :temporary
62
+
63
+ # @return [true, false] whether this invite is still valid.
64
+ attr_reader :revoked
65
+ alias_method :revoked?, :revoked
66
+
67
+ # @return [String] this invite's code
68
+ attr_reader :code
69
+
70
+ # @return [Integer, nil] the amount of members in the server. Will be nil if it has not been resolved.
71
+ attr_reader :member_count
72
+ alias_method :user_count, :member_count
73
+
74
+ # @return [Integer, nil] the amount of online members in the server. Will be nil if it has not been resolved.
75
+ attr_reader :online_member_count
76
+ alias_method :online_user_count, :online_member_count
77
+
78
+ # @return [Integer, nil] the invites max age before it expires, or nil if it's unknown. If the max age is 0, the invite will never expire unless it's deleted.
79
+ attr_reader :max_age
80
+
81
+ # @return [Time, nil] when this invite was created, or nil if it's unknown
82
+ attr_reader :created_at
83
+
84
+ # @!visibility private
85
+ def initialize(data, bot)
86
+ @bot = bot
87
+
88
+ @channel = if data["channel_id"]
89
+ bot.channel(data["channel_id"])
90
+ else
91
+ InviteChannel.new(data["channel"], bot)
92
+ end
93
+
94
+ @server = if data["guild_id"]
95
+ bot.server(data["guild_id"])
96
+ else
97
+ InviteServer.new(data["guild"], bot)
98
+ end
99
+
100
+ @uses = data["uses"]
101
+ @inviter = data["inviter"] ? bot.ensure_user(data["inviter"]) : nil
102
+ @temporary = data["temporary"]
103
+ @revoked = data["revoked"]
104
+ @online_member_count = data["approximate_presence_count"]
105
+ @member_count = data["approximate_member_count"]
106
+ @max_age = data["max_age"]
107
+ @created_at = data["created_at"]
108
+
109
+ @code = data["code"]
110
+ end
111
+
112
+ # Code based comparison
113
+ def ==(other)
114
+ other.respond_to?(:code) ? (@code == other.code) : (@code == other)
115
+ end
116
+
117
+ # Deletes this invite
118
+ # @param reason [String] The reason the invite is being deleted.
119
+ def delete(reason = nil)
120
+ API::Invite.delete(@bot.token, @code, reason)
121
+ end
122
+
123
+ alias_method :revoke, :delete
124
+
125
+ # The inspect method is overwritten to give more useful output
126
+ def inspect
127
+ "<Invite code=#{@code} channel=#{@channel} uses=#{@uses} temporary=#{@temporary} revoked=#{@revoked} created_at=#{@created_at} max_age=#{@max_age}>"
128
+ end
129
+
130
+ # Creates an invite URL.
131
+ def url
132
+ "https://discord.gg/#{@code}"
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,370 @@
1
+ module Rubycord
2
+ # Mixin for the attributes members and private members should have
3
+ module MemberAttributes
4
+ # @return [Time] when this member joined the server.
5
+ attr_reader :joined_at
6
+
7
+ # @return [Time, nil] when this member boosted this server, `nil` if they haven't.
8
+ attr_reader :boosting_since
9
+
10
+ # @return [String, nil] the nickname this member has, or `nil` if it has none.
11
+ attr_reader :nick
12
+ alias_method :nickname, :nick
13
+
14
+ # @return [Array<Role>] the roles this member has.
15
+ attr_reader :roles
16
+
17
+ # @return [Server] the server this member is on.
18
+ attr_reader :server
19
+
20
+ # @return [Time] When the user's timeout will expire.
21
+ attr_reader :communication_disabled_until
22
+ alias_method :timeout, :communication_disabled_until
23
+ end
24
+
25
+ # A member is a user on a server. It differs from regular users in that it has roles, voice statuses and things like
26
+ # that.
27
+ class Member < DelegateClass(User)
28
+ # @return [true, false] whether this member is muted server-wide.
29
+ def mute
30
+ voice_state_attribute(:mute)
31
+ end
32
+
33
+ # @return [true, false] whether this member is deafened server-wide.
34
+ def deaf
35
+ voice_state_attribute(:deaf)
36
+ end
37
+
38
+ # @return [true, false] whether this member has muted themselves.
39
+ def self_mute
40
+ voice_state_attribute(:self_mute)
41
+ end
42
+
43
+ # @return [true, false] whether this member has deafened themselves.
44
+ def self_deaf
45
+ voice_state_attribute(:self_deaf)
46
+ end
47
+
48
+ # @return [Channel] the voice channel this member is in.
49
+ def voice_channel
50
+ voice_state_attribute(:voice_channel)
51
+ end
52
+
53
+ alias_method :muted?, :mute
54
+ alias_method :deafened?, :deaf
55
+ alias_method :self_muted?, :self_mute
56
+ alias_method :self_deafened?, :self_deaf
57
+
58
+ include MemberAttributes
59
+
60
+ # @!visibility private
61
+ def initialize(data, server, bot)
62
+ @bot = bot
63
+
64
+ @user = bot.ensure_user(data["user"])
65
+ super(@user) # Initialize the delegate class
66
+
67
+ @server = server
68
+ @server_id = server&.id || data["guild_id"].to_i
69
+
70
+ @role_ids = data["roles"]&.map(&:to_i) || []
71
+
72
+ @nick = data["nick"]
73
+ @joined_at = data["joined_at"] ? Time.parse(data["joined_at"]) : nil
74
+ @boosting_since = data["premium_since"] ? Time.parse(data["premium_since"]) : nil
75
+ timeout_until = data["communication_disabled_until"]
76
+ @communication_disabled_until = timeout_until ? Time.parse(timeout_until) : nil
77
+ @permissions = Permissions.new(data["permissions"]) if data["permissions"]
78
+ end
79
+
80
+ # @return [Server] the server this member is on.
81
+ # @raise [Rubycord::Errors::NoPermission] This can happen when receiving interactions for servers in which the bot is not
82
+ # authorized with the `bot` scope.
83
+ def server
84
+ return @server if @server
85
+
86
+ @server = @bot.server(@server_id)
87
+ raise Rubycord::Errors::NoPermission, "The bot does not have access to this server" unless @server
88
+
89
+ @server
90
+ end
91
+
92
+ # @return [Array<Role>] the roles this member has.
93
+ # @raise [Rubycord::Errors::NoPermission] This can happen when receiving interactions for servers in which the bot is not
94
+ # authorized with the `bot` scope.
95
+ def roles
96
+ return @roles if @roles
97
+
98
+ update_roles(@role_ids)
99
+ @roles
100
+ end
101
+
102
+ # @return [true, false] if this user is a Nitro Booster of this server.
103
+ def boosting?
104
+ !@boosting_since.nil?
105
+ end
106
+
107
+ # @return [true, false] whether this member is the server owner.
108
+ def owner?
109
+ server.owner == self
110
+ end
111
+
112
+ # @param role [Role, String, Integer] the role to check or its ID.
113
+ # @return [true, false] whether this member has the specified role.
114
+ def role?(role)
115
+ role = role.resolve_id
116
+ roles.any?(role)
117
+ end
118
+
119
+ # @see Member#set_roles
120
+ def roles=(role)
121
+ set_roles(role)
122
+ end
123
+
124
+ # Check if the current user has communication disabled.
125
+ # @return [true, false]
126
+ def communication_disabled?
127
+ !@communication_disabled_until.nil? && @communication_disabled_until > Time.now
128
+ end
129
+
130
+ alias_method :timeout?, :communication_disabled?
131
+
132
+ # Set a user's timeout duration, or remove it by setting the timeout to `nil`.
133
+ # @param timeout_until [Time, nil] When the timeout will end.
134
+ def communication_disabled_until=(timeout_until)
135
+ raise ArgumentError, "A time out cannot exceed 28 days" if timeout_until && timeout_until > (Time.now + 2_419_200)
136
+
137
+ API::Server.update_member(@bot.token, @server_id, @user.id, communication_disabled_until: timeout_until.iso8601)
138
+ end
139
+
140
+ alias_method :timeout=, :communication_disabled_until=
141
+
142
+ # Bulk sets a member's roles.
143
+ # @param role [Role, Array<Role>] The role(s) to set.
144
+ # @param reason [String] The reason the user's roles are being changed.
145
+ def set_roles(role, reason = nil)
146
+ role_ids = role_id_array(role)
147
+ API::Server.update_member(@bot.token, @server_id, @user.id, roles: role_ids, reason: reason)
148
+ end
149
+
150
+ # Adds and removes roles from a member.
151
+ # @param add [Role, Array<Role>] The role(s) to add.
152
+ # @param remove [Role, Array<Role>] The role(s) to remove.
153
+ # @param reason [String] The reason the user's roles are being changed.
154
+ # @example Remove the 'Member' role from a user, and add the 'Muted' role to them.
155
+ # to_add = server.roles.find {|role| role.name == 'Muted'}
156
+ # to_remove = server.roles.find {|role| role.name == 'Member'}
157
+ # member.modify_roles(to_add, to_remove)
158
+ def modify_roles(add, remove, reason = nil)
159
+ add_role_ids = role_id_array(add)
160
+ remove_role_ids = role_id_array(remove)
161
+ old_role_ids = resolve_role_ids
162
+ new_role_ids = (old_role_ids - remove_role_ids + add_role_ids).uniq
163
+
164
+ API::Server.update_member(@bot.token, @server_id, @user.id, roles: new_role_ids, reason: reason)
165
+ end
166
+
167
+ # Adds one or more roles to this member.
168
+ # @param role [Role, Array<Role, String, Integer>, String, Integer] The role(s), or their ID(s), to add.
169
+ # @param reason [String] The reason the user's roles are being changed.
170
+ def add_role(role, reason = nil)
171
+ role_ids = role_id_array(role)
172
+
173
+ if role_ids.count == 1
174
+ API::Server.add_member_role(@bot.token, @server_id, @user.id, role_ids[0], reason)
175
+ else
176
+ old_role_ids = resolve_role_ids
177
+ new_role_ids = (old_role_ids + role_ids).uniq
178
+ API::Server.update_member(@bot.token, @server_id, @user.id, roles: new_role_ids, reason: reason)
179
+ end
180
+ end
181
+
182
+ # Removes one or more roles from this member.
183
+ # @param role [Role, Array<Role>] The role(s) to remove.
184
+ # @param reason [String] The reason the user's roles are being changed.
185
+ def remove_role(role, reason = nil)
186
+ role_ids = role_id_array(role)
187
+
188
+ if role_ids.count == 1
189
+ API::Server.remove_member_role(@bot.token, @server_id, @user.id, role_ids[0], reason)
190
+ else
191
+ old_role_ids = resolve_role_ids
192
+ new_role_ids = old_role_ids.reject { |i| role_ids.include?(i) }
193
+ API::Server.update_member(@bot.token, @server_id, @user.id, roles: new_role_ids, reason: reason)
194
+ end
195
+ end
196
+
197
+ # @return [Role] the highest role this member has.
198
+ def highest_role
199
+ roles.max_by(&:position)
200
+ end
201
+
202
+ # @return [Role, nil] the role this member is being hoisted with.
203
+ def hoist_role
204
+ hoisted_roles = roles.select(&:hoist)
205
+ return nil if hoisted_roles.empty?
206
+
207
+ hoisted_roles.max_by(&:position)
208
+ end
209
+
210
+ # @return [Role, nil] the role this member is basing their colour on.
211
+ def colour_role
212
+ coloured_roles = roles.select { |v| v.colour.combined.nonzero? }
213
+ return nil if coloured_roles.empty?
214
+
215
+ coloured_roles.max_by(&:position)
216
+ end
217
+ alias_method :color_role, :colour_role
218
+
219
+ # @return [ColourRGB, nil] the colour this member has.
220
+ def colour
221
+ return nil unless colour_role
222
+
223
+ colour_role.color
224
+ end
225
+ alias_method :color, :colour
226
+
227
+ # Server deafens this member.
228
+ def server_deafen
229
+ API::Server.update_member(@bot.token, @server_id, @user.id, deaf: true)
230
+ end
231
+
232
+ # Server undeafens this member.
233
+ def server_undeafen
234
+ API::Server.update_member(@bot.token, @server_id, @user.id, deaf: false)
235
+ end
236
+
237
+ # Server mutes this member.
238
+ def server_mute
239
+ API::Server.update_member(@bot.token, @server_id, @user.id, mute: true)
240
+ end
241
+
242
+ # Server unmutes this member.
243
+ def server_unmute
244
+ API::Server.update_member(@bot.token, @server_id, @user.id, mute: false)
245
+ end
246
+
247
+ # Bans this member from the server.
248
+ # @param message_days [Integer] How many days worth of messages sent by the member should be deleted.
249
+ # @param reason [String] The reason this member is being banned.
250
+ def ban(message_days = 0, reason: nil)
251
+ server.ban(@user, message_days, reason: reason)
252
+ end
253
+
254
+ # Unbans this member from the server.
255
+ # @param reason [String] The reason this member is being unbanned.
256
+ def unban(reason = nil)
257
+ server.unban(@user, reason)
258
+ end
259
+
260
+ # Kicks this member from the server.
261
+ # @param reason [String] The reason this member is being kicked.
262
+ def kick(reason = nil)
263
+ server.kick(@user, reason)
264
+ end
265
+
266
+ # @see Member#set_nick
267
+ def nick=(nick)
268
+ set_nick(nick)
269
+ end
270
+
271
+ alias_method :nickname=, :nick=
272
+
273
+ # Sets or resets this member's nickname. Requires the Change Nickname permission for the bot itself and Manage
274
+ # Nicknames for other users.
275
+ # @param nick [String, nil] The string to set the nickname to, or nil if it should be reset.
276
+ # @param reason [String] The reason the user's nickname is being changed.
277
+ def set_nick(nick, reason = nil)
278
+ # Discord uses the empty string to signify 'no nickname' so we convert nil into that
279
+ nick ||= ""
280
+
281
+ if @user.current_bot?
282
+ API::User.change_own_nickname(@bot.token, @server_id, nick, reason)
283
+ else
284
+ API::Server.update_member(@bot.token, @server_id, @user.id, nick: nick, reason: nil)
285
+ end
286
+ end
287
+
288
+ alias_method :set_nickname, :set_nick
289
+
290
+ # @return [String] the name the user displays as (nickname if they have one, global_name if they have one, username otherwise)
291
+ def display_name
292
+ nickname || global_name || username
293
+ end
294
+
295
+ # Update this member's roles
296
+ # @note For internal use only.
297
+ # @!visibility private
298
+ def update_roles(role_ids)
299
+ @roles = [server.role(@server_id)]
300
+ role_ids.each do |id|
301
+ # It is possible for members to have roles that do not exist
302
+ # on the server any longer.
303
+ role = server.role(id)
304
+ @roles << role if role
305
+ end
306
+ end
307
+
308
+ # Update this member's nick
309
+ # @note For internal use only.
310
+ # @!visibility private
311
+ def update_nick(nick)
312
+ @nick = nick
313
+ end
314
+
315
+ # Update this member's boosting timestamp
316
+ # @note For internal user only.
317
+ # @!visibility private
318
+ def update_boosting_since(time)
319
+ @boosting_since = time
320
+ end
321
+
322
+ # @!visibility private
323
+ def update_communication_disabled_until(time)
324
+ time = time ? Time.parse(time) : nil
325
+ @communication_disabled_until = time
326
+ end
327
+
328
+ # Update this member
329
+ # @note For internal use only.
330
+ # @!visibility private
331
+ def update_data(data)
332
+ update_roles(data["roles"]) if data["roles"]
333
+ update_nick(data["nick"]) if data.key?("nick")
334
+ @mute = data["mute"] if data.key?("mute")
335
+ @deaf = data["deaf"] if data.key?("deaf")
336
+
337
+ @joined_at = Time.parse(data["joined_at"]) if data["joined_at"]
338
+ timeout_until = data["communication_disabled_until"]
339
+ @communication_disabled_until = timeout_until ? Time.parse(timeout_until) : nil
340
+ end
341
+
342
+ include PermissionCalculator
343
+
344
+ # Overwriting inspect for debug purposes
345
+ def inspect
346
+ "<Member user=#{@user.inspect} server=#{@server&.inspect || @server_id} joined_at=#{@joined_at} roles=#{@roles&.inspect || @role_ids} voice_channel=#{@voice_channel.inspect} mute=#{@mute} deaf=#{@deaf} self_mute=#{@self_mute} self_deaf=#{@self_deaf}>"
347
+ end
348
+
349
+ private
350
+
351
+ # Utility method to get a list of role IDs from one role or an array of roles
352
+ def role_id_array(role)
353
+ if role.is_a? Array
354
+ role.map(&:resolve_id)
355
+ else
356
+ [role.resolve_id]
357
+ end
358
+ end
359
+
360
+ # Utility method to get data out of this member's voice state
361
+ def voice_state_attribute(name)
362
+ voice_state = server.voice_states[@user.id]
363
+ voice_state&.send name
364
+ end
365
+
366
+ def resolve_role_ids
367
+ @roles ? @roles.collect(&:id) : @role_ids
368
+ end
369
+ end
370
+ end