-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathbot.py
1809 lines (1485 loc) · 80.1 KB
/
bot.py
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
#version in line 1 of ./changelog.md
import asyncio
import os
import shutil
import json
import random
import copy
import sys
import traceback
import discord
from discord.utils import get
from datetime import datetime
from discord import app_commands
from typing import Union
#custom modules
sys.path.insert(0, 'utils/')
sys.path.insert(0, 'music/')
sys.path.insert(0, 'chessGame/')
#utils
import config
import getevn
from constants import SETTINGS_TEMPLATE, settings_folder
from config import Colors as col
from mPrint import mPrint as mp
#music and chessGame
import chessBridge
import musicBridge
if config.language == "IT":
from lang import it as lang
elif config.language == "EN":
from lang import en as lang
def mPrint(tag, value):mp(tag, 'bot', value)
print()
mPrint('WARN', "# =------------------------------------------------= #")
mPrint('WARN', "# THIS BOT IS CURRENTLY IN BETA. BUGS ARE EXPECTED #")
mPrint('WARN', "# =------------------------------------------------= #")
mPrint('WARN', "please submit issues https://github.com/NotLatif/CuloBot/issues")
print()
TOKEN = getevn.getenv('DISCORD_TOKEN', True)
try:
OWNER_ID = int(getevn.getenv('OWNER_ID')) #(optional) needed for the bot to send you feedback when users use /feedback command
# you will still see user-submitted feedback in the feedback.log file (will be createt automatically if not present)
except ValueError:
pass
intents = discord.Intents.all()
intents.members = True
intents.messages = True
# ===| INDIVIDUAL GUILD SETTINGS HANDLER (move to it's own module?) |=== #
global settings
settings = {}
def dumpSettings(guild_id:int = 0): # settings disctionary -> json files
"""Saves the settings to file"""
# mPrint('FUNC', f"dumping settings for {settings_folder}{guild_id}.json")
dump = settings[guild_id]
with open(settings_folder + f"{guild_id}.json", 'w') as f:
json.dump(dump, f, indent=2)
def loadSettings(guild_id:int): # json files -> settings dictionary
global settings
mPrint('FUNC', "Loading guild settings")
try:
with open(settings_folder + f"{guild_id}.json", 'r') as f:
settings[guild_id] = json.load(f)
except json.JSONDecodeError:
mPrint('ERROR', traceback.format_exc())
except Exception:
mPrint('ERROR', f"Could not load settings for [{guild_id}]{traceback.format_exc()}")
def createSettings(guild_id:int): # new json files
guild_id = int(guild_id)
mPrint('FUNC', "Generating new settings for guild")
if (not os.path.isdir(settings_folder)):
os.mkdir(settings_folder)
temp = SETTINGS_TEMPLATE["id"]
with open(settings_folder + f"{guild_id}.json", 'w') as f:
json.dump(temp, f, indent=2)
def checkSettingsIntegrity(guild_id:int):
guild_id = int(guild_id)
mPrint('FUNC', f'Checking settings integrity for guild')
try:
settingsToCheck = copy.deepcopy(settings[guild_id])
except KeyError:
mPrint('FATAL', f'Settings for guild were not initialized correctly\n{traceback.format_exc()}')
sys.exit(-1)
#check if there is more data than there should
for key in settingsToCheck:
if(key not in SETTINGS_TEMPLATE["id"]): #check if there is a key that should not be there
del settings[guild_id][key]
mPrint('DEBUG', f'Deleting: {key}')
if(type(settingsToCheck[key]) == dict):
#if(key in ["saved_playlists", "urlsync"]): continue #whitelist
for subkey in settingsToCheck[key]:
if(subkey not in SETTINGS_TEMPLATE["id"][key]): #check if there is a subkey that should not be there
del settings[guild_id][key][subkey]
mPrint('DEBUG', f'Deleting: {subkey}')
#check if data is missing
for key in SETTINGS_TEMPLATE["id"]:
if(key not in settings[guild_id]): #check if a key is missing
settings[guild_id][key] = SETTINGS_TEMPLATE["id"][key]
mPrint('DEBUG', f'Creating key: {key}')
#it it's a dict also check it's keys
if(type(SETTINGS_TEMPLATE["id"][key]) == dict):
for subkey in SETTINGS_TEMPLATE["id"][key]:
if(subkey not in settings[guild_id][key]): #check if a subkey is missing
settings[guild_id][key][subkey] = SETTINGS_TEMPLATE["id"][key][subkey]
mPrint('DEBUG', f'Creating subkey: {subkey}')
dumpSettings(guild_id)
# ===| helper functions |=== #
def getWord(all=False) -> Union[str, list]:
"""
:return: A random line from the words.txt file.
e.g. culo, i culi
"""
with open('botFiles/words.txt', 'r') as words:
lines = words.read().splitlines()
if(all):
return lines
return random.choice(lines)
def parseWord(message:str, i:int, words:str, articoli:list[str]) -> tuple[str, str]:
#message = 'No voglio il rosso'
#words = 'il culo, i culi'
article_word = words.split(', ') #['il culo', 'i culi']
#sorry for spaghetti code maybe will reparse later
if len(article_word) == 1: #word has only one form (eg: ['il culo'])
if len(article_word[0].split()) == 1:
return (message[i-1], article_word[0]) #eg. words = ['culo']
if message[i-1] not in articoli: #don't change the word before if not an article
return (message[i-1], article_word[0].split()[1]) #eg. words = ['il culo']
return (article_word[0].split()[0], article_word[0].split()[1]) #eg. words = ['il culo']
if message[i-1] not in articoli: #the word before is not an article
if message[i-1].isnumeric(): #e.g. '3 cavolfiori'
if message[i-1] == '1':
return (message[i-1], article_word[0].split()[1]) #'1 culo'
else:
return (message[i-1], article_word[1].split()[1]) #'3 culi'
return (message[i-1], article_word[0].split()[1]) #eg. returns ('ciao', 'culo')
if message[i-1] in ['il', 'lo', 'la']: #eg. returns ('il', 'culo')
return (article_word[0].split()[0], article_word[0].split()[1])
if message[i-1] in ['i', 'gli', 'le']: #eg. returns ('i', 'culi')
return (article_word[1].split()[0], article_word[1].split()[1])
return ('parsing error', 'parseWord(str, int, str, list[str]) -> tuple[str, str]')
# ----- CULOBOT ----- #
class CuloBot(discord.Client):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.isReady = False
async def on_error(self, *args, **kwargs):
mPrint('ERROR', f"DISCORDPY on_error:\n{traceback.format_exc()}")
mPrint('WARN', "ARGS:")
for x in args:
mPrint('ERROR', {x})
async def on_guild_join(self, guild:discord.Guild):
guild_id = int(guild.id)
mPrint("INFO", f"Joined guild {guild.name} (id: {guild_id})")
members = '\n - '.join([member.name for member in guild.members])
mPrint('DEBUG', f'Guild Members:\n - {members}')
if (guild_id not in settings):
createSettings(guild_id)
else:
mPrint('DEBUG', f'settings for {guild_id} are present in {settings}')
loadSettings(guild_id)
checkSettingsIntegrity(guild_id)
# voice_client : discord.VoiceClient = get(bot.voice_clients, guild=guild)
# if voice_client != None and voice_client.is_connected():
# await voice_client.disconnect()
async def on_guild_remove(self, guild:discord.Guild):
pass
async def on_ready(self):
if self.isReady: return # ensure that on_ready only runs one time
self.isReady = True
mPrint("DEBUG", "Called on_ready")
try:
self.dev = await bot.fetch_user(OWNER_ID)
except discord.errors.NotFound:
self.dev = None
except NameError:
pass
await bot.change_presence(status=config.bot_status, activity=discord.Activity(type=discord.ActivityType.listening, name=config.bot_description))
mPrint("INFO", f'Connected to {len(bot.guilds)} guild(s)')
for guild in bot.guilds:
guild_id = int(guild.id)
mPrint('DEBUG', f'Initializing guild {guild_id} -')
if (not os.path.isfile(settings_folder+f"{guild_id}.json")):
createSettings(guild_id)
loadSettings(guild_id)
# mPrint('DEBUG', json.dumps({guild_id: settings[guild_id]}, indent=2))
checkSettingsIntegrity(guild_id)
mPrint('DEBUG', "command tree sync...")
await tree.sync()
mPrint('INFO', "bot is ready.")
async def on_member_join(self, member : discord.Member):
if not config.discord_events: return
if settings[int(member.guild.id)]['responseSettings']['send_join_msg']:
joinString:str = settings[int(member.guild.id)]['responseSettings']['join_message']
joinString = joinString.replace('%name%', member.name)
await member.guild.system_channel.send(joinString)
async def on_member_remove(self, member : discord.Member):
if not config.discord_events: return
if settings[int(member.guild.id)]['responseSettings']['send_leave_msg']:
leaveString:str = settings[int(member.guild.id)]['responseSettings']['leave_message']
leaveString= leaveString.replace('%name%', member.name)
await member.guild.system_channel.send(leaveString)
async def on_guild_available(self, guild : discord.Guild):
pass
async def on_message(self, message : discord.Message):
if not config.reply: return
if len(message.content.split())==0: return
global settings
try:
respSettings = settings[int(message.guild.id)]["responseSettings"]
except AttributeError:
return #this gets triggered with ephemeral messages
if message.channel.id in settings[message.guild.id]['responseSettings']['disabled_channels']:
return #module is in blacklist for this channel
#don't respond to self, commands, messages with less than 2 words
if message.author.id == bot.user.id or message.content[0] in ["!", "/", "?", "|", '$', "&", ">", "<"] or len(message.content.split()) < 2:
return
#if guild does not want bot responses and sender is a bot, ignore the message
if message.author.bot and not respSettings["will_respond_to_bots"]: return 0
#culificazione
articoli = ['il', 'lo', 'la', 'i', 'gli', 'le'] #Italian specific
if random.randrange(1, 100) > respSettings["response_perc"]: #implement % of answering
return
msg = message.content.split() #trasforma messaggio in lista
for i in range(len(msg) // 3): #culifico al massimo un terzo delle parole
scelta = random.randrange(1, len(msg)) #scegli una parola
# se la parola scelta è un articolo (e non è l'ultima parola), cambio la prossima parola
# e.g "ciao sono il meccanico" (se prendo la parola DOPO "il") -> "ciao sono il culo"
if msg[scelta] in articoli and scelta < len(msg)-1:
scelta += 1
parola = getWord() #scegli con cosa cambiarla
articolo, parola = parseWord(msg, scelta, parola, articoli)
msg[scelta-1] = articolo
if(msg[scelta].isupper()): #controlla se la parola è maiuscola, o se la prima lettera è maiuscola
parola = parola.upper()
elif(msg[scelta][0].isupper()):
parola = parola[0].upper() + parola[1:]
msg[scelta] = parola #sostituisci parola
if(random.randrange(1, 100) > respSettings['other_response']):
i+=1
i+=1
msg = " ".join(msg) #trasforma messaggio in stringa
await message.reply(msg, mention_author=False)
mPrint('DEBUG', f'Ho risposto ad un messaggio.')
bot = CuloBot(intents = intents)
tree = app_commands.CommandTree(bot)
# ----- CULOBOT SLASH COMMANDS ----- #
# ======== REPLY ========= #
@tree.command(name="join-msg", description=lang.slash.join_msg)
async def joinmsg(interaction : discord.Interaction, message : str = None, enabled : bool = None):
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /join-msg {message}')
guildID = int(interaction.guild.id)
if enabled != None:
settings[guildID]['responseSettings']['send_join_msg'] = enabled
dumpSettings(guildID)
if message != None: #edit join-message or show help
settings[guildID]['responseSettings']['join_message'] = message
dumpSettings(guildID)
embed = discord.Embed(
title=lang.commands.join_msg_embed_title,
description=lang.commands.join_msg_embed_desc(settings[guildID]['responseSettings']['send_join_msg'], settings[guildID]['responseSettings']['join_message']),
color=col.orange
)
await interaction.response.send_message(embed=embed, ephemeral=True)
@tree.command(name="leave-msg", description=lang.slash.leave_msg)
async def leavemsg(interaction : discord.Interaction, message : str = None, enabled : bool = None):
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /leave-msg {message}')
guildID = int(interaction.guild.id)
if enabled != None:
settings[guildID]['responseSettings']['send_leave_msg'] = enabled
dumpSettings(guildID)
if message != None: #edit join-message or show help
settings[guildID]['responseSettings']['leave_message'] = message
dumpSettings(guildID)
embed = discord.Embed(
title=lang.commands.leave_msg_embed_title,
description=lang.commands.leave_msg_embed_desc(settings[guildID]['responseSettings']['send_leave_msg'], settings[guildID]['responseSettings']['leave_message']),
color=col.orange
)
await interaction.response.send_message(embed=embed, ephemeral=True)
@tree.command(name="respond-perc", description=lang.slash.respond_perc)
async def responsePerc(interaction : discord.Interaction, value : int = -1):
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /respond-perc {value}')
guildID = int(interaction.guild.id)
respSettings = settings[guildID]["responseSettings"] #readonly
if(value == -1):
await interaction.response.send_message(lang.commands.resp_info(str(respSettings["response_perc"])))
return
elif (respSettings['response_perc'] == value):
await interaction.response.send_message(lang.nothing_changed)
return
#else
#keep value between 0 and 100
value = 100 if value > 100 else 0 if value < 0 else value
await interaction.response.send_message(lang.commands.resp_newperc(value))
mPrint('INFO', f'{interaction.user.name} set response to {value}%')
settings[guildID]['responseSettings']['response_perc'] = value
dumpSettings(guildID)
@tree.command(name="respond-to-bots", description=lang.slash.respond_to_bots)
async def botRespToggle(interaction : discord.Interaction, value : bool):
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /respond-to-bots {value}')
guildID = int(interaction.guild.id)
if value == True:
response = lang.commands.resp_resp_to_bots_affirmative
else:
response = lang.commands.resp_resp_to_bots_negative
await interaction.response.send_message(response)
settings[guildID]['responseSettings']['will_respond_to_bots'] = value
dumpSettings(guildID)
return
@tree.command(name="respond-to-bots-perc", description=lang.slash.respond_to_bots_perc)
async def botRespPerc(interaction : discord.Interaction, value : int = -1):
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /respond-to-bots-perc {value}')
guildID = int(interaction.guild.id)
if value == -1:
await interaction.response.send_message(
lang.commands.resp_to_bots_info(
settings[guildID]["responseSettings"]["will_respond_to_bots"],
settings[guildID]["responseSettings"]["response_to_bots_perc"]
)
)
return
#else
#keep value between 0 and 100
value = 100 if value > 100 else 0 if value < 0 else value
await interaction.response.send_message(lang.commands.resp_to_bots_edit(value))
settings[guildID]['responseSettings']['response_to_bots_perc'] = value
dumpSettings(guildID)
return
@tree.command(name="dictionary", description=lang.slash.dictionary)
async def dictionary(interaction : discord.Interaction):
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /dictionary')
guildID = int(interaction.guild.id)
#1. Send a description
custom_words = settings[guildID]['responseSettings']['custom_words']
description = lang.commands.words_info(interaction.guild.name)
if not settings[guildID]['responseSettings']['use_global_words']:
description += lang.commands.words_use_global_words(interaction.guild.name)
embed = discord.Embed(
title = lang.commands.words_known_words,
description = description,
colour = col.orange
)
value = ''
#2a. get the global words
botWords = getWord(True)
#2b. if the guild uses the global words, append them to value
if settings[guildID]['responseSettings']['use_global_words']:
#is server uses default words
value = '\n'.join(botWords)
embed.add_field(name = lang.commands.words_bot_words, value=value)
value = ''
#2c. append the guild(local) words to value
for i, cw in enumerate(custom_words):
value += f'[`{i}`]: {cw}\n'
if value == '': value=lang.commands.words_guild_words
embed.add_field(name = lang.commands.words_guild_words(interaction.guild.name), value=value)
#3. send the words
await interaction.response.send_message(embed=embed)
@tree.command(name="dictionary-add", description=lang.slash.dictionary_add)
async def dictionary_add(interaction : discord.Interaction, new_word : str):
"""
Aggiunge una parola al dizionario.
:param new_word: La parola che si vuole aggiungere
"""
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /dictionary add {new_word}')
guildID = int(interaction.guild.id)
settings[guildID]['responseSettings']['custom_words'].append(new_word)
await interaction.response.send_message(lang.commands.words_learned, ephemeral=True)
dumpSettings(guildID)
return
@tree.command(name="dictionary-edit", description=lang.slash.dictionary_edit)
async def dictionary_edit(interaction : discord.Interaction, id : int, new_word : str):
"""
Modifica una parola del dizionario.
:param id: L'id della parola che vuoi modificare
:param new_word: La parola che vuoi rimpiazzare
"""
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /dictionary edit {id}, {new_word}')
guildID = int(interaction.guild.id)
editWord = id
if len(settings[guildID]['responseSettings']['custom_words']) > editWord:
settings[guildID]['responseSettings']['custom_words'][editWord] = new_word
dumpSettings(guildID)
await interaction.response.send_message(lang.done, ephemeral=True)
else:
await interaction.response.send_message(lang.commands.words_id_not_found, ephemeral=True)
return
@tree.command(name="dictionary-del", description=lang.slash.dictionary_del)
async def dictionary_del(interaction : discord.Interaction, id : int):
"""
It deletes a word from the dictionary.
:param id: L'id della parola che vuoi eliminare
"""
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /dictionary del {id}')
guildID = int(interaction.guild.id)
delWord = id
if len(settings[guildID]['responseSettings']['custom_words']) > delWord:
del settings[guildID]['responseSettings']['custom_words'][delWord]
dumpSettings(guildID)
await interaction.response.send_message(lang.done, ephemeral=True)
else:
await interaction.response.send_message(lang.commands.words_id_not_found, ephemeral=True)
return
@tree.command(name="dictionary-useglobal", description=lang.slash.dictionary_use_global)
async def dictionary_default(interaction : discord.Interaction, value : bool ):
if not config.reply:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /dictionary useglobal {value}')
guildID = int(interaction.guild.id)
settings[guildID]["responseSettings"]["use_global_words"] = value
await interaction.response.send_message(f'useDefault: {value}', ephemeral=True)
dumpSettings(guildID)
return
# ======== CHESS ========= #
@tree.command(name="chess", description=lang.slash.chess)
async def chess(interaction : discord.Interaction, challenge : Union[discord.Role, discord.User] = None):
"""
:param challenge: Il ruolo o l'utente da sfidare
:param fen: Il layout delle pedine nella scacchiera (Se numerico indica uno dei FEN salvati)
:param design: Il nome del design della scacchiera
"""
if not config.chess:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /chess: ch: {challenge}')
guildID = int(interaction.guild.id)
if interaction.channel.id in settings[guildID]['chessGame']['disabled_channels']:
await interaction.response.send_message(lang.module_not_enabled, ephemeral=True)
return
await interaction.channel.typing()
#1. Prepare the variables
#info about the player challenging another player or a role to a match
class Challenge:
type = 0 #can be: 0-> Everyone / 1-> Role / 2-> Player
whitelist = [] #list of user ids (int) (or 1 role id)
authorJoined = False #Needed when type = 1
challengeData = Challenge()
#2A. parse challenges
if challenge == None:
mPrint('DEBUG', 'Challenged everyone')
challengeData.type = 0
elif '<@&' in challenge.mention: #user challenged role
mPrint('DEBUG', f'challenged role: {challenge.id} ({challenge.name})')
challengeData.type = 1
challengeData.whitelist.append(challenge.id)
else: #user challenged user
mPrint('DEBUG', f'challenged user: {challenge.id} ({challenge.name})')
challengeData.type = 2
challengeData.whitelist.append(interaction.user.id)
challengeData.whitelist.append(challenge.id)
#useful for FENs: eg (!chess) game fen="bla bla bla" < str.strip will return ["game", "fen=bla", "bla", "bla"]
# args = splitString(args) #wheras splitString will return ["game", "fen=", "bla", "bla", "bla"]
#Ask user if he wants FEN and/or design
class GameData():
selectedLayout = None
selectedDesign = None
gameData = GameData()
#FEN options
globalBoards = chessBridge.getBoards()
guildBoards = settings[guildID]['chessGame']['boards']
layoutChoices = discord.ui.Select(options=[], placeholder=lang.chess.layout_render_select)
for layout in globalBoards: #global layouts
isDefault = False
if layout == settings[guildID]["chessGame"]["default_board"]:
isDefault = True
gameData.selectedLayout = globalBoards[layout]
layoutChoices.add_option(label=f"FEN: {layout}", description=globalBoards[layout], value=globalBoards[layout], default=isDefault)
for layout in guildBoards: #guild layouts
isDefault = False
if layout == settings[guildID]["chessGame"]["default_board"]:
isDefault = True
gameData.selectedLayout = guildBoards[layout]
layoutChoices.add_option(label=f"FEN: {layout}", description=guildBoards[layout], value=guildBoards[layout], default=isDefault)
#Design options
globalDesigns = chessBridge.chessMain.getDesignNames()
guildDesigns = settings[guildID]['chessGame']['designs']
designChoices = discord.ui.Select(options=[], placeholder=lang.chess.design_render_select)
for design in globalDesigns: #guild layouts
isDefault = False
if design == settings[guildID]["chessGame"]["default_design"]:
isDefault = True
gameData.selectedDesign = design
designChoices.add_option(label=f"Design: {design}", value=design, default=isDefault)
for design in guildDesigns: #guild layouts
isDefault = False
if design == settings[guildID]["chessGame"]["default_design"]:
isDefault = True
gameData.selectedDesign = design
designChoices.add_option(label=f"Design: {design}", description=str(guildDesigns[design]), value=design, default=isDefault)
#handlers for select choices
async def layoutChoice(interaction : discord.Interaction):
gameData.selectedLayout = str(layoutChoices.values[0])
mPrint('DEBUG', f'Selected layout {gameData.selectedLayout}')
await interaction.response.defer()
async def designChoice(interaction : discord.Interaction):
gameData.selectedDesign = str(designChoices.values[0])
mPrint('DEBUG', f'Selected design {gameData.selectedDesign}')
await interaction.response.defer()
layoutChoices.callback = layoutChoice
designChoices.callback = designChoice
async def btn_cancel(interaction : discord.Interaction): #User cancels matchmaking
confirm.disabled = True
cancel.disabled = True
layoutChoices.disabled = True
designChoices.disabled = True
await interaction.response.edit_message(view=view)
return
async def btn_confirm(interaction : discord.Interaction): # When user confirms the data this function starts the game
if (gameData.selectedDesign != None) and (gameData.selectedLayout != None):
confirm.disabled = True
cancel.disabled = True
layoutChoices.disabled = True
designChoices.disabled = True
await interaction.response.edit_message(view=view)
await interaction.channel.typing()
await startGame(interaction, gameData.selectedLayout, gameData.selectedDesign)
else:
await interaction.response.send_message(lang.chess.design_btn_confirm_response, ephemeral=True)
return
confirm = discord.ui.Button(label=lang.confirm, style = discord.ButtonStyle.primary)
confirm.callback = btn_confirm
cancel = discord.ui.Button(label=lang.cancel, style=discord.ButtonStyle.danger)
cancel.callback = btn_cancel
view = discord.ui.View()
view.add_item(layoutChoices)
view.add_item(designChoices)
view.add_item(confirm)
view.add_item(cancel)
await interaction.response.send_message(view=view, ephemeral=True)
async def startGame(interaction:discord.Interaction, gameFEN, gameDesign):
designName = gameDesign
#2C. double-check the data retreived
board = ()
if gameFEN != '': #if fen is provided, check if valid
if('k' not in gameFEN or 'K' not in gameFEN):
print(gameFEN)
embed = discord.Embed(
title = lang.chess.embedTitle_fen_king_missing,
description= lang.chess.embedDesc_fen_king_missing("black" if "k" not in gameFEN else "", "white" if "K" not in gameFEN else ""),
color = col.red
)
await interaction.response.send_message(embed=embed)
return -1
#else, fen is valid
board = ('FEN', gameFEN)
mPrint('TEST', f'Design {gameDesign} ')
# check if design exists
if gameDesign in settings[guildID]['chessGame']['designs']:
mPrint('TEST', f'Found Local design {gameDesign} ')
colors = settings[guildID]['chessGame']['designs'][gameDesign]
gameDesign = chessBridge.chessMain.gameRenderer.renderBoard(colors, interaction.id)
elif chessBridge.chessMain.gameRenderer.doesDesignExist(gameDesign):
mPrint('TEST', f'Found Global design {gameDesign}')
gameDesign = chessBridge.chessMain.gameRenderer.getGlobalDesign(gameDesign)
else:
mPrint('TEST', f'Design not found')
gameDesign = 'default'
# 3. All seems good, now let's send the embed to find some players
#3A. Challenge one user
class Challenge:
type = 0 #can be: 0-> Everyone / 1-> Role / 2-> Player
whitelist = [] #list of user ids (int) (or 1 role id)
authorJoined = False #Needed when type = 1
if challengeData.type == 2:
embed = discord.Embed(title = lang.chess.challenge_e1t(challenge.name, interaction.user.name),
description= lang.chess.challenge_ed(gameFEN, designName),
color = col.blu
)
#3B. Challenge one guild
elif challengeData.type == 1:
embed = discord.Embed(title = lang.chess.challenge_e2t(challenge.name, interaction.user.name),
description= lang.chess.challenge_ed(gameFEN, designName),
color = col.blu
)
#3C. Challenge everyone
else:
embed = discord.Embed(title = lang.chess.challenge_e3t,
description= lang.chess.challenge_ed(gameFEN, designName),
color = col.blu).set_footer(text=lang.chess.challenge_f(interaction.user.name))
#3D. SEND THE EMBED FINALLY
if challengeData.type != 0:
await interaction.channel.send(lang.chess.challenge_s(interaction.user, challenge.mention))
playerFetchMsg = await interaction.channel.send(embed=embed)
#4. Await player joins
#4A. setting up
reactions = ('⚪', '🌑') #('🤍', '🖤')
r1, r2 = reactions
availableTeams = [reactions[0], reactions[1]] # Needed to avoid players from joining the same team
players = [0, 0] #this will store discord.Members
mPrint('DEBUG','chessGame: challenge whitelist')
mPrint('DEBUG', challengeData.whitelist)
#add reactions to the embed that people can use as buttons to join the teams
await playerFetchMsg.add_reaction(r1)
await playerFetchMsg.add_reaction(r2)
await playerFetchMsg.add_reaction("❌") #if the author changes their mind
def fetchChecker(reaction : discord.Reaction, user : Union[discord.Member, discord.User]) -> bool: #this is one fat checker damn
"""Checks if user team join request is valid"""
# async def remove(reaction, user): #remove invalid reactions
# await reaction.remove(user) #will figure out some way
mPrint('DEBUG', f'chessGame: Check: {reaction}, {user}\nchallenge whitelist: {challengeData.whitelist}\navailable: {availableTeams}\n--------')
#1- prevent bot from joining teams
if (user == bot.user):
return False
if (reaction.message.id != playerFetchMsg.id): #the reaction was given to another message
return False
if(str(reaction.emoji) == "❌" and user == interaction.user):
return True #only the author can cancel the search
#2- check if color is still available (prevent two players from joining the same team)
if(str(reaction.emoji) not in availableTeams):
return False #remember to remove the reaction before every return True
userID = int(user.id)
#3a- If player challenged a user:
if(challengeData.type == 2): #0 everyone, 1 role, 2 user
#check if joining player is in whitelist
if userID not in challengeData.whitelist: return False
challengeData.whitelist.remove(userID) #prevent user from rejoining another team
availableTeams.remove(str(reaction.emoji)) #prevent player/s from joining the same team
return True
#3b- If player challenged a role:
elif(challengeData.type == 1):
challengedRole = challenge.id #challenge has only one entry containing the role id
#if the user joining is the author:
if user == interaction.user and challengeData.authorJoined == False: #the message author can join even if he does not have the role
mPrint('DEBUG', 'chessGame: User is author') #check the user BEFORE the role, so if the user has the role it does not get deleted
challengeData.authorJoined = True #prevent author from joining 2 teams
availableTeams.remove(str(reaction.emoji)) #prevent player/s from joining the same team
return True
#if the user joining isn't the author but has the role requested
elif user.get_role(challengedRole) != None: #user has the role
mPrint('DEBUG', 'chessGame: User has required role')
challengeData.whitelist = [] #delete the role to prevent two players with the role from joining (keeping out the author)
availableTeams.remove(str(reaction.emoji)) #prevent player/s from joining the same team
return True
mPrint('WARN',f'chessGame: User {user.name} is not allowerd to join (Role challenge)')
return False
#3c- If player challenged everyone:
else: #no need to check who joins (can also play with yourself)
availableTeams.remove(str(reaction.emoji)) #prevent player/s from joining the same team
return True
# fetchChecker end #
#4B. await user joins (with reactions)
async def stopsearch():
embed.title = lang.chess.stop_title
embed.description = ""
embed.color = col.red
designFolder = f'{chessBridge.chessMain.gameRenderer.spritesFolder}{design}'
if design.find('\\') != -1 or design.find('/') != -1:
shutil.rmtree(designFolder)
await playerFetchMsg.clear_reactions()
await playerFetchMsg.edit(embed=embed)
try:
#i. await first player
r1, players[0] = await bot.wait_for('reaction_add', timeout=60.0, check=fetchChecker)
if str(r1.emoji) == "❌":
await stopsearch()
return -2
embed.description += lang.chess.p_join(players[0], r1)
await playerFetchMsg.edit(embed=embed)
#ii. await second player
r2, players[1] = await bot.wait_for('reaction_add', timeout=60.0, check=fetchChecker)
if str(r2.emoji) == "❌":
await stopsearch()
return -2
embed.description += lang.chess.p2_join(players[0], r1)
embed.set_footer(text = 'fake loading screen lets goooo')
embed.color = col.green
await playerFetchMsg.edit(embed=embed)
#iii. fake sleep for professionality
await asyncio.sleep(random.randrange(0,2))
except asyncio.TimeoutError: #players did not join in time
embed = discord.Embed(
title = lang.chess.not_enough_players,
colour = col.red
)
await interaction.channel.send(embed=embed)
await playerFetchMsg.delete()
return -1
else: #players did join in time
if str(r1.emoji) == reactions[0]: #first player choose white
player1 = players[1] #white
player2 = players[0] #black
else: #first player choose black
player1 = players[0] #white
player2 = players[1] #black
mPrint('INFO', f'p1: {player1}\np2: {player2}')
#iv. Send an embed with the details of the game
embed = discord.Embed(
title = lang.chess.found_players_t(r1, player1, player2, r2),
description = lang.chess.found_players_d(gameFEN, gameDesign),
colour = col.green
)
#v. start a thread where the game will be disputed, join the players in there
thread = await interaction.channel.send(embed=embed)
gameThread = await thread.create_thread(name=(f'{str(player1)[:-5]} -VS- {str(player2)[:-5]}'), auto_archive_duration=60, reason='Chess')
await gameThread.add_user(player1)
await gameThread.add_user(player2)
await playerFetchMsg.delete()
mainThreadEmbed = (thread, embed)
#5. FINALLY, start the game
mPrint('TEST', f'design: {gameDesign}')
await chessBridge.loadGame(gameThread, bot, [player1, player2], mainThreadEmbed, board, gameDesign)
# #send them backwards (info on chessBrige.py) [black, white]
await gameThread.edit(archived=True, locked=True)
designFolder = f'{chessBridge.chessMain.gameRenderer.spritesFolder}{gameDesign}'
if gameDesign.find('\\') != -1 or design.find('/') != -1:
shutil.rmtree(designFolder)
@tree.command(name="chess-layout", description=lang.slash.chess_layout)
@app_commands.choices(sub_command=[
app_commands.Choice(name=lang.choices.info, value="0"),
app_commands.Choice(name=lang.choices.render, value="1"),
app_commands.Choice(name=lang.choices.add, value="2"),
app_commands.Choice(name=lang.choices.edit, value="3"),
app_commands.Choice(name=lang.choices.remove, value="4"),
])
@app_commands.describe(sub_command=lang.choices.description)
async def chess_layout(interaction : discord.Interaction, sub_command: app_commands.Choice[str]): #, name:str=None, fen:str = None
if not config.chess:
await interaction.response.send_message(lang.disabled_module, ephemeral=True)
return
mPrint('CMDS', f'called /chess-layout: {sub_command.name}')
guildID = int(interaction.guild.id)
response = int(sub_command.value)
botBoards = chessBridge.getBoards()
guildBoards = settings[guildID]['chessGame']['boards']
#send saved layouts
if response == 0:
embed = discord.Embed(
title = lang.chess.layout_description,
colour = col.orange
)
#ii. append the global data boards to the embed
value = ''
for b in botBoards:
value += f"**{b}**: {botBoards[b]}\n"
embed.add_field(name = lang.chess.layout_global_layouts, value=value, inline=False)
#iii. if guild data has boards, append them to the embed
if settings[guildID]['chessGame']['boards'] != {}:
guildBoards = ''
for b in settings[guildID]['chessGame']['boards']:
guildBoards += f"**{b}**: {settings[guildID]['chessGame']['boards'][b]}\n"
embed.add_field(name = lang.chess.layout_guild_layouts(interaction.guild.name), value=guildBoards, inline=False)
#iv. send the embed
await interaction.response.send_message(embed=embed)
return 0
elif response == 1: # renders a FEN and send it in chat
choices = discord.ui.Select(options= #global layouts
[discord.SelectOption(label=name, description=botBoards[name], value=botBoards[name]) for name in botBoards],
placeholder=lang.chess.layout_render_select
)
for layout in guildBoards: #guild layouts
choices.add_option(label=layout, description=guildBoards[layout], value=guildBoards[layout])
view = discord.ui.View()
view.add_item(choices)
async def render_and_send_image(interaction : discord.Interaction):
mPrint("TEST", choices.values)
layoutFEN = choices.values[0]
#ii. let the Engine make the image
try:
image = chessBridge.getBoardImgPath(layoutFEN, interaction.id)
mPrint('DEBUG', f'got image path: {image}')
except Exception:
await interaction.response.send_message(lang.chess.layout_render_error)
return -2
if image == 'Invalid':
await interaction.response.send_message(f"{lang.chess.layout_render_invalid} {layoutFEN}")
return -1
mPrint('DEBUG', f'rendered image: {image}')
#iii. Send the image to discord
imgpath = (f'{image[0]}')
with open(imgpath, "rb") as fh:
f = discord.File(fh, filename=imgpath)
#iv. data hoarding is bad
await interaction.response.send_message(lang.chess.layout_user_rendered(interaction.user.name, layoutFEN), file=f)
try:
os.remove(imgpath)
except PermissionError:
mPrint('ERROR', f'Could not delete file {imgpath}\n{traceback.format_exc()}')
try: