meshtastic- Experiments with Meshtastic 🛰️ |
git clone git://git.acid.vegas/meshtastic.git |
Log | Files | Refs | Archive | README | LICENSE |
meshirc.py (6405B)
1 #!/usr/bin/env python 2 # meshtastic irc relay - developed by acidvegas in python (https://git.acid.vegas/meshtastic) 3 4 import argparse 5 import asyncio 6 import logging 7 import ssl 8 import time 9 10 # 0xEF576MkXA3aEURbCfNn6p0FfZdua4I 11 12 # Formatting Control Characters / Color Codes 13 bold = '\x02' 14 italic = '\x1D' 15 underline = '\x1F' 16 reverse = '\x16' 17 reset = '\x0f' 18 white = '00' 19 black = '01' 20 blue = '02' 21 green = '03' 22 red = '04' 23 brown = '05' 24 purple = '06' 25 orange = '07' 26 yellow = '08' 27 light_green = '09' 28 cyan = '10' 29 light_cyan = '11' 30 light_blue = '12' 31 pink = '13' 32 grey = '14' 33 light_grey = '15' 34 35 36 # Logging Configuration 37 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(funcName)s - %(message)s') 38 39 40 def color(msg: str, foreground: str, background: str = None) -> str: 41 ''' 42 Color a string with the specified foreground and background colors. 43 44 :param msg: The string to color. 45 :param foreground: The foreground color to use. 46 :param background: The background color to use. 47 ''' 48 49 return f'\x03{foreground},{background}{msg}{reset}' if background else f'\x03{foreground}{msg}{reset}' 50 51 52 class Bot(): 53 def __init__(self): 54 self.nickname = 'MESHTASTIC' 55 self.reader = None 56 self.writer = None 57 self.last = time.time() 58 59 60 async def action(self, chan: str, msg: str): 61 ''' 62 Send an ACTION to the IRC server. 63 64 :param chan: The channel to send the ACTION to. 65 :param msg: The message to send to the channel. 66 ''' 67 68 await self.sendmsg(chan, f'\x01ACTION {msg}\x01') 69 70 71 async def raw(self, data: str): 72 ''' 73 Send raw data to the IRC server. 74 75 :param data: The raw data to send to the IRC server. (512 bytes max including crlf) 76 ''' 77 78 await self.writer.write(data[:510].encode('utf-8') + b'\r\n') 79 80 81 async def sendmsg(self, target: str, msg: str): 82 ''' 83 Send a PRIVMSG to the IRC server. 84 85 :param target: The target to send the PRIVMSG to. (channel or user) 86 :param msg: The message to send to the target. 87 ''' 88 89 await self.raw(f'PRIVMSG {target} :{msg}') 90 91 92 async def connect(self): 93 '''Connect to the IRC server.''' 94 95 while True: 96 try: 97 options = { 98 'host' : args.server, 99 'port' : args.port, 100 'limit' : 1024, 101 'ssl' : ssl._create_unverified_context() if args.ssl else None, # TODO: Do not use the args variable here 102 'family' : 2, # AF_INET = 2, AF_INET6 = 10 103 'local_addr' : None 104 } 105 106 self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15) 107 108 await self.raw(f'USER MESHT 0 * :git.acid.vegas/meshtastic') # Static for now 109 await self.raw('NICK ' + self.nickname) 110 111 while not self.reader.at_eof(): 112 data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), 300) 113 await self.handle(data.decode('utf-8').strip()) 114 115 except Exception as ex: 116 logging.error(f'failed to connect to {args.server} ({str(ex)})') 117 118 finally: 119 await asyncio.sleep(15) 120 121 122 async def eventPRIVMSG(self, data: str): 123 ''' 124 Handle the PRIVMSG event. 125 126 :param data: The data received from the IRC server. 127 ''' 128 129 parts = data.split() 130 131 ident = parts[0][1:] 132 nick = parts[0].split('!')[0][1:] 133 target = parts[2] 134 msg = ' '.join(parts[3:])[1:] 135 136 if target == args.channel: # TODO: Don't use the args variable here 137 if msg.startswith('!'): 138 if time.time() - self.last < 3: 139 if not self.slow: 140 self.slow = True 141 await self.sendmsg(target, color('Slow down nerd!', red)) 142 else: 143 self.slow = False 144 parts = msg.split() 145 if parts[0] == '!meshage' and len(parts) > 1: 146 message = ' '.join(parts[1:]) 147 if len(message) > 255: 148 await self.sendmsg(target, color('Message exceeds 255 bytes nerd!', red)) 149 # TODO: Send a meshtastic message (We have to ensure our outbounds from IRC don't loop back into IRC) 150 151 self.last = time.time() # Update the last command time if it starts with ! character to prevent command flooding 152 153 154 async def handle(self, data: str): 155 ''' 156 Handle the data received from the IRC server. 157 158 :param data: The data received from the IRC server. 159 ''' 160 161 logging.info(data) 162 163 try: 164 parts = data.split() 165 166 if parts[0] == 'PING': 167 await self.raw('PONG ' + parts[1]) 168 169 elif parts[1] == '001': # RPL_WELCOME 170 await self.raw(f'MODE {self.nickname} +B') 171 await self.sendmsg('NickServ', f'IDENTIFY {self.nickname} simps0nsfan420') 172 await asyncio.sleep(10) # Wait for NickServ to identify or any channel join delays 173 await self.raw(f'JOIN {args.channel} {args.key if args.key else ""}') 174 175 elif parts[1] == '433': # ERR_NICKNAMEINUSE 176 self.nickname += '_' # revamp this to be more unique 177 await self.raw('NICK ' + self.nickname) 178 179 elif parts[1] == 'INVITE': 180 target = parts[2] 181 chan = parts[3][1:] 182 if target == self.nickname and chan == args.channel: 183 await self.raw(f'JOIN {chan}') 184 185 elif parts[1] == 'KICK': 186 chan = parts[2] 187 kicked = parts[3] 188 if kicked == self.nickname and chan == args.channel: 189 await asyncio.sleep(3) 190 await self.raw(f'JOIN {args.channel} {args.key if args.key else ""}') 191 192 elif parts[1] == 'PRIVMSG': 193 await self.eventPRIVMSG(data) # We put this in a separate function since it will likely be the most used/handled event 194 195 except (UnicodeDecodeError, UnicodeEncodeError): 196 pass 197 198 except Exception as ex: 199 logging.exception(f'Unknown error has occured! ({ex})') 200 201 202 203 if __name__ == '__main__': 204 parser = argparse.ArgumentParser(description='Connect to an IRC server.') 205 parser.add_argument('server', help='The IRC server address.') 206 parser.add_argument('channel', help='The IRC channel to join.') 207 parser.add_argument('--port', type=int, help='The port number for the IRC server.') 208 parser.add_argument('--ssl', action='store_true', help='Use SSL for the connection.') 209 parser.add_argument('--key', default='', help='The key (password) for the IRC channel, if required.') 210 args = parser.parse_args() 211 212 if not args.channel.startswith('#'): 213 channel = '#' + args.channel 214 215 if not args.port: 216 args.port = 6697 if args.ssl else 6667 217 elif args.port < 1 or args.port > 65535: 218 raise ValueError('Port must be between 1 and 65535.') 219 220 print(f'Connecting to {args.server}:{args.port} (SSL: {args.ssl}) and joining {args.channel} (Key: {args.key or 'None'})') 221 222 bot = Bot() 223 224 asyncio.run(bot.connect())