Merge pull request 'implement-bot-presence' (#6) from implement-bot-presence into master
All checks were successful
release-tag / release-image (push) Successful in 1h14m4s
Lint / lint (push) Successful in 10s

Reviewed-on: #6
This commit is contained in:
2025-06-29 15:56:03 -04:00
6 changed files with 241 additions and 187 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ bot-poc.py
configs* configs*
.env .env
*.log* *.log*
.venv

View File

@@ -165,9 +165,15 @@ class DiscordBotManager:
LOGGER.info("Loaded OPUS library for armv7l") LOGGER.info("Loaded OPUS library for armv7l")
return "armv7l" return "armv7l"
async def set_presence(self, presence: str): async def set_presence(self, system_name: str):
""" Set the presense (activity) of the bot """ """ Set the presence (activity) of the bot """
if not self.bot:
LOGGER.warning("Bot is not running, cannot set presence.")
return
try: try:
await self.bot.change_presence(activity=Activity(type=ActivityType.listening, name=presence)) activity = Activity(type=ActivityType.listening, name=system_name)
await self.bot.change_presence(activity=activity)
LOGGER.info(f"Bot presence set to 'Listening to {system_name}'")
except Exception as pe: except Exception as pe:
LOGGER.error(f"Unable to set presence: '{pe}'") LOGGER.error(f"Unable to set presence: '{pe}'")

View File

@@ -0,0 +1,70 @@
import csv
import json
from models import TalkgroupTag
from typing import List
from internal.logger import create_logger
LOGGER = create_logger(__name__)
def save_talkgroup_tags(talkgroup_tags: List[TalkgroupTag]) -> None:
"""
Writes a list of tags to the tags file.
Args:
talkgroup_tags (List[TalkgroupTag]): The list of TalkgroupTag instances.
"""
with open("/configs/active.cfg.tags.tsv", 'w', newline='', encoding='utf-8') as file:
writer = csv.writer(file, delimiter='\t', lineterminator='\n')
# Write rows
for tag in talkgroup_tags:
writer.writerow([tag.talkgroup, tag.tagDec])
def save_whitelist(talkgroup_tags: List[int]) -> None:
"""
Writes a list of talkgroups to the whitelists file.
Args:
talkgroup_tags (List[int]): The list of decimals to whitelist.
"""
with open("/configs/active.cfg.whitelist.tsv", 'w', newline='', encoding='utf-8') as file:
writer = csv.writer(file, delimiter='\t', lineterminator='\n')
# Write rows
for tag in talkgroup_tags:
writer.writerow([tag])
def del_none_in_dict(d):
"""
Delete keys with the value ``None`` in a dictionary, recursively.
This alters the input so you may wish to ``copy`` the dict first.
"""
for key, value in list(d.items()):
LOGGER.info(f"Key: '{key}'\nValue: '{value}'")
if value is None:
del d[key]
elif isinstance(value, dict):
del_none_in_dict(value)
elif isinstance(value, list):
for iterative_value in value:
del_none_in_dict(iterative_value)
return d # For convenience
def get_current_system_from_config() -> str:
# Get the current config
with open('/configs/active.cfg.json', 'r') as f:
json_data = f.read()
if isinstance(json_data, str):
try:
data = json.loads(json_data)
except json.JSONDecodeError:
return None
elif isinstance(json_data, dict):
data = json_data
else:
return None
if "channels" in data and isinstance(data["channels"], list) and len(data["channels"]) > 0:
first_channel = data["channels"][0]
if "name" in first_channel:
return first_channel["name"]
return None

View File

@@ -3,6 +3,7 @@ import routers.op25_controller as op25_controller
import routers.pulse as pulse import routers.pulse as pulse
import routers.bot as bot import routers.bot as bot
from internal.logger import create_logger from internal.logger import create_logger
from internal.bot_manager import DiscordBotManager
# Initialize logging # Initialize logging
LOGGER = create_logger(__name__) LOGGER = create_logger(__name__)
@@ -10,6 +11,9 @@ LOGGER = create_logger(__name__)
# Define FastAPI app # Define FastAPI app
app = FastAPI() app = FastAPI()
app.include_router(op25_controller.router, prefix="/op25") # Initialize Discord Bot Manager
bot_manager_instance = DiscordBotManager()
app.include_router(op25_controller.create_op25_router(bot_manager=bot_manager_instance), prefix="/op25")
app.include_router(pulse.router, prefix="/pulse") app.include_router(pulse.router, prefix="/pulse")
app.include_router(bot.router, prefix="/bot") app.include_router(bot.create_bot_router(bot_manager=bot_manager_instance), prefix="/bot")

View File

@@ -5,15 +5,13 @@ from internal.logger import create_logger
LOGGER = create_logger(__name__) LOGGER = create_logger(__name__)
# Define FastAPI app # Function to create router
router = APIRouter() def create_bot_router(bot_manager: DiscordBotManager):
router = APIRouter()
# Initialize Discord Bot Manager # API Endpoints
bot_manager = DiscordBotManager() @router.post("/start_bot")
async def start_bot(config: BotConfig):
# API Endpoints
@router.post("/start_bot")
async def start_bot(config: BotConfig):
try: try:
await bot_manager.start_bot(config.token) await bot_manager.start_bot(config.token)
return {"status": "Bot started successfully."} return {"status": "Bot started successfully."}
@@ -21,8 +19,8 @@ async def start_bot(config: BotConfig):
LOGGER.error(f"Error starting bot: {e}") LOGGER.error(f"Error starting bot: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.post("/stop_bot") @router.post("/stop_bot")
async def stop_bot(): async def stop_bot():
try: try:
await bot_manager.stop_bot() await bot_manager.stop_bot()
return {"status": "Bot stopped successfully."} return {"status": "Bot stopped successfully."}
@@ -30,8 +28,8 @@ async def stop_bot():
LOGGER.error(f"Error stopping bot: {e}") LOGGER.error(f"Error stopping bot: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.post("/join_voice") @router.post("/join_voice")
async def join_voice_channel(request: VoiceChannelJoinRequest): async def join_voice_channel(request: VoiceChannelJoinRequest):
try: try:
await bot_manager.join_voice_channel(request.guild_id, request.channel_id) await bot_manager.join_voice_channel(request.guild_id, request.channel_id)
return {"status": f"Joined guild {request.guild_id} voice channel {request.channel_id}."} return {"status": f"Joined guild {request.guild_id} voice channel {request.channel_id}."}
@@ -39,8 +37,8 @@ async def join_voice_channel(request: VoiceChannelJoinRequest):
LOGGER.error(f"Error joining voice channel: {e}") LOGGER.error(f"Error joining voice channel: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.post("/leave_voice") @router.post("/leave_voice")
async def leave_voice_channel(request: VoiceChannelLeaveRequest): async def leave_voice_channel(request: VoiceChannelLeaveRequest):
try: try:
await bot_manager.leave_voice_channel(request.guild_id) await bot_manager.leave_voice_channel(request.guild_id)
return {"status": f"Left guild {request.guild_id} voice channel."} return {"status": f"Left guild {request.guild_id} voice channel."}
@@ -48,11 +46,13 @@ async def leave_voice_channel(request: VoiceChannelLeaveRequest):
LOGGER.error(f"Error leaving voice channel: {e}") LOGGER.error(f"Error leaving voice channel: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/status") @router.get("/status")
async def get_status(): async def get_status():
status = { status = {
"bot_running": bot_manager.bot is not None and not bot_manager.bot.is_closed(), "bot_running": bot_manager.bot is not None and not bot_manager.bot.is_closed(),
"connected_guilds": list(bot_manager.voice_clients.keys()), "connected_guilds": list(bot_manager.voice_clients.keys()),
"active_token": bot_manager.token "active_token": bot_manager.token
} }
return status return status
return router

View File

@@ -3,20 +3,22 @@ import subprocess
import os import os
import signal import signal
import json import json
import csv from models import ConfigGenerator, DecodeMode, ChannelConfig, DeviceConfig, TrunkingConfig, TrunkingChannelConfig, AudioConfig, TerminalConfig
from models import ConfigGenerator, DecodeMode, ChannelConfig, DeviceConfig, TrunkingConfig, TrunkingChannelConfig, AudioConfig, TerminalConfig, TalkgroupTag
from internal.logger import create_logger from internal.logger import create_logger
from typing import List from internal.bot_manager import DiscordBotManager
from internal.op25_config_utls import save_talkgroup_tags, save_whitelist, del_none_in_dict, get_current_system_from_config
router = APIRouter()
LOGGER = create_logger(__name__) LOGGER = create_logger(__name__)
op25_process = None op25_process = None
OP25_PATH = "/op25/op25/gr-op25_repeater/apps/" OP25_PATH = "/op25/op25/gr-op25_repeater/apps/"
OP25_SCRIPT = "run_multi-rx_service.sh" OP25_SCRIPT = "run_multi-rx_service.sh"
@router.post("/start") def create_op25_router(bot_manager: DiscordBotManager):
async def start_op25(): router = APIRouter()
@router.post("/start")
async def start_op25():
global op25_process global op25_process
if op25_process is None: if op25_process is None:
try: try:
@@ -28,8 +30,8 @@ async def start_op25():
else: else:
return {"status": "OP25 already running"} return {"status": "OP25 already running"}
@router.post("/stop") @router.post("/stop")
async def stop_op25(): async def stop_op25():
global op25_process global op25_process
if op25_process is not None: if op25_process is not None:
try: try:
@@ -41,12 +43,12 @@ async def stop_op25():
else: else:
return {"status": "OP25 is not running"} return {"status": "OP25 is not running"}
@router.get("/status") @router.get("/status")
async def get_status(): async def get_status():
return {"status": "running" if op25_process else "stopped"} return {"status": "running" if op25_process else "stopped"}
@router.post("/generate-config") @router.post("/generate-config")
async def generate_config(generator: ConfigGenerator): async def generate_config(generator: ConfigGenerator):
try: try:
if generator.type == DecodeMode.P25: if generator.type == DecodeMode.P25:
channels = [ChannelConfig( channels = [ChannelConfig(
@@ -105,49 +107,20 @@ async def generate_config(generator: ConfigGenerator):
with open('/configs/active.cfg.json', 'w') as f: with open('/configs/active.cfg.json', 'w') as f:
json.dump(del_none_in_dict(config_dict), f, indent=2) json.dump(del_none_in_dict(config_dict), f, indent=2)
# Set the presence of the bot (if it's online)
await bot_manager.set_presence(generator.systemName)
return {"message": "Config exported to '/configs/active.cfg.json'"} return {"message": "Config exported to '/configs/active.cfg.json'"}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
def save_talkgroup_tags(talkgroup_tags: List[TalkgroupTag]) -> None: @router.post("/update-prensece")
""" async def update_prensece():
Writes a list of tags to the tags file. current_system = get_current_system_from_config()
if not current_system:
raise HTTPException(status_code=500, detail="Unable to get current system.")
Args: await bot_manager.set_presence(current_system)
talkgroup_tags (List[TalkgroupTag]): The list of TalkgroupTag instances. return current_system
"""
with open("/configs/active.cfg.tags.tsv", 'w', newline='', encoding='utf-8') as file:
writer = csv.writer(file, delimiter='\t', lineterminator='\n')
# Write rows
for tag in talkgroup_tags:
writer.writerow([tag.talkgroup, tag.tagDec])
def save_whitelist(talkgroup_tags: List[int]) -> None: return router
"""
Writes a list of talkgroups to the whitelists file.
Args:
talkgroup_tags (List[int]): The list of decimals to whitelist.
"""
with open("/configs/active.cfg.whitelist.tsv", 'w', newline='', encoding='utf-8') as file:
writer = csv.writer(file, delimiter='\t', lineterminator='\n')
# Write rows
for tag in talkgroup_tags:
writer.writerow([tag])
def del_none_in_dict(d):
"""
Delete keys with the value ``None`` in a dictionary, recursively.
This alters the input so you may wish to ``copy`` the dict first.
"""
for key, value in list(d.items()):
LOGGER.info(f"Key: '{key}'\nValue: '{value}'")
if value is None:
del d[key]
elif isinstance(value, dict):
del_none_in_dict(value)
elif isinstance(value, list):
for iterative_value in value:
del_none_in_dict(iterative_value)
return d # For convenience