-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathfactoids.py
More file actions
2776 lines (2359 loc) · 94.8 KB
/
factoids.py
File metadata and controls
2776 lines (2359 loc) · 94.8 KB
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
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Name: Factoids
Info: Makes callable slices of text
Unit tests: No
Config: manage_roles, prefix
API: Linx
Databases: Postgres
Models: Factoid, FactoidJob
Subcommands: remember, forget, info, json, all, search, loop, deloop, job, jobs, hide, unhide,
alias, dealias
Defines: has_manage_factoids_role
"""
from __future__ import annotations
import asyncio
import datetime
import io
import json
import re
from dataclasses import dataclass
from enum import Enum
from socket import gaierror
from typing import TYPE_CHECKING, Self
import aiocron
import discord
import expiringdict
import munch
import ui
import yaml
from aiohttp.client_exceptions import InvalidURL
from botlogging import LogContext, LogLevel
from core import auxiliary, cogs, custom_errors, extensionconfig
from croniter import CroniterBadCronError
from discord import app_commands
from discord.ext import commands
from functions import holidays
if TYPE_CHECKING:
import bot
async def setup(bot: bot.TechSupportBot) -> None:
"""Loading the Factoid plugin into the bot
Args:
bot (bot.TechSupportBot): The bot object to register the cogs to
"""
# Sets up the config
config = extensionconfig.ExtensionConfig()
config.add(
key="manage_roles",
datatype="list",
title="Manage factoids roles",
description="The roles required to manage factoids",
default=["Factoids"],
)
config.add(
key="admin_roles",
datatype="list",
title="Admin factoids roles",
description="The roles required to administrate factoids",
default=["Admin"],
)
config.add(
key="prefix",
datatype="str",
title="Factoid prefix",
description="Prefix for calling factoids",
default="?",
)
config.add(
key="restricted_list",
datatype="list",
title="Restricted channels list",
description="List of channel IDs that restricted factoids are allowed to be used in",
default=[],
)
config.add(
key="disable_embeds",
datatype="bool",
title="Force disable embeds, for debug purposes",
description="This will force all factoids to not use embeds.",
default=False,
)
await bot.add_cog(
FactoidManager(
bot=bot,
extension_name="factoids",
)
)
bot.add_extension_config("factoids", config)
async def has_manage_factoids_role(ctx: commands.Context) -> bool:
"""A command check to determine if the invoker is allowed to modify basic factoids
Args:
ctx (commands.Context): The context the command was run
Returns:
bool: True if the command can be run, False if it can't
"""
config = ctx.bot.guild_configs[str(ctx.guild.id)]
return await has_given_factoids_role(
ctx.guild, ctx.author, config.extensions.factoids.manage_roles.value
)
async def has_admin_factoids_role(ctx: commands.Context) -> bool:
"""A command check to determine if the invoker is allowed to modify factoid properties
Args:
ctx (commands.Context): The context the command was run
Returns:
bool: True if the command can be run, False if it can't
"""
config = ctx.bot.guild_configs[str(ctx.guild.id)]
return await has_given_factoids_role(
ctx.guild, ctx.author, config.extensions.factoids.admin_roles.value
)
async def has_given_factoids_role(
guild: discord.Guild, invoker: discord.Member, check_roles: list[str]
) -> bool:
"""-COMMAND CHECK-
Checks if the invoker has a factoid management role
Args:
guild (discord.Guild): The guild the factoids command was called in
invoker (discord.Member): This is the member who called the factoids command
check_roles (list[str]): The list of string names of roles
Raises:
CommandError: No management roles assigned in the config
MissingAnyRole: Invoker doesn't have a factoid management role
Returns:
bool: Whether the invoker has a factoid management role
"""
factoid_roles = []
# Gets permitted roles
for name in check_roles:
factoid_role = discord.utils.get(guild.roles, name=name)
if not factoid_role:
continue
factoid_roles.append(factoid_role)
if not factoid_roles:
raise commands.CommandError(
"No factoid management roles found in the config file"
)
# Checking against the user to see if they have the roles specified in the config
if not any(
factoid_role in getattr(invoker, "roles", []) for factoid_role in factoid_roles
):
raise commands.MissingAnyRole(factoid_roles)
return True
@dataclass
class CalledFactoid:
"""A class to allow keeping the original factoid name in tact
Without having to call the database lookup function every time
Attributes:
original_call_str (str): The original name the user provided for a factoid
factoid_db_entry (bot.models.Factoid): The database entry for the original factoid
"""
original_call_str: str
factoid_db_entry: bot.models.Factoid
class Properties(Enum):
"""
This enum is for the new factoid all to be able to handle dynamic properties
Attributes:
HIDDEN (str): Representation of hidden
DISABLED (str): Representation of disabled
RESTRICTED (str): Representation of restricted
PROTECTED (str): Representation of protected
"""
HIDDEN: str = "hidden"
DISABLED: str = "disabled"
RESTRICTED: str = "restricted"
PROTECTED: str = "protected"
class FactoidManager(cogs.MatchCog):
"""
Manages all factoid features
Attributes:
CRON_REGEX (str): The regex to check if a cronjob is correct
factoid_app_group (app_commands.Group): Group for /factoid commands
"""
CRON_REGEX: str = (
r"^((\*|([0-5]?\d|\*\/\d+)(-([0-5]?\d))?)(,\s*(\*|([0-5]?\d|\*\/\d+)(-([0-5]"
+ r"?\d))?)){0,59}\s+){4}(\*|([0-7]?\d|\*(\/[1-9]|[1-5]\d)|mon|tue|wed|thu|fri|sat|sun"
+ r")|\*\/[1-9])$"
)
factoid_app_group: app_commands.Group = app_commands.Group(
name="factoid", description="Command Group for the Factoids Extension"
)
async def preconfig(self: Self) -> None:
"""Preconfig for factoid jobs"""
self.factoid_cache = expiringdict.ExpiringDict(
max_len=100, max_age_seconds=1200
)
# set a hard time limit on repeated cronjob DB calls
self.running_jobs = {}
self.factoid_all_cache = expiringdict.ExpiringDict(
max_len=1,
max_age_seconds=86400, # 24 hours, matches deletion on linx server
)
await self.bot.logger.send_log(
message="Loading factoid jobs",
level=LogLevel.DEBUG,
)
await self.kickoff_jobs()
# -- DB calls --
async def delete_factoid_call(
self: Self, factoid: bot.models.Factoid, guild: str
) -> None:
"""Calls the db to delete a factoid
Args:
factoid (bot.models.Factoid): The factoid to delete
guild (str): The guild ID for cache handling
"""
# Removes the `factoid all` cache since it has become outdated
if guild in self.factoid_all_cache:
del self.factoid_all_cache[guild]
# Deloops the factoid first (if it's looped)
jobs = await self.bot.models.FactoidJob.query.where(
self.bot.models.FactoidJob.factoid == factoid.factoid_id
).gino.all()
if jobs:
for job in jobs:
job_id = job.job_id
# Cancels the job
self.running_jobs[job_id]["task"].cancel()
# Removes it from the cache
del self.running_jobs[job_id]
# Removes the DB entry
await job.delete()
await self.handle_cache(guild, factoid.name)
await factoid.delete()
async def create_factoid_call(
self: Self,
factoid_name: str,
guild: str,
message: str,
embed_config: str,
alias: str = None,
) -> None:
"""Calls the DB to create a factoid
Args:
factoid_name (str): The name of the factoid
guild (str): Guild of the factoid
message (str): Message the factoid should send
embed_config (str): Whether the factoid has an embed set up
alias (str, optional): The parent factoid. Defaults to None.
Raises:
TooLongFactoidMessageError:
When the message argument is over 2k chars, discords limit
"""
if len(message) > 2000:
raise custom_errors.TooLongFactoidMessageError
# Removes the `factoid all` cache since it has become outdated
if guild in self.factoid_all_cache:
del self.factoid_all_cache[guild]
factoid = self.bot.models.Factoid(
name=factoid_name.lower(),
guild=guild,
message=message,
embed_config=embed_config,
alias=alias,
)
await factoid.create()
async def modify_factoid_call(
self: Self,
factoid: bot.models.Factoid,
) -> None:
"""Makes a DB call to modify a factoid
Args:
factoid (bot.models.Factoid): Factoid to modify.
Raises:
TooLongFactoidMessageError:
When the message argument is over 2k chars, discords limit
"""
if len(factoid.message) > 2000:
raise custom_errors.TooLongFactoidMessageError
# Removes the `factoid all` cache since it has become outdated
if factoid.guild in self.factoid_all_cache:
del self.factoid_all_cache[factoid.guild]
await factoid.update(
name=factoid.name,
message=factoid.message,
embed_config=factoid.embed_config,
hidden=factoid.hidden,
protected=factoid.protected,
disabled=factoid.disabled,
restricted=factoid.restricted,
alias=factoid.alias,
).apply()
await self.handle_cache(factoid.guild, factoid.name)
# -- Utility --
async def confirm_factoid_deletion(
self: Self, factoid_name: str, ctx: commands.Context, fmt: str
) -> bool:
"""Confirms if a factoid should be deleted/modified
Args:
factoid_name (str): The factoid that is being prompted for deletion
ctx (commands.Context): Used to return the message
fmt (str): Formatting for the returned message
Returns:
bool: Whether the factoid was deleted/modified
"""
view = ui.Confirm()
await view.send(
message=(
f"The factoid `{factoid_name}` already exists. Should I overwrite it?"
),
channel=ctx.channel,
author=ctx.author,
)
await view.wait()
if view.value is ui.ConfirmResponse.TIMEOUT:
return False
if view.value is ui.ConfirmResponse.DENIED:
await auxiliary.send_deny_embed(
message=f"The factoid `{factoid_name}` was not {fmt}.",
channel=ctx.channel,
)
return False
return True
async def check_valid_factoid_contents(
self: Self, ctx: commands.Context, factoid_name: str, message: str
) -> str:
"""Makes sure the factoid contents are valid
Args:
ctx (commands.Context): Used to make sure that the .factoid remember invokation message
didn't include any mentions
factoid_name (str): The name to check
message (str): The message to check
Returns:
str: The error message
"""
# Prevents factoids from being created with any mentions
if (
ctx.message.mention_everyone # @everyone
or ctx.message.role_mentions # @role
or ctx.message.mentions # @person
or ctx.message.channel_mentions # #Channel
):
return "I cannot remember factoids with user/role/channel mentions"
# Prevents factoids being created with html elements
if re.search(r"<[^>]+>", message) or re.search(r"<[^>]+>", factoid_name):
return "Cannot create factoids that contain HTML tags!"
# Prevents factoids being created with spaces
if " " in factoid_name:
return "Cannot create factoids with names that contain spaces!"
return None
async def handle_parent_change(
self: Self, ctx: commands.Context, aliases: list, new_name: str
) -> None:
"""Changes the list of aliases to point to a new name
Args:
ctx (commands.Context): Used for cache handling
aliases (list): A list of aliases to change
new_name (str): The name of the new parent
"""
for alias in aliases:
# Doesn't handle the initial, changed alias
if alias.name == new_name:
continue
# Updates the existing aliases to point to the new parent
alias.alias = new_name
await self.modify_factoid_call(factoid=alias)
await self.handle_cache(str(ctx.guild.id), alias.name)
async def check_alias_recursion(
self: Self,
channel: discord.TextChannel,
guild: str,
factoid_name: str,
alias_name: str,
) -> bool:
"""Makes sure an alias isn't already present in a factoids alias list
Args:
channel (discord.TextChannel): The channel to send the return message to
guild (str): The id of the guild from which the command was executed
factoid_name (str): The name of the parent
alias_name (str): The alias to check
Returns:
bool: Whether the alias recurses
"""
# Get list of aliases of the target factoid
factoid_aliases = (
await self.bot.models.Factoid.query.where(
self.bot.models.Factoid.alias == alias_name
)
.where(self.bot.models.Factoid.guild == guild)
.gino.all()
)
# Returns arue if the factoid and alias name is the same (.factoid alias a a)
if factoid_name == alias_name:
await auxiliary.send_deny_embed(
message="Can't set an alias for itself!", channel=channel
)
return True
# Returns True if the target has the alias already
# (.factoid alias b a, where b has a set already)
if factoid_name in [alias.name for alias in factoid_aliases]:
await auxiliary.send_deny_embed(
message=f"`{alias_name}` already has `{factoid_name}`"
+ "set as an alias!",
channel=channel,
)
return True
return False
def get_embed_from_factoid(
self: Self, factoid: bot.models.Factoid
) -> discord.Embed:
"""Gets the factoid embed from its message.
Args:
factoid (bot.models.Factoid): The factoid to get the json of
Returns:
discord.Embed: The embed of the factoid
"""
if not factoid.embed_config:
return None
embed_config = json.loads(factoid.embed_config)
return discord.Embed.from_dict(embed_config)
# -- Cache functions --
async def handle_cache(self: Self, guild: str, factoid_name: str) -> None:
"""Deletes factoid from the factoid cache
Args:
guild (str): The guild to get the cache key
factoid_name (str): The name of the factoid to remove from the cache
"""
key = self.get_cache_key(guild, factoid_name)
if key in self.factoid_cache:
del self.factoid_cache[key]
def get_cache_key(self: Self, guild: str, factoid_name: str) -> str:
"""Gets the cache key for a guild
Args:
guild (str): The ID of the guild
factoid_name (str): The name of the factoid
Returns:
str: The cache key
"""
return f"{guild}_{factoid_name}"
# -- Getting factoids --
async def get_all_factoids(
self: Self, guild: str = None, list_hidden: bool = False
) -> list:
"""Gets all factoids from a guild
Args:
guild (str, optional): The guild to get the factoids from.
Defaults to None, where all guilds are returned instead.
list_hidden (bool, optional): Whether to list hidden factoids as well.
Defaults to False.
Returns:
list: List of factoids
"""
# Gets factoids for a guild, including those that are hidden
if guild and list_hidden:
factoids = await self.bot.models.Factoid.query.where(
self.bot.models.Factoid.guild == guild
).gino.all()
# Gets factoids for a guild excluding the hidden ones
elif guild and not list_hidden:
factoids = (
await self.bot.models.Factoid.query.where(
self.bot.models.Factoid.guild == guild
)
# hiding hidden factoids
# pylint: disable=C0121
.where(self.bot.models.Factoid.hidden == False).gino.all()
)
# Gets ALL factoids for ALL guilds
else:
factoids = await self.bot.db.all(self.bot.models.Factoid.query)
# Sorts them alphabetically
if factoids:
factoids.sort(key=lambda factoid: factoid.name)
return factoids
async def get_raw_factoid_entry(
self: Self, factoid_name: str, guild: str
) -> bot.models.Factoid:
"""Searches the db for a factoid by its name, does NOT follow aliases
Args:
factoid_name (str): The name of the factoid to get
guild (str): The id of the guild for the factoid
Raises:
FactoidNotFoundError: Raised when the provided factoid doesn't exist
Returns:
bot.models.Factoid: The factoid
"""
cache_key = self.get_cache_key(guild, factoid_name.lower())
factoid = self.factoid_cache.get(cache_key)
# If the factoid isn't cached
if not factoid:
factoid = (
await self.bot.models.Factoid.query.where(
self.bot.models.Factoid.name == factoid_name.lower()
)
.where(self.bot.models.Factoid.guild == guild)
.gino.first()
)
# If the factoid doesn't exist
if not factoid:
raise custom_errors.FactoidNotFoundError(factoid=factoid_name)
# Caches it
self.factoid_cache[cache_key] = factoid
return factoid
async def get_factoid(
self: Self, factoid_name: str, guild: str
) -> bot.models.Factoid:
"""Gets the factoid from the DB, follows aliases
Args:
factoid_name (str): The name of the factoid to get
guild (str): The id of the guild for the factoid
Raises:
FactoidNotFoundError: If the factoid wasn't found
Returns:
bot.models.Factoid: The factoid
"""
factoid = await self.get_raw_factoid_entry(factoid_name, guild)
# Handling if the call is an alias
if factoid and factoid.alias not in ["", None]:
factoid = await self.get_raw_factoid_entry(factoid.alias, guild)
factoid_name = factoid.name
if not factoid:
raise custom_errors.FactoidNotFoundError(factoid=factoid_name)
return factoid
async def get_list_of_aliases(
self: Self, factoid_to_search: str, guild: str
) -> list[str]:
"""Gets an alphabetical list of all ways to call a factoid
This will include the internal parent AND all aliases
Args:
factoid_to_search (str): The name of the factoid to search for aliases of
guild (str): The guild to search for factoids in
Returns:
list[str]: The list of all ways to call the factoid, including what was passed
"""
factoid = await self.get_factoid(factoid_to_search, guild)
alias_list = [factoid.name]
factoids = await self.get_all_factoids(guild)
for test_factoid in factoids:
if test_factoid.alias and test_factoid.alias == factoid.name:
alias_list.append(test_factoid.name)
return sorted(alias_list)
# -- Adding and removing factoids --
async def add_factoid(
self: Self,
ctx: commands.Context,
factoid_name: str,
guild: str,
message: str,
embed_config: str,
alias: str = None,
) -> None:
"""Adds a factoid with confirmation, modifies it if it already exists
Args:
ctx (commands.Context): The context used for the confirmation message
factoid_name (str): The name of the factoid
guild (str): The guild of the factoid
message (str): The message of the factoid
embed_config (str): The embed config of the factoid
alias (str, optional): The parent of the factoid. Defaults to None.
"""
fmt = "added" # Changes to modified, used for the returned message
name = factoid_name # Name if the factoid doesn't exist
# Checks if the factoid exists already
try:
factoid = await self.get_factoid(factoid_name, guild)
if factoid.protected:
await auxiliary.send_deny_embed(
message=f"`{factoid.name}` is protected and cannot be modified",
channel=ctx.channel,
)
return
name = factoid.name.lower() # Name of the parent
# Adds the factoid if it doesn't exist already
except custom_errors.FactoidNotFoundError:
# If remember was called with an embed but not a message and the factoid does not exist
if not message:
await auxiliary.send_deny_embed(
message="You did not provide the factoid message!",
channel=ctx.channel,
)
return
await self.create_factoid_call(
factoid_name=name,
guild=guild,
message=message,
embed_config=embed_config,
alias=alias,
)
# Modifies the factoid if it already exists
else:
fmt = "modified"
# Confirms modification
if await self.confirm_factoid_deletion(factoid_name, ctx, fmt) is False:
return
# Modifies the old entry
factoid = await self.get_raw_factoid_entry(name, str(ctx.guild.id))
factoid.name = name
# if no message was supplied, keep the original factoid's message.
if message:
factoid.message = message
factoid.embed_config = embed_config
factoid.alias = alias
await self.modify_factoid_call(factoid=factoid)
# Removes the factoid from the cache
await self.handle_cache(guild, name)
await auxiliary.send_confirm_embed(
message=f"Successfully {fmt} the factoid `{factoid_name}`",
channel=ctx.channel,
)
async def delete_factoid(
self: Self, ctx: commands.Context, called_factoid: CalledFactoid
) -> bool:
"""Deletes a factoid with confirmation
Args:
ctx (commands.Context): Context to send the confirmation message to
called_factoid (CalledFactoid): The factoid to remove
Returns:
bool: Whether the factoid was deleted
"""
factoid = await self.get_raw_factoid_entry(
called_factoid.factoid_db_entry.name, str(ctx.guild.id)
)
aliases_list = await self.get_list_of_aliases(
called_factoid.factoid_db_entry.name, str(ctx.guild.id)
)
aliases_list.remove(called_factoid.original_call_str)
print_aliases_list = ", ".join(aliases_list)
send_message = (
f"This will remove the factoid `{called_factoid.original_call_str}`"
)
if print_aliases_list:
send_message += f" and all of it's aliases `({print_aliases_list})` forever"
send_message += ". Are you sure?"
view = ui.Confirm()
await view.send(
message=send_message,
channel=ctx.channel,
author=ctx.author,
)
await view.wait()
if view.value is ui.ConfirmResponse.TIMEOUT:
return False
if view.value is ui.ConfirmResponse.DENIED:
await auxiliary.send_deny_embed(
message=f"Factoid `{called_factoid.original_call_str}` was not deleted",
channel=ctx.channel,
)
return False
await self.delete_factoid_call(factoid, str(ctx.guild.id))
# Don't send the confirmation message if this is an alias either
confirm_message = (
f"Successfully deleted the factoid `{called_factoid.original_call_str}`"
)
if print_aliases_list:
confirm_message += f" and all of it's aliases `({print_aliases_list})`"
await auxiliary.send_confirm_embed(message=confirm_message, channel=ctx.channel)
return True
# -- Getting and responding with a factoid --
async def match(
self: Self, config: munch.Munch, _: commands.Context, message_contents: str
) -> bool:
"""Checks if a message started with the prefix from the config
Args:
config (munch.Munch): The config to get the prefix from
message_contents (str): The message to check
Returns:
bool: Whether the message starts with the prefix or not
"""
return message_contents.startswith(config.extensions.factoids.prefix.value)
async def response(
self: Self,
config: munch.Munch,
ctx: commands.Context,
message_content: str,
_: bool,
) -> None:
"""Responds to a factoid call
Args:
config (munch.Munch): The server config
ctx (commands.Context): Context of the call
message_content (str): Content of the call
Raises:
TooLongFactoidMessageError:
Raised when the raw message content is over discords 2000 char limit
"""
if not ctx.guild:
return
# Checks if the first word of the content after the prefix is a valid factoid
# Replaces \n with spaces so factoid can be called even with newlines
prefix = config.extensions.factoids.prefix.value
query = message_content[len(prefix) :].replace("\n", " ").split(" ")[0].lower()
try:
factoid = await self.get_factoid(query, str(ctx.guild.id))
except custom_errors.FactoidNotFoundError:
await self.bot.logger.send_log(
message=f"Invalid factoid call {query} from {ctx.guild.id}",
level=LogLevel.DEBUG,
context=LogContext(guild=ctx.guild, channel=ctx.channel),
)
return
# Checking for disabled or restricted
if factoid.disabled:
return
if (
factoid.restricted
and str(ctx.channel.id)
not in config.extensions.factoids.restricted_list.value
):
return
if not config.extensions.factoids.disable_embeds.value:
embed = self.get_embed_from_factoid(factoid)
else:
embed = None
# if the json doesn't include non embed argument, then don't send anything
# otherwise send message text with embed
try:
plaintext_content = factoid.message if not embed else None
except ValueError:
# The not embed causes a ValueError in certain cases. This ensures fallback works
plaintext_content = factoid.message
mentions = auxiliary.construct_mention_string(ctx.message.mentions)
content = " ".join(filter(None, [mentions, plaintext_content])) or None
if content and len(content) > 2000:
await auxiliary.send_deny_embed(
message="I ran into an error sending that factoid: "
+ "The factoid message is longer than the discord size limit (2000)",
channel=ctx.channel,
)
raise custom_errors.TooLongFactoidMessageError
try:
# define the message and send it
await ctx.reply(content=content, embed=embed, mention_author=not mentions)
# log it in the logging channel with type info and generic content
config = self.bot.guild_configs[str(ctx.guild.id)]
log_channel = config.get("logging_channel")
await self.bot.logger.send_log(
message=(
f"Sending factoid: {query} (triggered by {ctx.author} in"
f" #{ctx.channel.name})"
),
level=LogLevel.INFO,
context=LogContext(guild=ctx.guild, channel=ctx.channel),
channel=log_channel,
)
# If something breaks, also log it
except discord.errors.HTTPException as exception:
config = self.bot.guild_configs[str(ctx.guild.id)]
log_channel = config.get("logging_channel")
await self.bot.logger.send_log(
message="Could not send factoid",
level=LogLevel.ERROR,
context=LogContext(guild=ctx.guild, channel=ctx.channel),
channel=log_channel,
exception=exception,
)
# Sends the raw factoid instead of the embed as fallback
await ctx.reply(
f"{mentions+' ' if mentions else ''}{factoid.message}",
mention_author=not mentions,
)
await self.send_to_irc(ctx.channel, ctx.message, factoid.message)
async def send_to_irc(
self: Self,
channel: discord.abc.Messageable,
message: discord.Message,
factoid_message: str,
) -> None:
"""Send a factoid to IRC channel, if it was called in a linked channel
Args:
channel (discord.abc.Messageable): The channel the factoid was sent in
message (discord.Message): The message object of the invocation
factoid_message (str): The text of the factoid to send
"""
# Don't attempt to send a message if irc if irc is disabled
irc_config = self.bot.file_config.api.irc
if not irc_config.enable_irc:
return
await self.bot.irc.irc_cog.handle_factoid(
channel=channel,
discord_message=message,
factoid_message=factoid_message,
)
@factoid_app_group.command(
name="call",
description="Calls a factoid from the database and sends it publicy in the channel.",
extras={
"usage": "[factoid_name]",
"module": "factoids",
},
)
async def factoid_call_command(
self: Self, interaction: discord.Interaction, factoid_name: str
) -> None:
"""This is an app command version of typing {prefix}call
Args:
interaction (discord.Interaction): The interaction that triggered this command
factoid_name (str): The factoid name to search for and print
Raises:
TooLongFactoidMessageError: If the plaintext exceed 2000 characters
"""
query = factoid_name.replace("\n", " ").split(" ")[0].lower()
config = self.bot.guild_configs[str(interaction.guild.id)]
try:
factoid = await self.get_factoid(query, str(interaction.guild.id))
except custom_errors.FactoidNotFoundError:
embed = auxiliary.prepare_deny_embed(
message=f"The factoid {factoid_name} couldn't be found"
)
await interaction.response.send_message(embed=embed, ephemeral=True)
await self.bot.logger.send_log(
message=f"Invalid factoid call {query} from {interaction.guild.id}",
level=LogLevel.DEBUG,
context=LogContext(
guild=interaction.guild, channel=interaction.channel
),
)
return
# Checking for disabled or restricted
if factoid.disabled:
embed = auxiliary.prepare_deny_embed(
message=f"The factoid {factoid_name} is disabled."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
if (
factoid.restricted
and str(interaction.channel.id)
not in config.extensions.factoids.restricted_list.value
):
embed = auxiliary.prepare_deny_embed(
message=f"The factoid {factoid_name} is restricted and not allowed in this channel."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
if not config.extensions.factoids.disable_embeds.value:
embed = self.get_embed_from_factoid(factoid)
else:
embed = None
# if the json doesn't include non embed argument, then don't send anything
# otherwise send message text with embed
try:
content = factoid.message if not embed else None
except ValueError:
# The not embed causes a ValueError in certain cases. This ensures fallback works
content = factoid.message
if content and len(content) > 2000:
embed = auxiliary.prepare_deny_embed(
message="I ran into an error sending that factoid: "
+ "The factoid message is longer than the discord size limit (2000)",
)
await interaction.response.send_message(embed=embed, ephemeral=True)
raise custom_errors.TooLongFactoidMessageError
try:
# define the message and send it