This commit is contained in:
Athena Funderburg
2026-05-25 07:05:17 +00:00
commit 4b463a3432
682 changed files with 47796 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
from .entry import register
+463
View File
@@ -0,0 +1,463 @@
from typing import Tuple, Optional, Iterable, List, Any, Callable, Dict
import io, asyncio, settings
from datetime import datetime
from enum import IntEnum
from util.misc import Logger
from core import event
from core.models import (
Contact, Substatus, User, Circle, CircleRole, TextWithData,
MessageData, MessageType, LoginOption, OIM,
)
from core.backend import Backend, BackendSession, Chat, ChatSession
from core.client import Client
class IRCCtrl:
__slots__ = (
'logger', 'reader', 'writer', 'peername', 'close_callback', 'closed', 'transport',
'backend', 'bs', 'client',
'password', 'username', 'chat_sessions'
)
logger: Logger
reader: 'IRCReader'
writer: 'IRCWriter'
peername: Tuple[str, int]
close_callback: Optional[Callable[[], None]]
closed: bool
transport: Optional[asyncio.WriteTransport]
backend: Backend
bs: Optional[BackendSession]
client: Client
password: Optional[str]
username: Optional[str]
chat_sessions: Dict[Chat, ChatSession]
def __init__(self, logger: Logger, via: str, backend: Backend) -> None:
self.logger = logger
self.reader = IRCReader(logger)
self.writer = IRCWriter(logger)
self.peername = ('0.0.0.0', 6667)
self.close_callback = None
self.closed = False
self.transport = None
self.backend = backend
self.bs = None
self.client = Client('irc', '?', via)
self.password = None
self.username = None
self.chat_sessions = {}
def _m_pass(self, pwd: str) -> None:
self.password = pwd
def _m_user(self, email: str, junk1: str, junk2: str, realname: str) -> None:
password = self.password
self.password = None
assert password is not None
uuid = self.backend.user_service.login(email, password)
if uuid is not None:
bs = self.backend.login(uuid, self.client, BackendEventHandler(self), option = LoginOption.BootOthers)
else:
bs = None
if bs is None:
self.send_numeric(Err.PasswdMismatch, ':Wrong email/password')
return
self.bs = bs
user = bs.user
if user.suspended:
self.send_numeric(Err.YoureBannedCreep, ':Your CrossTalk account has been suspended. You may not connect to the service.')
self.close()
return
self.bs.me_update({ 'substatus': Substatus.Online })
self.send_numeric(RPL.Welcome, email, ':Log on successful.')
self._m_motd()
def _m_ping(self, *servers: str) -> None:
self.send_reply('PONG', *servers)
def _m_join(self, channel: str, keys: Optional[str] = None) -> None:
assert self.bs is not None
email = self.bs.user.email
chat = self._channel_to_chat(channel)
if chat is None:
chat = self.backend.chat_create()
chat.add_id('irc', channel)
cs = self._channel_to_chatsession(channel)
if cs is None:
cs = chat.join('irc', self.bs, ChatEventHandler(self))
chat.send_participant_joined(cs)
self.chat_sessions[chat] = cs
self.send_numeric(RPL.NamReply, email, '=', channel, ':' + ' '.join(
cs.user.email for cs in chat.get_roster_single()
))
# TODO: Chats created in other frontends are usually secret+private.
#self.send_numeric(Err.InviteOnlyChan, email, channel, ":Cannot join channel")
def _m_invite(self, user_email: str, channel: str) -> None:
assert self.bs is not None
cs = self._channel_to_chatsession(channel)
assert cs is not None
uuid = self.backend.util_get_uuid_from_email(user_email)
assert uuid is not None
user = self.backend.user_service.get(uuid)
assert user is not None
cs.invite(user)
self.send_numeric(RPL.Inviting, self.bs.user.email, user_email, channel)
def _m_list(self, arg1: Optional[str] = None, arg2: Optional[str] = None) -> None:
assert self.bs is not None
email = self.bs.user.email
for chat in self.backend.get_chats_by_scope('irc'):
self.send_numeric(RPL.List, email, chat.ids['irc'], str(len(list(chat.get_roster_single()))))
self.send_numeric(RPL.ListEnd, email, ":End of /LIST")
def _m_mode(self, channel: str) -> None:
#self.send_numeric(RPL.ChannelModeIs, self.bs.user.email, channel, '+tnl', 200)
pass
def _m_nick(self, nickname: str) -> None:
self.bs.me_update({ 'name': nickname })
def _m_userhost(self, email: str) -> None:
self._reply_unsupported('USERHOST')
def _m_who(self, channel: str) -> None:
assert self.bs is not None
cs = self._channel_to_chatsession(channel)
assert cs is not None
for cs_other in cs.chat.get_roster():
email = cs_other.user.email
name = cs_other.user.status.name or email
self.send_numeric(RPL.WhoReply, self.bs.user.email, channel, email, 'host', 'server', email, 'H', ':0 0PNE ' + name)
def _m_part(self, channel: str, message: Optional[str] = None) -> None:
assert self.bs is not None
cs = self._channel_to_chatsession(channel)
assert cs is not None
cs.close()
self.send_reply('PART', channel, source = self.bs.user.email)
def _m_privmsg(self, dest: str, message: str) -> None:
assert self.bs is not None
cs = self._channel_to_chatsession(dest)
assert cs is not None
cs.send_message_to_everyone(MessageData(sender = self.bs.user, type = MessageType.Chat, text = message))
def _m_away(self, message: Optional[str]) -> None:
assert self.bs is not None
if self.bs.user.status.substatus == Substatus.Online:
awaymsg = None
if len(message) > 0:
awaymsg = message
self.bs.me_update({ 'substatus': Substatus.Away, message: awaymsg })
self.send_numeric(RPL.NowAway, ':You are now away.')
else:
self.bs.me_update({ 'substatus': Substatus.Online, 'message': None })
self.send_numeric(RPL.UnAway, ':You are no longer away.')
def _m_motd(self) -> None:
self.send_numeric(RPL.MOTDStart, ": ----- CrossTalk (BETA) -----")
self.send_numeric(RPL.MOTD, ": - Welcome to CrossTalk! This frontend is undergoing a massive overhaul, so expect things to be iffy for now.")
self.send_numeric(RPL.MOTD, ": - We support MSNP, YMSG, and OSCAR as well.")
self.send_numeric(RPL.MOTD, ": - Development is going at a faster pace, so this won't stay the case for long. Stay tuned!")
self.send_numeric(RPL.MOTDEnd, ": - End of the MOTD.")
def _m_version(self) -> None:
self.send_numeric(RPL.Version, ": - AZUL (BETA) - IRC")
def _m_quit(self, reason: Optional[str]) -> None:
self.close()
def _m_cap(self, subcommand: str, capabilities: Optional[str] = None) -> None:
self._reply_unsupported('CAP')
def _m_time(self) -> None:
self.send_numeric(RPL.Time, ': - {}'.format(datetime.now()))
def _reply_unsupported(self, cmd: str) -> None:
self.send_numeric(Err.UnknownCommand, cmd, ":Not supported")
def _channel_to_chatsession(self, channel: str) -> Optional[ChatSession]:
chat = self._channel_to_chat(channel)
assert chat is not None
return self.chat_sessions.get(chat)
def _channel_to_chat(self, channel: str) -> Optional[Chat]:
return self.backend.chat_get('irc', channel)
def data_received(self, data: bytes, *, transport: Optional[asyncio.BaseTransport] = None) -> None:
if transport is None:
transport = self.transport
assert transport is not None
self.peername = transport.get_extra_info('peername')
for m in self.reader.data_received(data):
try:
f = getattr(self, '_m_{}'.format(m[0].lower()))
f(*m[1:])
except Exception as ex:
self.logger.error(ex)
def send_numeric(self, n: int, *m: str, source: Optional[str] = None) -> None:
self.send_reply('{:03}'.format(n), *m, source = source)
def send_reply(self, *m: str, source: Optional[str] = None) -> None:
if source is None:
source = 'localhost'
self.writer.write((':' + source,) + m)
transport = self.transport
if transport is not None:
transport.write(self.flush())
def flush(self) -> bytes:
return self.writer.flush()
def close(self) -> None:
if self.closed: return
self.closed = True
for cs in list(self.chat_sessions.values()):
cs.close()
if self.close_callback:
self.close_callback()
if self.bs:
self.bs.close()
class BackendEventHandler(event.BackendEventHandler):
__slots__ = ('ctrl', 'bs')
ctrl: IRCCtrl
bs: BackendSession
def __init__(self, ctrl: IRCCtrl) -> None:
self.ctrl = ctrl
def on_client_alert(self, icon_url: Optional[str], url: Optional[str], message: str = '') -> None:
self.ctrl.send_reply('PRIVMSG', '$*', ':' + message, source = "System")
def on_maintenance_message(self, *args: Any, **kwargs: Any) -> None:
if args[1] is not None:
self.ctrl.send_reply(
'NOTICE', '$*', ':CrossTalk is going to go down for maintenance in {} minutes. Now is a good time to wrap up any conversations.'.format(str(args[1])), source = 'System',
)
def on_maintenance_boot(self) -> None:
msg = ':CrossTalk is now in maintenance mode. Try connecting to the service later..'
self.ctrl.send_reply(
'KILL', '$*', msg, source = 'System'
)
self.ctrl.close()
def on_presence_notification(
self, ctc: Contact, on_contact_add: bool, old_substatus: Substatus, *,
trid: Optional[str] = None, update_status: bool = True, update_info_other: bool = True,
send_status_on_bl: bool = False, sess_id: Optional[int] = None, updated_phone_info: Optional[Dict[str, Any]] = None,
) -> None:
if update_status:
self.ctrl.send_reply('NOTICE', ":{} is now {}".format(ctc.head.email, ctc.status.substatus))
def on_presence_self_notification(self, old_substatus: Substatus, *, update_status: bool = True, update_info: bool = True) -> None:
pass
def on_circle_created(self, circle: Circle) -> None:
pass
def on_circle_updated(self, circle: Circle) -> None:
pass
def on_left_circle(self, circle: Circle) -> None:
pass
def on_accepted_circle_invite(self, circle: Circle) -> None:
pass
def on_circle_invite_revoked(self, chat_id: str) -> None:
pass
def on_circle_role_updated(self, chat_id: str, role: CircleRole) -> None:
pass
def on_chat_invite(
self, chat: Chat, inviter: User, *, circle: bool = False, inviter_id: Optional[str] = None, invite_msg: str = '',
) -> None:
if circle: return
self.ctrl.send_reply('INVITE', self.bs.user.email, chat.ids['main'], source = inviter.email)
def on_declined_chat_invite(self, chat: Chat, circle: bool = False) -> None:
pass
def on_added_me(self, user: User, *, adder_id: Optional[str] = None, message: Optional[TextWithData] = None) -> None:
self.ctrl.send_reply('NOTICE', ":{} has added you to their friends list".format(user.email), source = user.email)
if message:
self.ctrl.send_reply('NOTICE', ":\"{}\"".format(message.text), source = user.email)
def on_removed_me(self, user: User) -> None:
pass
def on_contact_request_denied(self, user_added: User, message: Optional[str], *, contact_id: Optional[str] = None) -> None:
self.ctrl.send_reply('NOTICE', ":{} has declined your friend request".format(user_added.email), source = user_added.email)
if message:
self.ctrl.send_reply('NOTICE', ":\"{}\"".format(message), source = user_added.email)
def on_oim_sent(self, oim: 'OIM') -> None:
pass
def on_login_elsewhere(self, option: LoginOption) -> None:
if option is LoginOption.BootOthers:
self.ctrl.send_reply('NOTICE', ":You are being booted because you are logging in from another location.")
else:
self.ctrl.send_reply('NOTICE', ":You are now logged in from multiple locations.")
def on_close(self) -> None:
self.ctrl.close()
class ChatEventHandler(event.ChatEventHandler):
__slots__ = ('ctrl', 'cs')
ctrl: IRCCtrl
cs: ChatSession
def __init__(self, ctrl: IRCCtrl) -> None:
self.ctrl = ctrl
def on_close(self) -> None:
self.ctrl.chat_sessions.pop(self.cs.chat, None)
def on_participant_joined(self, cs_other: ChatSession, first_pop: bool, initial_join: bool) -> None:
if first_pop:
self.ctrl.send_reply('JOIN', self.cs.chat.ids['irc'], source = cs_other.user.email)
def on_participant_left(self, cs_other: ChatSession, last_pop: bool) -> None:
if last_pop:
self.ctrl.send_reply('PART', self.cs.chat.ids['irc'], source = cs_other.user.email)
def on_chat_invite_declined(
self, chat: Chat, invitee: User, *, invitee_id: Optional[str] = None, message: Optional[str] = None, circle: bool = False,
) -> None:
if circle: return
self.ctrl.send_reply('NOTICE', ":{} declined the invitation".format(invitee.email), source = invitee.email)
if message:
self.ctrl.send_reply('NOTICE', ":\"{}\"".format(message), source = invitee.email)
def on_chat_updated(self) -> None:
pass
def on_chat_roster_updated(self) -> None:
pass
def on_participant_status_updated(self, cs_other: ChatSession, first_pop: bool, initial: bool, old_substatus: Substatus) -> None:
pass
def on_message(self, data: MessageData) -> None:
if data.type is not MessageType.Chat:
return
if data.text is None:
return
self.ctrl.send_reply('PRIVMSG', self.cs.chat.ids['irc'], ':' + data.text, source = data.sender.email)
class IRCReader:
__slots__ = ('_logger', '_data')
_logger: Logger
_data: bytes
def __init__(self, logger: Logger) -> None:
self._logger = logger
self._data = b''
def data_received(self, data: bytes) -> Iterable[List[str]]:
if self._data:
self._data += data
else:
self._data = data
while self._data:
m = self._read()
if m is None: break
self._logger.debug('[Client]', *m)
yield m
def _read(self) -> Optional[List[str]]:
try:
i = self._data.index(b'\r\n')
except IndexError:
return None
except ValueError:
return None
chunk = self._data[:i].decode('utf-8')
self._data = self._data[i+2:]
# TODO: Support @foo :bar prefixes
toks = []
while True:
chunk = chunk.lstrip(' ')
if chunk[:1] == ':':
toks.append(chunk[1:])
break
k = chunk.find(' ')
if k < 0:
tok = chunk
else:
tok = chunk[:k]
chunk = chunk[k:]
if tok:
toks.append(tok)
if k < 0:
break
return toks
class IRCWriter:
__slots__ = ('_logger', '_buf')
_logger: Logger
_buf: io.BytesIO
def __init__(self, logger: Logger) -> None:
self._logger = logger
self._buf = io.BytesIO()
def write(self, m: Iterable[Any]) -> None:
self._logger.debug('[Server]', *m)
self._buf.write(' '.join(map(str, m)).encode('utf-8'))
self._buf.write(b'\r\n')
def flush(self) -> bytes:
data = self._buf.getvalue()
if data:
self._buf = io.BytesIO()
return data
class Err(IntEnum):
UnknownError = 400
UnknownCommand = 421
NoNicknameGiven = 431
NicknameInUse = 433
PasswdMismatch = 464
YoureBannedCreep = 465
InviteOnlyChan = 473
class RPL(IntEnum):
Welcome = 1
List = 322
ListEnd = 323
NamReply = 353
ChannelModeIs = 324
Inviting = 341
WhoReply = 352
MOTDStart = 375
MOTD = 372
MOTDEnd = 376
UnAway = 305
NowAway = 306
Away = 301
Version = 351
Time = 391
+61
View File
@@ -0,0 +1,61 @@
from typing import Optional, Callable
import asyncio, settings
from core.backend import Backend
from util.misc import Logger
from .ctrl import IRCCtrl
def register(loop: asyncio.AbstractEventLoop, backend: Backend, *, devmode: bool = False) -> None:
from util.misc import ProtocolRunner
backend.add_runner(ProtocolRunner('0.0.0.0', 6667, ListenerIRC, args = ['IRC', backend, IRCCtrl], service = 'IRC'))
if settings.ENABLE_FRONT_IRC_SSL:
if devmode:
from brutus import Brutus
ssl_context = Brutus('CrossTalk').create_ssl_context()
else:
from core.tls import TLSContext
ssl_context = TLSContext(settings.CERT_ROOT, settings.CERT_DIR).create_ssl_context()
backend.add_runner(ProtocolRunner('0.0.0.0', 6697, ListenerIRC, args = ['IRC + TLS', backend, IRCCtrl], ssl_context = ssl_context, service = 'IRC/TLS'))
class ListenerIRC(asyncio.Protocol):
logger: Logger
backend: Backend
controller: IRCCtrl
transport: Optional[asyncio.WriteTransport]
def __init__(self, logger_prefix: str, backend: Backend, controller_factory: Callable[[Logger, str, Backend], IRCCtrl]) -> None:
super().__init__()
self.logger = Logger(logger_prefix, self)
self.backend = backend
self.controller = controller_factory(self.logger, 'direct', backend)
self.controller.close_callback = self._on_close
self.transport = None
def connection_made(self, transport: asyncio.BaseTransport) -> None:
assert isinstance(transport, asyncio.WriteTransport)
self.transport = transport
self.logger.log_connect()
def connection_lost(self, exc: Optional[Exception]) -> None:
self.controller.close()
self.logger.log_disconnect()
self.transport = None
def data_received(self, data: bytes) -> None:
transport = self.transport
assert transport is not None
if self.backend.maintenance_mode:
transport.close()
return
self.controller.transport = None
if self.controller.transport is None:
self.controller.transport = self.transport
self.controller.data_received(data)
transport.write(self.controller.flush())
self.controller.transport = transport
def _on_close(self) -> None:
if self.transport is None: return
self.transport.close()
+4
View File
@@ -0,0 +1,4 @@
https://en.wikipedia.org/wiki/List_of_Internet_Relay_Chat_commands
https://tools.ietf.org/html/rfc1459
https://modern.ircdocs.horse
https://www.alien.net.au/irc/irc2numerics.html