Compare commits

..

No commits in common. "main" and "swee-patch-1" have entirely different histories.

9 changed files with 473 additions and 784 deletions

View file

@ -7,4 +7,4 @@ jobs:
with:
additional: python3
- uses: actions/checkout@v4
- run: python -m compileall .
- run: python -m py_compile server.py

View file

@ -9,6 +9,9 @@ host: 127.0.0.1
# The identifier for this server, such as the location (Used in whois)
identifier: somewhere in the universe
# The nickserv account to automatically be IRC opped
admin-nick: admin
# The path of the data file to be used by NickServ, ChanServ, etc
# This MUST be a file path.
# Recommended to use a .db extension because it is an SQLite database
@ -42,21 +45,9 @@ ban-provider: /path/to/bans.txt
# ban-provider: sql
# Mail server settings for PawServ
smtp_host: smtp.example.com
smtp_port: 25
smtp_starttls: off
smtp_username: pawserv@example.com
smtp_password: examplePassword
# If you setup a webchat, enter the passphrase for it.
webirc_pass: helloworld
# Use of modules in the /modules folder, or in an absolute path specified.
# You want your protection modules BEFORE the ban engine.
modules:
- sqlite_local
- botnet_protect
- ban_engine
- pawserv

View file

@ -2,13 +2,10 @@ import threading
__ircat_type__ = "allsocket"
__ircat_requires__ = ["ban-provider"]
__ircat_giveme__ = ["sql"] # Only command and allsocket have these.
__ircat_fakechannels__ = {"#IRCATSUCKS": "B0tn3t pr0t3ct10n, do not join."} # Fake channels that plugins control.
__ircat_fakechannels__ = {"#IRCATSUCKS": "WHATEVER YOU DO, DO NOT JOIN IF YOU ARE HUMAN"}
class IRCatModule:
sus_strings = [
# Known SupernetS botnet texts
# Contribute here: https://discuss.swee.codes/t/61
" .''.", # 2025 new year
"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀" # "The United States of America" LATEST
" .''." # Latest Supernets spambot!
]
memory = {} # {nick: {channel: trustlevel}} one can also be {nick: True} if it is whitelisted for the session.
useSQLengine = False
@ -18,27 +15,27 @@ class IRCatModule:
self.useSQLengine = True
self.SQLengine = sql
def ban(self, ip):
del self.memory[ip] # Forget this all happened
# Add the ban
del self.memory[ip]
if self.useSQLengine:
self.SQLengine.addban(ip, "Botnet detected!") # Use the SQL provider if {'ban-provider': 'sql'}
cur = self.SQLengine.conn.cursor()
else:
open(self.ban_provider, "a").write(f"\n{ip} Botnet detected!") # Else, write on the banfile.
raise Exception("Botnet detected!") # Kill the connection
open(self.ban_provider, "a").write(f"\n{ip} Botnet detected!")
raise Exception("Botnet detected!")
def onSocket(self, ip, socket, value, cachedNick=None, validated=False):
if cachedNick != None:
print(value)
if "JOIN" in value:
target = value.split(" ")[1]
self.memory[ip] = 1 # 1: Just joined the channel, continue observing.
print("Autoban> Somebody joined " + target)
if target.lower() == "#ircatsucks":
self.ban(ip) # Ruh roh
self.ban(ip)
elif "PRIVMSG" in value:
if not (ip in self.memory and self.memory[ip] == 0): # Continue observing
if not (ip in self.memory and self.memory[ip] == 0):
target = value.split(" ")[1]
content = " ".join(value.split(" ")[2:])[1:]
if content in self.sus_strings:
if ip in self.memory: # Hey stinky! YOU'RE BANNED
if ip in self.memory:
if self.memory[ip] == 1:
self.ban(ip)
else:

View file

@ -1,55 +1,12 @@
import os, traceback
from cryptography.fernet import Fernet
from cloudflare import Cloudflare # Please make sure you install this module from pip, not package manager.
import requests, os
__ircat_type__ = "sql.provider" # The type of module
__ircat_requires__ = ["cf_accountid", "cf_apitoken", "cf_d1database", "fernet-key"] # The required config.yml entries.
__ircat_requires__ = ["cf_accountid", "cf_apitoken", "cf_d1database"] # The required config.yml entries.
class broker:
def __init__(self, cf_accountid:str, cf_apitoken:str, cf_d1database:str, fernet_key:str):
def __init__(self, cf_accountid, cf_apitoken, cf_d1database):
self.account_id = cf_accountid
self.api_token = cf_apitoken
self.database = cf_d1database
self.client = Cloudflare(api_token=cf_apitoken)
self.fnet = Fernet(fernet_key)
self.client.d1.database.query(
database_id=self.database,
account_id=self.account_id,
sql="CREATE table IF NOT EXISTS bans (ip varchar(255), reason varchar(255)); CREATE table IF NOT EXISTS nickserv (user varchar(255), modes varchar(255), hash varchar(255), email varchar(255)); CREATE table IF NOT EXISTS groups (name varchar(255), owner varchar(255)); CREATE table IF NOT EXISTS chanserv (name varchar(255), modes varchar(255), params varchar(255), owner varchar(255), usermodes varchar(255), optimodes varchar(255))",
)
def cfexec(self, command:str, params:list):
query = self.client.d1.database.query(
database_id=self.database,
account_id=self.account_id,
sql=command,
params=params
)
return query[0].results
def parse2sqlite(self, results):
temp = []
for k, v in results.items():
temp.append(v)
return temp
def nickserv_identify(self, nick, password:str):
f = self.cfexec("SELECT * FROM groups WHERE name=?;", [nick])
if len(f) != 0:
nick = f[0]["owner"]
e = self.cfexec("SELECT * FROM nickserv WHERE user=?;", [nick])
if len(e) == 0:
return False
else:
try:
return self.parse2sqlite(e[0]) if self.fnet.decrypt(bytes(e[0]["hash"], "UTF-8")).decode() == password else False
except:
print(traceback.format_exc())
return False
def nickserv_register(self, nick, password, email):
hashed = self.fnet.encrypt(bytes(password, "UTF-8")).decode()
e = self.cfexec("INSERT INTO nickserv values(?, 'iw', ?, ?);", [nick, hashed, email])
def nickserv_isexist(self, nick):
e = self.cfexec("SELECT * FROM nickserv WHERE user=?;", [nick])
f = self.cfexec("SELECT * FROM groups WHERE name=?;", [nick])
return len(e) != 0 or len(f) != 0
def nickserv_group(self, nick, account):
self.cfexec("INSERT INTO groups VALUES (?, ?);", [nick.lower(), account.lower()])
def nickserv_drop(self, account):
self.cfexec("DELETE FROM nickserv WHERE user=?;", [account.lower()])
self.cfexec("DELETE FROM groups WHERE owner=?;", [account.lower()])
self.base_url = f"https://api.cloudflare.com/client/v4/accounts/{self.account_id}/d1/database"
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_token}"
}

View file

@ -1,123 +0,0 @@
# Replacement for services bots.
import traceback, smtplib, uuid, ssl
__ircat_type__ = "command"
__ircat_requires__ = ["name", "smtp_host", "smtp_port", "smtp_starttls", "smtp_username", "smtp_password", "host"]
__ircat_giveme__ = ["sql"] # Only command and allsocket have these.
__ircat_fakeusers__ = {
"NickServ": {
"host": "PawServ",
"username": "Meow",
"realname": "PawServ plugin - Identification bot",
"modes": "iw",
"away": False,
"identified": False,
"ssl": False
},
"ChanServ": {
"host": "PawServ",
"username": "Meow",
"realname": "PawServ plugin - Channel management bot",
"modes": "iw",
"away": False,
"identified": False,
"ssl": False
}
}
class IRCatModule:
def __init__(self, sql, smtp_host, smtp_port, smtp_starttls, smtp_username, smtp_password, name, host):
self.sql = sql
self.smtp_server = smtp_host
self.smtp_port = smtp_port
self.smtp_starttls = smtp_starttls
self.smtp_username = smtp_username
self.smtp_password = smtp_password
self.net_name = name
self.hostname = host
self.memory = {} # {nick: [authtoken, password, email]}
print("PawServ loaded!")
def command(self, command, args, ip, nick, connection, user):
try:
if command == "NICKSERV" or (command == "PRIVMSG" and args[0].lower() == "nickserv") or command == "PASS":
if command == "PASS":
command = "NICKSERV"
args = ["IDENTIFY", args[0]]
if command == "PRIVMSG":
args = args[1:]
args[0] = args[0][1:] if args[0][0] == ":" else args[0]
if len(args) > 0 and args[0].lower() == "verify":
if len(args) == 3:
if args[1].lower() in self.memory:
if args[2] == self.memory[args[1].lower()][0]:
self.sql.nickserv_register(nick=args[1].lower(), password=self.memory[args[1].lower()][1], email=self.memory[args[1].lower()][2])
nck = args[1].lower()
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Done, you may now identify as {nck}.\r\n", "UTF-8"))
del self.memory[args[1].lower()]
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Invalid verification.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Nickname doesn't exist, try registering again?\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Invalid verification.\r\n", "UTF-8"))
elif len(args) > 0 and args[0].lower() == "group":
if len(args) == 1:
if user["identified"]:
if not self.sql.nickserv_isexist(nick.lower()):
self.sql.nickserv_group(nick, user["identusername"])
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Nickname {nick} already exists.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :You are not logged in.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Does not requre arguments\r\n", "UTF-8"))
elif len(args) > 0 and args[0].lower() == "register":
if not user["identified"]:
if len(args) == 3:
if not self.sql.nickserv_isexist(nick.lower()):
if not nick in self.memory:
context = ssl.create_default_context()
token = str(uuid.uuid4())
message = f"Subject: {self.net_name} Account Verification\n\nHi,\nIt appears you have tried to register an account ({nick}) with this email on {self.net_name},\nIf you did not register an account, feel free to delete this email.\nIf you did, use this command:\n/nickserv verify {nick} {token}"
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
server.ehlo()
if self.smtp_starttls:
server.starttls(context=context)
server.ehlo()
server.login(self.smtp_username, self.smtp_password)
server.sendmail(self.smtp_username, args[2], message)
self.memory[nick.lower()] = [token, args[1], args[2]]
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Email sent, please verify.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :A verification is already pending.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :The user {nick} already exists.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Needs 3 arguments, nickname, password, and email.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :You're already logged in.\r\n", "UTF-8"))
elif len(args) > 0 and args[0].lower() == "identify":
if not user["identified"]:
nck = nick if len(args) == 2 else args[2]
temp = self.sql.nickserv_identify(nick=nck.lower(), password=args[1])
if temp != False:
hostmask = user["host"]
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :You are now identified as {nck}.\r\n", "UTF-8"))
connection.sendall(bytes(f":{self.hostname} 900 {nick} {hostmask} {nck} :You are now logged in as {nck}.\r\n", "UTF-8"))
return {"success": True, "identify": temp}
else:
if nick.lower() in self.memory:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Your account isn't verified, please verify now.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Invalid username/password.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :You're already logged in.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :NickServ Usage:\r\n","UTF-8"))
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :IDENTIFY pass <nick> - Identifies your nickname\r\n","UTF-8"))
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :REGISTER pass email - Register your nickname\r\n","UTF-8"))
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :GROUP - Allows you to sign in to your account with different nicknames\r\n","UTF-8"))
return {"success": True}
else:
return {"success": False}
except:
print(traceback.format_exc())
return {"success": False}

View file

@ -1,6 +1,6 @@
# IRCat module for local SQLite database (default)
import sqlite3, os, traceback
from cryptography.fernet import Fernet
from cryptography import Fernet
__ircat_type__ = "sql.provider" # The type of module
__ircat_requires__ = ["data-path", "fernet-key"] # The required config.yml entries.
class broker:
@ -8,46 +8,22 @@ class broker:
if not os.path.isfile(data_path):
print("Creating database file...")
open(data_path, "w").write("")
self.conn = sqlite3.connect(data_path, check_same_thread=False)
self.conn = sqlite3.connect(data_path)
self.fnet = Fernet(fernet_key)
db = self.conn.cursor()
db.execute("""CREATE table IF NOT EXISTS bans (ip varchar(255), reason varchar(255))""")
db.execute("""CREATE table IF NOT EXISTS nickserv (user varchar(255), modes varchar(255), hash varchar(255), email varchar(255))""")
db.execute("""CREATE table IF NOT EXISTS bans (ip varchar(255), reason varchar(255)""")
db.execute("""CREATE table IF NOT EXISTS nickserv (user varchar(255), modes varchar(255), hash varchar(255), cloak varchar(255))""")
db.execute("""CREATE table IF NOT EXISTS groups (name varchar(255), owner varchar(255))""")
db.execute("""CREATE table IF NOT EXISTS chanserv (name varchar(255), modes varchar(255), params varchar(255), owner varchar(255), usermodes varchar(255), optimodes varchar(255))""")
def nickserv_identify(self, nick, password:str):
db = self.conn.cursor()
db.execute("SELECT * FROM groups WHERE name=?", [nick])
f = db.fetchall()
if f != []:
nick = f[0][1]
db.execute("SELECT * FROM nickserv WHERE user=?;", [nick])
e = db.fetchall()
if e == []:
return False
else:
try:
return e[0] if self.fnet.decrypt(bytes(e[0][2], "UTF-8")).decode() == password else False
return e[0] if self.fnet.decrypt(e[0][2]) == password else False
except:
print(traceback.format_exc())
return False
def nickserv_register(self, nick, password, email):
hashed = self.fnet.encrypt(bytes(password, "UTF-8")).decode()
db = self.conn.cursor()
db.execute("INSERT INTO nickserv values(?, 'iw', ?, ?);", [nick, hashed, email])
self.conn.commit()
def nickserv_isexist(self, nick):
db = self.conn.cursor()
db.execute("SELECT * FROM nickserv WHERE user=?;", [nick.lower()])
e = db.fetchall()
db.execute("SELECT * FROM groups WHERE name=?;", [nick.lower()])
f = db.fetchall()
return e != [] or f != []
def nickserv_group(self, nick, account):
db = self.conn.cursor()
db.execute("INSERT INTO groups VALUES (?, ?);", [nick.lower(), account.lower()])
self.conn.commit()
def nickserv_drop(self, account):
db = self.conn.cursor()
db.execute("DELETE FROM nickserv WHERE user=?;", [account.lower()])
db.execute("DELETE FROM groups WHERE owner=?;", [account.lower()])

View file

@ -1,4 +1,2 @@
cloudflare>=4.0.0
requests
PyOpenSSL
pyyaml

287
server.py
View file

@ -1,9 +1,8 @@
#!/usr/bin/python3
__version__ = "0.0.5"
__version__ = "0.0.4"
print(f"Codename IRCat v{__version__}")
print("Welcome! /ᐠ ˵> ⩊ <˵マ")
import socket, time, ssl, threading, traceback, sys, subprocess, yaml, sqlite3, os, importlib
from OpenSSL import SSL
import socket, ssl, time, threading, traceback, sys, subprocess, yaml, sqlite3, os, bcrypt, importlib
from requests import get
if not len(sys.argv) == 2:
print("IRCat requires the following arguments: config.yml")
@ -24,50 +23,11 @@ This server doesn't have a MOTD in its configuration, or is invalid."""
motd_file = None
ping_timeout = 255
restrict_ip = ''
def isalphanumeric(text:str):
return False
def getident(hostt:str, clientport:int, ssll:bool):
try:
identsender = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
identsender.settimeout(5)
responsee = ""
try:
identsender.connect((hostt, 113))
except Exception as ex:
return {"success": False, "response": f"Could not connect to your ident server: {ex}"}
serverport = "6697" if ssll else "6667"
try:
identsender.sendall(bytes(f"{clientport}, {serverport}\r\n", "UTF-8"))
responsee = identsender.recv(2048).decode().replace(" ", "").replace("\r", "").replace("\n", "")
print(responsee)
except Exception as ex:
return {"success": False, "response": f"Could not send packets to your server: {ex}"}
if "ERROR:NO-USER" in responsee:
return {"success": False, "response": "No user was found by the server."}
elif "ERROR:" in responsee:
return {"success": False, "response": "The ident server had an error."}
elif responsee == "":
return {"success": False, "response": "The connection was closed."}
else:
print(responsee.split(",")[0])
print(responsee.split(",")[1].split(":")[0])
if responsee.split(",")[0] != str(clientport):
return {"success": False, "response": "The ident server sent an invalid client port."}
elif responsee.split(",")[1].split(":")[0] != serverport:
return {"success": False, "response": "The ident server doesn't know what the server port is."}
else:
return {"success": True, "response": responsee.split(",")[1].split(":")[3]}
return {"success": False, "response": "Unknown error."}
except:
print(traceback.format_exc())
return {"success": False, "response": "Unknown error."}
global mods
mods = {"sql_provider": None, "command": [], "allsocket": [], "identified": False, "ssl": False}
mods = {"sql_provider": None, "command": [], "allsocket": []}
with open(sys.argv[1], 'r') as file:
global data2
global data
data = yaml.safe_load(file)
data2 = data
try: server = data["host"]
except: print("using fallback server address")
try: displayname = data["name"]
@ -149,14 +109,11 @@ for i in mods['allsocket']:
print(i.__ircat_fakechannels__)
topic_list = {**topic_list, **i.__ircat_fakechannels__}
for j, v in i.__ircat_fakechannels__.items():
channels_list[j] = ["CatServ"]
channels_list[j] = ["NickServ"]
except Exception as ex:
print(str(ex))
socketListeners.append(i.IRCatModule(**requires))
commandProviders = []
nickname_list = {} # Stores nicknames and the respective sockets
lower_nicks = {"catserv": "CatServ"} # Nicknames in lowercase
property_list = {"CatServ": {"host": "IRCatCore", "username": "Meow", "realname": "Updates bot", "modes": "iw", "away": False}} # Stores properties for active users and channels
for i in mods['command']:
requires = {}
for j in i.__ircat_requires__:
@ -167,6 +124,7 @@ for i in mods['command']:
print(i.__ircat_fakeusers__)
property_list = {**property_list, **i.__ircat_fakeusers__}
for j, v in i.__ircat_fakeusers__.items():
nickname_list.append(j)
lower_nicks[j.lower()] = j
except Exception as ex:
print(str(ex))
@ -177,18 +135,24 @@ sockets_ssl = {}
for i in restrict_ip.split(" "):
sockets[i] = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sockets[i].setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sockets[i].settimeout(None)
sockets[i].bind((i,6667))
sockets[i].listen(1)
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.options |= ssl.OP_NO_TLSv1 # Disable TLS 1.0
context.options |= ssl.OP_NO_TLSv1_1 # Disable TLS 1.1
if ssl_option:
print(f"Loading SSL cert {ssl_cert} with key {ssl_pkey}")
context.load_cert_chain(ssl_cert, keyfile=ssl_pkey)
for i in restrict_ip.split(" "):
sockets_ssl[i] = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sockets_ssl[i].setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sockets_ssl[i].settimeout(None)
sockets_ssl[i].bind((i,6697))
sockets_ssl[i].listen(1)
opened=True
nickname_list = {} # Stores nicknames and the respective sockets
lower_nicks = {"gitserv": "GitServ"} # Nicknames in lowercase
lower_chans = {} # Channel names in lowercase
property_list = {"GitServ": {"host": "IRCatCore", "username": "IRCat", "realname": "Updates bot", "modes": "iw", "away": False}} # Stores properties for active users and channels
def pinger(nick, connection):
global property_list
while nick in property_list:
@ -200,7 +164,6 @@ def pinger(nick, connection):
try:
connection.sendall(bytes(f"PING {server}\r\n","UTF-8"))
except Exception as ex:
print(traceback.format_exc())
property_list[nick]["cause"] = "Send error: " + str(ex)
print("SHUTTING DOWN FOR " + nick)
connection.shutdown(socket.SHUT_WR)
@ -213,7 +176,7 @@ def pinger(nick, connection):
connection.shutdown(socket.SHUT_WR)
connection.close()
break
def session(connection, client, ip, isssl=False):
def session(connection, client, ip, ssl=False):
global property_list
global channels_list
global nickname_list
@ -222,58 +185,31 @@ def session(connection, client, ip, isssl=False):
ready = False # If the client gave the server a USER packet
finished = False # If the server gave the client its information, indicating it's ready.
username = "oreo" # Username/ident specified by client
rident = "~oreo"
hostname = "" # Hostname, can be IP or domain
realname = "realname" # Realname specified by client
safe_quit = False # If the client safely exited, or if the server should manually drop the connection
cause = "Unknown" # The cause of the unexpected exit
usesIRCv3 = False
CAPEND = False
clident = None
pendingCommands = "" # list of commands that were executed before verification
unfinished = False
textt = ""
try:
print("Connected to client IP: {}".format(client))
connection.sendall(bytes(f":{server} NOTICE * :*** Looking for your hostname...\r\n","UTF-8"))
connection.sendall(bytes(f":{server} NOTICE * :*** Checking your ident...\r\n","UTF-8"))
try:
hostname = socket.gethostbyaddr(client[0])[0]
connection.sendall(bytes(f":{server} NOTICE * :*** Got hostname! {hostname}\r\n","UTF-8"))
connection.sendall(bytes(f":{server} NOTICE * :*** Got it! {hostname}\r\n","UTF-8"))
except:
hostname = client[0]
connection.sendall(bytes(f":{server} NOTICE * :*** Oof! Can't find your hostname, using IP...\r\n","UTF-8"))
try:
identQuery = getident(hostname, client[1], isssl)
responseee = identQuery["response"]
print(identQuery)
if not identQuery["success"]:
connection.sendall(bytes(f":{server} NOTICE * :*** Uhm, Couldn't find your ident: {responseee}\r\n","UTF-8"))
else:
connection.sendall(bytes(f":{server} NOTICE * :*** Got ident! {responseee}\r\n","UTF-8"))
clident = responseee
rident = responseee
except:
print(traceback.format_exc())
connection.sendall(bytes(f":{server} NOTICE * :*** Uhm, Couldn't find your ident: Unknown error.\r\n","UTF-8"))
while True:
try:
data = connection.recv(2048)
if not data:
cause = "Remote host closed the connection"
break
except ssl.SSLEOFError:
pass
except ssl.SSLZeroReturnError:
cause = "Remote host closed the connection"
break
except Exception as ex:
cause = "Read error: " + str(ex)
break
print("Received data: {}".format(data))
try:
textt += data.decode()
if textt[-1] == "\n":
textt = data.decode()
for text in textt.replace("\r", "").split("\n"):
for i in socketListeners:
if "onSocket" in dir(i):
@ -285,7 +221,7 @@ def session(connection, client, ip, isssl=False):
pass
if command == "NICK" and not finished:
pending = text.split(" ")[1]
if pending[0] == ":": pending = pending[1:]
if pending[0] == ":": pending[1:]
if "!" in pending or ":" in pending or "#" in pending or "*" in pending:
connection.sendall(bytes(f":{server} 432 * {pending} :Erroneus nickname\r\n","UTF-8"))
pending = "*"
@ -301,38 +237,18 @@ def session(connection, client, ip, isssl=False):
realname = " ".join(text.split(" ")[4:])[1:]
ready = True
elif command == "CAP":
#usesIRCv3 = True
if args[0].upper() == "LS":
if args[0] == "LS":
connection.sendall(bytes(f":{server} CAP * LS :ircat.xyz/foo\r\n", "UTF-8"))
elif args[0].upper() == "REQ":
if args[1].lower() == ":sasl":
pass
#connection.sendall(f":{server} CAP * ACK :sasl")
elif args[0].upper() == "END":
CAPEND = True
elif command == "WEBIRC" and not finished:
try:
if args[0] == data2["webirc_pass"]:
hostname = args[2]
client = (args[3], client[1])
connection.sendall(bytes(f":{server} NOTICE * :*** WebIRC detected, welcome to IRC!\r\n", "UTF-8"))
if hostname != client[0]:
connection.sendall(bytes(f":{server} NOTICE * :*** Got WebIRC hostname! {hostname}\r\n", "UTF-8"))
except:
print(traceback.format_exc())
break
elif (ready and already_set) and (CAPEND if usesIRCv3 else True) and not finished:
elif (ready and already_set) and not finished:
cleanup_manual()
print(f"User {pending} successfully logged in.")
nickname_list[pending] = connection
property_list[pending] = {"host": hostname, "username": clident if clident != None else f"~{username }", "realname": realname, "modes": "iw", "last_ping": time.time(), "ping_pending": False, "away": False, "identified": False, "ssl": isssl}
property_list[pending] = {"host": hostname, "username": username, "realname": realname, "modes": "iw", "last_ping": time.time(), "ping_pending": False, "away": False}
lower_nicks[pending.lower()] = pending
for i in socketListeners:
if "onValidate" in dir(i):
i.onValidate(socket=connection, ip=client[0])
threading.Thread(target=pinger, args=[pending, connection]).start()
if clident == None:
rident = f"~{username}"
connection.sendall(bytes(f":{server} 001 {pending} :Welcome to the {displayname} Internet Relay Chat Network {pending}\r\n", "UTF-8"))
connection.sendall(bytes(f":{server} 002 {pending} :Your host is {server}[{ip}/6667], running version IRCat-v{__version__}\r\n", "UTF-8"))
connection.sendall(bytes(f":{server} 004 {pending} {server} IRCat-{__version__} iow ovmsitnlbkq\r\n", "UTF-8"))
@ -351,59 +267,38 @@ def session(connection, client, ip, isssl=False):
motd = open(motd_file).read()
connection.sendall(bytes(f":{server} 375 {pending} :- {server} Message of the Day -\r\n", "UTF-8"))
for i in motd.rstrip().split("\n"):
connection.sendall(bytes(f":{server} 372 {pending} :- {i}\r\n", "UTF-8"))
connection.sendall(bytes(f":{server} 376 {pending} :End of /MOTD command\r\n", "UTF-8"))
connection.sendall(bytes(f":{server} 376 {pending} :- {i}\r\n", "UTF-8"))
connection.sendall(bytes(f":{server} 372 {pending} :End of /MOTD command\r\n", "UTF-8"))
# End the MOTD
connection.sendall(bytes(f":{pending} MODE {pending} +iw\r\n","UTF-8"))
finished = True
elif command == "PING":
try:
e = text.split(" ")[1]
e = f":{e}" if e[0] != ":" else e
connection.sendall(bytes(f":{server} PONG {server} {e}\r\n","UTF-8"))
except:
connection.sendall(bytes(f":{server} PONG {server}\r\n","UTF-8"))
print("Replying with \"" + str([f":{server} PONG {server} :{e}\r\n"]) + "\"")
connection.sendall(bytes(f":{server} PONG {server} :{e}\r\n","UTF-8"))
elif command == "LIST":
connection.sendall(bytes(f":{server} 321 {pending} Channel :Users Name\r\n","UTF-8"))
for key, value in topic_list.items():
usersin = len(channels_list[key])
connection.sendall(bytes(f":{server} 322 {pending} {key} {usersin} :{value}\r\n","UTF-8"))
connection.sendall(bytes(f":{server} 323 {pending} :End of /LIST\r\n","UTF-8"))
elif command == "MOTD":
if motd_file != None:
motd = open(motd_file).read()
connection.sendall(bytes(f":{server} 375 {pending} :- {server} Message of the Day -\r\n", "UTF-8"))
for i in motd.rstrip().split("\n"):
connection.sendall(bytes(f":{server} 372 {pending} :- {i}\r\n", "UTF-8"))
connection.sendall(bytes(f":{server} 376 {pending} :End of /MOTD command\r\n", "UTF-8"))
connection.sendall(bytes(f":{server} 376 {pending} :- {i}\r\n", "UTF-8"))
connection.sendall(bytes(f":{server} 372 {pending} :End of /MOTD command\r\n", "UTF-8"))
elif finished:
pendingCommands += text
for comd in pendingCommands.split("\r\n"):
command = comd.split(" ")[0].upper()
args = comd.split(" ")[1:]
text = comd
processedExternally = False
for i in commandProviders:
cmdrun = i.command(command=command, args=args, nick=pending, ip=client[0], user=property_list[pending], connection=connection)
if cmdrun["success"]:
if "identify" in cmdrun:
if cmdrun["identify"] == "logout":
if "o" in property_list[pending]["modes"]:
connection.sendall(bytes(f":{pending} MODE {pending} -o\r\n","UTF-8"))
if not "i" in property_list[pending]["modes"]:
connection.sendall(bytes(f":{pending} MODE {pending} +i\r\n","UTF-8"))
if not "w" in property_list[pending]["modes"]:
connection.sendall(bytes(f":{pending} MODE {pending} +w\r\n","UTF-8"))
property_list[pending]["modes"] = "iw"
property_list[pending]["identified"] = False
else:
property_list[pending]["identified"] = True
property_list[pending]["identusername"] = cmdrun["identify"][0]
temp_mode = cmdrun["identify"][1]
property_list[pending]["modes"] = temp_mode
connection.sendall(bytes(f":{pending} MODE {pending} +{temp_mode}\r\n","UTF-8"))
if i.command():
processedExternally = True
break
if processedExternally:
pass
elif command == "JOIN":
channels = text.split(" ")[1]
if channels[0] == ":":
channels = channels[1:]
for channelt in channels.split(","):
channel = channelt.strip()
if channel.lower() in lower_chans:
@ -426,7 +321,7 @@ def session(connection, client, ip, isssl=False):
print(channels_list)
for i in channels_list[channel]:
try:
nickname_list[i].sendall(bytes(f":{pending}!{rident}@{hostname} JOIN {channel}\r\n","UTF-8"))
nickname_list[i].sendall(bytes(f":{pending}!~{username}@{hostname} JOIN {channel}\r\n","UTF-8"))
except:
pass
# Code re-used in the NAMES command
@ -436,15 +331,9 @@ def session(connection, client, ip, isssl=False):
connection.sendall(bytes(f":{server} 353 {pending} = {channel} :{users}\r\n","UTF-8"))
connection.sendall(bytes(f":{server} 366 {pending} {channel} :End of /NAMES list.\r\n","UTF-8"))
print("Successfully pre-loaded /NAMES list")
elif command == "LIST":
connection.sendall(bytes(f":{server} 321 {pending} Channel :Users Name\r\n","UTF-8"))
for key, value in topic_list.items():
usersin = len(channels_list[key])
connection.sendall(bytes(f":{server} 322 {pending} {key} {usersin} :{value}\r\n","UTF-8"))
connection.sendall(bytes(f":{server} 323 {pending} :End of /LIST\r\n","UTF-8"))
elif command == "PONG":
e = text.split(" ")[1]
if e == server or e == f":{server}":
if e == server:
print(pending + " replied to PING.")
property_list[pending]["last_ping"] = time.time()
property_list[pending]["ping_pending"] = False
@ -455,14 +344,14 @@ def session(connection, client, ip, isssl=False):
pass
else:
pending2 = text.split(" ")[1]
if pending2[0] == ":": pending2 = pending2[1:]
if pending2[0] == ":": pending2[1:]
if "!" in pending2 or ":" in pending2 or "#" in pending2 or "*" in pending2:
connection.sendall(bytes(f":{server} 432 {pending} {pending2} :Erroneus nickname\r\n","UTF-8"))
elif pending2.lower() in lower_nicks:
connection.sendall(bytes(f":{server} 433 {pending} {pending2} :Nickname is already in use.\r\n","UTF-8"))
else:
print("Sending nickname change...")
connection.sendall(bytes(f":{pending}!{rident}@{hostname} NICK {pending2}\r\n","UTF-8"))
connection.sendall(bytes(f":{pending}!~{username}@{hostname} NICK {pending2}\r\n","UTF-8"))
# Broadcast the nickname change
done = []
for i, users in channels_list.items():
@ -470,7 +359,7 @@ def session(connection, client, ip, isssl=False):
for j in users:
if j != pending and j != pending2 and not j in done:
print("Broadcasting on " + j)
nickname_list[j].sendall(bytes(f":{pending}!{rident}@{hostname} {text}\r\n","UTF-8"))
nickname_list[j].sendall(bytes(f":{pending}!~{username}@{hostname} {text}\r\n","UTF-8"))
done.append(j)
# Replace the nickname
try:
@ -498,7 +387,7 @@ def session(connection, client, ip, isssl=False):
channel = text.split(" ")[1]
for i in channels_list[channel]:
try:
nickname_list[i].sendall(bytes(f":{pending}!{rident}@{hostname} {text}\r\n","UTF-8"))
nickname_list[i].sendall(bytes(f":{pending}!~{username}@{hostname} {text}\r\n","UTF-8"))
except:
pass
try:
@ -526,13 +415,13 @@ def session(connection, client, ip, isssl=False):
who_user = property_list[i]["username"]
who_realname = property_list[i]["realname"]
who_away = "G" if property_list[i]["away"] else "H"
connection.sendall(bytes(f":{server} 352 {pending} {channel} {who_user} {who_host} {server} {i} {who_away} :0 {who_realname}\r\n","UTF-8"))
connection.sendall(bytes(f":{server} 352 {pending} {channel} ~{who_user} {who_host} {server} {i} {who_away} :0 {who_realname}\r\n","UTF-8"))
elif channel in nickname_list:
who_host = property_list[channel]["host"]
who_user = property_list[channel]["username"]
who_realname = property_list[channel]["realname"]
who_away = "G" if property_list[channel]["away"] else "H"
connection.sendall(bytes(f":{server} 352 {pending} * {who_user} {who_host} {server} {channel} {who_away} :0 {who_realname}\r\n","UTF-8"))
connection.sendall(bytes(f":{server} 352 {pending} * ~{who_user} {who_host} {server} {channel} {who_away} :0 {who_realname}\r\n","UTF-8"))
connection.sendall(bytes(f":{server} 315 {pending} {channel} :End of /WHO list.\r\n","UTF-8"))
elif command == "WHOIS":
@ -546,27 +435,17 @@ def session(connection, client, ip, isssl=False):
who_user = property_list[target]["username"]
who_realname = property_list[target]["realname"]
who_host = property_list[target]["host"]
who_identified = property_list[target]["identified"]
who_ssl = property_list[target]["ssl"]
if who_identified:
who_identifying = property_list[target]["identusername"]
else:
who_identifying = None
try:
who_flags = property_list[target]["modes"]
except:
who_flags = None
connection.sendall(bytes(f":{server} 311 {pending} {target} {who_user} {who_host} * :{who_realname}\r\n","UTF-8"))
connection.sendall(bytes(f":{server} 311 {pending} {target} ~{who_user} {who_host} * :{who_realname}\r\n","UTF-8"))
connection.sendall(bytes(f":{server} 312 {pending} {target} {server} :{identifier}\r\n","UTF-8"))
if "o" in who_flags: connection.sendall(bytes(f":{server} 313 {pending} {target} :is an IRC operator\r\n","UTF-8"))
who_away = property_list[target]["away"]
if who_away:
who_reason = who_away = property_list[target]["reason"]
connection.sendall(bytes(f":{server} 301 {pending} {target} :{who_reason}\r\n","UTF-8"))
if who_identified:
connection.sendall(bytes(f":{server} 330 {pending} {target} {who_identifying} :is logged in as\r\n","UTF-8"))
if who_ssl:
connection.sendall(bytes(f":{server} 671 {pending} {target} :is using a secure connection\r\n","UTF-8"))
#connection.sendall(bytes(f":{server} 317 {pending} {target} {time} :seconds idle\r\n","UTF-8")) # I haven't implemented idle time yet.
if who_flags != None and who_flags != "iw":
connection.sendall(bytes(f":{server} 379 {pending} {target} :Is using modes +{who_flags}\r\n","UTF-8"))
@ -593,11 +472,11 @@ def session(connection, client, ip, isssl=False):
for i in channels_list[channel]:
try:
if i != pending:
nickname_list[i].sendall(bytes(f":{pending}!{rident}@{hostname} {text}\r\n","UTF-8"))
nickname_list[i].sendall(bytes(f":{pending}!~{username}@{hostname} {text}\r\n","UTF-8"))
except:
pass
elif target in nickname_list:
nickname_list[target].sendall(bytes(f":{pending}!{rident}@{hostname} {text}\r\n","UTF-8"))
nickname_list[target].sendall(bytes(f":{pending}!~{username}@{hostname} {text}\r\n","UTF-8"))
else:
connection.sendall(bytes(f":{server} 401 {pending} {target} :No such nick/channel\r\n","UTF-8"))
else:
@ -620,7 +499,7 @@ def session(connection, client, ip, isssl=False):
if pending in users:
for j in users:
if j != pending and not j in done:
nickname_list[j].sendall(bytes(f":{pending}!{rident}@{hostname} {text}\r\n","UTF-8"))
nickname_list[j].sendall(bytes(f":{pending}!~{username}@{hostname} {text}\r\n","UTF-8"))
done.append(j)
# Remove the quitting user from the channel.
try:
@ -629,7 +508,7 @@ def session(connection, client, ip, isssl=False):
print(traceback.format_exc())
# Confirm QUIT and close the socket.
try:
connection.sendall(bytes(f":{pending}!{rident}@{hostname} {text}\r\n","UTF-8"))
connection.sendall(bytes(f":{pending}!~{username}@{hostname} {text}\r\n","UTF-8"))
connection.sendall(bytes(f"ERROR :Closing Link: {hostname} ({msg})\r\n","UTF-8"))
finally:
connection.close()
@ -662,7 +541,7 @@ def session(connection, client, ip, isssl=False):
else:
connection.sendall(bytes(f":{server} 505 {pending} :Cant change mode for other users\r\n","UTF-8"))
elif command == "CATSERV" or (command == "PRIVMSG" and args[0].lower() == "catserv"):
elif command == "GITSERV" or (command == "PRIVMSG" and args[0].lower() == "gitserv"):
if command == "PRIVMSG":
args = args[1:]
if args[0][0] == ":":
@ -672,22 +551,42 @@ def session(connection, client, ip, isssl=False):
elif args[0].upper() == "PULL":
updater = subprocess.run(["git", "pull"], stdout=subprocess.PIPE)
if updater.stdout.decode().strip() == "Already up to date.":
connection.sendall(bytes(f":CatServ!Meow@IRCatCore NOTICE {pending} :Codename IRCat is already up-to-date.\r\n","UTF-8"))
connection.sendall(bytes(f":GitServ!~IRCat@IRCatCore NOTICE {pending} :Codename IRCat is already up-to-date.\r\n","UTF-8"))
else:
connection.sendall(bytes(f":CatServ!Meow@IRCatCore NOTICE {pending} :Done, it is recommended to use /RESTART if you're an IRC op\r\n","UTF-8"))
connection.sendall(bytes(f":GitServ!~IRCat@IRCatCore NOTICE {pending} :Done, it is recommended to use /RESTART if you're an IRC op\r\n","UTF-8"))
elif args[0].upper() == "VERSION":
connection.sendall(bytes(f":CatServ!Meow@IRCatCore NOTICE {pending} :Codename IRCat version {__version__}\r\n","UTF-8"))
connection.sendall(bytes(f":CatServ!Meow@IRCatCore NOTICE {pending} :This is Codename IRCat's integrated services.\r\n","UTF-8"))
connection.sendall(bytes(f":GitServ!~IRCat@IRCatCore NOTICE {pending} :Codename IRCat version {__version__}\r\n","UTF-8"))
connection.sendall(bytes(f":GitServ!~IRCat@IRCatCore NOTICE {pending} :This is Codename IRCat's integrated services.\r\n","UTF-8"))
else:
connection.sendall(bytes(f":CatServ!Meow@IRCatCore NOTICE {pending} :CatServ Usage:\r\n","UTF-8"))
connection.sendall(bytes(f":CatServ!Meow@IRCatCore NOTICE {pending} :PULL - Pulls the latest version of Codename IRCat\r\n","UTF-8"))
connection.sendall(bytes(f":CatServ!Meow@IRCatCore NOTICE {pending} :VERSION - Gets the version number of this service.\r\n","UTF-8"))
connection.sendall(bytes(f":GitServ!~IRCat@IRCatCore NOTICE {pending} :GitServ Usage:\r\n","UTF-8"))
connection.sendall(bytes(f":GitServ!~IRCat@IRCatCore NOTICE {pending} :PULL - Pulls the latest version of Codename IRCat\r\n","UTF-8"))
connection.sendall(bytes(f":GitServ!~IRCat@IRCatCore NOTICE {pending} :VERSION - Gets the version number of this service.\r\n","UTF-8"))
elif command == "NICKSERV" or (command == "PRIVMSG" and args[0].lower() == "nickserv"):
if command == "PRIVMSG":
args = args[1:]
if args[0][0] == ":":
args[0] = args[0][1:]
if len(args) == 0:
connection.sendall(bytes(f":{server} 461 {pending} {command} :Not enough parameters\r\n","UTF-8"))
elif args[0].upper() == "IDENTIFY":
pass
elif args[0].upper() == "VERSION":
connection.sendall(bytes(f":NickServ!~IRCat@IRCatCore NOTICE {pending} :Codename IRCat version {__version__}\r\n","UTF-8"))
connection.sendall(bytes(f":NickServ!~IRCat@IRCatCore NOTICE {pending} :This is Codename IRCat's integrated services.\r\n","UTF-8"))
else:
connection.sendall(bytes(f":NickServ!~IRCat@IRCatCore NOTICE {pending} :NickServ Usage:\r\n","UTF-8"))
connection.sendall(bytes(f":NickServ!~IRCat@IRCatCore NOTICE {pending} :IDENTIFY - Identifies your nickname\r\n","UTF-8"))
connection.sendall(bytes(f":NickServ!~IRCat@IRCatCore NOTICE {pending} :VERSION - Gets the version number of this service.\r\n","UTF-8"))
elif command == "RESTART":
if "o" in property_list[pending]["modes"]:
tcp_socket.shutdown(socket.SHUT_RDWR)
tcp_socket.close()
global opened
opened = False
else:
connection.sendall(bytes(f":{server} 481 {pending} :Permission Denied- You're not an IRC operator\r\n","UTF-8"))
elif command == "PRIVMSG":
if len(args) >= 2:
target = text.split(" ")[1]
@ -701,18 +600,21 @@ def session(connection, client, ip, isssl=False):
try:
if i != pending:
print(i)
print(f":{pending}!{rident}@{hostname} {text}\r\n")
nickname_list[i].sendall(bytes(f":{pending}!{rident}@{hostname} {text}\r\n","UTF-8"))
print(f":{pending}!~{username}@{hostname} {text}\r\n")
nickname_list[i].sendall(bytes(f":{pending}!~{username}@{hostname} {text}\r\n","UTF-8"))
else:
print(i + " Is the current user!")
except:
print(traceback.format_exc)
elif target in nickname_list:
nickname_list[target].sendall(bytes(f":{pending}!{rident}@{hostname} {text}\r\n","UTF-8"))
nickname_list[target].sendall(bytes(f":{pending}!~{username}@{hostname} {text}\r\n","UTF-8"))
else:
connection.sendall(bytes(f":{server} 401 {pending} {target} :No such nick/channel\r\n","UTF-8"))
else:
connection.sendall(bytes(f":{server} 461 {pending} {command} :Not enough parameters\r\n","UTF-8"))
# Ignore empty text
elif text.split(" ")[0] == "":
pass
@ -720,12 +622,7 @@ def session(connection, client, ip, isssl=False):
# Unknown command
cmd = text.split(" ")[0]
connection.sendall(bytes(f":{server} 421 {pending} {cmd} :Unknown command\r\n","UTF-8"))
pendingCommands = ""
else:
pendingCommands += text
textt = ""
except ssl.SSLEOFError:
print("EOF occured...")
except Exception as ex:
print(traceback.format_exc())
cause = "" + str(ex)
@ -746,7 +643,7 @@ def session(connection, client, ip, isssl=False):
for j in users:
if j != pending and not j in done:
try:
nickname_list[j].sendall(bytes(f":{pending}!{rident}@{hostname} QUIT :{cause}\r\n","UTF-8"))
nickname_list[j].sendall(bytes(f":{pending}!~{username}@{hostname} QUIT :{cause}\r\n","UTF-8"))
done.append(j)
except:
print(traceback.format_exc())
@ -773,7 +670,7 @@ def cleanup_manual():
i.remove(h)
for k in channels_list[j]:
if k != h and k in nickname_list:
nickname_list[k].sendall(f":{h}!DISCONNECTED@DISCONNECTED PART {j} :IRCat Cleanup: Found missing connection!!\r\n")
nickname_list[k].sendall(f":{h}!~DISCONNECTED@DISCONNECTED PART {j} :IRCat Cleanup: Found missing connection!!\r\n")
def tcp_session(sock):
while True:
@ -786,28 +683,22 @@ def tcp_session(sock):
except:
print("Something went wrong...")
print(traceback.format_exc())
def ssl_session(sock):
def ssl_session(sock2):
with context.wrap_socket(sock2, server_side=True) as sock:
while True:
try:
while opened:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.set_ciphers('DEFAULT:@SECLEVEL=0')
context.load_cert_chain(ssl_cert, keyfile=ssl_pkey)
print("Waiting for connection...")
connection, client = sock.accept()
ip_to = restrict_ip
threading.Thread(target=session, daemon=True, args=[context.wrap_socket(connection, server_side=True), client, ip_to, True]).start()
threading.Thread(target=session, daemon=True, args=[connection, client, ip_to]).start()
except:
print("Something went wrong...")
print(traceback.format_exc())
for ip, i in sockets.items():
print("Now listening on port 6667 with IP " + ip)
threading.Thread(target=tcp_session, args=[i], daemon=True).start()
threading.Thread(target=tcp_session, args=[i]).start()
if ssl_option:
for ip, i in sockets_ssl.items():
print("Now listening on SSL port 6697 with IP " + ip)
threading.Thread(target=ssl_session, args=[i], daemon=True).start()
while opened:
pass
print("Shutting down...")
threading.Thread(target=ssl_session, args=[i]).start()

30
todo.md
View file

@ -12,11 +12,12 @@
- [x] Send PING and wait for PONG
- [x] Reply PONG if received PING
- [x] [Change of nicknames](https://mastodon.swee.codes/@swee/113642104470536887)
- [ ] Change of hostnames
- [x] Away
- [ ] Multi-server support
- [x] `LIST`
- [ ] `LIST`
- [ ] `TOPIC`
- [ ] [Database support](https://discuss.swee.codes/t/41)
- [ ] [Data file with SQLite](https://discuss.swee.codes/t/41/2)
- [ ] User Flags
- [ ] i (invisible)
- [ ] o (IRCOP)
@ -39,29 +40,30 @@
- [ ] Destructive features for IRCOPS
- [ ] `KILL <user> <comment>`
- [ ] `MODE <external user>`
- [x] `RESTART`
- [ ] `RESTART`
- [ ] Extra commands
- [x] `NAMES`
- [ ] `USERS`
- [x] `WHOIS`
- [ ] `WHOWAS`
- [ ] [Implement services.](modules/pawserv.py)
- [ ] Implement services.
- [ ] Nickserv
- [ ] ChanServ
- [x] CatServ (Outside of PawServ)
- [x] Link `PRIVMSG *serv` to `*serv`
- [x] Extra ~~(not planned)~~ features
- [x] ident support
- [x] GitServ (Custom user for pull)
- [ ] Link `PRIVMSG *serv` to `*serv`
- [ ] Extra (not planned) features
- [ ] ident support
- [ ] Authentication
- [x] Store credentials in an SQLite3 file.
- [x] Map NickServ IDENTIFY
- [ ] Make the server able to change the client's host
- [ ] Store credentials in an SQLite3 file.
- [ ] Map NickServ IDENTIFY
- [ ] Map PASS
- [x] SSL/TLS
- [ ] Mock SASL PLAIN
- [ ] SSL/TLS
- [x] [Use a thread to accept connections on SSL port 6697](https://mastodon.swee.codes/@swee/113762525145710774)
- [x] Automatically reload the certificate ~~if defined in config.~~
- [ ] Automatically reload the certificate if defined in config.
- [ ] Add IRCv3 features.
- [x] List capabilities (`CAP LS 302`)
- [ ] `away-notify`
- [ ] `tls` (STARTTLS)
- [ ] `sasl`
- Will research later.
I am going to fully read [RFC 1459](https://datatracker.ietf.org/doc/html/rfc1459) soon and add each part to the TODO.