Pull me. #8
9 changed files with 86 additions and 230 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,4 +2,5 @@ __pycache__
|
|||
data
|
||||
logs
|
||||
test.json
|
||||
elitebot
|
||||
elitebot
|
||||
.idea
|
||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "plugins"]
|
||||
path = plugins
|
||||
url = https://git.raiza.dev/Raiza.dev/EliteBot-Plugins.git
|
|
@ -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
1
plugins
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit bc8424c1c8a01994cf04836ba589dfceea90fec1
|
|
@ -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')
|
|
@ -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]}.'
|
135
src/bot.py
135
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,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:
|
||||
|
|
12
src/sasl.py
12
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,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')
|
||||
|
|
Loading…
Add table
Reference in a new issue