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

3
.gitignore vendored
View file

@ -2,4 +2,5 @@ __pycache__
data data
logs logs
test.json test.json
elitebot 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 #!/usr/bin/env python3
import asyncio
import sys import sys
from src.bot import Bot from src.bot import Bot
@ -22,7 +23,7 @@ def main():
try: try:
print('EliteBot started successfully!') print('EliteBot started successfully!')
bot.start() asyncio.run(bot.start())
except Exception as e: except Exception as e:
print(f'Error starting EliteBot: {e}') print(f'Error starting EliteBot: {e}')
sys.exit(1) 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 #!/usr/bin/env python3
import asyncio
import importlib.util import importlib.util
import inspect import inspect
import json import json
import os import os
import socket
import ssl import ssl
import sys import sys
import time
import yaml import yaml
from src.channel_manager import ChannelManager from src.channel_manager import ChannelManager
@ -24,7 +24,8 @@ class Bot:
self.channel_manager = ChannelManager() self.channel_manager = ChannelManager()
self.logger = Logger('logs/elitebot.log') self.logger = Logger('logs/elitebot.log')
self.connected = False self.connected = False
self.ircsock = None self.reader = None
self.writer = None
self.running = True self.running = True
self.plugins = [] self.plugins = []
self.load_plugins() self.load_plugins()
@ -32,7 +33,7 @@ class Bot:
def validate_config(self, config): def validate_config(self, config):
required_fields = [ required_fields = [
['Connection', 'Port'], ['Connection', 'Port'],
['Connection' 'Hostname'], ['Connection', 'Hostname'],
['Connection', 'Nick'], ['Connection', 'Nick'],
['Connection', 'Ident'], ['Connection', 'Ident'],
['Connection', 'Name'], ['Connection', 'Name'],
@ -92,11 +93,12 @@ class Bot:
self.logger.error('Could not decode byte string with any known encoding') self.logger.error('Could not decode byte string with any known encoding')
return bytes.decode('utf-8', 'ignore') return bytes.decode('utf-8', 'ignore')
def ircsend(self, msg): async def ircsend(self, msg):
try: try:
if msg != '': if msg != '':
self.logger.info(f'Sending command: {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: except Exception as e:
self.logger.error(f'Error sending IRC message: {e}') self.logger.error(f'Error sending IRC message: {e}')
raise raise
@ -120,91 +122,98 @@ class Bot:
args.append(' '.join(parts[trailing_arg_start:])[1:]) args.append(' '.join(parts[trailing_arg_start:])[1:])
return source, command, args return source, command, args
def connect(self): async def connect(self):
try: try:
self.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssl_context = None
if str(self.config['Connection'].get('Port'))[:1] == '+': if str(self.config['Connection'].get('Port'))[:1] == '+':
context = ssl.create_default_context() ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) # Corrected here
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'))
if 'BindHost' in self.config: self.reader, self.writer = await asyncio.open_connection(
self.ircsock.bind((self.config['Connection'].get('BindHost'), 0)) 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)) await self.ircsend(f'NICK {self.config["Connection"].get("Nick")}')
self.ircsend(f'NICK {self.config["Connection"].get("Nick")}') await self.ircsend(
self.ircsend(f'USER {self.config["Connection"].get("Ident")} * * :{self.config["Connection"].get("Name")}') f'USER {self.config["Connection"].get("Ident")} * * :{self.config["Connection"].get("Name")}')
if self.config['SASL'].get('UseSASL'): if self.config['SASL'].get('UseSASL'):
self.ircsend('CAP REQ :sasl') await self.ircsend('CAP REQ :sasl')
except Exception as e: except Exception as e:
self.logger.error(f'Error establishing connection: {e}') self.logger.error(f'Error establishing connection: {e}')
self.connected = False self.connected = False
return return
def start(self): async def start(self):
while True: while True:
if not self.connected: if not self.connected:
try: try:
self.connect() await self.connect()
self.connected = True self.connected = True
except Exception as e: except Exception as e:
self.logger.error(f'Connection error: {e}') self.logger.error(f'Connection error: {e}')
time.sleep(60) await asyncio.sleep(60)
continue continue
try: try:
recvText = self.ircsock.recv(2048) recvText = await self.reader.read(2048)
if not recvText: if not recvText:
self.connected = False self.connected = False
continue continue
ircmsg = self.decode(recvText) 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': if '\r\n' in ircmsg:
nospoof = args[0][1:] if args[0].startswith(':') else args[0] messages = ircmsg.split('\r\n')
self.ircsend(f'PONG :{nospoof}') elif '\n' in ircmsg:
continue messages = ircmsg.split('\n')
else:
messages = [ircmsg] # If no newline characters, treat the whole message as a single message
if command == 'PRIVMSG': for message in messages:
channel, message = args[0], args[1] if message: # Check if message is not empty
source_nick = source.split('!')[0] source, command, args = self.parse_message(message)
if message.startswith('&'): self.logger.debug(f'Received: source: {source} | command: {command} | args: {args}')
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)
elif command == 'CAP' and args[1] == 'ACK' and 'sasl' in args[2]: match command:
handle_sasl(self.config, self.ircsend) 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': if message.startswith('&'):
handle_authenticate(args, self.config, self.ircsend) cmd, *cmd_args = message[1:].split()
self.handle_command(source_nick, channel, cmd, cmd_args)
elif command == '903': elif args[1].startswith('\x01VERSION\x01'):
handle_903(self.ircsend) source_nick = source.split('!')[0]
await self.ircsend(f'NOTICE {source_nick} :\x01VERSION EliteBot 0.1\x01')
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')
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: except Exception as e:
self.logger.error(f'General error: {e}') self.logger.error(f'General error: {e}')
self.connected = False self.connected = False
@ -213,7 +222,7 @@ class Bot:
if __name__ == '__main__': if __name__ == '__main__':
try: try:
bot = Bot(sys.argv[1]) bot = Bot(sys.argv[1])
bot.start() asyncio.run(bot.start())
except KeyboardInterrupt: except KeyboardInterrupt:
print('\nEliteBot has been stopped.') print('\nEliteBot has been stopped.')
except Exception as e: except Exception as e:

View file

@ -5,7 +5,7 @@ NULL_BYTE = '\x00'
ENCODING = 'UTF-8' ENCODING = 'UTF-8'
def handle_sasl(config, ircsend): async def handle_sasl(config, ircsend):
""" """
Handles SASL authentication by sending an AUTHENTICATE command. Handles SASL authentication by sending an AUTHENTICATE command.
@ -13,10 +13,10 @@ def handle_sasl(config, ircsend):
config (dict): Configuration dictionary config (dict): Configuration dictionary
ircsend (function): Function to send IRC commands 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. Handles the AUTHENTICATE command response.
@ -31,16 +31,16 @@ def handle_authenticate(args, config, ircsend):
f'{config["SASL"]["SASLNick"]}{NULL_BYTE}' f'{config["SASL"]["SASLNick"]}{NULL_BYTE}'
f'{config["SASL"]["SASLPassword"]}') f'{config["SASL"]["SASLPassword"]}')
ap_encoded = base64.b64encode(authpass.encode(ENCODING)).decode(ENCODING) ap_encoded = base64.b64encode(authpass.encode(ENCODING)).decode(ENCODING)
ircsend(f'AUTHENTICATE {ap_encoded}') await ircsend(f'AUTHENTICATE {ap_encoded}')
else: else:
raise KeyError('SASLNICK and/or SASLPASS not found in config') 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. Handles the 903 command by sending a CAP END command.
Parameters: Parameters:
ircsend (function): Function to send IRC commands ircsend (function): Function to send IRC commands
""" """
ircsend('CAP END') await ircsend('CAP END')