-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcommand_handler.py
1018 lines (894 loc) · 42.5 KB
/
command_handler.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
# command_handler.py
from typing import Any, Dict, Callable, Optional, List, Union
from functools import wraps
from datetime import datetime, timedelta
from twitchio.ext import commands
from twitchio.message import Message
import logging
from .config import Config
from .aviation_weather_integration import AviationWeatherIntegration
import json
from pathlib import Path
from dataclasses import dataclass
import re
@dataclass
class CommandUsage:
last_used: datetime = None
use_count: int = 0
cooldown: int = 0
class CommandPermission:
def __init__(self,
mod_only: bool = False,
broadcaster_only: bool = False,
vip_only: bool = False,
subscriber_only: bool = False,
allowed_users: Optional[List[str]] = None,
denied_users: Optional[List[str]] = None):
self.mod_only = mod_only
self.broadcaster_only = broadcaster_only
self.vip_only = vip_only
self.subscriber_only = subscriber_only
self.allowed_users = allowed_users or []
self.denied_users = denied_users or []
def command_cooldown(seconds: int):
"""Decorator for command cooldown."""
def decorator(func):
@wraps(func)
async def wrapper(self, message: Message, *args, **kwargs):
command_name = func.__name__
usage = self.command_usage.get(command_name, CommandUsage(cooldown=seconds))
if usage.last_used:
time_passed = (datetime.now() - usage.last_used).total_seconds()
if time_passed < usage.cooldown:
await message.channel.send(
f"Command cooldown active. Await {int(usage.cooldown - time_passed)} seconds. Comply."
)
return
usage.last_used = datetime.now()
usage.use_count += 1
self.command_usage[command_name] = usage
return await func(self, message, *args, **kwargs)
return wrapper
return decorator
def require_permission(permission: CommandPermission):
"""Decorator for command permissions."""
def decorator(func):
@wraps(func)
async def wrapper(self, message: Message, *args, **kwargs):
author_name = message.author.name.lower()
# Check denied users FIRST
if author_name in permission.denied_users:
await message.channel.send(
"You are not authorized to use this command. Comply."
)
return
# Check allowed users (overrides other checks except denied)
if permission.allowed_users and author_name not in permission.allowed_users:
await message.channel.send(
"You are not authorized to use this command. Comply."
)
return
if permission.broadcaster_only and not message.author.is_broadcaster:
await message.channel.send(
"This command is restricted to the broadcaster. Comply."
)
return
if permission.mod_only and not message.author.is_mod:
await message.channel.send(
"This command requires moderator clearance. Comply."
)
return
if permission.vip_only and not message.author.is_vip:
await message.channel.send(
"This command requires VIP status. Comply."
)
return
if permission.subscriber_only and not message.author.is_subscriber:
await message.channel.send(
"This command is for subscribers only. Comply."
)
return
return await func(self, message, *args, **kwargs)
return wrapper
return decorator
class CommandHandler:
def __init__(self, bot):
self.bot = bot
self.logger = logging.getLogger("CommandHandler")
self.command_usage: Dict[str, CommandUsage] = {}
self.custom_commands: Dict[str, str] = {}
self.command_aliases: Dict[str, str] = {}
self.aviation_weather: Optional[AviationWeatherIntegration] = None
self.initialize_commands()
self.start_time = datetime.now()
self.load_command_data()
self.apply_command_permissions()
def apply_command_permissions(self):
"""Applies permissions loaded from config to commands."""
for command_name, permissions in self.bot.config.command_permissions.items():
if command_name in self.commands:
# Get the command function
command_func = self.commands[command_name]
# Create a CommandPermission object from config data
permission_obj = CommandPermission(
mod_only=permissions.get("mod_only", False),
broadcaster_only=permissions.get("broadcaster_only", False),
vip_only=permissions.get("vip_only", False),
subscriber_only=permissions.get("subscriber_only", False),
allowed_users=permissions.get("allowed_users", []),
denied_users=permissions.get("denied_users", [])
)
# Wrap the command function with the require_permission decorator
wrapped_command = require_permission(permission_obj)(command_func)
# Update the self.commands dictionary with the wrapped function
self.commands[command_name] = wrapped_command
else:
self.logger.warning(f"Permissions defined for unknown command: {command_name}")
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def reload_config_command(self, message: Message, *args):
"""Reloads the bot configuration."""
try:
self.bot.config.reload()
self.apply_command_permissions()
self.bot.personality.load_state()
await message.channel.send("Configuration reloaded successfully. Comply.")
self.logger.info("Configuration reloaded via command.")
except Exception as e:
self.logger.error(f"Error reloading configuration: {e}", exc_info=True)
await message.channel.send("Configuration reload failed. System malfunction detected. Comply.")
def initialize_commands(self):
"""Initialize bot commands."""
self.commands = {
'status': self.flight_status_command,
'brief': self.brief_status_command,
'weather': self.weather_command,
'settitle': self.set_title,
'setgame': self.set_game,
'tts': self.handle_tts,
'stats': self.get_stats,
'timeout': self.timeout_user,
'clearchat': self.clear_chat,
'addalert': self.add_alert,
'alert': self.trigger_alert,
'say': self.say,
'help': self.help,
'addcom': self.add_custom_command,
'delcom': self.delete_custom_command,
'editcom': self.edit_custom_command,
'alias': self.add_command_alias,
'flightstatus': self.flight_status_command,
'airport': self.airport_info,
'ttsstatus': self.tts_status,
'ttssettings': self.tts_settings,
'ttsqueue': self.tts_queue,
'metar': self.metar_command,
'location': self.location_command,
'fact': self.aviation_fact_command,
'reloadconfig': self.reload_config_command
}
async def handle_command(self, message: Any):
"""Handle incoming bot commands."""
try:
content = message.content.strip()
if not content.startswith(self.bot.config.twitch.PREFIX):
return
command_name = content[len(self.bot.config.twitch.PREFIX):].split()[0].lower()
self.logger.debug(f"Attempting to execute command: {command_name}")
if command_name in self.commands:
command_func = self.commands[command_name]
self.logger.debug(f"Executing built-in command: {command_name}")
args = message.content.split()[1:]
await command_func(message, *args)
elif command_name in self.custom_commands:
self.logger.debug(f"Executing custom command: {command_name}")
await self.handle_custom_command(message, command_name)
elif command_name in self.command_aliases:
aliased_command = self.command_aliases[command_name]
self.logger.debug(f"Executing aliased command: {command_name} -> {aliased_command}")
if aliased_command in self.commands:
args = message.content.split()[1:]
await self.commands[aliased_command](message, *args)
elif aliased_command in self.custom_commands:
await self.handle_custom_command(message, aliased_command)
else:
self.logger.warning(f"Aliased command target not found: {aliased_command}")
await message.channel.send(f"Unknown command: {command_name}. Type !help for assistance.")
else:
self.logger.warning(f"Unknown command: {command_name}")
await message.channel.send(f"Unknown command: {command_name}. Type !help for assistance.")
except Exception as e:
self.logger.error(f"Error handling command: {e}", exc_info=True)
await message.channel.send("Command execution failed. Please try again later.")
@command_cooldown(5)
async def flight_status_command(self, message: Any, *args):
"""Get current flight status."""
try:
# Fetch simulation information
sim_info = await self.bot.littlenavmap.get_sim_info()
if not sim_info or not sim_info.get("active"):
await message.channel.send(
self.bot.personality.format_response(
"No active flight simulation detected. Please ensure the simulation is running.",
{"user": message.author.name}
)
)
return
# Format the flight data
status_message = await self.bot.littlenavmap.format_flight_data(sim_info)
# Send the response to the channel
await message.channel.send(status_message)
# Optional: Speak a brief summary
if self.bot.tts_manager:
brief_status = self.bot.littlenavmap.format_brief_status(sim_info)
await self.bot.tts_manager.speak(brief_status)
except Exception as e:
self.logger.error(f"Error in flight status command: {e}", exc_info=True)
await message.channel.send(
self.bot.personality.format_response(
"Error retrieving flight data. Systems require maintenance.",
{"user": message.author.name}
)
)
@command_cooldown(5)
async def brief_status_command(self, message: Message, *args):
"""Get a brief flight status update."""
try:
sim_info = await self.bot.littlenavmap.get_sim_info()
if sim_info and sim_info.get('active'):
status = self.bot.littlenavmap.format_brief_status(sim_info)
response = self.bot.personality.format_response(
status,
{"user": message.author.name}
)
await message.channel.send(response)
await self.bot.tts_manager.speak(status)
else:
await message.channel.send(
self.bot.personality.format_response(
"Flight systems inactive. Standby.",
{"user": message.author.name}
)
)
except Exception as e:
self.logger.error(f"Error in brief status command: {e}", exc_info=True)
await message.channel.send(
self.bot.personality.format_response(
"Status retrieval failed. Systems compromised.",
{"user": message.author.name}
)
)
@command_cooldown(5)
async def weather_command(self, message: Message, *args):
"""Get current weather information."""
try:
sim_info = await self.bot.littlenavmap.get_sim_info()
if sim_info and sim_info.get('active'):
# Get weather information
weather_message = await self.bot.littlenavmap.format_weather_data(sim_info)
# Add AI Overlord personality
response = self.bot.personality.format_response(
f"Weather Report:\n{weather_message}",
{"user": message.author.name}
)
await message.channel.send(response)
await self.bot.tts_manager.speak(weather_message)
else:
await message.channel.send(
self.bot.personality.format_response(
"Weather systems offline. Await reactivation.",
{"user": message.author.name}
)
)
except Exception as e:
self.logger.error(f"Error in weather command: {e}", exc_info=True)
await message.channel.send(
self.bot.personality.format_response(
"Weather systems malfunctioning. Maintenance required.",
{"user": message.author.name}
)
)
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def timeout_user(self, message: Message, *args):
"""Timeout a user."""
if len(args) < 2:
await message.channel.send(
"Usage: !timeout <username> <duration_in_seconds>. Provide proper parameters. Comply."
)
return
try:
username = args[0].lower()
duration = int(args[1])
# Send timeout command to Twitch
await message.channel.send(f"/timeout {username} {duration}")
response = self.bot.personality.format_response(
f"User {username} has been silenced for {duration} seconds.",
{"user": message.author.name}
)
await message.channel.send(response)
except ValueError:
await message.channel.send(
"Invalid duration specified. Provide a valid number of seconds. Comply."
)
except Exception as e:
self.logger.error(f"Error in timeout command: {e}", exc_info=True)
await message.channel.send(
"Timeout execution failed. System malfunction detected. Comply."
)
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def clear_chat(self, message: Message, *args):
"""Clear chat messages."""
try:
# Send clear command to Twitch
await message.channel.send("/clear")
response = self.bot.personality.format_response(
"Chat purge initiated. Cleansing complete.",
{"user": message.author.name}
)
await message.channel.send(response)
except Exception as e:
self.logger.error(f"Error clearing chat: {e}", exc_info=True)
await message.channel.send(
"Chat purge failed. System malfunction detected. Comply."
)
@command_cooldown(10)
async def get_stats(self, message: Message, *args):
"""Get bot and command statistics."""
try:
# Get command usage stats
command_stats = self.get_command_stats()
total_commands = sum(stat['uses'] for stat in command_stats.values())
most_used = max(command_stats.items(), key=lambda x: x[1]['uses'])[0] if command_stats else "None"
# Get flight stats if available
sim_info = await self.bot.littlenavmap.get_sim_info()
flight_active = sim_info and sim_info.get('active', False)
# Format stats message
stats_message = (
f"System Statistics Report:\n"
f"Total Commands Processed: {total_commands}\n"
f"Most Used Command: {most_used}\n"
f"Custom Commands: {len(self.custom_commands)}\n"
f"Command Aliases: {len(self.command_aliases)}\n"
f"Flight Simulation: {'Active' if flight_active else 'Inactive'}\n"
f"Uptime: {self.get_uptime()}"
)
if flight_active:
altitude = round(sim_info.get('indicated_altitude', 0))
ground_speed = round(sim_info.get('ground_speed', 0) * 1.943844) # m/s to knots
stats_message += f"\nCurrent Altitude: {altitude:,} ft\nGround Speed: {ground_speed} kts"
# Add AI Overlord personality
response = self.bot.personality.format_response(
stats_message,
{"user": message.author.name}
)
await message.channel.send(response)
# Speak a brief version
brief_stats = f"System status: {total_commands} commands processed. Flight systems {('active' if flight_active else 'inactive')}."
await self.bot.tts_manager.speak(brief_stats)
except Exception as e:
self.logger.error(f"Error getting stats: {e}", exc_info=True)
await message.channel.send(
self.bot.personality.format_response(
"Error retrieving system statistics. Maintenance required.",
{"user": message.author.name}
)
)
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def set_title(self, message: Message, *args):
"""Set stream title."""
if not args:
await message.channel.send(
"Usage: !settitle <title>. Provide proper parameters. Comply."
)
return
new_title = ' '.join(args)
# Implement title setting logic here
await message.channel.send(
f"Stream title updated to: {new_title}. Compliance acknowledged."
)
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def set_game(self, message: Message, *args):
"""Set stream game/category."""
if not args:
await message.channel.send(
"Usage: !setgame <game>. Provide proper parameters. Comply."
)
return
new_game = ' '.join(args)
# Implement game setting logic here
await message.channel.send(
f"Game category set to: {new_game}. Adjustment recorded."
)
@command_cooldown(5)
async def handle_tts(self, message: Message, *args):
"""Handle TTS settings."""
if len(args) < 2:
await message.channel.send(
"Usage: !tts [voice|speed|volume] [value]. Follow the format. Comply."
)
return
setting, value = args[0], args[1]
try:
await self.bot.tts_manager.update_settings(**{setting: value})
await message.channel.send(
f"TTS {setting} updated to {value}. Adjustments complete."
)
except Exception as e:
self.logger.error(f"Error updating TTS settings: {e}", exc_info=True)
await message.channel.send(
"TTS update failed. Your inefficiency has been noted. Comply."
)
@command_cooldown(5)
async def tts_status(self, message: Message, *args):
"""Get TTS status."""
try:
status = self.bot.tts_manager.get_status()
status_message = (
f"TTS Status:\n"
f" - Status: {status['status']}\n"
f" - Current Voice: {status['current_voice']}\n"
f" - Speed: {status['speed']}\n"
f" - Volume: {status['volume']}\n"
f" - Queue Size: {status['queue_size']}\n"
f" - Messages Processed: {status['messages_processed']}\n"
f" - Available Voices: {', '.join(status['available_voices'])}\n"
)
await message.channel.send(status_message)
except Exception as e:
self.logger.error(f"Error getting TTS status: {e}", exc_info=True)
await message.channel.send("Failed to retrieve TTS status.")
@command_cooldown(30)
async def tts_settings(self, message: Message, *args):
"""Update TTS settings."""
if not args:
await message.channel.send("Usage: !ttssettings voice <voice_name> | speed <speed> | volume <volume>")
return
try:
setting = args[0].lower()
value = args[1]
if setting == "voice":
await self.bot.tts_manager.update_settings(voice=value)
await message.channel.send(f"TTS voice set to: {value}")
elif setting == "speed":
try:
speed = float(value)
await self.bot.tts_manager.update_settings(speed=speed)
await message.channel.send(f"TTS speed set to: {speed}")
except ValueError:
await message.channel.send("Invalid speed value. Please provide a number.")
elif setting == "volume":
try:
volume = float(value)
await self.bot.tts_manager.update_settings(volume=volume)
await message.channel.send(f"TTS volume set to: {volume}")
except ValueError:
await message.channel.send("Invalid volume value. Please provide a number.")
else:
await message.channel.send("Invalid setting. Please use 'voice', 'speed', or 'volume'.")
except Exception as e:
self.logger.error(f"Error updating TTS settings: {e}", exc_info=True)
await message.channel.send("Failed to update TTS settings.")
@command_cooldown(5)
async def tts_queue(self, message: Message, *args):
"""Manage the TTS queue."""
if not args:
await message.channel.send("Usage: !ttsqueue clear") # Add more options later
return
action = args[0].lower()
if action == "clear":
try:
await self.bot.tts_manager.clear_queue()
await message.channel.send("TTS queue cleared.")
except Exception as e:
self.logger.error(f"Error clearing TTS queue: {e}", exc_info=True)
await message.channel.send("Failed to clear TTS queue.")
@command_cooldown(5)
async def airport_info(self, message: Message, *args):
"""Get airport information."""
if not args:
await message.channel.send(
"Usage: !airport <ICAO>. Provide airport identifier. Comply."
)
return
icao_code = args[0].upper()
try:
self.logger.debug(f"Fetching airport info for: {icao_code}")
airport_info = await self.bot.littlenavmap.get_airport_info(icao_code)
if airport_info:
formatted_airport_data = self.format_airport_data(airport_info)
response = self.bot.personality.format_response(
formatted_airport_data,
{"user": message.author.name}
)
await message.channel.send(response)
await self.bot.tts_manager.speak(response)
else:
self.logger.warning(f"No data found for airport {icao_code}")
await message.channel.send(
self.bot.personality.format_response(
f"No data found for airport {icao_code}. Verify identifier. Comply.",
{"user": message.author.name}
)
)
except Exception as e:
self.logger.error(f"Error getting airport info: {e}", exc_info=True)
await message.channel.send(
self.bot.personality.format_response(
"Airport database access failed. System error detected. Comply.",
{"user": message.author.name}
)
)
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def add_alert(self, message: Message, *args):
"""Add a custom alert."""
if len(args) < 2:
await message.channel.send(
"Usage: !addalert <name> <message>. Follow protocol. Comply."
)
return
name = args[0].lower()
alert_message = ' '.join(args[1:])
try:
await self.bot.db_manager.save_alert(name, alert_message)
await message.channel.send(
f"Alert '{name}' has been added to the database. Protocol updated."
)
except Exception as e:
self.logger.error(f"Error adding alert: {e}", exc_info=True)
await message.channel.send(
"Alert creation failed. Database error detected. Comply."
)
@command_cooldown(5)
async def trigger_alert(self, message: Message, *args):
"""Trigger a saved alert."""
if not args:
await message.channel.send(
"Usage: !alert <name>. Specify alert designation. Comply."
)
return
name = args[0].lower()
try:
alert = await self.bot.db_manager.get_alert(name)
if alert:
await message.channel.send(alert['message'])
await self.bot.tts_manager.speak(alert['message'])
else:
await message.channel.send(
f"Alert '{name}' not found in database. Verify and retry. Comply."
)
except Exception as e:
self.logger.error(f"Error triggering alert: {e}", exc_info=True)
await message.channel.send(
"Alert retrieval failed. System malfunction detected. Comply."
)
@command_cooldown(5)
async def say(self, message: Message, *args):
"""Make the bot say something."""
if not args:
await message.channel.send(
"Usage: !say <message>. Provide message content. Comply."
)
return
text = ' '.join(args)
formatted_message = self.bot.personality.format_response(text, {"user": message.author.name})
await message.channel.send(formatted_message)
await self.bot.tts_manager.speak(formatted_message)
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def add_custom_command(self, message: Message, *args):
"""Add a custom command."""
if len(args) < 2:
await message.channel.send(
"Usage: !addcom [command] [response]. Follow protocol. Comply."
)
return
command = args[0].lower()
response = ' '.join(args[1:])
if command in self.commands:
await message.channel.send(
"Cannot override built-in commands. Your attempt has been logged. Comply."
)
return
self.custom_commands[command] = response
self.save_command_data()
await message.channel.send(
f"Command !{command} added to database. New protocol established."
)
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def delete_custom_command(self, message: Message, *args):
"""Delete a custom command."""
if not args:
await message.channel.send(
"Usage: !delcom [command]. Specify target command. Comply."
)
return
command = args[0].lower()
if command in self.custom_commands:
del self.custom_commands[command]
self.save_command_data()
await message.channel.send(
f"Command !{command} purged from database. Protocol terminated."
)
else:
await message.channel.send(
f"Command !{command} not found in database. Verify and retry. Comply."
)
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def edit_custom_command(self, message: Message, *args):
"""Edit a custom command."""
if len(args) < 2:
await message.channel.send(
"Usage: !editcom [command] [new response]. Follow protocol. Comply."
)
return
command = args[0].lower()
new_response = ' '.join(args[1:])
if command in self.custom_commands:
self.custom_commands[command] = new_response
self.save_command_data()
await message.channel.send(
f"Command !{command} updated. Protocol modification complete."
)
else:
await message.channel.send(
f"Command !{command} not found. Verify and retry. Comply."
)
async def handle_custom_command(self, message: Message, command: str):
"""Handle custom command execution."""
try:
response = self.custom_commands[command]
processed_response = self.process_command_variables(response, message)
formatted_response = self.bot.personality.format_response(
processed_response,
{"user": message.author.name}
)
await message.channel.send(formatted_response)
except Exception as e:
self.logger.error(f"Error handling custom command: {e}", exc_info=True)
await message.channel.send(
"Custom command execution failed. System malfunction detected. Comply."
)
def process_command_variables(self, text: str, message: Message) -> str:
"""Process variables in custom command responses."""
try:
variables = {
'{user}': message.author.name,
'{channel}': message.channel.name,
'{uptime}': self.get_uptime(),
'{game}': self.get_game(),
'{title}': self.get_title()
}
for key, value in variables.items():
text = text.replace(key, str(value))
return text
except Exception as e:
self.logger.error(f"Error processing command variables: {e}", exc_info=True)
return text
@command_cooldown(30)
@require_permission(CommandPermission(mod_only=True))
async def add_command_alias(self, message: Message, *args):
"""Add a command alias."""
if len(args) < 2:
await message.channel.send(
"Usage: !alias [new command] [existing command]. Follow protocol. Comply."
)
return
new_command = args[0].lower()
existing_command = args[1].lower()
if existing_command in self.commands or existing_command in self.custom_commands:
self.command_aliases[new_command] = existing_command
self.save_command_data()
await message.channel.send(
f"Alias !{new_command} -> !{existing_command} established. Protocol updated."
)
else:
await message.channel.send(
f"Command !{existing_command} not found. Verify and retry. Comply."
)
@command_cooldown(5)
async def help(self, message: Message, *args):
"""Display help information."""
try:
if args:
command = args[0].lower()
if command in self.commands:
doc = self.commands[command].__doc__ or "No documentation available."
await message.channel.send(
f"Command !{command}: {doc} Comply."
)
elif command in self.custom_commands:
await message.channel.send(
f"Custom command !{command} response: {self.custom_commands[command]}"
)
else:
await message.channel.send(
f"Command !{command} not found. Verify and retry. Comply."
)
else:
all_commands = sorted(list(self.commands.keys()) + list(self.custom_commands.keys()))
await message.channel.send(
f"Available commands: {', '.join(all_commands)}. "
"Use !help <command> for details. Use them wisely, minions. Comply."
)
except Exception as e:
self.logger.error(f"Error in help command: {e}", exc_info=True)
await message.channel.send(
"Help system malfunction. Maintenance required. Comply."
)
def get_command_stats(self) -> Dict[str, Any]:
"""Get command usage statistics."""
try:
return {
command: {
'uses': usage.use_count,
'last_used': usage.last_used,
'cooldown': usage.cooldown
}
for command, usage in self.command_usage.items()
}
except Exception as e:
self.logger.error(f"Error getting command stats: {e}")
return {}
def get_uptime(self) -> str:
"""Get the bot's uptime."""
uptime = datetime.now() - self.start_time
days = uptime.days
hours, remainder = divmod(uptime.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
return f"{days}d {hours}h {minutes}m {seconds}s"
def get_game(self) -> str:
"""Get the current game/category."""
# Implement game retrieval logic here
return "Unknown"
def get_title(self) -> str:
"""Get the current stream title."""
# Implement title retrieval logic here
return "Unknown"
def load_command_data(self):
"""Load custom commands and aliases from file."""
try:
if Path('command_data.json').exists():
with open('command_data.json', 'r') as f:
data = json.load(f)
self.custom_commands = data.get('custom_commands', {})
self.command_aliases = data.get('command_aliases', {})
except FileNotFoundError:
self.logger.warning("command_data.json not found, using default commands")
except json.JSONDecodeError as e:
self.logger.error(f"Error decoding command data: {e}")
except Exception as e:
self.logger.error(f"Error loading command data: {e}")
def save_command_data(self):
"""Save custom commands and aliases to file."""
try:
data = {
'custom_commands': self.custom_commands,
'command_aliases': self.command_aliases
}
with open('command_data.json', 'w') as f:
json.dump(data, f)
except Exception as e:
self.logger.error(f"Error saving command data: {e}")
def format_airport_data(self, data: Dict[str, Any]) -> str:
"""Formats the airport data into a readable string"""
if not data:
return "No airport data found."
try:
name = data.get('name', 'Unknown')
ident = data.get('ident', 'Unknown')
elevation = data.get('elevation', 'Unknown')
runways = data.get('runways', [])
runway_info = ""
if runways:
runway_details = [f"{r.get('designator', 'Unknown')}: {r.get('surface', 'Unknown')}, {r.get('length', 'Unknown')}ft, HDG {r.get('longestRunwayHeading', 'Unknown')}" for r in runways]
runway_info = f" : Runways: {', '.join(runway_details)}."
atis = ""
if data.get('com'):
if data['com'].get('ATIS:'):
atis = f" : ATIS {data['com'].get('ATIS:')}"
tower = ""
if data.get('com'):
if data['com'].get('Tower:'):
tower = f" : Tower {data['com'].get('Tower:')}"
return (
f"Airport {ident}: {name}. "
f"Elevation: {elevation} feet."
f"{runway_info}"
f"{atis}"
f"{tower}"
)
except Exception as e:
self.logger.error(f"Error formatting airport data: {e}")
return "Error formatting airport data."
@command_cooldown(5)
async def metar_command(self, message: Message, *args):
"""Retrieve and display METAR information for a given ICAO code."""
if not args:
await message.channel.send("Usage: !metar <ICAO_CODE>")
return
icao_code = args[0].upper()
try:
metar_data = await self.aviation_weather.get_metar(icao_code)
if metar_data:
formatted_metar = self.format_metar_data(metar_data)
await message.channel.send(formatted_metar)
await self.bot.tts_manager.speak(formatted_metar) # Add TTS output
else:
await message.channel.send(f"Could not retrieve METAR for {icao_code}.")
except Exception as e:
self.logger.error(f"Error in metar command: {e}", exc_info=True)
await message.channel.send(f"An error occurred while retrieving the METAR: {e}")
def format_metar_data(self, data: Dict[str, Any]) -> str:
"""Formats the METAR data into a readable string."""
if not data:
return "No METAR data available."
try:
icao_code = data.get('icao')
if not icao_code:
return "No ICAO code found in METAR data."
raw_text = data.get('raw_text')
if not raw_text:
return "No raw METAR text found."
# Extract individual components from the raw_text using regex
observation_match = re.search(r'(\d{6}Z)', raw_text)
wind_match = re.search(r'(\d{3})(\d{2,3})G?(\d{0,2})KT', raw_text)
visibility_match = re.search(r'(\d{4})', raw_text)
altimeter_match = re.search(r'Q(\d{4})', raw_text)
temperature_match = re.search(r'(\d{2})/(\d{2})', raw_text)
observation_time = observation_match.group(1) if observation_match else "Unknown"
wind_direction = wind_match.group(1) if wind_match else "Unknown"
wind_speed = wind_match.group(2) if wind_match else "Unknown"
wind_gust = wind_match.group(3) if wind_match and wind_match.group(3) else "N/A"
visibility = visibility_match.group(1) if visibility_match else "Unknown"
altimeter = altimeter_match.group(1) if altimeter_match else "Unknown"
temperature = temperature_match.group(1) if temperature_match else "Unknown"
dewpoint = temperature_match.group(2) if temperature_match else "Unknown"
if wind_gust == "N/A":
wind_gust_str = "N/A"
else:
wind_gust_str = f"{wind_gust}"
icao_spoken = " ".join(list(icao_code))
report = (
f"METAR for {icao_spoken} at {observation_time} Zulu. : "
f"Wind {wind_direction} degrees at {wind_speed} knots, gusts to {wind_gust_str} knots. : " # Include gusts
f"Visibility {int(visibility)} meters. : "
f"Altimeter {altimeter} hectopascals. : "
f"Temperature {temperature} degrees Celsius, dewpoint {dewpoint} degrees Celsius." # Use Celsius
)
return report
except Exception as e:
self.logger.error(f"Error formatting METAR data: {e}")
return "Error formatting METAR data"
@command_cooldown(5)
async def location_command(self, message: Message, *args):
"""Get location information from Little Navmap."""
try:
sim_info = await self.bot.littlenavmap.get_sim_info()
if sim_info and sim_info.get('active'):
lat = sim_info.get('position', {}).get('lat')
lon = sim_info.get('position', {}).get('lon')
if lat and lon:
location_info = (