diff --git a/bot/cogs/main_commands.py b/bot/cogs/main_commands.py index 4777e9f..37540f1 100644 --- a/bot/cogs/main_commands.py +++ b/bot/cogs/main_commands.py @@ -1,36 +1,93 @@ -import discord +import datetime +from zoneinfo import ZoneInfo from discord.ext import commands +import discord + +import utils +class LookupFlags(commands.FlagConverter): + node_id: str = commands.flag(description='Node ID') class MainCommands(commands.Cog): - def __init__(self, bot): + def __init__(self, bot, config, data): self.bot = bot + self.config = config + self.data = data @commands.Cog.listener() async def on_ready(self): - print(f'Logged in as {self.user} (ID: {self.user.id})') - await self.tree.sync(guild=self.guilds[0]) - print('Synced Slash Commands') + print('Discord: Logged in') - @commands.Cog.listener() - async def on_message(message): - print(f'Discord: {message.channel.id}: {message.author}: {message.content}') - if message.content.startswith('!test'): - await message.channel.send('Test successful!') + @commands.hybrid_command(name="lookup", description="Look up a node by ID") + async def lookup_node(self, ctx, *, flags: LookupFlags): + print(f"Discord: /lookup: Looking up {flags.node_id}") + try: + id_int = int(flags.node_id, 10) + id_hex = utils.convert_node_id_from_int_to_hex(id_int) + except ValueError: + id_hex = flags.node_id + + if id_hex not in self.data.nodes: + for node_id, node in self.data.nodes.items(): + if node['shortname'] == flags.node_id: + id_hex = node_id + break - @commands.command() + if id_hex not in self.data.nodes: + print(f"Discord: /lookup: Node {id_hex} not found.") + await ctx.send(f"Node {id_hex} not found.") + return + + id_int = utils.convert_node_id_from_hex_to_int(id_hex) + node = self.data.nodes[id_hex] + print(f"Discord: /lookup: Found {node['id']}") + + embed = discord.Embed( + title=f"Node {node['shortname']}: {node['longname']}", + url=f"https://svm1.meshinfo.network/node_{node['id']}.html", + color=discord.Color.blue()) + embed.set_thumbnail(url=f"https://api.dicebear.com/9.x/bottts-neutral/svg?seed={node['id']}") + embed.add_field(name="ID (hex)", value=id_hex, inline=True) + embed.add_field(name="ID (int)", value=id_int, inline=True) + embed.add_field(name="Shortname", value=node['shortname'], inline=False) + embed.add_field(name="Hardware", value=node['hardware'], inline=False) + embed.add_field(name="Last Seen", value=node['last_seen'], inline=False) + embed.add_field(name="Status", value=("Online" if node['active'] else "Offline"), inline=False) + await ctx.send(embed=embed) + + @commands.hybrid_command(name="mesh", description="Information about the mesh") + async def mesh_info(self, ctx): + print(f"Discord: /mesh: Mesh info requested by {ctx.author}") + embed = discord.Embed( + title=f"Information about {self.config['mesh']['name']}", + url="https://svm1.meshinfo.network", + color=discord.Color.blue()) + embed.add_field(name="Name", value=self.config['mesh']['name'], inline=False) + embed.add_field(name="Shortname", value=self.config['mesh']['shortname'], inline=False) + embed.add_field(name="Description", value=self.config['mesh']['description'], inline=False) + embed.add_field(name="Official Website", value=self.config['mesh']['url'], inline=False) + location = f"{self.config['mesh']['metro']}, {self.config['mesh']['region']}, {self.config['mesh']['country']}" + embed.add_field(name="Location", value=location, inline=False) + embed.add_field(name="Timezone", value=self.config['server']['timezone'], inline=False) + embed.add_field(name="Known Nodes", value=len(self.data.nodes), inline=True) + embed.add_field(name="Online Nodes", value=len([n for n in self.data.nodes.values() if n['active']]), inline=True) + uptime = datetime.datetime.now().astimezone(ZoneInfo(self.config['server']['timezone'])) - self.config['server']['start_time'] + embed.add_field(name="Server Uptime", value=f"{uptime.days}d {uptime.seconds // 3600}h {uptime.seconds // 60}m {uptime.seconds % 60}s", inline=False) + embed.add_field(name="Messages Since Start", value=len(self.data.messages), inline=True) + await ctx.send(embed=embed) + + @commands.hybrid_command(name="ping", description="Ping the bot") async def ping(self, ctx): + print(f"Discord: /ping: Pinged by {ctx.author}") await ctx.send(f'Pong! {round(self.bot.latency * 1000)}ms') - @commands.command( - name="lookup", - description="Look up a node by ID", - guild=discord.Object(id=1234910729480441947) - ) - async def lookup_node(ctx: discord.Interaction, node_id: str): - global nodes - node = nodes[node_id] - if node is None: - await ctx.response.send_message(f"Node {node_id} not found.") - return - await ctx.response.send_message(f"Node {node['id']}: Short Name = {node['shortname']}, Long Name = {node['longname']}, Hardware = {node['hardware']}, Position = {node['position']}, Last Seen = {node['last_seen']}, Active = {node['active']}") + @commands.hybrid_command(name="uptime", description="Uptime of MeshInfo instance") + async def uptime(self, ctx): + print(f"Discord: /uptime: Uptime requested by {ctx.author}") + now = datetime.datetime.now().astimezone(ZoneInfo(self.config['server']['timezone'])) + print(now) + print(self.config['server']['start_time']) + uptime = now - self.config['server']['start_time'] + print(uptime) + print(f"{uptime.days}d {uptime.seconds // 3600}h {uptime.seconds // 60}m {uptime.seconds % 60}s") + await ctx.send(f'MeshInfo uptime: {uptime.days}d {uptime.seconds // 3600}h {uptime.seconds // 60}m {uptime.seconds % 60}s') diff --git a/bot/discord.py b/bot/discord.py index b40ab5b..5179c47 100644 --- a/bot/discord.py +++ b/bot/discord.py @@ -1,55 +1,68 @@ #!/usr/bin/env python3 import asyncio -import os -from typing import List - -import discord from discord.ext import commands +import discord from dotenv import load_dotenv from bot.cogs.main_commands import MainCommands +from memory_data_store import MemoryDataStore class DiscordBot(commands.Bot): - guilds = [] - def __init__( self, *args, - initial_guilds: List[int], + config: dict, + data: MemoryDataStore, **kwargs, ): super().__init__(*args, **kwargs) - self.guilds = initial_guilds + self.config = config + self.data = data + self.synced = False + async def on_ready(self): + print('Discord: Ready!') + await self.wait_until_ready() + if not self.synced: + print("Discord: Syncing commands") + guild = discord.Object(id=self.config['integrations']['discord']['guild']) + self.tree.copy_global_to(guild=guild) + await self.tree.sync(guild = discord.Object(id=self.config['integrations']['discord']['guild'])) + self.synced = True -async def main(): - load_dotenv() - if os.environ.get("DISCORD_TOKEN") is not None: - token = os.environ["DISCORD_TOKEN"] - channel_id = os.environ["DISCORD_CHANNEL_ID"] - bot = DiscordBot( - command_prefix="!", - intents=discord.Intents.all(), - initial_guilds=[1234910729480441947], - ) - print("Adding cog MainCommands") - await bot.add_cog(MainCommands(bot)) - print("Starting bot") - await bot.start(token) - print("Bot started") - await bot.get_channel(channel_id).send("Hello.") - else: - print("Not running bot because DISCORD_TOKEN not set") - - -async def start_bot(): - print("Starting Discord Bot") - await main() - print("Discord Bot Done!") + async def on_message(self, message): + print(f'Discord: {message.channel.id}: {message.author}: {message.content}') + if message.content.startswith('!test'): + await message.channel.send('Test successful!') + await self.process_commands(message) + + async def start_server(self): + print("Starting Discord Bot") + await self.add_cog(MainCommands(self, self.config, self.data)) + await self.start(self.config['integrations']['discord']['token']) + print("Discord Bot Done!") +async def main(): + load_dotenv() + # if os.environ.get("DISCORD_TOKEN") is not None: + # token = os.environ["DISCORD_TOKEN"] + # channel_id = os.environ["DISCORD_CHANNEL_ID"] + # bot = DiscordBot( + # command_prefix="!", + # intents=discord.Intents.all(), + # initial_guilds=[1234910729480441947], + # ) + # print("Adding cog MainCommands") + # await bot.add_cog(MainCommands(bot)) + # print("Starting bot") + # await bot.start(token) + # print("Bot started") + # await bot.get_channel(channel_id).send("Hello.") + # else: + # print("Not running bot because DISCORD_TOKEN not set") if __name__ == "__main__": asyncio.run(main(), debug=True) diff --git a/config.json.sample b/config.json.sample index 4c9dbab..3686537 100644 --- a/config.json.sample +++ b/config.json.sample @@ -1,4 +1,22 @@ { + "mesh": { + "name": "Sac Valley Mesh", + "shortname": "SVM", + "description": "Serving Meshtastic to the Sacramento Valley and surrounding areas.", + "url": "https://sacvalleymesh.com", + "contact": "https://sacvalleymesh.com", + "country": "US", + "region": "California", + "metro": "Sacramento", + "latitude": 38.58, + "longitude": -121.49, + "altitude": 0, + "timezone": "America/Los_Angeles", + "announce": { + "enabled": true, + "interval": 60 + } + }, "broker": { "enabled": true, "host": "mqtt.meshtastic.org", @@ -49,36 +67,17 @@ "max_depth": 10 } }, - "mesh": { - "name": "Sac Valley Mesh", - "shortname": "SVM", - "description": "Serving Meshtastic to the Sacramento Valley and surrounding areas.", - "url": "https://sacvalleymesh.com", - "contact": "https://sacvalleymesh.com", - "country": "US", - "region": "California", - "metro": "Sacramento", - "latitude": 38.58, - "longitude": -121.49, - "altitude": 0, - "timezone": "America/Los_Angeles", - "announce": { - "enabled": true, - "interval": 60 - } - }, "integrations": { "discord": { - "enabled": true, - "token": "token", - "channel": "1247618108810596392", - "webhook": "webhook" + "enabled": false, + "token": "REPLACE_WITH_TOKEN", + "guild": "REPLACE_WITH_GUILD_ID" }, "geocoding": { "enabled": false, "provider": "geocode.maps.co", "geocode.maps.co": { - "api_key": "XXXXXXXXXXXXXXXX" + "api_key": "REPLACE_WITH_API_KEY" } } }, diff --git a/main.py b/main.py index e71918c..adb1d23 100644 --- a/main.py +++ b/main.py @@ -4,9 +4,11 @@ import datetime from zoneinfo import ZoneInfo import os +import discord from dotenv import load_dotenv from api import api +from bot import discord as discord_bot from config import Config from memory_data_store import MemoryDataStore from mqtt import MQTT @@ -39,41 +41,9 @@ async def main(): if config['broker']['enabled'] is True: mqtt = MQTT(config, data) tg.create_task(mqtt.connect()) - # tg.create_task(discord.start_bot()) - - # discord - # if os.environ.get('DISCORD_TOKEN') is not None: - # config['integrations']['discord']['token'] = os.environ['DISCORD_TOKEN'] - # config['integrations']['discord']['channel_id'] = os.environ['DISCORD_CHANNEL_ID'] - # config['integrations']['discord']['enabled'] = True - # discord_client = discord.Client(intents=discord.Intents.all()) - # tree = app_commands.CommandTree(discord_client) - - # @tree.command( - # name="lookup", - # description="Look up a node by ID", - # guild=discord.Object(id=1234910729480441947) - # ) - # async def lookup_node(ctx: Interaction, node_id: str): - # node = nodes[node_id] - # if node is None: - # await ctx.response.send_message(f"Node {node_id} not found.") - # return - # await ctx.response.send_message(f"Node {node['id']}: Short Name = {node['shortname']}, Long Name = {node['longname']}, Hardware = {node['hardware']}, Position = {node['position']}, Last Seen = {node['last_seen']}, Active = {node['active']}") - - # @discord_client.event - # async def on_ready(): - # print(f'Discord: Logged in as {discord_client.user} (ID: {discord_client.user.id})') - # await tree.sync(guild=discord.Object(id=1234910729480441947)) - # print("Discord: Synced slash commands") - - # @discord_client.event - # async def on_message(message): - # print(f'Discord: {message.channel.id}: {message.author}: {message.content}') - # if message.content.startswith('!test'): - # await message.channel.send('Test successful!') - - # discord_client.run(config['integrations']['discord']['token']) + if config['integrations']['discord']['enabled'] is True: + bot = discord_bot.DiscordBot(command_prefix="!", intents=discord.Intents.all(), config=config, data=data) + tg.create_task(bot.start_server()) if __name__ == "__main__": asyncio.run(main()) diff --git a/mqtt.py b/mqtt.py index 782e170..e58ba62 100644 --- a/mqtt.py +++ b/mqtt.py @@ -82,7 +82,7 @@ async def process_mqtt_msg(self, client, msg): mp = se.packet outs = json.loads(MessageToJson(mp, preserving_proto_field_name=True, ensure_ascii=False, indent=2, sort_keys=True, use_integers_for_enums=True)) print(f"Decoded protobuf message: {outs}") - except Exception as e: + except Exception as _: # print(f"*** ParseFromString: {str(e)}") pass @@ -282,6 +282,7 @@ async def handle_nodeinfo(self, msg): if 'sender' in msg and msg['sender'] and isinstance(msg['sender'], str): msg['sender'] = msg['sender'].replace('!', '') + # TODO: Reduce the replicated code here id = msg['payload']['id'] if id in self.data.nodes: node = self.data.nodes[id] @@ -289,14 +290,20 @@ async def handle_nodeinfo(self, msg): node['hardware'] = msg['payload']['hardware'] elif 'hw_model' in msg['payload']: node['hardware'] = msg['payload']['hw_model'] + if 'longname' in msg['payload']: node['longname'] = msg['payload']['longname'] elif 'long_name' in msg['payload']: node['longname'] = msg['payload']['long_name'] + if 'shortname' in msg['payload']: node['shortname'] = msg['payload']['shortname'] elif 'short_name' in msg['payload']: node['shortname'] = msg['payload']['short_name'] + + if 'role' in msg['payload']: + node['role'] = msg['payload']['role'] + self.data.update_node(id, node) print(f"Node {id} updated") else: @@ -305,14 +312,20 @@ async def handle_nodeinfo(self, msg): node['hardware'] = msg['payload']['hardware'] elif 'hw_model' in msg['payload']: node['hardware'] = msg['payload']['hw_model'] + if 'longname' in msg['payload']: node['longname'] = msg['payload']['longname'] elif 'long_name' in msg['payload']: node['longname'] = msg['payload']['long_name'] + if 'shortname' in msg['payload']: node['shortname'] = msg['payload']['shortname'] elif 'short_name' in msg['payload']: node['shortname'] = msg['payload']['short_name'] + + if 'role' in msg['payload']: + node['role'] = msg['payload']['role'] + self.data.update_node(id, node) print(f"Node {id} added") self.sort_nodes_by_shortname() diff --git a/static_html_renderer.py b/static_html_renderer.py index 54fbf97..0451056 100644 --- a/static_html_renderer.py +++ b/static_html_renderer.py @@ -250,6 +250,7 @@ def _serialize_node(self, node): "shortname": node["shortname"], "longname": node["longname"], "hardware": node["hardware"], + "role": node["role"] if "role" in node else None, "position": self._serialize_position(node["position"]) if node["position"] else None, "neighborinfo": self._serialize_neighborinfo(node) if node['neighborinfo'] else None, "telemetry": node["telemetry"], diff --git a/templates/static/node.html.j2 b/templates/static/node.html.j2 index 0da3860..ca7248e 100644 --- a/templates/static/node.html.j2 +++ b/templates/static/node.html.j2 @@ -110,6 +110,39 @@ {% endif %} +