2024-12-08 16:54:04 -08:00
#!/usr/bin/python3
2024-12-08 20:06:57 -08:00
__version__ = " 0.0.1-pre-alpha "
2024-12-08 16:54:04 -08:00
print ( f " INTERNET RELAY CAT v { __version__ } " )
print ( " Welcome! /ᐠ ˵> ⩊ <˵マ " )
2024-12-09 18:27:40 -08:00
import socket , time , threading , traceback , sys , subprocess , yaml
2024-12-08 16:54:04 -08:00
from requests import get
2024-12-08 18:55:43 -08:00
if not len ( sys . argv ) == 2 :
print ( " IRCat requires the following arguments: config.yml " )
2024-12-08 19:04:37 -08:00
sys . exit ( 1 )
server = " 127.0.0.1 "
displayname = " foo "
2024-12-09 15:33:56 -08:00
identifier = " somewhere in the universe "
2024-12-08 19:04:37 -08:00
with open ( sys . argv [ 1 ] , ' r ' ) as file :
data = yaml . safe_load ( file )
try : server = data [ " host " ]
except : print ( " using fallback server address " )
try : displayname = data [ " name " ]
except : print ( " using fallback display name " )
2024-12-09 15:39:05 -08:00
try : identifier = data [ " identifier " ]
2024-12-09 15:33:56 -08:00
except : print ( " using fallback identifier " )
2024-12-08 19:04:37 -08:00
file . close ( )
2024-12-08 20:03:55 -08:00
print ( " Successfully loaded config! " )
2024-12-08 16:54:04 -08:00
ip = get ( ' https://api.ipify.org ' ) . content . decode ( ' utf8 ' )
2024-12-08 17:36:59 -08:00
tcp_socket = socket . socket ( socket . AF_INET , socket . SOCK_STREAM )
tcp_socket . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEADDR , 1 )
2024-12-09 12:52:13 -08:00
server_address = ( ' ' , 6667 )
2024-12-08 16:54:04 -08:00
tcp_socket . bind ( server_address )
tcp_socket . listen ( 1 )
2024-12-09 14:35:01 -08:00
reserved = [ " nickserv " , " chanserv " , " gitserv " ] # Reserved nicknames
2024-12-09 12:27:44 -08:00
nickname_list = { } # Stores nicknames and the respective sockets
2024-12-09 18:56:12 -08:00
lower_nicks = { " gitserv " : " GitServ " } # Nicknames in lowercase
2024-12-09 12:27:44 -08:00
channels_list = { } # Store channels and their user lists
2024-12-09 18:56:12 -08:00
property_list = { " GitServ " : { " host " : " IRCatCore " , " username " : " IRCat " , " realname " : " Codename IRCat Integrated services - Updates bot " } } # Stores properties for active users and channels
2024-12-08 16:54:04 -08:00
print ( " Now listening on port 6667 " )
2024-12-09 16:45:21 -08:00
def pinger ( nick , connection ) :
while nick in property_list :
if ( time . time ( ) - property_list [ nick ] [ " last_ping " ] ) > 60 and not property_list [ nick ] [ " ping_pending " ] :
2024-12-09 16:46:15 -08:00
print ( " Sent ping message to " + nick )
2024-12-09 16:45:21 -08:00
connection . sendall ( bytes ( f " PING { server } \r \n " , " UTF-8 " ) )
2024-12-09 16:47:46 -08:00
property_list [ nick ] [ " ping_pending " ] = True
elif property_list [ nick ] [ " ping_pending " ] and ( ( time . time ( ) - property_list [ nick ] [ " last_ping " ] ) > 120 ) :
2024-12-09 16:45:21 -08:00
property_list [ nick ] [ " cause " ] = " Ping timeout: 120 seconds "
2024-12-09 17:02:20 -08:00
connection . shutdown ( socket . SHUT_WR )
2024-12-09 16:45:21 -08:00
connection . close ( )
break
2024-12-08 16:54:04 -08:00
def session ( connection , client ) :
2024-12-09 09:30:20 -08:00
pending = " * " # The nickname of the client
2024-12-08 20:02:59 -08:00
already_set = False # If the client gave the server a NICK packet
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.
2024-12-09 14:08:31 -08:00
username = " oreo " # Username/ident specified by client
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
2024-12-08 16:54:04 -08:00
try :
print ( " Connected to client IP: {} " . format ( client ) )
connection . sendall ( bytes ( f " : { server } NOTICE * :*** Looking for your hostname... \r \n " , " UTF-8 " ) )
2024-12-09 12:41:46 -08:00
try :
hostname = socket . gethostbyaddr ( client [ 0 ] ) [ 0 ]
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 " ) )
2024-12-08 16:54:04 -08:00
while True :
try :
data = connection . recv ( 2048 )
except :
print ( " Disconnected. " )
print ( " Received data: {} " . format ( data ) )
try :
textt = data . decode ( )
for text in textt . split ( " \r \n " ) :
2024-12-09 11:16:30 -08:00
command = text . split ( " " ) [ 0 ] . upper ( )
try :
args = text . split ( " " ) [ 1 : ]
except :
pass
2024-12-09 11:07:26 -08:00
if command == " NICK " :
2024-12-08 16:54:04 -08:00
pending = text . split ( " " ) [ 1 ]
2024-12-09 15:33:56 -08:00
if pending [ 0 ] == " : " : pending [ 1 : ]
2024-12-09 15:57:24 -08:00
if pending . lower ( ) in lower_nicks or pending in reserved :
2024-12-08 16:54:04 -08:00
connection . sendall ( bytes ( f " : { server } 433 * { pending } :Nickname is already in use. \r \n " , " UTF-8 " ) )
2024-12-09 09:30:20 -08:00
pending = " * "
2024-12-08 16:54:04 -08:00
else :
if not already_set :
already_set = True
2024-12-09 11:07:26 -08:00
elif command == " USER " :
2024-12-08 16:54:04 -08:00
if not ready :
username = text . split ( " " ) [ 1 ]
2024-12-09 13:44:15 -08:00
realname = " " . join ( text . split ( " " ) [ 4 : ] ) [ 1 : ]
2024-12-08 20:06:57 -08:00
ready = True
2024-12-09 13:33:03 -08:00
elif command == " CAP " :
2024-12-09 13:33:26 -08:00
if args [ 0 ] == " LS " :
2024-12-09 13:35:49 -08:00
connection . sendall ( bytes ( f " : { server } CAP * LS : \r \n " , " UTF-8 " ) )
2024-12-08 20:02:59 -08:00
elif ( ready and already_set ) and not finished :
2024-12-09 14:35:01 -08:00
nickname_list [ pending ] = connection
2024-12-09 16:45:21 -08:00
property_list [ pending ] = { " host " : hostname , " username " : username , " realname " : realname , " modes " : " iw " , " last_ping " : time . time ( ) , " ping_pending " : False }
2024-12-09 15:57:24 -08:00
lower_nicks [ pending . lower ( ) ] = pending
2024-12-09 16:45:36 -08:00
threading . Thread ( target = pinger , args = [ pending , connection ] ) . start ( )
2024-12-08 20:02:59 -08:00
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 " ) )
2024-12-08 20:21:29 -08:00
connection . sendall ( bytes ( f " : { server } 005 { pending } CHANMODES=bq NETWORK= { displayname } CHANTYPES=# :are supported by this server \r \n " , " UTF-8 " ) )
2024-12-08 20:02:59 -08:00
connection . sendall ( bytes ( f " : { pending } MODE { pending } +iw \r \n " , " UTF-8 " ) )
finished = True
2024-12-09 11:07:26 -08:00
elif command == " PING " :
2024-12-09 18:11:30 -08:00
e = " " . join ( text . split ( " " ) [ 1 : ] )
2024-12-08 21:44:32 -08:00
print ( " Replied to PING. " )
2024-12-09 18:11:30 -08:00
connection . sendall ( bytes ( f " : { server } PONG { server } : { e } \r \n " , " UTF-8 " ) )
2024-12-08 20:02:59 -08:00
elif ( ready and already_set ) and finished :
2024-12-09 11:07:26 -08:00
if command == " JOIN " :
2024-12-09 12:04:47 -08:00
channels = text . split ( " " ) [ 1 ]
for channelt in channels . split ( " , " ) :
channel = channelt . strip ( )
success = True
if channel in channels_list :
if pending in channels_list [ channel ] :
success = False
print ( f " { pending } is already in { channel } , ignoring JOIN request. " )
if success :
2024-12-08 22:28:45 -08:00
try :
2024-12-09 12:04:47 -08:00
if channel in channels_list :
channels_list [ channel ] . append ( pending )
else :
channels_list [ channel ] = [ pending ]
2024-12-08 22:28:45 -08:00
except :
2024-12-09 12:04:47 -08:00
connection . sendall ( bytes ( f " : { server } NOTICE * :*** Could not join { channel } \r \n " , " UTF-8 " ) )
print ( channels_list )
for i in channels_list [ channel ] :
try :
2024-12-09 13:26:23 -08:00
nickname_list [ i ] . sendall ( bytes ( f " : { pending } !~ { username } @ { hostname } JOIN { channel } \r \n " , " UTF-8 " ) )
2024-12-09 12:04:47 -08:00
except :
pass
2024-12-09 13:26:23 -08:00
# Code re-used in the NAMES command
2024-12-09 12:04:47 -08:00
if channel in channels_list :
if pending in channels_list [ channel ] :
users = " " . join ( channels_list [ channel ] )
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 " ) )
2024-12-09 12:41:46 -08:00
print ( " Successfully pre-loaded /NAMES list " )
2024-12-09 16:20:40 -08:00
elif command == " PONG " :
e = text . split ( " " ) [ 1 ]
if e == server :
2024-12-09 16:46:15 -08:00
print ( pending + " replied to PING. " )
2024-12-09 16:45:21 -08:00
property_list [ pending ] [ " last_ping " ] = time . time ( )
property_list [ pending ] [ " ping_pending " ] = False
2024-12-09 11:07:26 -08:00
elif command == " PART " :
2024-12-09 18:27:40 -08:00
if len ( args ) == 0 :
connection . sendall ( bytes ( f " : { server } { pending } 461 { command } :Not enough parameters \r \n " , " UTF-8 " ) )
else :
channel = text . split ( " " ) [ 1 ]
for i in channels_list [ channel ] :
try :
nickname_list [ i ] . sendall ( bytes ( f " : { pending } !~ { username } @ { hostname } { text } \r \n " , " UTF-8 " ) )
except :
pass
2024-12-08 20:02:59 -08:00
try :
2024-12-09 18:27:40 -08:00
channels_list [ channel ] . remove ( pending )
2024-12-08 20:02:59 -08:00
except :
2024-12-09 18:27:40 -08:00
print ( traceback . format_exc ( ) )
2024-12-09 11:07:26 -08:00
elif command == " WHO " :
2024-12-09 18:27:40 -08:00
if len ( args ) == 0 :
connection . sendall ( bytes ( f " : { server } { pending } 461 { command } :Not enough parameters \r \n " , " UTF-8 " ) )
else :
channel = text . split ( " " ) [ 1 ]
if channel in channels_list :
for i in channels_list [ channel ] :
who_host = property_list [ i ] [ " host " ]
who_user = property_list [ i ] [ " username " ]
who_realname = property_list [ i ] [ " realname " ]
connection . sendall ( bytes ( f " : { server } 352 { pending } { who_user } ~ { who_host } { server } { i } H :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 " ]
connection . sendall ( bytes ( f " : { server } 352 { pending } * { who_user } ~ { who_host } { server } { channel } H :0 { who_realname } \r \n " , " UTF-8 " ) )
2024-12-09 12:27:44 -08:00
2024-12-09 18:27:40 -08:00
connection . sendall ( bytes ( f " : { server } 366 { pending } { channel } :End of /WHO list. \r \n " , " UTF-8 " ) )
2024-12-09 14:35:01 -08:00
elif command == " WHOIS " :
2024-12-09 18:27:40 -08:00
if len ( args ) == 0 :
connection . sendall ( bytes ( f " : { server } { pending } 461 { command } :Not enough parameters \r \n " , " UTF-8 " ) )
2024-12-09 14:35:01 -08:00
else :
2024-12-09 18:27:40 -08:00
target = text . split ( " " ) [ 1 ]
2024-12-09 18:56:12 -08:00
if target . lower ( ) in lower_nicks :
target = lower_nicks [ target . lower ( ) ]
2024-12-09 18:27:40 -08:00
if target in property_list :
who_user = property_list [ target ] [ " username " ]
who_realname = property_list [ target ] [ " realname " ]
who_host = property_list [ target ] [ " host " ]
2024-12-09 18:56:12 -08:00
connection . sendall ( bytes ( f " : { server } 311 { pending } { target } ~ { who_user } { who_host } * : { who_realname } \r \n " , " UTF-8 " ) )
2024-12-09 18:27:40 -08:00
connection . sendall ( bytes ( f " : { server } 312 { pending } { target } { server } : { identifier } \r \n " , " UTF-8 " ) )
#connection.sendall(bytes(f":{server} 313 {target} :is an IRC operator\r\n","UTF-8")) # I haven't implemented modes yet.
#connection.sendall(bytes(f":{server} 317 {target} {time} :seconds idle\r\n","UTF-8")) # I haven't implemented idle time yet.
connection . sendall ( bytes ( f " : { server } 318 { pending } { target } :End of /WHOIS list \r \n " , " UTF-8 " ) )
else :
connection . sendall ( bytes ( f " : { server } 401 { pending } { target } :No such nick/channel \r \n " , " UTF-8 " ) )
2024-12-09 13:26:23 -08:00
elif command == " NAMES " :
2024-12-09 18:27:40 -08:00
if len ( args ) == 0 :
connection . sendall ( bytes ( f " : { server } { pending } 461 { command } :Not enough parameters \r \n " , " UTF-8 " ) )
else :
channel = text . split ( " " ) [ 1 ]
if channel in channels_list :
if pending in channels_list [ channel ] :
users = " " . join ( channels_list [ channel ] )
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 " ) )
2024-12-09 11:07:26 -08:00
elif command == " PRIVMSG " :
2024-12-09 18:27:40 -08:00
if len ( args ) > = 2 :
target = text . split ( " " ) [ 1 ]
2024-12-09 18:56:12 -08:00
if target . lower ( ) in lower_nicks :
target = lower_nicks [ target . lower ( ) ]
2024-12-09 18:27:40 -08:00
if target in channels_list :
if pending in channels_list [ target ] :
for i in channels_list [ channel ] :
try :
if i != pending :
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 } !~ { username } @ { hostname } { text } \r \n " , " UTF-8 " ) )
else :
connection . sendall ( bytes ( f " : { server } 401 { pending } { target } :No such nick/channel \r \n " , " UTF-8 " ) )
2024-12-09 18:15:10 -08:00
else :
2024-12-09 18:27:40 -08:00
connection . sendall ( bytes ( f " : { server } { pending } 461 { command } :Not enough parameters \r \n " , " UTF-8 " ) )
2024-12-09 18:15:10 -08:00
elif command == " NOTICE " :
2024-12-09 18:27:40 -08:00
if len ( args ) > = 2 :
target = text . split ( " " ) [ 1 ]
2024-12-09 18:56:12 -08:00
if target . lower ( ) in lower_nicks :
target = lower_nicks [ target . lower ( ) ]
2024-12-09 18:27:40 -08:00
if target in channels_list :
if pending in channels_list [ target ] :
for i in channels_list [ channel ] :
try :
if i != pending :
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 } !~ { username } @ { hostname } { text } \r \n " , " UTF-8 " ) )
else :
connection . sendall ( bytes ( f " : { server } 401 { pending } { target } :No such nick/channel \r \n " , " UTF-8 " ) )
2024-12-08 20:02:59 -08:00
else :
2024-12-09 18:27:40 -08:00
connection . sendall ( bytes ( f " : { server } { pending } 461 { command } :Not enough parameters \r \n " , " UTF-8 " ) )
2024-12-09 11:07:26 -08:00
elif command == " QUIT " :
2024-12-08 20:02:59 -08:00
# Parse the quit message.
done = [ ]
msg = text . split ( " " ) [ 1 : ]
if len ( msg ) > 0 :
mse = " " . join ( msg )
msg = f " Quit: { mse } "
else :
2024-12-09 15:33:56 -08:00
msg = " Quit: " + pending
2024-12-08 20:02:59 -08:00
text = f " QUIT : { msg } "
2024-12-08 20:21:29 -08:00
# Broadcast all users in the joined channels that the person quit.
2024-12-08 20:02:59 -08:00
for i , users in channels_list . items ( ) :
if pending in users :
for j in users :
if j != pending and not j in done :
2024-12-09 13:26:23 -08:00
nickname_list [ j ] . sendall ( bytes ( f " : { pending } !~ { username } @ { hostname } { text } \r \n " , " UTF-8 " ) )
2024-12-08 20:02:59 -08:00
done . append ( j )
# Remove the quitting user from the channel.
2024-12-08 16:54:04 -08:00
try :
2024-12-08 20:02:59 -08:00
channels_list [ i ] . remove ( pending )
2024-12-08 16:54:04 -08:00
except :
2024-12-08 20:02:59 -08:00
print ( traceback . format_exc ( ) )
2024-12-08 20:48:30 -08:00
# Confirm QUIT and close the socket.
2024-12-09 13:53:01 -08:00
try :
2024-12-09 13:48:45 -08:00
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 " ) )
2024-12-09 13:53:01 -08:00
except :
2024-12-09 17:29:46 -08:00
break
2024-12-08 20:48:30 -08:00
connection . close ( )
2024-12-09 13:45:38 -08:00
safe_quit = True
2024-12-08 20:02:59 -08:00
break
2024-12-09 11:07:26 -08:00
elif command == " GITSERV " :
2024-12-09 18:27:40 -08:00
if len ( args ) == 0 :
connection . sendall ( bytes ( f " : { server } { pending } 461 { command } :Not enough parameters \r \n " , " UTF-8 " ) )
elif args [ 0 ] . upper ( ) == " PULL " :
2024-12-09 18:30:43 -08:00
updater = subprocess . run ( [ " git " , " pull " ] , stdout = subprocess . PIPE )
2024-12-09 18:27:40 -08:00
if updater . stdout . decode ( ) . strip ( ) == " Already up to date. " :
connection . sendall ( bytes ( f " :GitServ!~IRCat@IRCatCore NOTICE { pending } :IRCat is already up-to-date. \r \n " , " UTF-8 " ) )
else :
connection . sendall ( bytes ( f " :GitServ!~IRCat@IRCatCore NOTICE { pending } :Done, it is recommended to use /UPDATE if you ' re an IRC op \r \n " , " UTF-8 " ) )
elif args [ 0 ] . upper ( ) == " VERSION " :
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 " :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 " ) )
2024-12-09 10:22:43 -08:00
# Ignore empty text
elif text . split ( " " ) [ 0 ] == " " :
pass
else :
# Unknown command
cmd = text . split ( " " ) [ 0 ]
connection . sendall ( bytes ( f " : { server } 421 { pending } { cmd } :Unknown command \r \n " , " UTF-8 " ) )
2024-12-08 21:44:32 -08:00
2024-12-09 14:08:31 -08:00
except Exception as ex :
2024-12-08 16:54:04 -08:00
print ( traceback . format_exc ( ) )
2024-12-09 14:22:31 -08:00
cause = " Read error: " + str ( ex )
2024-12-09 13:53:01 -08:00
break
2024-12-08 16:54:04 -08:00
if not data :
2024-12-09 14:22:31 -08:00
cause = " Remote host closed the connection "
2024-12-08 16:54:04 -08:00
break
finally :
connection . close ( )
2024-12-09 16:45:21 -08:00
if " cause " in property_list [ pending ] :
2024-12-09 16:54:10 -08:00
cause = property_list [ pending ] [ " cause " ]
2024-12-09 09:30:20 -08:00
if pending != " * " :
2024-12-08 16:54:04 -08:00
del nickname_list [ pending ]
2024-12-09 13:26:23 -08:00
del property_list [ pending ]
2024-12-09 15:57:24 -08:00
del lower_nicks [ pending . lower ( ) ]
2024-12-09 13:26:23 -08:00
if not safe_quit :
2024-12-09 13:53:46 -08:00
done = [ ]
2024-12-09 13:26:23 -08:00
for i , users in channels_list . items ( ) :
if pending in users :
for j in users :
if j != pending and not j in done :
2024-12-09 14:22:31 -08:00
nickname_list [ j ] . sendall ( bytes ( f " : { pending } !~ { username } @ { hostname } QUIT : { cause } \r \n " , " UTF-8 " ) )
2024-12-09 13:26:23 -08:00
done . append ( j )
# Remove the quitting user from the channel.
try :
channels_list [ i ] . remove ( pending )
except :
print ( traceback . format_exc ( ) )
2024-12-08 16:54:04 -08:00
try :
while True :
connection , client = tcp_socket . accept ( )
threading . Thread ( target = session , daemon = True , args = [ connection , client ] ) . start ( )
except :
print ( " Shutting down... " )
time . sleep ( 2 )
2024-12-08 17:36:59 -08:00
tcp_socket . shutdown ( 1 )
2024-12-08 16:54:04 -08:00
tcp_socket . close ( )