From 2a4a630752b65287c1666f30cfc5aa66b42aafd6 Mon Sep 17 00:00:00 2001 From: Yuuki Chan Date: Wed, 21 Feb 2024 21:13:36 +0900 Subject: [PATCH 1/7] Update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1899225..1b882a1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __pycache__ data logs test.json -elitebot \ No newline at end of file +elitebot +.idea -- 2.34.1 From c91e763619b26fbd9efe6da5336d3db5880a4285 Mon Sep 17 00:00:00 2001 From: Yuuki Chan Date: Wed, 21 Feb 2024 21:13:49 +0900 Subject: [PATCH 2/7] Remove plugins folder. --- plugins/__init__.py | 0 plugins/commands.py | 59 -------------------------- plugins/cookie.py | 100 -------------------------------------------- 3 files changed, 159 deletions(-) delete mode 100644 plugins/__init__.py delete mode 100644 plugins/commands.py delete mode 100644 plugins/cookie.py diff --git a/plugins/__init__.py b/plugins/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/commands.py b/plugins/commands.py deleted file mode 100644 index 47e145a..0000000 --- a/plugins/commands.py +++ /dev/null @@ -1,59 +0,0 @@ -import sys - -from src.channel_manager import ChannelManager -from src.plugin_base import PluginBase - - -class Plugin(PluginBase): - def handle_message(self, source_nick, channel, message): - message_parts = message.split() - self.channel_manager = ChannelManager() - if message_parts[0] == '!hello': - self.bot.ircsend(f'PRIVMSG {channel} :Hello, {source_nick}!') - - elif message_parts[0] == '!join': - if len(message_parts) == 0: - self.bot.ircsend(f'PRIVMSG {channel} :Please specify a channel to join') - return - else: - self.channel_manager.save_channel(message_parts[1]) - self.bot.ircsend(f'JOIN {message_parts[1]}') - - elif message_parts[0] == '!part': - if len(message_parts) == 1: - self.bot.ircsend(f'PART {channel}') - self.channel_manager.remove_channel(channel) - else: - self.bot.ircsend(f'PART {message_parts[1]}') - self.channel_manager.remove_channel(message_parts[1]) - - elif message_parts[0] == '!quit': - if len(message_parts) == 0: - quit_message = 'EliteBot!' - else: - quit_message = message[len(message_parts[0]) + 1:] - self.bot.ircsend(f'QUIT :{quit_message}') - self.bot.ircsock.close() - self.bot.connected = False - sys.exit() - - elif message_parts[0] == '!raw': - if len(message_parts) > 1: - if message_parts[1].upper() == 'PRIVMSG' and len(message_parts) > 3: - raw_command = ' '.join(message_parts[1:3]) + " :" + ' '.join(message_parts[3:]) - else: - raw_command = ' '.join(message_parts[1:]) - self.bot.ircsend(raw_command) - - elif message_parts[0] == '!me': - if len(message_parts) > 1: - action_message = ' '.join(message_parts[1:]) - self.bot.ircsend(f'PRIVMSG {channel} :\x01ACTION {action_message}\x01') - else: - self.bot.ircsend(f'PRIVMSG {channel} :Please specify an action') - - elif message_parts[0] == '!ping': - if len(message_parts) > 1: - self.bot.ircsend(f'PRIVMSG {channel} :Pinging {message_parts[1]}') - else: - self.bot.ircsend(f'PRIVMSG {channel} :Please specify a nick to ping') diff --git a/plugins/cookie.py b/plugins/cookie.py deleted file mode 100644 index d7926a5..0000000 --- a/plugins/cookie.py +++ /dev/null @@ -1,100 +0,0 @@ -import random -import time -from datetime import datetime - -from sqlalchemy import Table, Column, Integer, String, MetaData, insert, select - -from src.channel_manager import ChannelManager -from src.db import Database -from src.plugin_base import PluginBase - -meta = MetaData() -cookie_table = Table( - 'Cookie', - meta, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('name', String, unique=True, nullable=False), - Column('cookies', Integer, default=0), - Column('last', String, default='1999/01/01 00:00:00'), -) -c_db = Database(cookie_table, meta) - - -class Plugin(PluginBase): - def handle_message(self, source_nick, channel, message): - parts = message.split() - c_db.create_table('Cookie') - self.channel_manager = ChannelManager() - - if parts[0].lower() == '!cookie': - if len(parts) == 1: - self.insert_user(source_nick) - - cookies = c_db.get(source_nick, 2) - rnd = random.randint(1, 10) - last = datetime.strptime(c_db.get(source_nick, 3), '%Y/%m/%d %H:%M:%S') - current = datetime.strptime(datetime.now().strftime('%Y/%m/%d %H:%M:%S'), '%Y/%m/%d %H:%M:%S') - diff = round((current - last).total_seconds() / 60.0) - - if diff >= 30: - c1 = 'no cookies' if rnd == 0 \ - else f'{rnd} cookie' if rnd == 1 \ - else f'{rnd} cookies' - c2 = 'no cookies' if (cookies + rnd) == 0 \ - else f'{(cookies + rnd)} cookie' if (cookies + rnd) == 1 \ - else f'{(cookies + rnd)} cookies' - - self.bot.ircsend(f'PRIVMSG {channel} :\x01ACTION gives {c1} to {source_nick}.\x01') - self.bot.ircsend(f'PRIVMSG {channel} :You now have a total of {c2}.') - - c_db.set(source_nick, { - 'cookies': (cookies + rnd), - 'last': current.strftime('%Y/%m/%d %H:%M:%S') - }) - else: - rem = self.remaining_time(last.strftime('%Y/%m/%d %H:%M:%S'), 30 * 60000) - self.bot.ircsend(f'PRIVMSG {channel} :Remaining time: {rem}') - elif len(parts) == 2: - cookies = c_db.get(parts[1], 2) - - if cookies == -1: - self.bot.ircsend(f'PRIVMSG {channel} :I\'ve looked everywhere for {parts[1]}, but I couldn\'t ' - f'find them.') - else: - c = 'no cookies' if cookies == 0 \ - else f'{cookies} cookie' if cookies == 1 \ - else f'{cookies} cookies' - self.bot.ircsend(f'PRIVMSG {channel} :{parts[1]} currently has {c}.') - - def insert_user(self, user: str): - with c_db.engine.connect() as conn: - stmt = select(cookie_table).where(cookie_table.c.name == user) - cnt = len(conn.execute(stmt).fetchall()) - - if cnt == 0: - conn.execute(( - insert(cookie_table). - values({'name': user}) - )) - conn.commit() - - def remaining_time(self, date: str, timeout: int): - diff = (int(time.mktime( - datetime.strptime(datetime.now().strftime('%Y/%m/%d %H:%M:%S'), '%Y/%m/%d %H:%M:%S').timetuple()) * 1000) - - int(time.mktime(datetime.strptime(date, '%Y/%m/%d %H:%M:%S').timetuple()) * 1000)) - h = int((timeout - diff) / (60 * 60 * 1000) % 24) - m = int((timeout - diff) / (60 * 1000) % 60) - s = int((timeout - diff) / 1000 % 60) - hms = '' - - if h == 0 and m == 0 and s == 0: - return 0 - - if h != 0: - hms += f'{h}h ' - if m != 0: - hms += f'{m}m ' - if s != 0: - hms += f'{s}s ' - - return f'{hms[:-1]}.' \ No newline at end of file -- 2.34.1 From 68cfe5dffe3035787f2668b513b2b2e0ca3e9af2 Mon Sep 17 00:00:00 2001 From: Yuuki Chan Date: Wed, 21 Feb 2024 21:16:03 +0900 Subject: [PATCH 3/7] Add plugins submodule. --- .gitmodules | 3 +++ plugins | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 plugins diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f9e4411 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "plugins"] + path = plugins + url = https://git.raiza.dev/Raiza.dev/EliteBot-Plugins.git diff --git a/plugins b/plugins new file mode 160000 index 0000000..f11f90e --- /dev/null +++ b/plugins @@ -0,0 +1 @@ +Subproject commit f11f90e01d91fc37859c1451c2b0599779ba9c1f -- 2.34.1 From cb9a52d90a9f2ab6788e06e609ec4fd3e19ecf17 Mon Sep 17 00:00:00 2001 From: Yuuki Chan Date: Wed, 21 Feb 2024 21:43:37 +0900 Subject: [PATCH 4/7] Updated plugins submodule. --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index f11f90e..b1f6ca2 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit f11f90e01d91fc37859c1451c2b0599779ba9c1f +Subproject commit b1f6ca23c5996a403be8bd12d1ab264dfe3c9a2c -- 2.34.1 From a50a5f019e1eb9d2fae08203e938d23eab196126 Mon Sep 17 00:00:00 2001 From: Yuuki Chan Date: Thu, 22 Feb 2024 18:41:50 +0900 Subject: [PATCH 5/7] Updated and fixed eltiebot.py, bot.py and sasl.py to use asyncio. --- elitebot.py | 3 +- src/bot.py | 134 ++++++++++++++++++++++++++++------------------------ src/sasl.py | 2 +- 3 files changed, 74 insertions(+), 65 deletions(-) diff --git a/elitebot.py b/elitebot.py index 4f2d53a..5bac82c 100755 --- a/elitebot.py +++ b/elitebot.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import asyncio import sys from src.bot import Bot @@ -22,7 +23,7 @@ def main(): try: print('EliteBot started successfully!') - bot.start() + asyncio.run(bot.start()) except Exception as e: print(f'Error starting EliteBot: {e}') sys.exit(1) diff --git a/src/bot.py b/src/bot.py index 635d45d..93cba71 100644 --- a/src/bot.py +++ b/src/bot.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 +import asyncio import importlib.util import inspect import json import os -import socket import ssl import sys -import time + import yaml from src.channel_manager import ChannelManager @@ -24,7 +24,8 @@ class Bot: self.channel_manager = ChannelManager() self.logger = Logger('logs/elitebot.log') self.connected = False - self.ircsock = None + self.reader = None + self.writer = None self.running = True self.plugins = [] self.load_plugins() @@ -32,7 +33,7 @@ class Bot: def validate_config(self, config): required_fields = [ ['Connection', 'Port'], - ['Connection' 'Hostname'], + ['Connection', 'Hostname'], ['Connection', 'Nick'], ['Connection', 'Ident'], ['Connection', 'Name'], @@ -92,11 +93,12 @@ class Bot: self.logger.error('Could not decode byte string with any known encoding') return bytes.decode('utf-8', 'ignore') - def ircsend(self, msg): + async def ircsend(self, msg): try: if msg != '': self.logger.info(f'Sending command: {msg}') - self.ircsock.send(bytes(f'{msg}\r\n', 'UTF-8')) + self.writer.write(bytes(f'{msg}\r\n', 'UTF-8')) + await self.writer.drain() except Exception as e: self.logger.error(f'Error sending IRC message: {e}') raise @@ -120,91 +122,97 @@ class Bot: args.append(' '.join(parts[trailing_arg_start:])[1:]) return source, command, args - def connect(self): + async def connect(self): try: - self.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - + ssl_context = None if str(self.config['Connection'].get('Port'))[:1] == '+': - context = ssl.create_default_context() - self.ircsock = context.wrap_socket(self.ircsock, - server_hostname=self.config['Connection'].get('Hostname')) - port = int(self.config['Connection'].get('Port')[1:]) - else: - port = int(self.config['Connection'].get('Port')) + ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) # Corrected here - if 'BindHost' in self.config: - self.ircsock.bind((self.config['Connection'].get('BindHost'), 0)) + self.reader, self.writer = await asyncio.open_connection( + self.config['Connection'].get('Hostname'), + int(self.config['Connection'].get('Port')[1:]) if ssl_context else int( + self.config['Connection'].get('Port')), + ssl=ssl_context + ) - self.ircsock.connect_ex((self.config['Connection'].get('Hostname'), port)) - self.ircsend(f'NICK {self.config["Connection"].get("Nick")}') - self.ircsend(f'USER {self.config["Connection"].get("Ident")} * * :{self.config["Connection"].get("Name")}') + await self.ircsend(f'NICK {self.config["Connection"].get("Nick")}') + await self.ircsend( + f'USER {self.config["Connection"].get("Ident")} * * :{self.config["Connection"].get("Name")}') if self.config['SASL'].get('UseSASL'): - self.ircsend('CAP REQ :sasl') + await self.ircsend('CAP REQ :sasl') except Exception as e: self.logger.error(f'Error establishing connection: {e}') self.connected = False return - def start(self): + async def start(self): while True: if not self.connected: try: - self.connect() + await self.connect() self.connected = True except Exception as e: self.logger.error(f'Connection error: {e}') - time.sleep(60) + await asyncio.sleep(60) continue try: - recvText = self.ircsock.recv(2048) + recvText = await self.reader.read(2048) if not recvText: self.connected = False continue ircmsg = self.decode(recvText) - source, command, args = self.parse_message(ircmsg) - self.logger.debug(f'Received: source: {source} | command: {command} | args: {args}') - if command == 'PING': - nospoof = args[0][1:] if args[0].startswith(':') else args[0] - self.ircsend(f'PONG :{nospoof}') - continue + if '\r\n' in ircmsg: + messages = ircmsg.split('\r\n') + elif '\n' in ircmsg: + messages = ircmsg.split('\n') + else: + messages = [ircmsg] # If no newline characters, treat the whole message as a single message - if command == 'PRIVMSG': - channel, message = args[0], args[1] - source_nick = source.split('!')[0] - if message.startswith('&'): - cmd, *cmd_args = message[1:].split() - self.handle_command(source_nick, channel, cmd, cmd_args) - for plugin in self.plugins: - plugin.handle_message(source_nick, channel, message) + for message in messages: + if message: # Check if message is not empty + source, command, args = self.parse_message(message) + self.logger.debug(f'Received: source: {source} | command: {command} | args: {args}') - elif command == 'CAP' and args[1] == 'ACK' and 'sasl' in args[2]: - handle_sasl(self.config, self.ircsend) + match command: + case 'PING': + nospoof = args[0][1:] if args[0].startswith(':') else args[0] + await self.ircsend(f'PONG :{nospoof}') + continue + case 'PRIVMSG': + channel, message = args[0], args[1] + source_nick = source.split('!')[0] - elif command == 'AUTHENTICATE': - handle_authenticate(args, self.config, self.ircsend) - - elif command == '903': - handle_903(self.ircsend) - - if command == 'PRIVMSG' and args[1].startswith('\x01VERSION\x01'): - source_nick = source.split('!')[0] - self.ircsend(f'NOTICE {source_nick} :\x01VERSION EliteBot 0.1\x01') - - if command == '001': - for channel in self.channel_manager.get_channels(): - self.ircsend(f'JOIN {channel}') - - if command == 'INVITE': - channel = args[1] - self.ircsend(f'JOIN {channel}') - self.channel_manager.save_channel(channel) - - if command == 'VERSION': - self.ircsend('NOTICE', f'{source_nick} :I am a bot version 1.0.0') + if message.startswith('&'): + cmd, *cmd_args = message[1:].split() + self.handle_command(source_nick, channel, cmd, cmd_args) + elif args[1].startswith('\x01VERSION\x01'): + source_nick = source.split('!')[0] + await self.ircsend(f'NOTICE {source_nick} :\x01VERSION EliteBot 0.1\x01') + for plugin in self.plugins: + await plugin.handle_message(source_nick, channel, message) + case 'CAP': + if args[1] == 'ACK' and 'sasl' in args[2]: + handle_sasl(self.config, self.ircsend) + case 'AUTHENTICATE': + handle_authenticate(args, self.config, self.ircsend) + case 'INVITE': + channel = args[1] + await self.ircsend(f'JOIN {channel}') + self.channel_manager.save_channel(channel) + case 'VERSION': + await self.ircsend(f'NOTICE {source_nick} :I am a bot version 1.0.0') + case '001': + await self.ircsend('JOIN #YuukiTest') + # for channel in self.channel_manager.get_channels(): + # await self.ircsend(f'JOIN {channel}') + case '903': + await handle_903(self.ircsend) + case _: + continue except Exception as e: self.logger.error(f'General error: {e}') self.connected = False @@ -213,7 +221,7 @@ class Bot: if __name__ == '__main__': try: bot = Bot(sys.argv[1]) - bot.start() + asyncio.run(bot.start()) except KeyboardInterrupt: print('\nEliteBot has been stopped.') except Exception as e: diff --git a/src/sasl.py b/src/sasl.py index 513bf93..3581224 100644 --- a/src/sasl.py +++ b/src/sasl.py @@ -36,7 +36,7 @@ def handle_authenticate(args, config, ircsend): raise KeyError('SASLNICK and/or SASLPASS not found in config') -def handle_903(ircsend): +async def handle_903(ircsend): """ Handles the 903 command by sending a CAP END command. -- 2.34.1 From 3cb3a43f4ed2a900fa4700e4f9f266cd1bd0bda5 Mon Sep 17 00:00:00 2001 From: Yuuki Chan Date: Thu, 22 Feb 2024 18:43:45 +0900 Subject: [PATCH 6/7] Updated plugins. --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index b1f6ca2..bc8424c 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit b1f6ca23c5996a403be8bd12d1ab264dfe3c9a2c +Subproject commit bc8424c1c8a01994cf04836ba589dfceea90fec1 -- 2.34.1 From 71b10eaba165542a1bc1a81c2922c415bb338447 Mon Sep 17 00:00:00 2001 From: Yuuki Chan Date: Thu, 22 Feb 2024 19:34:28 +0900 Subject: [PATCH 7/7] Updated bot.py and sasl.py --- src/bot.py | 5 +++-- src/sasl.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/bot.py b/src/bot.py index 93cba71..0c1c92e 100644 --- a/src/bot.py +++ b/src/bot.py @@ -196,9 +196,9 @@ class Bot: await plugin.handle_message(source_nick, channel, message) case 'CAP': if args[1] == 'ACK' and 'sasl' in args[2]: - handle_sasl(self.config, self.ircsend) + await handle_sasl(self.config, self.ircsend) case 'AUTHENTICATE': - handle_authenticate(args, self.config, self.ircsend) + await handle_authenticate(args, self.config, self.ircsend) case 'INVITE': channel = args[1] await self.ircsend(f'JOIN {channel}') @@ -206,6 +206,7 @@ class Bot: case 'VERSION': await self.ircsend(f'NOTICE {source_nick} :I am a bot version 1.0.0') case '001': + await asyncio.sleep(1) await self.ircsend('JOIN #YuukiTest') # for channel in self.channel_manager.get_channels(): # await self.ircsend(f'JOIN {channel}') diff --git a/src/sasl.py b/src/sasl.py index 3581224..c32666f 100644 --- a/src/sasl.py +++ b/src/sasl.py @@ -5,7 +5,7 @@ NULL_BYTE = '\x00' ENCODING = 'UTF-8' -def handle_sasl(config, ircsend): +async def handle_sasl(config, ircsend): """ Handles SASL authentication by sending an AUTHENTICATE command. @@ -13,10 +13,10 @@ def handle_sasl(config, ircsend): config (dict): Configuration dictionary ircsend (function): Function to send IRC commands """ - ircsend('AUTHENTICATE PLAIN') + await ircsend('AUTHENTICATE PLAIN') -def handle_authenticate(args, config, ircsend): +async def handle_authenticate(args, config, ircsend): """ Handles the AUTHENTICATE command response. @@ -31,7 +31,7 @@ def handle_authenticate(args, config, ircsend): f'{config["SASL"]["SASLNick"]}{NULL_BYTE}' f'{config["SASL"]["SASLPassword"]}') ap_encoded = base64.b64encode(authpass.encode(ENCODING)).decode(ENCODING) - ircsend(f'AUTHENTICATE {ap_encoded}') + await ircsend(f'AUTHENTICATE {ap_encoded}') else: raise KeyError('SASLNICK and/or SASLPASS not found in config') @@ -43,4 +43,4 @@ async def handle_903(ircsend): Parameters: ircsend (function): Function to send IRC commands """ - ircsend('CAP END') + await ircsend('CAP END') -- 2.34.1