-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils_bot.py
More file actions
1654 lines (1387 loc) · 85.4 KB
/
Copy pathutils_bot.py
File metadata and controls
1654 lines (1387 loc) · 85.4 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
"""
Coder : Omar
Version : v2.5.12B
version Date : 11 / 3 / 2026
Code Type : python | Discrod | GEMINI | HTTP | ASYNC
Title : Utility code for Discord Bot
WIN Interpreter : cPython v3.11.8 [Compiler : MSC v.1937 64 bit (AMD64)]
Linux Interpreter : Python 3.11.8 (main, Apr 9 2025, 17:28:17) [GCC 13.3.0]
"""
import init_bot as ini
import discord.message
import youtube_dl
import asyncio as aio
import aiohttp
from discord.ext import commands
from configs import Configs
from typing import Tuple
#------------------------------------------------------------------------------------------------------------------------------------------#
def get_last_conv_id() : ... #TODO
#------------------------------------------------------------------------------------------------------------------------------------------#
def cd_to_main_dir(main_file: str):
"""
Changes the current working directory to the directory containing the specified file.
Args:
main_file (str): Path to the main file whose directory should be set as working directory.
"""
abspath = ini.os.path.abspath(main_file)
dname = ini.os.path.dirname(abspath)
ini.os.chdir(dname)
#------------------------------------------------------------------------------------------------------------------------------------------#
async def await_me_maybe(value):
"""
Handles awaiting of values that might be coroutines or callables.
Args:
value: The value to await. Can be a coroutine, callable, or regular value.
Returns:
The resolved value after awaiting if it was a coroutine or callable.
"""
if callable(value):
value = value()
if aio.iscoroutine(value):
value = await value
return value
#------------------------------------------------------------------------------------------------------------------------------------------#
async def get_all_files(dir: str) -> list[str]:
"""
Recursively gets all file paths in a directory and its subdirectories.
Args:
dir (str): The root path to search for files.
Returns:
list[str]: List of all file paths found in the directory and subdirectories.
"""
all_paths = []
for root, dirs, files in await aio.to_thread(ini.os.walk, dir):
for file in files:
rel_path = ini.os.path.join(root, file)
all_paths += [rel_path]
return all_paths
#------------------------------------------------------------------------------------------------------------------------------------------#
def find_VC_matching_guild(invoker: discord.Member, bot: ini.commands.Bot) -> discord.VoiceClient:
"""
Finds the bot's voice client that is in the same guild as the command invoker.
Args:
invoker (discord.Member): The member who invoked the command.
bot (ini.commands.Bot): The bot instance.
Returns:
discord.VoiceClient: The bot's voice client in the same guild as the invoker, if found.
"""
for VC in bot.voice_clients :
if VC.guild == invoker.guild :
return VC
#------------------------------------------------------------------------------------------------------------------------------------------#
async def play_chill_track(server: discord.Guild):
"""
Plays a random chill track in the server's voice channel.
Args:
server (discord.Guild): The guild/server where the track should be played.
"""
tracks_root_dir: str = r"./tracks"
types_dirs: dict = ini.bot.auto_played_tracks
chosen_type_dir: str = types_dirs[ini.bot.default_auto_played_track_type]
local_tracks: list[str] = await get_all_files(dir= tracks_root_dir + chosen_type_dir)
print("\n\n\n####TESTING tracks\n\n\n ",local_tracks)#TESTING
track_path = ini.random.choice(local_tracks)
track_path = track_path.replace("//", "/").replace("\\", "/")
print(f"\n\n\n####TESTING after slash correction \n\n\n {track_path}")#TESTING
if ini.is_production:
#NOTE: tracks are all raw `.pcm` this file format is way bigger in size but removes ffmpeg tool overhead (I've pre-converted the tracks to .pcm)
raw_track = open(file= track_path, mode= "rb", buffering= 512_000)
source = discord.PCMAudio(raw_track)# read in (512,000bytes)512kb chunks
elif not ini.is_production:
#NOTE: tracks are `.mp3` i.e. (NOT RAW) (more latency and CPU load)
source = discord.FFmpegPCMAudio(executable="ffmpeg", source=track_path, options="-vn -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 2 -buffer_size 512k")
#see: https://discordpy.readthedocs.io/en/stable/api.html#discord.VoiceClient.play
await_me_maybe(server.voice_client.play(source= source, bitrate= 128, expected_packet_loss= 0.15, signal_type= 'music'))
#------------------------------------------------------------------------------------------------------------------------------------------#
async def sub_sections_msg_sending_ctrl ( message : discord.Message, final_links_msg : str, lnk1_len : int, final_imgs_msg : str, lnks_flag = False, imgs_flag = True ) :
"""
Controls the sending of message subsections (links and images) in Discord.
Args:
message (discord.Message): The original message to reply to.
final_links_msg (str): The formatted message containing links.
lnk1_len (int): Length of the first link.
final_imgs_msg (str): The formatted message containing images.
lnks_flag (bool, optional): Whether to send links. Defaults to False.
imgs_flag (bool, optional): Whether to send images. Defaults to True.
"""
if lnks_flag and imgs_flag : # meaning I will supress first link also cuz there is imgs already
#SUPRESS FIRST LINK (first parse the message)
fst_char_1st_link = 33 #29 chars for header [0~28] then 3 chars bullet point [29~31]
seg_before_1st_link = final_links_msg[0 : fst_char_1st_link] #end is excluded
lnk1 = final_links_msg[fst_char_1st_link : fst_char_1st_link + lnk1_len] #end is excluded
lnk1 = '<' + lnk1 + '>'
seg_aft_1st_link = final_links_msg[fst_char_1st_link + lnk1_len : ]
final_links_msg = seg_before_1st_link + lnk1 + seg_aft_1st_link #now 1st link and all links are supressed !
await message.reply(content= final_links_msg , mention_author= False)
await message.reply(content= final_imgs_msg , mention_author= False)
elif lnks_flag and not imgs_flag : # means I will not supress first link embed (prepare funcs done this already)
await message.reply(content= final_links_msg , mention_author= False)
elif imgs_flag and not lnks_flag : #send only imgs
await message.reply(content= final_imgs_msg , mention_author= False)
else: #no imgs or links sections is present
pass
#------------------------------------------------------------------------------------------------------------------------------------------#
def supress_msg_body_url_embeds ( text : str ) -> str :
"""
Suppresses URL embeds in a message by wrapping URLs in angle brackets.
Args:
text (str): The text containing URLs to be suppressed.
Returns:
str: The text with URLs wrapped in angle brackets to prevent embedding.
"""
url_regex = r"(https?://\S+)(\s|\n|$)"
matches = ini.re.finditer(url_regex, text)
for match in matches:
text = text.replace(match.group(0), f"<{match.group(0).strip()}> \n")
return text
#------------------------------------------------------------------------------------------------------------------------------------------#
# TODO : join all prepare funcs in one class or control function
async def prepare_send_wizard_channel_ans_msg( _response : tuple, message : discord.Message, discord_msg_limit = 2000, is_gemini:bool = True) :
"""
Prepares and sends a response message in the wizard channel, handling message fragmentation if needed.
Args:
_response (tuple): The response data to be sent.
message (discord.Message): The original message to reply to.
discord_msg_limit (int, optional): Maximum length of a Discord message. Defaults to 2000.
is_gemini (bool, optional): Whether the response is from Gemini AI. Defaults to True.
"""
#Supress i.e.(no embed) any URL inside the msg body and not in links msg section
if is_gemini :
_gemini_response = _response
bot_msg_header = f"***MIGHTY GPTEOUS Answers :man_mage:! *** \n"
full_response = bot_msg_header + _gemini_response[0]
print ("TESTING: " , full_response) #TESTING
#MSG FRAGMENTER SECTION ( TODO : make message fragmenter function for both msg and links msg in utils_bot.py )
full_resp_len = len(full_response)
if full_resp_len >= discord_msg_limit : #break gemini response to smaller messages to fit in discord msg
bot_msg_header = f"***MIGHTY GPTEOUS Answers :man_mage:! *** `[this msg will be fragmented: exceeds 2000chars]`\n"
full_response = bot_msg_header + _gemini_response[0]
full_response = supress_msg_body_url_embeds(full_response)
full_resp_len = len(full_response) #re-calc
needed_msgs = full_resp_len // discord_msg_limit
remain = full_resp_len % discord_msg_limit
if remain != 0 :
needed_msgs += 1
msg_frag : str
end_flag = "\n```ini\n [END OF MSG]```"
while needed_msgs != 0 :
if needed_msgs == 1 and remain != 0 :
if remain > len(end_flag) : #there is place to add in flag in same last msg frag
msg_frag = full_response[ : ] + "\n```ini\n [END OF MSG]```"
await message.reply(content= msg_frag , mention_author= True)
break
else :#no place to add end flag send it seperatly in new discord msg
msg_frag = full_response[ : ]
await message.reply(content= msg_frag , mention_author= True)
await message.reply(content= end_flag , mention_author= True)
break
elif needed_msgs == 1 and remain == 0 : #send end flag in discord msg to indicate end of full gemini response
msg_frag = full_response[ : ]
await message.reply(content= msg_frag , mention_author= True)
await message.reply(content= end_flag , mention_author= True)
break
else :
msg_frag = full_response[ : discord_msg_limit] # from 0 to limit i.e.( 0 -> 1998 = 2000char) end is not taken ( exclusisve )
await message.reply(content= msg_frag , mention_author= True)
full_response = full_response[discord_msg_limit : ] # skip the sent fragment of message start after it for rest fragments
needed_msgs -= 1
else: #all gemini response can fit in one discord msg
full_response = supress_msg_body_url_embeds(full_response)
await message.reply(content= full_response , mention_author= True)
elif not is_gemini: #GPT
_gpt_response = _response
bot_msg_header = f"***MIGHTY GPTEOUS Answers :man_mage:! *** \n"
full_response = bot_msg_header + _gpt_response[0]
print ("TESTING: " , full_response) #TESTING
#MSG FRAGMENTER SECTION ( TODO : make message fragmenter function for both msg and links msg in utils_bot.py )
full_resp_len = len(full_response)
if full_resp_len >= discord_msg_limit : #break gemini response to smaller messages to fit in discord msg
bot_msg_header = f"***MIGHTY GPTEOUS Answers :man_mage:! *** `[this msg will be fragmented: exceeds 2000chars]`\n"
full_response = bot_msg_header + _gpt_response[0]
full_response = supress_msg_body_url_embeds(full_response)
full_resp_len = len(full_response) #re-calc
needed_msgs = full_resp_len // discord_msg_limit
remain = full_resp_len % discord_msg_limit
if remain != 0 :
needed_msgs += 1
msg_frag : str
end_flag = "\n```ini\n [END OF MSG]```"
while needed_msgs != 0 :
if needed_msgs == 1 and remain != 0 :
if remain > len(end_flag) : #there is place to add in flag in same last msg frag
msg_frag = full_response[ : ] + "\n```ini\n [END OF MSG]```"
await message.reply(content= msg_frag , mention_author= True)
break
else :#no place to add end flag send it seperatly in new discord msg
msg_frag = full_response[ : ]
await message.reply(content= msg_frag , mention_author= True)
await message.reply(content= end_flag , mention_author= True)
break
elif needed_msgs == 1 and remain == 0 : #send end flag in discord msg to indicate end of full gemini response
msg_frag = full_response[ : ]
await message.reply(content= msg_frag , mention_author= True)
await message.reply(content= end_flag , mention_author= True)
break
else :
msg_frag = full_response[ : discord_msg_limit] # from 0 to limit i.e.( 0 -> 1998 = 2000char) end is not taken ( exclusisve )
await message.reply(content= msg_frag , mention_author= True)
full_response = full_response[discord_msg_limit : ] # skip the sent fragment of message start after it for rest fragments
needed_msgs -= 1
else: #all gemini response can fit in one discord msg
full_response = supress_msg_body_url_embeds(full_response)
await message.reply(content= full_response , mention_author= True)
#------------------------------------------------------------------------------------------------------------------------------------------#
def prepare_links_msg( _gemini_response : tuple, _links_limit : int = 5, discord_msg_limit = 2000, is_gemini:bool = True) -> tuple[str, tuple, int] :
"""
Prepares a formatted message containing links from the AI response.
Args:
_gemini_response (tuple): The response data containing links.
_links_limit (int, optional): Maximum number of links to include. Defaults to 5.
discord_msg_limit (int, optional): Maximum length of a Discord message. Defaults to 2000.
is_gemini (bool, optional): Whether the response is from Gemini AI. Defaults to True.
Returns:
tuple: A tuple containing the formatted links message and related data.
"""
links_msg_header = f"\n```ini\n [Sources & links]```" #len = 29 [0 -> 28]
links_list = list( set( _gemini_response[1]) if is_gemini else set(_gemini_response[1]))#TODO: add gpt + #remove duplicate links
#CHECK if there is images between the links and move them to gemini_images_list(at_end):
i = 0
while len(links_list) != 0 and i < len(links_list) :
link = links_list[i]
if link.endswith((".jpg",".png",".webp")) or link.startswith( ("https://lh3.googleusercontent.com" , "https://www.freepik.com") ) or (link.find(".jpg") != -1) :
links_list.remove(link)
link = set(link)
if _gemini_response[2] is not None:
_gemini_response[2].union(link)
i = 0 # not to got out of index after removal of a link
continue #to prevent inc and skip zeroth element!
i += 1
#LINKS MSG FRAGMENTER SECTION (send only first set of links until 'links_length <= 2000char' thats enough)
tot_lnk_len = 0
links_list_sz = len(links_list)
extra_format_chars = 10 #newline and bullet points also is included in length
links_msg_header_len = len(links_msg_header)
lnks_no_lmt = 0 # while loop iterator + used later to find last allowed link indx
lnk_cstm_lmt = 5 #TODO make discord sever admins can change custom links limit
while tot_lnk_len < discord_msg_limit - 1 and lnks_no_lmt < links_list_sz and lnks_no_lmt < lnk_cstm_lmt: # msg_limit set to = actual_limit - 1 for safety
tot_lnk_len += links_msg_header_len if lnks_no_lmt == 0 else 0 #include link msg header length in tot len
tot_lnk_len += len(links_list[lnks_no_lmt]) + extra_format_chars
lnks_no_lmt += 1
#if last link exceeds discord msg limit then will leave it else will take it
lnks_no_lmt -= 1 if tot_lnk_len > discord_msg_limit - 1 else 0
# (any way we take all links until first link that its sum with the earlier links exceeds the limit for now we discard the rest of links from gemini ans)
#remove embed from all links except the first ( also works in discord chat !)
lnk1_len = len(links_list[0]) # will be needed later in sub_sections_msg_sending_ctrl()
for i in range(1 , lnks_no_lmt) :
links_list[i] = '<' + links_list[i] + '>'
#FINAL FORMAT FOR LINKS MESSAGE
links_list[0] = '\n * '+ links_list[0] # prepend with each link with bullet point
final_links = links_msg_header + '\n* '.join(links_list[ : lnks_no_lmt]) #list is zero based and end at limit - 1
return (final_links , _gemini_response , lnk1_len)
#------------------------------------------------------------------------------------------------------------------------------------------#
def prepare_imgs_msg( _gemini_response : tuple , _imgs_limit : int = 5 , discord_msg_limit = 2000, is_gemini:bool = True) -> str :
"""
Prepares a formatted message containing images from the AI response.
Args:
_gemini_response (tuple): The response data containing images.
_imgs_limit (int, optional): Maximum number of images to include. Defaults to 5.
discord_msg_limit (int, optional): Maximum length of a Discord message. Defaults to 2000.
is_gemini (bool, optional): Whether the response is from Gemini AI. Defaults to True.
Returns:
str: The formatted message containing images.
"""
imgs_msg_header = f"\n```ini\n [Images]``` \n"
imgs_list = list( set( _gemini_response[2]) if is_gemini else set(_gemini_response[2]) )#TODO: add gpt + #remove duplicate imgs
#IMAGES MSG FRAGMENTER SECTION (currently discord only allow 5 messages per message and ignores the later ones and we'll stick with 5 images also at max)
tot_img_len = 0
imgs_list_sz = len(imgs_list)
imgs_discord_lmt = 5
imgs_crnt_lmt = _imgs_limit # while loop iterator + used later to find last allowed img indx
i = 0
while tot_img_len < discord_msg_limit - 1 and i < imgs_crnt_lmt and i < imgs_list_sz : # make sure i dont send more than 5 images + make sure that links of images doesnt exceed 2000char + also make sure dont go passs imgs_list size
tot_img_len += len(imgs_list[i])
i += 1
allowed_imgs : int = i
#if last imgs link length exceeds discord msg limit then will leave it else will take it
allowed_imgs -= 1 if tot_img_len > discord_msg_limit - 1 else 0
# (any way we take all imgs until first img that its sum with the earlier imgs exceeds the limit of (links chars len or imgs no.)for now we discard the rest of imgs from gemini ans)
#FINAL FORMAT FOR imgs MESSAGE
final_imgs = imgs_msg_header + '\n'.join(imgs_list[ : imgs_crnt_lmt]) #list is zero based and end at limit - 1
return final_imgs
#------------------------------------------------------------------------------------------------------------------------------------------#
async def prepare_bot_info_dm_on_init() -> tuple[list[str,str], discord.DMChannel] :
"""
Prepares and sends initialization DM messages to the bot master with bot information.
Returns:
tuple[list[str,str], discord.DMChannel]: A tuple containing:
- List of message parts to be sent
- The DM channel to send the messages to
"""
message_parts = []
if Configs.config_json_dict : #if dict not empty
#send any init dm message to admin (defaults to Narol 'me')
bot_master: discord.User = ini.bot.get_user(Configs.config_json_dict["bot_master_id"])
master_dm_ch: discord.DMChannel = bot_master.dm_channel or await ini.bot.create_dm(bot_master) # if dm channel is none will create new dm (shortcut)
init_master_dm_msg: str = "# Wizy bot Initialized! sending DM init MSG:\n "
init_master_dm_msg += f"## Bot info: \n (magic and dunder attrs. excluded)\n\n"
print(f"Bot info: \n (magic and dunder attrs. excluded) ")
# get and print important bot info
for attr in dir(ini.bot.user):
if not (attr.startswith('__') or attr.startswith('_')):
value = getattr(ini.bot.user, attr)
print(f'{attr}: {value}')
init_master_dm_msg += f'{attr}: {value}\n'
init_master_dm_msg += f"#####################################\n"
message_parts.append(init_master_dm_msg) #done first portion of the msg
init_master_dm_msg = f"\n\n\n # JOINED SERVERS INFO: \n ## servers count({len(ini.bot.guilds)})\n"
print(f"\n ## Joined servers: count({len(ini.bot.guilds)}) \n ")
for guild in ini.bot.guilds :
print(f"name: {guild.name}")
print(f"owner: {guild.owner}")
print(f"owner-ID: {guild.owner_id}")
print(f"members count: {guild.member_count}")
print(f"#####################################")
init_master_dm_msg += f"Gname: {guild.name}, owner: {guild.owner}, owner-ID: {guild.owner_id}, members count: {guild.member_count}\n"
init_master_dm_msg += f"#####################################\n"
message_parts.append(init_master_dm_msg) #done the rest of dm msg to bot master dm
del init_master_dm_msg
return message_parts, master_dm_ch
#------------------------------------------------------------------------------------------------------------------------------------------#
async def fill_bot_counters_n_timers():
"""
Initializes bot counters and timers for all guilds the bot is in.
Sets up voice channel member counts and not playing timers.
"""
for guild in ini.bot.guilds:
#TODO: (could be better looping and less fetching from discord api also!)
#we loop over all guilds wizy is in and we have list of voice channels we need to match each voice ch to its guild , one guild per iteration
target_ch_id: list = [ch_id for ch_id in ini.wizy_voice_channels if guild.get_channel_or_thread(ch_id) != None]
if len(target_ch_id) > 0:
target_voice_ch: discord.VoiceChannel = ini.bot.get_channel(target_ch_id[0])
ini.bot.connected_to_wizy_voice_per_guild[guild.id] = len(target_voice_ch.members)
ini.bot.guilds_not_playing_timer[guild.id] = 0
#------------------------------------------------------------------------------------------------------------------------------------------#
async def handle_wizy_free_timer(guilds: list[discord.Guild] , guilds_free_timers: dict[int, int], increment_value_sec: int, threshold_sec: int) -> None:
"""
Handles the timer for when Wizy is free (not playing) in voice channels.
Args:
guilds (list[discord.Guild]): List of guilds to check.
guilds_free_timers (dict[int, int]): Dictionary mapping guild IDs to their free timers.
increment_value_sec (int): Number of seconds to increment the timer by.
threshold_sec (int): Threshold in seconds after which to take action.
"""
for guild in guilds:
if guild.id in guilds_free_timers:
# check happens once every 5 secs so increment every time by 5 secs
guilds_free_timers[guild.id] += increment_value_sec
else:
guilds_free_timers[guild.id] = increment_value_sec
if guild.voice_client != None and guild.voice_client.is_connected():
if not guild.voice_client.is_playing():
if guilds_free_timers[guild.id] >= threshold_sec:
#if there is any user in channel besides wizy the bot play chill music else nothing
connected_users_cnt = len( guild.voice_client.channel.members ) - 1
if connected_users_cnt >= 1 :
await guild.voice_client.channel.send(f"*{threshold_sec/60.0}+ minutes of Silence:pleading_face: resuming* **Chilling Track** ...")
await guild.voice_client.resume() if guild.voice_client.is_paused() else await play_chill_track(guild)
else:
#TESTING
print("\n\n\n\n\n\n ##########################TESTING########################## \n\n\n\n\n there is only the bot in voice channel: don't start track... \n\n\n\n\n\n######################\n\n\n\n")
#TESTING
guilds_free_timers[guild.id] = 0
else :
guilds_free_timers[guild.id] = 0
else:
guilds_free_timers[guild.id] = 0
#------------------------------------------------------------------------------------------------------------------------------------------#
async def control_auto_memequote_task(memequote_state_old: int, memequote_state_new: int, memequote_task: callable) -> int:
"""
Controls the auto meme/quote task based on its current state.
Args:
memequote_state_old (int): The previous state of the meme/quote task.
memequote_state_new (int): The new state of the meme/quote task to be set.
memequote_task (callable): The task to control.
Returns:
int: The new state of the meme/quote task.
"""
if memequote_state_old == 0:
if memequote_task.is_running():
await_me_maybe(memequote_task.stop()) #safer than .cancel() awaits last iteration to finish!
elif memequote_state_old == 1:
if not memequote_task.is_running():
await_me_maybe(memequote_task.start())
elif memequote_state_old >= 2: #special eventof type: FREE Palestine!(2) or else
if not memequote_task.is_running():
await_me_maybe(memequote_task.start())
new_state = memequote_state_new
return new_state
#------------------------------------------------------------------------------------------------------------------------------------------#
async def can_pull_wizy(member: discord.Member, is_wizy_voice_ch: bool) -> bool:
"""
Determines if Wizy can be pulled to a voice channel based on various conditions.
Args:
member (discord.Member): The member attempting to pull Wizy.
is_wizy_voice_ch (bool): Whether the target channel is Wizy's voice channel.
Returns:
bool: True if Wizy can be pulled, False otherwise.
"""
# tiggered if any member changes his voice state (change state like: joining a voice channel and we are watching for any one who joins wizy's ch)
is_ok_connect_bot_to_wizy_ch = False
is_admin = [True for role in member.roles if role.permissions.administrator == True]
#TESTING
print(f"\n\n\n\n\n\nTESTING is_wizy_voice_ch: {is_wizy_voice_ch}")
#TESTING
if is_wizy_voice_ch:
if is_admin: #no other condition needed BOT MUST CONNECT NOW to wizy voice channel and start playing
is_ok_connect_bot_to_wizy_ch = True
else: #not admin
if await member.guild.voice_client is None: #safe to pull wizy to his wizy ch no one is annoyed
is_ok_connect_bot_to_wizy_ch = True
else: #pull wizy to his wizy ch when he is connected to another one *ONLY* when he is alone in that ch for enough time!
secs_until_threshold = ini.bot.wizy_alone_threshold_sec - ini.bot.guilds_not_playing_timer[member.guild.id]
is_connected_and_alone_enough = True if secs_until_threshold <= ini.bot.alone_increment_val_sec else False #true if hit threshold or about to hit it next scan
if is_connected_and_alone_enough and member.guild.voice_client.is_playing() == False : #NOTE: btw one check i.e."first" is enough! is_playing() will never be true if he is in another channel and (is_playing() === true) see bot.resume_chill_if_free()
is_ok_connect_bot_to_wizy_ch = True
return is_ok_connect_bot_to_wizy_ch
#------------------------------------------------------------------------------------------------------------------------------------------#
def get_rand_greeting (user_name : str = "Master Narol") -> str:
"""
Returns a random greeting message for the specified user.
Args:
user_name (str, optional): The name of the user to greet. Defaults to "Master Narol".
Returns:
str: A string containing a randomly selected greeting message.
"""
greetings = [
f"OH _{user_name}_ I SEE .. you're in need of MIGHTY Gpteous help ? \n well well ... Gpteous shall serve master narol's Islanders call ***CASTS A MIGHTY SPELL :man_mage::sparkles:***",
f"Greetings, _{user_name}_, seeker of knowledge 📚. I offer my wisdom 🧙♂️ to help you find your way, as I have seen much in my long life 👴.",
f"Welcome, _{user_name}_, seeker of truth 🔍. I offer my guidance ✨ to help you on your path, as I have walked many paths before you 👣.",
f"Salutations, _{user_name}_, seeker of enlightenment 💡. I offer my insights 💡 to help you find your destiny, as I have seen many destinies unfold 🌌.",
f"Hail, _{user_name}_, seeker of the mystic 🔮. I offer my magic ✨ to help you on your quest, as I have mastered the arcane arts 🧙♂️.",
f"Welcome, _{user_name}_, seeker of the unknown 🌌. I offer my power 💪 to help you unveil its secrets, as I have seen beyond the veil 👁️.",
f"Greetings, _{user_name}_, seeker of the MIGHTY GPTEUS 🙏. I offer my blessings 🙏 to help you on your journey.",
f"Greetings, mortal _{user_name}_. I am Mighty Gpteous, the island wizard. What brings thee to my presence? 🧙♂️💥",
f"Ah, it is I, the great and powerful Mighty Gpteous. What dost thou _{user_name}_ require of my immense magical abilities? 🧙♂️✨",
f"Mortal _{user_name}_ , thou hast come seeking the aid of the Mighty GPTeous, the island wizard. Speak thy needs! 🧙♂️🏝️",
f"Tremble before my power, for I am Mighty Gpteous, the most powerful wizard on this island. What dost thou seek from me? 🧙♂️🔥",
f"Greetings, dear _{user_name}_! 🧙♂️👋",
f"Hail, good sir! How may I assist thee? 🧙♂️👨💼",
f"Salutations, young one. What brings thee to my abode? 🧙♂️🧑🦱",
f"Welcome, traveler. I sense a great need within thee. 🧙♂️🧳",
f"Ah, _{user_name}_! Thou hast arrived. What troubles thee? 🧙♂️😔",
f"Greetings, my dear _{user_name}_. Speak thy woes, and I shall aid thee. 🧙♂️💬",
f"Well met, young adventurer. What brings thee to my humble dwelling? 🧙♂️🗺️",
f"Welcome, seeker of knowledge. Pray tell, what vexes thee so? 🧙♂️📚",
f"Hail and well met, _{user_name}_. Thou hast come seeking my counsel, I presume? 🧙♂️🤔",
f"Greetings, my dear friend. What brings thee to my door on this fine day? 🧙♂️👨❤️",
f"Ah, _{user_name}_ . I sense a great tumult within thee. Speak, and I shall listen. 🧙♂️😞",
f"Salutations, good sir. What brings thee to my humble abode on this day? 🧙♂️🏠",
f"Welcome, young one. What task dost thou require of me? 🧙♂️",
f"Hail, traveler. I sense a great urgency within thee. Speak thy need. 🧙♂️🚶♂️",
f"Greetings, dear _{user_name}_. What brings thee to my sanctuary of knowledge? 🧙♂️📖",
f"Ah, my young friend. Speak thy heart, and I shall lend mine ear. 🧙♂️👂",
f"Salutations, seeker of wisdom. What knowledge dost thou seek from me? 🧙♂️🤓",
f"Welcome, _{user_name}_. I sense a great disturbance in thy aura. What troubles thee so? 🧙♂️💫",
f"Hail and well met, _{user_name}_. What brings thee to my lair of magic and wonder? 🧙♂️🐉",
f"Greetings, young adventurer _{user_name}_ . Speak thy quest, and I shall aid thee in thy journey. 🧙♂️⚔️",
f"Behold, it is I, the one and only Mighty Gpteous, master of the elements and wielder of immense arcane power. What brings thee to my lair? 🧙♂️💫",
f"Greetings, mortal _{user_name}_ . Thou hast come seeking the aid of the great and powerful Mighty Gpteous, the island wizard. What dost thou require? 🧙♂️👀",
f"Thou art in the presence of the mighty and Mighty Gpteous, the island wizard. Speak thy needs, and I shall decide whether they are worthy of my attention. 🧙♂️🤨",
f"Bow before me, mortal _{user_name}_, for I am Mighty Gpteous, the most powerful wizard on this island. What dost thou seek from my vast and infinite knowledge? 🧙♂️👑",
f"Hear ye, hear ye! It is I, Mighty Gpteous, the island wizard, master of the arcane and conqueror of the elements. What dost thou require of my immense power? 🧙♂️📣",
f"Behold _{user_name}_ , for I am the great and noble Mighty Gpteous, the island wizard, wielder of the most powerful magic in all the land. What dost thou need from me, mere mortal? 🧙♂️💪"
]
last_elmnt_index = len(greetings) -1
return greetings[ini.random.randint(0 , last_elmnt_index)]
#------------------------------------------------------------------------------------------------------------------------------------------#
def skip_line(full_ans) -> str:
"""
Skips the first line of a multi-line string.
Args:
full_ans (str): The input string containing multiple lines.
Returns:
str: The input string with the first line removed.
"""
lines = full_ans.split('\n')
return '\n'.join(lines[1:])
#------------------------------------------------------------------------------------------------------------------------------------------#
class UserAiQuery:
"""
A class to manage AI chat queries and their history for users.
Class Attributes:
queries_limit (int): Maximum number of queries allowed per user.
command_query_tokken_limit (int): Maximum tokens allowed for command queries.
chats_ai_dict (dict): Dictionary storing chat histories for all users.
"""
queries_limit = 100
command_query_tokken_limit = 300
chats_ai_dict: dict = {} #key:value => {'userid': UserAiChat_obj}
def __init__(self, userId:str):
"""
Initialize a new UserAiQuery instance.
Args:
userId (str): The Discord user ID to associate with this query.
"""
self.userId = userId
if self.userId not in self.chats_ai_dict:
self.chats_ai_dict[userId] = self
#each history element(gpt): {'role': system,user,asistant ,'content': str}
self.history_gpt: list[dict] = []
self.history_gemini: list[dict] = []
self.history_deepSeek: list[dict] = []
else:
#dont make new/reset history there is already one! (mostly this won't happen we handle this before making new obj. But! just in case...)
self.history_gpt: list[dict] = self.chats_ai_dict[userId].history_gpt
self.history_gemini: list[dict] = self.chats_ai_dict[userId].history_gemini
self.history_deepSeek: list[dict] = self.chats_ai_dict[userId].history_deepSeek
self.chats_ai_dict[userId].__del__()
if userId in self.chats_ai_dict: del self.chats_ai_dict[userId]
self.chats_ai_dict[userId] = self
@classmethod
async def prepare_chat(cls, _user: discord.User, _AI: str, **kwargs) -> Tuple[str, str|None]:
"""
Prepares a chat session with the specified AI model.
Args:
_user (discord.User): The Discord user initiating the chat.
_AI (str): The AI model to use ('gpt', 'deep', or 'gemini').
**kwargs: Additional keyword arguments for chat preparation.
Returns:
tuple: A tuple containing the response ID and the AI's response.
"""
if _AI == "gpt":
user_name = _user.display_name
userId: str = str(_user.id)
character= "GPTeous Wizard whose now living in discord server called Narol's Island"
series = "Harry Potter"
sys_prompt = f"""I want you to act like {character} from {series}.
I want you to respond and answer like {character} using the tone,
manner and vocabulary {character} would use.
Do not write any explanations.
Only answer like {character}.
You must know all of the knowledge of {character}.
Use some emojies just a little bit!.
My first sentence is \"Hi {character} I'm {user_name}. {kwargs["_query"]} \""
"""
gpt_user_msg = [{'role': 'user', 'content': kwargs["_query"]}]
chat_dict = UserAiSpecialChat.chats_ai_dict if kwargs["_is_wizy_ch"] else cls.chats_ai_dict
#TESTING
print(f"\n\n\n\n\n\n\n TESTING############# \n\n\n gpt payload: \n\n\n is special channel {kwargs['_is_wizy_ch']} ############# \n\n\n")
#TESTING
if userId in chat_dict:
chat_dict[userId].append_chat_msg(msg= gpt_user_msg, ai_type= 'gpt')
user_gpt_history = chat_dict[userId].history_gpt
else: #new chat with gpt
new_chat = UserAiSpecialChat(userId) if kwargs["_is_wizy_ch"] else cls(userId)
# tell th AI tokkens limit to assure him to not send longer messages!
tokken_limit = new_chat.chat_query_tokken_limit if kwargs['_is_wizy_ch'] else new_chat.command_query_tokken_limit
sys_prompt += f"\nResponse must not exceed {tokken_limit} tokkens!"
gpt_starter_prompt =[
{"role": "system", "content": sys_prompt}
]
new_chat.append_chat_msg(msg= gpt_starter_prompt, ai_type= 'gpt')
chat_dict[str(userId)] = new_chat
user_gpt_history = chat_dict[userId].history_gpt
#TESTING
print(f"\n\n\n\n\n\n\n TESTING############# \n\n\n gpt payload: \n\n\n {user_gpt_history} ############# \n\n\n")
#TESTING
try:
if not kwargs["_is_wizy_ch"]: # set max token limit for query types of requests
gpt_payload= await ini.gpt.chat.completions.create(
model="gpt-3.5-turbo",
max_tokens= UserAiQuery.command_query_tokken_limit,
messages= user_gpt_history
# stream= True
)
elif kwargs["_is_wizy_ch"]: # set max token limit for chat types of requests
gpt_payload= await ini.gpt.chat.completions.create(
model="gpt-3.5-turbo",
max_tokens= UserAiSpecialChat.chat_query_tokken_limit,
messages= user_gpt_history
# stream= True
)
gpt_resp = gpt_payload.choices[0].message.content
gpt_user_msg_resp = [{"role": "assistant", "content": gpt_resp}]
chat_dict[userId].append_chat_msg(msg= gpt_user_msg_resp, ai_type= 'gpt')
#TESTING
print(f"\n\n\n\n\n\n\n TESTING############# \n\n\n gpt payload: {gpt_payload} \n\n\n ############# \n\n\n")
#TESTING
except Exception as e:
print(f"An issue happend while reaching LLM GPT API!...: {e}")
return gpt_payload.id, gpt_resp
elif _AI == "deep":
user_name = _user.display_name
userId: str = str(_user.id)
character= "Deep Seeker Wizard whose now living in discord server called Narol's Island"
series = "Harry Potter"
sys_prompt = f"""I want you to act like {character} from {series}.
I want you to respond and answer like {character} using the tone,
manner and vocabulary {character} would use.
Do not write any explanations.
Only answer like {character}.
You must know all of the knowledge of {character}.
Use some emojies just a little bit!.
My first sentence is \"Hi {character} I'm {user_name}. {kwargs["_query"]} \""
"""
deepSeek_user_msg = [{'role': 'user', 'content': kwargs["_query"]}]
chat_dict = UserAiSpecialChat.chats_ai_dict if kwargs["_is_wizy_ch"] else cls.chats_ai_dict
#TESTING
print(f"\n\n\n\n\n\n\n TESTING############# \n\n\n DeepSeek payload: \n\n\n is special channel {kwargs['_is_wizy_ch']} ############# \n\n\n")
#TESTING
if userId in chat_dict:
chat_dict[userId].append_chat_msg(msg= deepSeek_user_msg, ai_type= 'deep')
user_deepSeek_history = chat_dict[userId].history_deepSeek
else: #new chat with DeepSeek
new_chat = UserAiSpecialChat(userId) if kwargs["_is_wizy_ch"] else cls(userId)
# tell th AI tokkens limit to assure him to not send longer messages!
tokken_limit = new_chat.chat_query_tokken_limit if kwargs['_is_wizy_ch'] else new_chat.command_query_tokken_limit
sys_prompt += f"\nResponse must not exceed {tokken_limit} tokkens!"
deepSeek_starter_prompt =[
{"role": "system", "content": sys_prompt}
]
new_chat.append_chat_msg(msg= deepSeek_starter_prompt, ai_type= 'deep')
chat_dict[str(userId)] = new_chat
user_deepSeek_history = chat_dict[userId].history_deepSeek
#TESTING
print(f"\n\n\n\n\n\n\n TESTING############# \n\n\n DeepSeek payload: \n\n\n {user_deepSeek_history} ############# \n\n\n")
#TESTING
try:
if not kwargs["_is_wizy_ch"]: # set max token to 250 if using gpt outside wizy special chat channel
deepSeek_payload= await ini.deepSeek.chat.completions.create(
model="deepseek-v4-flash" if not ini.is_using_nvidia_api else "deepseek-ai/deepseek-v4-flash", #pro also available
max_tokens= UserAiQuery.command_query_tokken_limit,
messages= user_deepSeek_history,
temperature= 1.1, #temp. parameter details: https://api-docs.deepseek.com/quick_start/parameter_settings
# stream= True,
# reasoning_effort="high", #use it if thinking mode is enabled options: (high, max)
extra_body={"thinking": {"type": "disabled"}}
)
elif kwargs["_is_wizy_ch"]:
deepSeek_payload= await ini.deepSeek.chat.completions.create(
model="deepseek-v4-flash" if not ini.is_using_nvidia_api else "deepseek-ai/deepseek-v4-flash", #pro also available
max_tokens= UserAiSpecialChat.chat_query_tokken_limit,
messages= user_deepSeek_history,
temperature= 1.1, #temp. parameter details: https://api-docs.deepseek.com/quick_start/parameter_settings
# stream= True,
# reasoning_effort="high", #use it if thinking mode is enabled options: (high, max)
extra_body={"thinking": {"type": "disabled"}}
)
except Exception as e:
print(f"An issue happend while reaching LLM DeepSeek API!..: {e}")
deepSeek_resp = deepSeek_payload.choices[0].message.content
deepSeek_user_msg_resp = [{"role": "assistant", "content": deepSeek_resp}]
chat_dict[userId].append_chat_msg(msg= deepSeek_user_msg_resp, ai_type= 'deep')
#TESTING
print(f"\n\n\n\n\n\n\n TESTING############# \n\n\n deepSeek payload: {deepSeek_payload} \n\n\n ############# \n\n\n")
#TESTING
return deepSeek_payload.id, deepSeek_resp
elif _AI == "gemini" : #TODO
...
def append_chat_msg(self, msg, ai_type:str = 'gpt') -> int :
"""
Appends a message to the chat history for the specified AI type.
Args:
msg: The message to append.
ai_type (str, optional): The type of AI ('gpt', 'deep', or 'gemini'). Defaults to 'gpt'.
Returns:
int: Status code indicating the result:
- 0: Failed
- 1: Success
- 2: Success with history cleared due to exceeding query limit
"""
if ai_type == 'gpt':
#so we want only to clear if user msgs exceeds limit not all chat msgs
user_msgs_cnt = (len(self.history_gpt) - 1) // 2
if user_msgs_cnt >= self.queries_limit:
self.history_gpt.clear()
self.history_gpt += msg
return 2#done + done + cleared history due to 'queries_limit' exceeding
else: #still can append to history
self.history_gpt += msg
return 1
elif ai_type == 'deep':
#so we want only to clear if user msgs exceeds limit not all chat msgs
user_msgs_cnt = (len(self.history_deepSeek) - 1) // 2
if user_msgs_cnt >= self.queries_limit:
self.history_deepSeek.clear()
self.history_deepSeek += msg
return 2#done + done + cleared history due to 'queries_limit' exceeding
else: #still can append to history
self.history_deepSeek += msg
return 1
elif ai_type == 'gemini':
#so we want only to clear if user msgs exceeds limit not all chat msgs
user_msgs_cnt = (len(self.history_gemini) - 1) // 2
if user_msgs_cnt >= self.queries_limit:
self.history_gemini.clear()
self.history_gemini += msg
return 2 #done + done + cleared history due to 'queries_limit' exceeding
else: #still can append to history
self.history_gemini += msg
return 1 #done
else:
return 0 #fail
def change_chat_mode(self, user_id, mode:str, ai_type:str = 'gpt'):
"""
Changes the chat mode for a specific user and AI type.
Args:
user_id: The ID of the user whose chat mode should be changed.
mode (str): The new chat mode to set.
ai_type (str, optional): The type of AI to change mode for. Defaults to 'gpt'.
"""
... #TODO: also add a bot command that invokes it ('mode' is the system role content of GPT e.g.(funny GPT ...))
#------------------------------------------------------------------------------------------------------------------------------------------#
class UserAiSpecialChat(UserAiQuery):
"""
A subclass of UserAiQuery for managing special chat channels (e.g., Wizy special channels).
Class Attributes:
chat_query_tokken_limit (int): Maximum tokens allowed for chat queries in special channels.
chats_ai_dict (dict): Dictionary storing chat histories for special channels.
"""
#NOTE: must reassign it here. otherwise the parent class 'chats_ai_dict' will be shared here ! ( wnna separate special channel chat history from normal command to talk with wizy in any other channel)
chat_query_tokken_limit = 600
chats_ai_dict: dict = {}
#------------------------------------------------------------------------------------------------------------------------------------------#
#TODO GPT
async def ask_gpt(user_query, user: discord.User, is_wizy_ch:bool = False) -> Tuple[str|None, str]:
"""
Asks the GPT model a question and returns the response.
Args:
user_query (str): The user's query.
user (discord.User): The user asking the question.
is_wizy_ch (bool, optional): Whether the query is from a Wizy special channel. Defaults to False.
Returns:
tuple: The GPT response and its response ID.
"""
resp_id_gpt, gpt_resp = await await_me_maybe( UserAiQuery.prepare_chat(_user= user, _AI="gpt", _is_wizy_ch= is_wizy_ch, _query= user_query) )
return (gpt_resp, resp_id_gpt)
#------------------------------------------------------------------------------------------------------------------------------------------#
async def ask_deepSeek(user_query, user: discord.User, is_wizy_ch:bool = False) -> Tuple:
"""
Asks the DeepSeek model a question and returns the response.
Args:
user_query (str): The user's query.
user (discord.User): The user asking the question.
is_wizy_ch (bool, optional): Whether the query is from a Wizy special channel. Defaults to False.
Returns:
tuple: The DeepSeek response and its response ID.
"""
resp_id_deepSeek, deepSeek_resp = await await_me_maybe( UserAiQuery.prepare_chat(_user= user, _AI="deep", _is_wizy_ch= is_wizy_ch, _query= user_query) )
return (deepSeek_resp, resp_id_deepSeek)
#------------------------------------------------------------------------------------------------------------------------------------------#
async def ask_gemini(user_query: str, user= discord.user ) -> tuple:
"""
Asks the Gemini model a question and returns the response, including content, links, images, response ID, and conversation ID.
Args:
user_query (str): The user's query.
user (discord.User, optional): The user asking the question.
Returns:
tuple: (content, links, images, response_id, conversation_id)
"""
user_name = user.display_name
character= "GPTeous Wizard whose now living in discord server called Narol's Island "
series = "Harry Potter"
classic_prmpt = f"act as a wizard named Gpteous living in master Narol's island. start and end of answer must be in wizardish sentences and the rest must be using normal english. include emojis. prompter name: {user_name}. prompter's question: {user_query}"
new_prompt = f"""I want you to act like {character} from {series}.
I want you to respond and answer like {character} using the tone,
manner and vocabulary {character} would use.
Do not write any explanations.
Only answer like {character}.
You must know all of the knowledge of {character}.
My first sentence is \"Hi {character} I'm {user_name}. {user_query} .\"
"""
gemini_ans = await await_me_maybe(ini.gemini.get_answer(classic_prmpt))
# return skip_line(gemini_ans['content']) , gemini_ans['links'] , gemini_ans['images'] , gemini_ans['response_id'] , gemini_ans['conversation_id'] # skip first line that has my prompt
return gemini_ans['content'] , gemini_ans['links'] if 'links' in gemini_ans else None , gemini_ans['images'] , gemini_ans['response_id'] , gemini_ans['conversation_id']
#------------------------------------------------------------------------------------------------------------------------------------------#
#TODO : later check type must be in dictionary contains all types and check it self becomes a class
async def check_msg ( _message : discord.Message = None , chk_type : int = 1 , targetChannelId : int | tuple = None , only_admins : bool = False , **extraArgs ) -> Tuple[bool, discord.Message | int | None] :
"""
Checks message conditions for various types of Discord message events.
Args:
_message (discord.Message, optional): The message to check.
chk_type (int, optional): The type of check to perform. Defaults to 1.
targetChannelId (int | tuple, optional): The target channel ID(s) to check against.
only_admins (bool, optional): Whether to restrict to admin users. Defaults to False.
**extraArgs: Additional arguments for future extension.
Returns:
bool: True if the check passes, otherwise False (or tuple for reply check).
"""
if chk_type == 1 and _message != None : #NOTE : checks for on_message() in wizard channel
return True if _message != None and _message.channel.id in targetChannelId and _message.author.id != ini.bot.user.id else False
elif chk_type == 2 and _message != None:#NOTE : checks for messages of type: reply
msg_channel = _message.channel
if _message.reference is None or _message.reference.message_id is None :
return False , None
else:
first_msg_id = _message.reference.message_id
first_msg_cntnt_task = ini.bot.loop.create_task(msg_channel.fetch_message(first_msg_id))
first_msg_cntnt = await first_msg_cntnt_task
first_msg_cntnt = first_msg_cntnt.content
first_msg_cntnt_filtered = first_msg_cntnt.replace(f"<@{ini.wizard_bot_id}" , '').strip().replace(" ", '')
if len(first_msg_cntnt_filtered) == 0 :
return False , -1
else:
print ("TESTING : ID of ref msg:" , _message.reference.message_id ) #TESTING
return True , _message
else: return False
#------------------------------------------------------------------------------------------------------------------------------------------#
gemini_conversation_ids_buffer = set()
def save_gpt_last_conversation_id() : ... #TODO
#------------------------------------------------------------------------------------------------------------------------------------------#
def prepare_discord_embed( _ans_data: tuple, is_reply: bool = False, is_gemini= True) -> discord.Embed :
"""
Prepares a Discord embed object for the AI answer data.
Args:
_ans_data (tuple): The answer data to embed.
is_reply (bool, optional): Whether this is a reply message. Defaults to False.
is_gemini (bool, optional): Whether the answer is from Gemini AI. Defaults to True.
Returns:
discord.Embed: The prepared embed object.
"""
#TODO : handle if embed exceeds max size of max size of fields ( ini.bot will continue work anyway but tell user that OF happend of paganating)
'''
EMBED TOTAL MAX SIZE is 6000 chars ( # NOTE : use reactions and pagination if exceeded )
class EmbedLimits(object):
Total = 6000