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())