Pull me. #8

Closed
Yuuki wants to merge 7 commits from (deleted):yuuki-dev into master
9 changed files with 86 additions and 230 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ data
logs
test.json
elitebot
.idea

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "plugins"]
path = plugins
url = https://git.raiza.dev/Raiza.dev/EliteBot-Plugins.git

View file

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

1
plugins Submodule

@ -0,0 +1 @@
Subproject commit bc8424c1c8a01994cf04836ba589dfceea90fec1

View file

View file

@ -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')

View file

@ -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]}.'

View file

@ -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,98 @@ 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]:
await handle_sasl(self.config, self.ircsend)
case 'AUTHENTICATE':
await 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 asyncio.sleep(1)
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 +222,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:

View file

@ -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,16 +31,16 @@ 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')
def handle_903(ircsend):
async def handle_903(ircsend):
"""
Handles the 903 command by sending a CAP END command.
Parameters:
ircsend (function): Function to send IRC commands
"""
ircsend('CAP END')
await ircsend('CAP END')