-
Notifications
You must be signed in to change notification settings - Fork 145
/
Client.lua
798 lines (704 loc) · 20.6 KB
/
Client.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
--[=[
@c Client x Emitter
@t ui
@op options table
@d The main point of entry into a Discordia application. All data relevant to
Discord is accessible through a client instance or its child objects after a
connection to Discord is established with the `run` method. In other words,
client data should not be expected and most client methods should not be called
until after the `ready` event is received. Base emitter methods may be called
at any time. See [[client options]].
]=]
local fs = require('fs')
local json = require('json')
local constants = require('constants')
local enums = require('enums')
local package = require('../../package.lua')
local API = require('client/API')
local Shard = require('client/Shard')
local Resolver = require('client/Resolver')
local GroupChannel = require('containers/GroupChannel')
local Guild = require('containers/Guild')
local PrivateChannel = require('containers/PrivateChannel')
local User = require('containers/User')
local Invite = require('containers/Invite')
local Webhook = require('containers/Webhook')
local Relationship = require('containers/Relationship')
local Cache = require('iterables/Cache')
local WeakCache = require('iterables/WeakCache')
local Emitter = require('utils/Emitter')
local Logger = require('utils/Logger')
local Mutex = require('utils/Mutex')
local VoiceManager = require('voice/VoiceManager')
local encode, decode, null = json.encode, json.decode, json.null
local readFileSync, writeFileSync = fs.readFileSync, fs.writeFileSync
local band, bor, bnot = bit.band, bit.bor, bit.bnot
local logLevel = assert(enums.logLevel)
local activityType = assert(enums.activityType)
local gatewayIntent = assert(enums.gatewayIntent)
local wrap = coroutine.wrap
local time, difftime = os.time, os.difftime
local format = string.format
local CACHE_AGE = constants.CACHE_AGE
local API_VERSION = constants.API_VERSION
-- do not change these options here
-- pass a custom table on client initialization instead
local defaultOptions = {
routeDelay = 250,
maxRetries = 5,
shardCount = 0,
firstShard = 0,
lastShard = -1,
largeThreshold = 100,
cacheAllMembers = false,
autoReconnect = true,
compress = true,
bitrate = 64000,
logFile = 'discordia.log',
logLevel = logLevel.info,
gatewayFile = 'gateway.json',
dateTime = '%F %T',
syncGuilds = false,
gatewayIntents = 3243773, -- all non-privileged intents
}
local function parseOptions(customOptions)
if type(customOptions) == 'table' then
local options = {}
for k, default in pairs(defaultOptions) do -- load options
local custom = customOptions[k]
if custom ~= nil then
options[k] = custom
else
options[k] = default
end
end
for k, v in pairs(customOptions) do -- validate options
local default = type(defaultOptions[k])
local custom = type(v)
if default ~= custom then
return error(format('invalid client option %q (%s expected, got %s)', k, default, custom), 3)
end
if custom == 'number' and (v < 0 or v % 1 ~= 0) then
return error(format('invalid client option %q (number must be a positive integer)', k), 3)
end
end
return options
else
return defaultOptions
end
end
local Client, get = require('class')('Client', Emitter)
function Client:__init(options)
Emitter.__init(self)
options = assert(parseOptions(options))
self._options = options
self._shards = {}
self._api = API(self)
self._mutex = Mutex()
self._users = Cache({}, User, self)
self._guilds = Cache({}, Guild, self)
self._group_channels = Cache({}, GroupChannel, self)
self._private_channels = Cache({}, PrivateChannel, self)
self._relationships = Cache({}, Relationship, self)
self._webhooks = WeakCache({}, Webhook, self) -- used for audit logs
self._logger = Logger(options.logLevel, options.dateTime, options.logFile)
self._voice = VoiceManager(self)
self._role_map = {}
self._emoji_map = {}
self._sticker_map = {}
self._channel_map = {}
self._events = require('client/EventHandler')
self._intents = options.gatewayIntents
end
for name, level in pairs(logLevel) do
Client[name] = function(self, fmt, ...)
local msg = self._logger:log(level, fmt, ...)
return self:emit(name, msg or format(fmt, ...))
end
end
function Client:_deprecated(clsName, before, after)
local info = debug.getinfo(3)
return self:warning(
'%s:%s: %s.%s is deprecated; use %s.%s instead',
info.short_src,
info.currentline,
clsName,
before,
clsName,
after
)
end
local function run(self, token)
self:info('Discordia %s', package.version)
self:info('Connecting to Discord...')
local api = self._api
local users = self._users
local options = self._options
if options.cacheAllMembers and bit.band(self._intents, gatewayIntent.guildMembers) == 0 then
self:warning('Cannot cache all members while guildMembers intent is disabled')
options.cacheAllMembers = false
end
local user, err1 = api:authenticate(token)
if not user then
return self:error('Could not authenticate, check token: ' .. err1)
end
self._user = users:_insert(user)
self._token = token
self:info('Authenticated as %s#%s', user.username, user.discriminator)
local now = time()
local url, count, owner
local cache = readFileSync(options.gatewayFile)
cache = cache and decode(cache)
if cache then
local d = cache[user.id]
if d and difftime(now, d.timestamp) < CACHE_AGE then
url = cache.url
if user.bot then
count = d.shards
owner = d.owner
else
count = 1
owner = user
end
end
else
cache = {}
end
if not url or not owner then
if user.bot then
local gateway, err2 = api:getGatewayBot()
if not gateway then
return self:error('Could not get gateway: ' .. err2)
end
local app, err3 = api:getCurrentApplicationInformation()
if not app then
return self:error('Could not get application information: ' .. err3)
end
url = gateway.url
count = gateway.shards
owner = app.owner
cache[user.id] = {owner = owner, shards = count, timestamp = now}
else
local gateway, err2 = api:getGateway()
if not gateway then
return self:error('Could not get gateway: ' .. err2)
end
url = gateway.url
count = 1
owner = user
cache[user.id] = {timestamp = now}
end
cache.url = url
writeFileSync(options.gatewayFile, encode(cache))
end
self._owner = users:_insert(owner)
if options.shardCount > 0 then
if count ~= options.shardCount then
self:warning('Requested shard count (%i) is different from recommended count (%i)', options.shardCount, count)
end
count = options.shardCount
end
local first, last = options.firstShard, options.lastShard
if last < 0 then
last = count - 1
end
if last < first then
return self:error('First shard ID (%i) is greater than last shard ID (%i)', first, last)
end
local d = last - first + 1
if d > count then
return self:error('Shard count (%i) is less than target shard range (%i)', count, d)
end
if first == last then
self:info('Launching shard %i (%i out of %i)...', first, d, count)
else
self:info('Launching shards %i through %i (%i out of %i)...', first, last, d, count)
end
self._total_shard_count = count
self._shard_count = d
for id = first, last do
self._shards[id] = Shard(id, self)
end
local path = format('/?v=%i&encoding=json', API_VERSION)
for _, shard in pairs(self._shards) do
wrap(shard.connect)(shard, url, path)
shard:identifyWait()
end
end
--[=[
@m run
@t ws
@p token string
@op presence table
@r nil
@d Authenticates the current user via HTTPS and launches as many WSS gateway
shards as are required or requested. By using coroutines that are automatically
managed by Luvit libraries and a libuv event loop, multiple clients per process
and multiple shards per client can operate concurrently. This should be the last
method called after all other code and event handlers have been initialized. If
a presence table is provided, it will act as if the user called `setStatus`
and `setActivity` after `run`.
]=]
function Client:run(token, presence)
self._presence = presence or {}
return wrap(run)(self, token)
end
--[=[
@m stop
@t ws
@r nil
@d Disconnects all shards and effectively stops their loops. This does not
empty any data that the client may have cached.
]=]
function Client:stop()
for _, shard in pairs(self._shards) do
shard:disconnect()
end
end
local function getIntent(i, ...)
local v = select(i, ...)
local n = Resolver.gatewayIntent(v)
if not n then
return error('Invalid gateway intent: ' .. tostring(v), 2)
end
return n
end
--[=[
@m getIntents
@t mem
@r number
@d Returns a number that represents the gateway intents enabled for this client.
]=]
function Client:getIntents()
return self._intents
end
--[=[
@m setIntents
@t mem
@p intents Intents-Resolvable
@r nothing
@d Sets the gateway intents that this client will use. The new value will not be
used internally until the client (re-)identifies.
]=]
function Client:setIntents(intents)
self._intents = tonumber(intents) or 0
end
--[=[
@m enableIntents
@t mem
@p ... Intents-Resolvables
@r nothing
@d Enables individual gateway intents for this client. The new value will not be
used internally until the client (re-)identifies.
]=]
function Client:enableIntents(...)
for i = 1, select('#', ...) do
local intent = getIntent(i, ...)
self._intents = bor(self._intents, intent)
end
end
--[=[
@m disableIntents
@t mem
@p ... Intents-Resolvables
@r nothing
@d Disables individual gateway intents for this client. The new value will not be
used internally until the client (re-)identifies.
]=]
function Client:disableIntents(...)
for i = 1, select('#', ...) do
local intent = getIntent(i, ...)
self._intents = band(self._intents, bnot(intent))
end
end
--[=[
@m enableAllIntents
@t mem
@r nothing
@d Enables all known gateway intents for this client. The new value will not be
used internally until the client (re-)identifies.
]=]
function Client:enableAllIntents()
for _, value in pairs(gatewayIntent) do
self._intents = bor(self._intents, value)
end
return self
end
--[=[
@m disableAllIntents
@t mem
@r nothing
@d Disables all gateway intents for this client. The new value will not be
used internally until the client (re-)identifies.
]=]
function Client:disableAllIntents()
self._intents = 0
end
function Client:_modify(payload)
local data, err = self._api:modifyCurrentUser(payload)
if data then
data.token = nil
self._user:_load(data)
return true
else
return false, err
end
end
--[=[
@m setUsername
@t http
@p username string
@r boolean
@d Sets the client's username. This must be between 2 and 32 characters in
length. This does not change the application name.
]=]
function Client:setUsername(username)
return self:_modify({username = username or null})
end
--[=[
@m setAvatar
@t http
@p avatar Base64-Resolvable
@r boolean
@d Sets the client's avatar. To remove the avatar, pass an empty string or nil.
This does not change the application image.
]=]
function Client:setAvatar(avatar)
avatar = avatar and Resolver.base64(avatar)
return self:_modify({avatar = avatar or null})
end
--[=[
@m createGuild
@t http
@p name string
@r boolean
@d Creates a new guild. The name must be between 2 and 100 characters in length.
This method may not work if the current user is in too many guilds. Note that
this does not return the created guild object; wait for the corresponding
`guildCreate` event if you need the object.
]=]
function Client:createGuild(name)
local data, err = self._api:createGuild({name = name})
if data then
return true
else
return false, err
end
end
--[=[
@m createGroupChannel
@t http
@r GroupChannel
@d Creates a new group channel. This method is only available for user accounts.
]=]
function Client:createGroupChannel()
local data, err = self._api:createGroupDM()
if data then
return self._group_channels:_insert(data)
else
return nil, err
end
end
--[=[
@m getWebhook
@t http
@p id string
@r Webhook
@d Gets a webhook object by ID. This always makes an HTTP request to obtain a
static object that is not cached and is not updated by gateway events.
]=]
function Client:getWebhook(id)
local data, err = self._api:getWebhook(id)
if data then
return Webhook(data, self)
else
return nil, err
end
end
--[=[
@m getInvite
@t http
@p code string
@op counts boolean
@r Invite
@d Gets an invite object by code. This always makes an HTTP request to obtain a
static object that is not cached and is not updated by gateway events.
]=]
function Client:getInvite(code, counts)
local data, err = self._api:getInvite(code, counts and {with_counts = true})
if data then
return Invite(data, self)
else
return nil, err
end
end
--[=[
@m getUser
@t http?
@p id User-ID-Resolvable
@r User
@d Gets a user object by ID. If the object is already cached, then the cached
object will be returned; otherwise, an HTTP request is made. Under circumstances
which should be rare, the user object may be an old version, not updated by
gateway events.
]=]
function Client:getUser(id)
id = Resolver.userId(id)
local user = self._users:get(id)
if user then
return user
else
local data, err = self._api:getUser(id)
if data then
return self._users:_insert(data)
else
return nil, err
end
end
end
--[=[
@m getGuild
@t mem
@p id Guild-ID-Resolvable
@r Guild
@d Gets a guild object by ID. The current user must be in the guild and the client
must be running the appropriate shard that serves this guild. This method never
makes an HTTP request to obtain a guild.
]=]
function Client:getGuild(id)
id = Resolver.guildId(id)
return self._guilds:get(id)
end
--[=[
@m getChannel
@t mem
@p id Channel-ID-Resolvable
@r Channel
@d Gets a channel object by ID. For guild channels, the current user must be in
the channel's guild and the client must be running the appropriate shard that
serves the channel's guild.
For private channels, the channel must have been previously opened and cached.
If the channel is not cached, `User:getPrivateChannel` should be used instead.
]=]
function Client:getChannel(id)
id = Resolver.channelId(id)
local guild = self._channel_map[id]
if guild then
return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id)
else
return self._private_channels:get(id) or self._group_channels:get(id)
end
end
--[=[
@m getRole
@t mem
@p id Role-ID-Resolvable
@r Role
@d Gets a role object by ID. The current user must be in the role's guild and
the client must be running the appropriate shard that serves the role's guild.
]=]
function Client:getRole(id)
id = Resolver.roleId(id)
local guild = self._role_map[id]
return guild and guild._roles:get(id)
end
--[=[
@m getEmoji
@t mem
@p id Emoji-ID-Resolvable
@r Emoji
@d Gets an emoji object by ID. The current user must be in the emoji's guild and
the client must be running the appropriate shard that serves the emoji's guild.
]=]
function Client:getEmoji(id)
id = Resolver.emojiId(id)
local guild = self._emoji_map[id]
return guild and guild._emojis:get(id)
end
--[=[
@m getSticker
@t mem
@p id Sticker-ID-Resolvable
@r Sticker
@d Gets a sticker object by ID. The current user must be in the sticker's guild
and the client must be running the appropriate shard that serves the sticker's guild.
]=]
function Client:getSticker(id)
id = Resolver.stickerId(id)
local guild = self._sticker_map[id]
return guild and guild._stickers:get(id)
end
--[=[
@m listVoiceRegions
@t http
@r table
@d Returns a raw data table that contains a list of voice regions as provided by
Discord, with no formatting beyond what is provided by the Discord API.
]=]
function Client:listVoiceRegions()
return self._api:listVoiceRegions()
end
--[=[
@m getConnections
@t http
@r table
@d Returns a raw data table that contains a list of connections as provided by
Discord, with no formatting beyond what is provided by the Discord API.
This is unrelated to voice connections.
]=]
function Client:getConnections()
return self._api:getUsersConnections()
end
--[=[
@m getApplicationInformation
@t http
@r table
@d Returns a raw data table that contains information about the current OAuth2
application, with no formatting beyond what is provided by the Discord API.
]=]
function Client:getApplicationInformation()
return self._api:getCurrentApplicationInformation()
end
local function updateStatus(self)
local presence = self._presence
presence.afk = presence.afk or null
presence.activities = presence.activity and {presence.activity} or null
presence.since = presence.since or null
presence.status = presence.status or null
for _, shard in pairs(self._shards) do
shard:updateStatus(presence)
end
end
--[=[
@m setStatus
@t ws
@p status string/nil
@r nil
@d Sets the current user's status on all shards that are managed by this client.
See the `status` enumeration for acceptable status values.
Passing `nil` removes previously set status.
]=]
function Client:setStatus(status)
if type(status) == 'string' then
self._presence.status = status
if status == 'idle' then
self._presence.since = 1000 * time()
else
self._presence.since = null
end
else
self._presence.status = null
self._presence.since = null
end
return updateStatus(self)
end
function Client:setGame(game)
self:_deprecated(self.__name, 'setGame', 'setActivity')
return self:setActivity(game)
end
--[=[
@m setActivity
@t ws
@p activity string/table/nil
@r nil
@d Sets the current user's activity on all shards that are managed by this client.
If a string is passed, it is treated as the activity name. If a table is passed, it
must have a `name` field and may optionally have a `url` or `type` field. Pass `nil` to
remove the activity status.
Passing `nil` removes previously set activities.
]=]
function Client:setActivity(activity)
if type(activity) == 'string' then
activity = {name = activity, type = activityType.default}
elseif type(activity) == 'table' then
if type(activity.name) == 'string' then
if type(activity.type) ~= 'number' then
if type(activity.url) == 'string' then
activity.type = activityType.streaming
else
activity.type = activityType.default
end
end
else
activity = null
end
else
activity = null
end
self._presence.activity = activity
return updateStatus(self)
end
--[=[
@m setAFK
@t ws
@p afk boolean/nil
@r nil
@d Set the current user's AFK status on all shards that are managed by this client.
This generally applies to user accounts and their push notifications.
Passing `nil` removes AFK status.
]=]
function Client:setAFK(afk)
if type(afk) == 'boolean' then
self._presence.afk = afk
else
self._presence.afk = null
end
return updateStatus(self)
end
--[=[@p shardCount number/nil The number of shards that this client is managing.]=]
function get.shardCount(self)
return self._shard_count
end
--[=[@p totalShardCount number/nil The total number of shards that the current user is on.]=]
function get.totalShardCount(self)
return self._total_shard_count
end
--[=[@p user User/nil User object representing the current user.]=]
function get.user(self)
return self._user
end
--[=[@p owner User/nil User object representing the current user's owner.]=]
function get.owner(self)
return self._owner
end
--[=[@p verified boolean/nil Whether the current user's owner's account is verified.]=]
function get.verified(self)
return self._user and self._user._verified
end
--[=[@p mfaEnabled boolean/nil Whether the current user's owner's account has multi-factor (or two-factor)
authentication enabled. This is equivalent to `verified`]=]
function get.mfaEnabled(self)
return self._user and self._user._verified
end
--[=[@p email string/nil The current user's owner's account's email address (user-accounts only).]=]
function get.email(self)
return self._user and self._user._email
end
--[=[@p guilds Cache An iterable cache of all guilds that are visible to the client. Note that the
guilds present here correspond to which shards the client is managing. If all
shards are managed by one client, then all guilds will be present.]=]
function get.guilds(self)
return self._guilds
end
--[=[@p users Cache An iterable cache of all users that are visible to the client.
To access a user that may exist but is not cached, use `Client:getUser`.]=]
function get.users(self)
return self._users
end
--[=[@p privateChannels Cache An iterable cache of all private channels that are visible to the client. The
channel must exist and must be open for it to be cached here. To access a
private channel that may exist but is not cached, `User:getPrivateChannel`.]=]
function get.privateChannels(self)
return self._private_channels
end
--[=[@p groupChannels Cache An iterable cache of all group channels that are visible to the client. Only
user-accounts should have these.]=]
function get.groupChannels(self)
return self._group_channels
end
--[=[@p relationships Cache An iterable cache of all relationships that are visible to the client. Only
user-accounts should have these.]=]
function get.relationships(self)
return self._relationships
end
return Client