mirror of
https://git.ugnet.gay/CrossTalk/azul.git
synced 2026-05-27 22:59:49 +00:00
production init
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from .entry import register
|
||||
from .foodgroups import *
|
||||
@@ -0,0 +1,222 @@
|
||||
import asyncio
|
||||
import random
|
||||
import struct
|
||||
|
||||
from core.backend import Backend, BackendSession, Chat, ChatSession
|
||||
from core.client import Client
|
||||
from core.models import LoginOption
|
||||
from itertools import cycle
|
||||
from typing import Optional, Callable, Tuple
|
||||
from util.misc import Logger
|
||||
|
||||
from .proto.backend import BackendEventHandler, FOODGROUP_VERSIONS, login, LoginError, bos_cookies
|
||||
from .proto.snac import OSCARClient, OSCARContext, SNACMessage, foodgroups
|
||||
from .proto.tlv import unmarshal_tlvs, find_tlv
|
||||
|
||||
|
||||
ROASTING_CHARS = b'\xF3\x26\x81\xC4\x39\x86\xDB\x92\x71\xA3\xB9\xE6\x53\x7A\x95\x7C'
|
||||
|
||||
|
||||
def roast(password: bytes) -> bytes:
|
||||
chars = cycle(ROASTING_CHARS)
|
||||
return bytes(byte ^ next(chars) for byte in password)
|
||||
|
||||
|
||||
# TODO(subpurple): as the foodgroups are no longer handled just in this file, might want to combine this and entry
|
||||
class OSCARCtrl:
|
||||
logger: Logger
|
||||
transport: Optional[asyncio.WriteTransport]
|
||||
|
||||
close_callback: Optional[Callable[[], None]]
|
||||
closed: bool
|
||||
|
||||
backend: Backend
|
||||
bs: Optional[BackendSession]
|
||||
|
||||
client: Client
|
||||
|
||||
oscarClient: OSCARClient
|
||||
context: OSCARContext
|
||||
sequence: int = random.randint(0x0000, 0xFFFF)
|
||||
|
||||
def __init__(self, logger: Logger, via: str, backend: Backend) -> None:
|
||||
self.logger = logger
|
||||
self.transport = None
|
||||
|
||||
self.close_callback = None
|
||||
self.closed = False
|
||||
|
||||
self.backend = backend
|
||||
self.bs = None
|
||||
self.client = Client('aim', '?', via)
|
||||
|
||||
self.context = OSCARContext(self.backend, self.client)
|
||||
self.oscarClient = OSCARClient(self)
|
||||
|
||||
def send_specific_frame(self, frame: int, data: bytes) -> None:
|
||||
if self.sequence == 0xFFFF:
|
||||
self.sequence = 0x0000
|
||||
else:
|
||||
self.sequence += 1
|
||||
|
||||
packet = b''.join([
|
||||
struct.pack('>BBHH', 0x2A, frame, self.sequence, len(data)),
|
||||
data
|
||||
])
|
||||
|
||||
self.transport.write(packet)
|
||||
|
||||
def send_snac(self, msg: SNACMessage) -> None:
|
||||
self.send_specific_frame(0x02, msg.marshal())
|
||||
|
||||
def on_connect(self) -> None:
|
||||
self.send_specific_frame(0x01, struct.pack('>L', 1))
|
||||
|
||||
def on_signon_frame(self, data: bytes) -> None:
|
||||
if len(data) > 4:
|
||||
tlvs = unmarshal_tlvs(data[4:])
|
||||
|
||||
if (cookie_tlv := find_tlv(tlvs, 0x0006)) is not None:
|
||||
found = False
|
||||
|
||||
for i, d in enumerate(bos_cookies):
|
||||
for cookie in d.items():
|
||||
if cookie[0] == cookie_tlv.data:
|
||||
if hasattr(cookie[1], 'user') and getattr(cookie[1].user, 'uuid', None) is not None:
|
||||
uuid = cookie[1].user.uuid
|
||||
elif getattr(cookie[1], 'uuid', None) is not None:
|
||||
uuid = cookie[1].uuid
|
||||
else:
|
||||
uuid = cookie[1]
|
||||
|
||||
self.logger.info('found BOS cookie')
|
||||
|
||||
self.context.bs = self.backend.login(uuid,
|
||||
self.client,
|
||||
BackendEventHandler(self),
|
||||
option=LoginOption.BootOthers)
|
||||
self.context.user = self.context.bs.user
|
||||
self.bs = self.context.bs
|
||||
|
||||
bos_cookies.pop(i)
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
|
||||
if found:
|
||||
# NINA also sends OSERVICE__WELL_KNOWN_URLS right after (should we?)
|
||||
self.logger.info('[Server] OSERVICE__HOST_ONLINE')
|
||||
|
||||
msg = SNACMessage(0x0001, 0x0003)
|
||||
|
||||
for foodgroup in FOODGROUP_VERSIONS.keys():
|
||||
msg.write_u16(foodgroup)
|
||||
|
||||
self.send_snac(msg)
|
||||
self.bs.front_data['oscar0'] = True
|
||||
self.bs.front_data['oscar0_chats'] = {}
|
||||
else:
|
||||
self.logger.info('invalid BOS cookie given')
|
||||
|
||||
else:
|
||||
self.logger.info('Using FLAP-level authentication')
|
||||
|
||||
screen_name_tlv = find_tlv(tlvs, 0x0001)
|
||||
roasted_pw_tlv = find_tlv(tlvs, 0x0002)
|
||||
|
||||
screen_name = screen_name_tlv.data.decode()
|
||||
roasted_pw = roasted_pw_tlv.data
|
||||
|
||||
self.logger.info('Screen Name (client-given):', screen_name)
|
||||
self.logger.info('Password (roasted):', roasted_pw.hex())
|
||||
|
||||
context = self.context
|
||||
error_code = None
|
||||
|
||||
if (uuid := context.backend.util_get_uuid_from_username(screen_name)) is None:
|
||||
error_code = LoginError.UnregisteredScreenname
|
||||
|
||||
self.logger.info('Unregistered screenname')
|
||||
else:
|
||||
unroasted_pw = roast(roasted_pw)
|
||||
|
||||
if context.backend.user_service.login_with_username(screen_name, unroasted_pw.decode()) is None:
|
||||
error_code = LoginError.IncorrectPassword
|
||||
|
||||
self.logger.info('Incorrect password')
|
||||
|
||||
self.bs.front_data['oscar0'] = True
|
||||
self.bs.front_data['oscar0_chats'] = {}
|
||||
self.send_specific_frame(0x04, login(self.logger, self.context, tlvs, uuid, error_code))
|
||||
|
||||
def on_data_frame(self, message: SNACMessage) -> None:
|
||||
found = False
|
||||
|
||||
# kick client off if we haven't authenticated and client is trying to access something outside of BUCP
|
||||
if message.foodgroup != 0x0017 and self.bs is None:
|
||||
self.close()
|
||||
return
|
||||
|
||||
for value, cls in foodgroups.items():
|
||||
if value == message.foodgroup:
|
||||
if not hasattr(cls, 'logger'):
|
||||
cls.logger = self.logger
|
||||
|
||||
if message.subgroup in cls.subgroups:
|
||||
found = True
|
||||
|
||||
func = cls.subgroups[message.subgroup]
|
||||
func(cls, self.oscarClient, self.context, message)
|
||||
|
||||
if not found:
|
||||
self.logger.info(f'[Client] Unknown SNAC({hex(message.foodgroup)},{hex(message.subgroup)})')
|
||||
self.logger.info('[Client]', message.data.hex())
|
||||
|
||||
def on_error_frame(self, data: bytes) -> None:
|
||||
self.logger.info('[Client] Recieved error frame with:', data.hex())
|
||||
|
||||
def on_signoff_frame(self, data: bytes) -> None:
|
||||
self.logger.info('[Client] Recieved signoff frame')
|
||||
|
||||
def close(self) -> None:
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
|
||||
if self.close_callback:
|
||||
self.close_callback()
|
||||
|
||||
if self.bs:
|
||||
self.bs.close()
|
||||
|
||||
def _get_private_chat_with(self, other_user_uuid: str) -> Tuple[ChatSession, 'ChatEventHandler']:
|
||||
bs = self.bs
|
||||
assert bs is not None
|
||||
user = bs.user
|
||||
detail = user.detail
|
||||
assert detail is not None
|
||||
|
||||
other_user = self.backend._load_user_record(other_user_uuid)
|
||||
if other_user is None:
|
||||
raise error.ContactNotOnContactList()
|
||||
other_user_ctc = detail.contacts.get(other_user.uuid)
|
||||
if other_user_ctc is not None and other_user_ctc.lists & ContactList.BL:
|
||||
raise error.ContactNotOnContactList()
|
||||
if other_user_uuid not in bs.front_data['oscar0_chats']:
|
||||
other_user_detail = self.backend._load_detail(other_user)
|
||||
if other_user_detail is None: raise error.ContactNotOnline()
|
||||
ctc_self = other_user_detail.contacts.get(user.uuid)
|
||||
if ctc_self is not None:
|
||||
if ctc_self.lists & ContactList.BL: raise error.ContactNotOnline()
|
||||
chat = self.backend.chat_create()
|
||||
chat.front_data['oscar0'] = True
|
||||
|
||||
# `user` joins
|
||||
evt = ChatEventHandler(self.backend.loop, self, bs)
|
||||
cs = chat.join('icbm', bs, evt)
|
||||
bs.front_data['oscar0_chats'][other_user_uuid] = (cs, evt)
|
||||
cs.invite(other_user)
|
||||
elif other_user.status.is_offlineish():
|
||||
raise error.ContactNotOnline()
|
||||
return bs.front_data['oscar0_chats'].get(other_user_uuid)
|
||||
@@ -0,0 +1,109 @@
|
||||
import asyncio
|
||||
import struct
|
||||
import settings
|
||||
|
||||
from core.backend import Backend
|
||||
from typing import Optional, Callable
|
||||
from threading import Thread
|
||||
from util.misc import Logger, ProtocolRunner
|
||||
|
||||
from .ctrl import OSCARCtrl
|
||||
from .proto.snac import SNACMessage
|
||||
|
||||
|
||||
def register(loop: asyncio.AbstractEventLoop, backend: Backend) -> None:
|
||||
backend.add_runner(ProtocolRunner('0.0.0.0', 5190, ListenerOSCAR, args=['OSCAR', backend, OSCARCtrl], service = 'OSCAR'))
|
||||
|
||||
|
||||
class ListenerOSCAR(asyncio.Protocol):
|
||||
logger: Logger
|
||||
backend: Backend
|
||||
controller: OSCARCtrl
|
||||
transport: Optional[asyncio.WriteTransport]
|
||||
|
||||
buffer: bytes = b''
|
||||
data_thread: Thread = None
|
||||
|
||||
def __init__(self,
|
||||
logger_prefix: str,
|
||||
backend: Backend,
|
||||
controller_factory: Callable[[Logger, str, Backend], OSCARCtrl]) -> 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:
|
||||
self.controller.transport = transport
|
||||
self.controller.on_connect()
|
||||
self.logger.log_connect()
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
self.controller.close()
|
||||
self.logger.log_disconnect()
|
||||
|
||||
def data_received(self, packet: bytes) -> None:
|
||||
if self.backend.maintenance_mode:
|
||||
self.transport.close()
|
||||
return
|
||||
|
||||
self.buffer += packet
|
||||
|
||||
if self.data_thread is None or not self.data_thread.is_alive():
|
||||
self.data_thread = Thread(target=self.parse_buffer)
|
||||
self.data_thread.start()
|
||||
|
||||
def parse_buffer(self) -> None:
|
||||
while True:
|
||||
if self.buffer[0] != 0x2A:
|
||||
break
|
||||
|
||||
# TODO(subpurple): Bufferize this?
|
||||
frame, sequence, length = struct.unpack('>BHH', self.buffer[1:6])
|
||||
|
||||
if len(self.buffer) < length + 6:
|
||||
break
|
||||
|
||||
if len(self.buffer) >= length + 6:
|
||||
data = self.buffer[6:length + 6]
|
||||
|
||||
match frame:
|
||||
# SIGNON
|
||||
case 0x01:
|
||||
self.controller.on_signon_frame(data)
|
||||
|
||||
# DATA (always contains a SNAC)
|
||||
case 0x02:
|
||||
if len(data) < 10:
|
||||
return
|
||||
|
||||
msg = SNACMessage()
|
||||
msg.unmarshal(data)
|
||||
self.controller.on_data_frame(msg)
|
||||
|
||||
# ERROR
|
||||
case 0x03:
|
||||
self.controller.on_error_frame(data)
|
||||
|
||||
# SIGNOFF
|
||||
case 0x04:
|
||||
self.controller.on_signoff_frame(data)
|
||||
|
||||
# KEEP_ALIVE
|
||||
case 0x05:
|
||||
pass
|
||||
|
||||
case _:
|
||||
self.logger.info('Recieved unknown frame:', str(frame), 'with data:', data.hex())
|
||||
|
||||
self.buffer = self.buffer[length + 6:]
|
||||
|
||||
if len(self.buffer) < 6:
|
||||
break
|
||||
|
||||
def _on_close(self) -> None:
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
@@ -0,0 +1,10 @@
|
||||
__all__ = [
|
||||
'oservice',
|
||||
'locate',
|
||||
'buddy',
|
||||
'icbm',
|
||||
'bos',
|
||||
'stats',
|
||||
'feedbag',
|
||||
'bucp'
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
import struct
|
||||
|
||||
from util.misc import Logger
|
||||
|
||||
from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup
|
||||
from ..proto.tlv import TLV
|
||||
|
||||
|
||||
@Foodgroup(0x0009)
|
||||
class BOSFoodgroup:
|
||||
logger: Logger
|
||||
|
||||
@Subgroup(0x0002)
|
||||
def rights_query(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] BOS__RIGHTS_QUERY')
|
||||
|
||||
response_msg = SNACMessage(0x0009, 0x0003)
|
||||
response_msg.write_tlvs([
|
||||
TLV(0x0001, struct.pack('>H', 1000)), # max permits user is allowed
|
||||
TLV(0x0002, struct.pack('>H', 1000)), # max deny entries user is allowed
|
||||
TLV(0x0003, struct.pack('>H', 1000)) # max temp permits user is allowed
|
||||
])
|
||||
|
||||
self.logger.info('[Server] BOS__RIGHTS_REPLY')
|
||||
client.send_snac(response_msg)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
from util.misc import Logger
|
||||
from util.hash import gen_salt
|
||||
|
||||
from ..proto.backend import login, LoginError
|
||||
from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup
|
||||
from ..proto.tlv import unmarshal_tlvs, find_tlv
|
||||
|
||||
pw_change_url_format = 'http://aim.aol.com/redirects/password/change_password.adp?ScreenName={}&ccode=us&lang=en'
|
||||
|
||||
|
||||
@Foodgroup(0x0017)
|
||||
class BUCPFoodgroup:
|
||||
logger: Logger
|
||||
|
||||
@Subgroup(0x0006)
|
||||
def challenge_request(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] BUCP__CHALLENGE_REQUEST')
|
||||
|
||||
tlvs = unmarshal_tlvs(message.data)
|
||||
screen_name_tlv = find_tlv(tlvs, 0x0001)
|
||||
screen_name = screen_name_tlv.data.decode()
|
||||
|
||||
salt = context.backend.user_service.aim_get_md5_salt(screen_name)
|
||||
|
||||
if salt is None:
|
||||
# screen name doesn't exist or user did not enable OSCAR
|
||||
salt = gen_salt()
|
||||
|
||||
response_msg = SNACMessage(0x0001, 0x0007)
|
||||
response_msg.write_u16(len(salt))
|
||||
response_msg.write_string(salt)
|
||||
|
||||
self.logger.info('[Server] BUCP__CHALLENGE_RESPONSE (salt:', salt + ')')
|
||||
client.send_snac(response_msg)
|
||||
|
||||
@Subgroup(0x0002)
|
||||
def login_request(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] BUCP__LOGIN_REQUEST')
|
||||
|
||||
tlvs = unmarshal_tlvs(message.data)
|
||||
|
||||
screen_name_tlv = find_tlv(tlvs, 0x0001)
|
||||
hashed_pw_tlv = find_tlv(tlvs, 0x0025)
|
||||
|
||||
screen_name = screen_name_tlv.data.decode()
|
||||
|
||||
self.logger.info('Screen Name (client-given):', screen_name)
|
||||
self.logger.info('Password (hashed):', hashed_pw_tlv.data)
|
||||
|
||||
response_msg = SNACMessage(0x0017, 0x0003)
|
||||
|
||||
error_code = None
|
||||
|
||||
if (uuid := context.backend.util_get_uuid_from_username(screen_name)) is None:
|
||||
error_code = LoginError.UnregisteredScreenname
|
||||
|
||||
self.logger.info('Unregistered screenname')
|
||||
else:
|
||||
password = context.backend.user_service.aim_get_md5_password(screen_name)
|
||||
|
||||
if password != hashed_pw_tlv.data:
|
||||
error_code = LoginError.IncorrectPassword
|
||||
|
||||
self.logger.info('Incorrect password')
|
||||
|
||||
response_msg.write_bytes(login(self.logger, context, tlvs, uuid, error_code))
|
||||
|
||||
client.send_snac(response_msg)
|
||||
@@ -0,0 +1,111 @@
|
||||
import struct
|
||||
import time
|
||||
|
||||
from core.models import Contact
|
||||
from util.misc import Logger
|
||||
|
||||
from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup
|
||||
from ..proto.tlv import TLV, unmarshal_tlvs
|
||||
|
||||
|
||||
# should be piped to .ctrl.send_snac()
|
||||
def build_presence_notif(contact: Contact) -> SNACMessage:
|
||||
# Packet dump from NINA's servers when a contact goes online:
|
||||
# ===
|
||||
# 0000 04 64 6b 61 79 00 00 00 07 00 30 00 04 67 19 67 .dkay.....0..g.g
|
||||
# 0010 4c 00 0d 00 c0 09 46 13 45 4c 7f 11 d1 82 22 44 L.....F.EL...."D
|
||||
# 0020 45 53 54 00 00 09 46 01 ff 4c 7f 11 d1 82 22 44 EST...F..L...."D
|
||||
# 0030 45 53 54 00 00 74 8f 24 20 62 87 11 d1 82 22 44 EST..t.$ b...."D
|
||||
# 0040 45 53 54 00 00 09 46 13 43 4c 7f 11 d1 82 22 44 EST...F.CL...."D
|
||||
# 0050 45 53 54 00 00 09 46 13 41 4c 7f 11 d1 82 22 44 EST...F.AL...."D
|
||||
# 0060 45 53 54 00 00 09 46 01 04 4c 7f 11 d1 82 22 44 EST...F..L...."D
|
||||
# 0070 45 53 54 00 00 09 46 01 05 4c 7f 11 d1 82 22 44 EST...F..L...."D
|
||||
# 0080 45 53 54 00 00 09 46 01 02 4c 7f 11 d1 82 22 44 EST...F..L...."D
|
||||
# 0090 45 53 54 00 00 09 46 01 03 4c 7f 11 d1 82 22 44 EST...F..L...."D
|
||||
# 00a0 45 53 54 00 00 09 46 01 01 4c 7f 11 d1 82 22 44 EST...F..L...."D
|
||||
# 00b0 45 53 54 00 00 09 46 13 4a 4c 7f 11 d1 82 22 44 EST...F.JL...."D
|
||||
# 00c0 45 53 54 00 00 09 46 13 46 4c 7f 11 d1 82 22 44 EST...F.FL...."D
|
||||
# 00d0 45 53 54 00 00 00 1d 00 12 00 01 00 05 02 01 d2 EST.............
|
||||
# 00e0 04 72 00 00 00 05 02 01 d2 04 72 00 01 00 02 00 .r........r.....
|
||||
# 00f0 08 00 03 00 04 67 19 67 4b 00 0f 00 04 00 00 00 .....g.gK.......
|
||||
# 0100 02 00 05 00 04 62 7c 7a cd .....b|z.
|
||||
#
|
||||
# Buddy length: 0x04
|
||||
# Buddy name: dkay
|
||||
# TLVs (7):
|
||||
# - TLV 0x3000: unknown (data = 67 19 67 4C)
|
||||
# - TLV 0x000D: capability list
|
||||
# - TLV 0x001D: BART info (data = 00 01 00 05 02 01 D2 04 72 00 00 00 05 02 01 D2 04 72
|
||||
# - TLV 0x0001: user class (data = 00 00 00 08)
|
||||
# - TLV 0x0003: account signon time (value = 1729718091, in UNIX time)
|
||||
# - TLV 0x000F: session length (value = 2)
|
||||
# - TLV 0x0005: account creation time (value = 1652325069, in UNIX time)
|
||||
#
|
||||
|
||||
msg = SNACMessage(0x0003, 0x000B if not contact.status.is_offlineish() else 0x000C)
|
||||
|
||||
msg.write_string_u8(contact.head.username)
|
||||
msg.write_u16(0)
|
||||
|
||||
if contact.status.is_offlineish():
|
||||
msg.write_tlv(TLV(0x0001, struct.pack('>L', 0x0008))) # User class (bitfield)
|
||||
else:
|
||||
capabilities = [
|
||||
"{09461345-4C7F-11D1-8222-444553540000}", # Direct ICBM
|
||||
"{094601FF-4C7F-11D1-8222-444553540000}", # Smart caps
|
||||
"{748F2420-6287-11D1-8222-444553540000}", # Chat
|
||||
"{09461343-4C7F-11D1-8222-444553540000}", # File transfer
|
||||
"{09461341-4C7F-11D1-8222-444553540000}", # Voice chat
|
||||
"{09460104-4C7F-11D1-8222-444553540000}", # RTC audio
|
||||
"{09460105-4C7F-11D1-8222-444553540000}", # Unknown
|
||||
"{09460102-4C7F-11D1-8222-444553540000}", # Camera
|
||||
"{09460103-4C7F-11D1-8222-444553540000}", # Microphone
|
||||
"{09460101-4C7F-11D1-8222-444553540000}", # RTC video
|
||||
"{0946134A-4C7F-11D1-8222-444553540000}", # Games
|
||||
"{09461346-4C7F-11D1-8222-444553540000}" # BART
|
||||
]
|
||||
capabilities_bytes = b''
|
||||
|
||||
for capability in capabilities:
|
||||
capabilities_bytes += bytes.fromhex(capability
|
||||
.lstrip('{')
|
||||
.rstrip('}')
|
||||
.replace('-', ''))
|
||||
|
||||
date_created_unix = int(time.mktime(contact.head.date_created.timetuple()))
|
||||
date_login_unix = int(time.mktime(contact.head.date_login.timetuple())) if contact.head.date_login else 0
|
||||
|
||||
# usually would include 0x001D (BART info), but since CrossTalk doesn't support
|
||||
# it rn I've decided to omit it
|
||||
msg.write_tlv_block([
|
||||
TLV(0x3000, struct.pack('>L', 0x6719674C)), # Unknown
|
||||
TLV(0x000D, capabilities_bytes), # Capability info
|
||||
TLV(0x0001, struct.pack('>H', 0x0008)), # User class (bitfield)
|
||||
TLV(0x0003, struct.pack('>L', date_login_unix)), # Account signon time (unix time_t)
|
||||
TLV(0x000F, struct.pack('>L', 0)), # Session length
|
||||
TLV(0x0005, struct.pack('>L', date_created_unix)), # Account creation time (unix time_t)
|
||||
])
|
||||
|
||||
print('Subgroup:', hex(msg.subgroup))
|
||||
print('TLVs:', unmarshal_tlvs(msg.data))
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
@Foodgroup(0x0003)
|
||||
class BuddyFoodgroup:
|
||||
logger: Logger
|
||||
|
||||
@Subgroup(0x0002)
|
||||
def rights_query(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] BUDDY_RIGHTS_QUERY')
|
||||
|
||||
response_msg = SNACMessage(0x0003, 0x0003)
|
||||
response_msg.write_tlvs([
|
||||
TLV(0x0001, struct.pack('>H', 1000)), # max buddies
|
||||
TLV(0x0002, struct.pack('>H', 3000)), # max watchers
|
||||
TLV(0x0004, struct.pack('>H', 160)) # max temp buddies
|
||||
])
|
||||
|
||||
self.logger.info('[Server] BUDDY_RIGHTS_REPLY')
|
||||
client.send_snac(response_msg)
|
||||
@@ -0,0 +1,297 @@
|
||||
import time
|
||||
import struct
|
||||
|
||||
from array import array
|
||||
from core.models import ContactList, Substatus
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
from util.misc import Logger
|
||||
|
||||
from .buddy import build_presence_notif # TODO(subpurple): move build_presence_notif out of buddy?
|
||||
from ..proto.tlv import TLV
|
||||
from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup
|
||||
from ..proto.buffer import Buffer
|
||||
|
||||
|
||||
# I only put the classes I need in this IntEnum
|
||||
#
|
||||
# For a complete list of feedbag classes, see https://wiki.nina.chat/wiki/Protocols/OSCAR/Foodgroups/FEEDBAG/Items#Class:_FEEDBAG_CLASS_IDS
|
||||
class FeedbagClass(IntEnum):
|
||||
Buddy = 0x0000,
|
||||
Group = 0x0001,
|
||||
PdInfo = 0x0004,
|
||||
BuddyPrefs = 0x0005
|
||||
|
||||
|
||||
class FeedbagAttributes(IntEnum):
|
||||
Order = 0x00C8,
|
||||
PdMode = 0x00CA,
|
||||
WirelessPdMode = 0x00D0,
|
||||
WirelessIgnoreMode = 0x00D1,
|
||||
FishPdMode = 0x00D2,
|
||||
FishIgnoreMode = 0x00D3,
|
||||
PdMask = 0x00CB,
|
||||
BuddyPrefs = 0x00C9
|
||||
|
||||
|
||||
@dataclass
|
||||
class FeedbagItem:
|
||||
name: str
|
||||
group_id: int
|
||||
item_id: int
|
||||
class_id: int
|
||||
attributes: array[TLV]
|
||||
|
||||
def __init__(self, name: str, group_id: int, item_id: int, class_id: int, attributes: Optional[array[TLV]] = None) -> None:
|
||||
self.name = name
|
||||
self.group_id = group_id
|
||||
self.item_id = item_id
|
||||
self.class_id = class_id
|
||||
self.attributes = attributes or []
|
||||
|
||||
def marshal(self) -> bytes:
|
||||
buf = Buffer()
|
||||
buf.write_string_u16(self.name)
|
||||
buf.write_u16(self.group_id)
|
||||
buf.write_u16(self.item_id)
|
||||
buf.write_u16(self.class_id)
|
||||
buf.write_tlv_l_block(self.attributes)
|
||||
|
||||
return buf.data
|
||||
|
||||
|
||||
def get_items(context: OSCARContext) -> array[FeedbagItem]:
|
||||
user = context.user
|
||||
detail = user.detail
|
||||
|
||||
contacts = list({
|
||||
*detail.get_contacts_by_list(ContactList.AL),
|
||||
*detail.get_contacts_by_list(ContactList.FL)
|
||||
})
|
||||
|
||||
group_feedbag = []
|
||||
group_order = b''
|
||||
|
||||
contact_feedbag = []
|
||||
|
||||
for id, group in detail._groups_by_id.items():
|
||||
order = b''
|
||||
|
||||
for contact in contacts:
|
||||
user = contact.head
|
||||
|
||||
for grp in contact._groups.copy():
|
||||
if grp.id == id:
|
||||
contact_feedbag.append(FeedbagItem(user.username, int(group.id), user.id, FeedbagClass.Buddy))
|
||||
order += struct.pack('>H', user.id)
|
||||
|
||||
group_feedbag.append(FeedbagItem(group.name, int(group.id), 0x0000, FeedbagClass.Group, [
|
||||
TLV(FeedbagAttributes.Order, order)
|
||||
]))
|
||||
|
||||
group_order += struct.pack('>H', int(group.id))
|
||||
|
||||
# deal with any ungrouped contacts left
|
||||
if ungrouped_contacts := [contact for contact in contacts if not contact._groups]:
|
||||
no_group_gid = len(group_feedbag) + 1
|
||||
|
||||
order = b''
|
||||
|
||||
for contact in ungrouped_contacts:
|
||||
user = contact.head
|
||||
|
||||
contact_feedbag.append(FeedbagItem(user.username, no_group_gid, user.id, FeedbagClass.Buddy))
|
||||
|
||||
order += struct.pack('>H', user.id)
|
||||
|
||||
group_feedbag.append(FeedbagItem('(No Group)', no_group_gid, 0x0000, FeedbagClass.Group, [
|
||||
TLV(FeedbagAttributes.Order, order)
|
||||
]))
|
||||
|
||||
return [
|
||||
FeedbagItem('', 0x0000, 0x0000, FeedbagClass.Group, [
|
||||
TLV(FeedbagAttributes.Order, group_order)
|
||||
]),
|
||||
|
||||
FeedbagItem('', 0x0000, 0x0E63, FeedbagClass.PdInfo, [
|
||||
TLV(FeedbagAttributes.PdMode, struct.pack('>H', 0x0004)),
|
||||
TLV(FeedbagAttributes.WirelessPdMode, struct.pack('>B', 0x0001)),
|
||||
TLV(FeedbagAttributes.WirelessIgnoreMode, struct.pack('>B', 0x0001)),
|
||||
TLV(FeedbagAttributes.FishPdMode, struct.pack('>B', 0x0001)),
|
||||
TLV(FeedbagAttributes.FishIgnoreMode, struct.pack('>B', 0x0001)),
|
||||
TLV(FeedbagAttributes.PdMask, struct.pack('>H', 0xFFFF))
|
||||
]),
|
||||
|
||||
FeedbagItem('', 0x0000, 0x4B1D, FeedbagClass.BuddyPrefs, [
|
||||
TLV(FeedbagAttributes.BuddyPrefs, struct.pack('>L', 0x00000400))
|
||||
]),
|
||||
|
||||
*group_feedbag,
|
||||
*contact_feedbag
|
||||
]
|
||||
|
||||
|
||||
def marshal_items(items: array[FeedbagItem]) -> bytes:
|
||||
return b''.join([item.marshal() for item in items])
|
||||
|
||||
|
||||
def unmarshal_items(item_bytes: bytes) -> array[FeedbagItem]:
|
||||
buf = Buffer(item_bytes)
|
||||
items = []
|
||||
|
||||
# Minimum feedbag item size (in bytes):
|
||||
# Name = +2 = 2
|
||||
# Group ID = +2 = 4
|
||||
# Item ID = +2 = 6
|
||||
# Class ID = +2 = 8
|
||||
# Attributes = +2 = 10
|
||||
#
|
||||
while len(buf) > 10:
|
||||
name = buf.read_string_u16()
|
||||
group_id = buf.read_u16()
|
||||
item_id = buf.read_u16()
|
||||
class_id = buf.read_u16()
|
||||
attributes = buf.read_tlv_l_block()
|
||||
|
||||
items.append(FeedbagItem(name, group_id, item_id, class_id, attributes))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
@Foodgroup(0x0013)
|
||||
class FeedbagFoodgroup:
|
||||
logger: Logger
|
||||
|
||||
@Subgroup(0x0002)
|
||||
def rights_query(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] FEEDBAG__RIGHTS_QUERY')
|
||||
|
||||
response_msg = SNACMessage(0x0013, 0x0003)
|
||||
|
||||
# https://wiki.nina.chat/wiki/Protocols/OSCAR/SNAC/FEEDBAG_RIGHTS_REPLY#TLV_Class:_FEEDBAG_RIGHTS_REPLY_TAGS
|
||||
response_msg.write_tlvs([
|
||||
TLV(0x0002, struct.pack('>H', 254)), # max class attrs
|
||||
TLV(0x0003, struct.pack('>H', 1698)), # max item attrs
|
||||
|
||||
# max items
|
||||
TLV(0x0004, struct.pack(f'>{'H' * 67}',
|
||||
1000, # max num of contacts
|
||||
100, # max num of groups
|
||||
1000, # max num of visible contacts
|
||||
1000, # max num of invisible contacts
|
||||
1, # max vis/invis bitmasks
|
||||
1, # max presence info fields
|
||||
150, # limit for item type 06
|
||||
12, # limit for item type 07
|
||||
12, # limit for item type 08
|
||||
3, # limit for item type 09
|
||||
50, # limit for item type 0a
|
||||
50, # limit for item type 0b
|
||||
0, # limit for item type 0c
|
||||
128, # limit for item type 0d
|
||||
1000, # max ignore list entries
|
||||
20, # limit for item type 0f
|
||||
200, # limit for item 10
|
||||
1, # limit for item 11
|
||||
100, # limit for item 12
|
||||
1, # limit for item 13
|
||||
25, # limit for item 14
|
||||
|
||||
# These values are unknown but are here in the sake of keeping response
|
||||
# parity with NINA:
|
||||
1, 40, 1, 10, 200, 1, 60, 200, 1, 8, 20, 1, 10000, 1000, 1000, 50, 1, 5,
|
||||
500, 1, 8, 10000, 1, 1, 1, 10000, 0, 0, 1, 2000, 0, 60, 24, 10, 1, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1000, 1, 1)),
|
||||
|
||||
TLV(0x0005, struct.pack('>H', 100)), # max client items
|
||||
TLV(0x0006, struct.pack('>H', 97)), # max item name len
|
||||
TLV(0x0007, struct.pack('>H', 2000)), # max recent buddies
|
||||
TLV(0x0008, struct.pack('>H', 10)), # interaction buddies
|
||||
TLV(0x0009, struct.pack('>L', 432000)), # interaction half life - in 2^(-age/half_life) in seconds
|
||||
TLV(0x000A, struct.pack('>L', 14)), # interaction max score
|
||||
TLV(0x000B, struct.pack('>H', 0)), # unknown
|
||||
TLV(0x000C, struct.pack('>H', 600)), # max buddies per group
|
||||
TLV(0x000D, struct.pack('>H', 200)), # max allowed bot buddies
|
||||
TLV(0x000E, struct.pack('>H', 32)) # max smart groups
|
||||
])
|
||||
|
||||
self.logger.info('[Server] FEEDBAG__RIGHTS_REPLY')
|
||||
client.send_snac(response_msg)
|
||||
|
||||
@Subgroup(0x0004)
|
||||
def query(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] FEEDBAG__QUERY')
|
||||
|
||||
items = get_items(context)
|
||||
|
||||
response_msg = SNACMessage(0x0013, 0x0006, 0x0000, message.request_id)
|
||||
response_msg.write_u8(0) # feedbag protocol version - always 0
|
||||
response_msg.write_u16(len(items)) # no. of feedbag items
|
||||
response_msg.write_bytes(marshal_items(items)) # list of feedbag items
|
||||
response_msg.write_u32(int(time.time())) # feedbag last change time - TODO: should pull from db
|
||||
|
||||
self.logger.info('[Server] FEEDBAG__REPLY')
|
||||
client.send_snac(response_msg)
|
||||
|
||||
@Subgroup(0x0005)
|
||||
def query_if_modified(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] FEEDBAG__QUERY_IF_MODIFIED (not implemented)')
|
||||
|
||||
# 66 51 29 47 00 0D
|
||||
#
|
||||
# 66 51 29 47 - u32 for unix timestamp of cached client-side feedbag
|
||||
# 00 0D - u16 for number of items in cached client-side feedbag
|
||||
cached_feedbag_timestamp = message.read_u32()
|
||||
cached_feedbag_num_items = message.read_u16()
|
||||
|
||||
self.logger.info('[Client] Cached feedbag timestamp:', cached_feedbag_timestamp)
|
||||
self.logger.info('[Client] Cached feedbag items num:', cached_feedbag_num_items)
|
||||
|
||||
# TODO(subpurple): do check
|
||||
self.query(client, context, message)
|
||||
|
||||
@Subgroup(0x0007)
|
||||
def use(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] FEEDBAG__USE')
|
||||
|
||||
# set our status to Online
|
||||
context.bs.me_update({
|
||||
'substatus': Substatus.Online
|
||||
})
|
||||
|
||||
# notify the client if any contacts are online
|
||||
user = context.user
|
||||
detail = user.detail
|
||||
|
||||
contacts = list({
|
||||
*detail.get_contacts_by_list(ContactList.AL)
|
||||
})
|
||||
|
||||
for contact in contacts:
|
||||
if not contact.status.is_offlineish():
|
||||
client.send_snac(build_presence_notif(contact))
|
||||
|
||||
# stubs because I cba to implement more of feedbag rn
|
||||
@Subgroup(0x0011)
|
||||
def start_cluster(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] FEEDBAG__START_CLUSTER (not implemented)')
|
||||
|
||||
@Subgroup(0x0012)
|
||||
def end_cluster(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] FEEDBAG__END_CLUSTER (not implemented)')
|
||||
|
||||
@Subgroup(0x0008)
|
||||
def insert_item(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] FEEDBAG__INSERT_ITEM (not implemented)')
|
||||
self.logger.info('[Client]', unmarshal_items(message.data))
|
||||
|
||||
@Subgroup(0x0009)
|
||||
def update_item(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] FEEDBAG__UPDATE_ITEM (not implemented)')
|
||||
self.logger.info('[Client]', unmarshal_items(message.data))
|
||||
|
||||
@Subgroup(0x000A)
|
||||
def delete_item(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] FEEDBAG__DELETE_ITEM (not implemented)')
|
||||
self.logger.info('[Client]', unmarshal_items(message.data))
|
||||
@@ -0,0 +1,143 @@
|
||||
import time, struct
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from util.misc import Logger
|
||||
from core.models import MessageData, MessageType, User
|
||||
|
||||
from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup
|
||||
from ..proto.tlv import unmarshal_tlvs, marshal_tlvs, TLV
|
||||
|
||||
class ICBMChannel(IntEnum):
|
||||
AOLIM = 0x0001,
|
||||
Rendezvous = 0x0002,
|
||||
Mime = 0x0003,
|
||||
ICQ = 0x0004,
|
||||
CoBrowser = 0x0005
|
||||
|
||||
@Foodgroup(0x0004)
|
||||
class ICBMFoodgroup:
|
||||
logger: Logger
|
||||
|
||||
@Subgroup(0x0002)
|
||||
def add_paramenters(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] ICBM__ADD_PARAMENTERS (not implemented)')
|
||||
self.logger.info('[Client]', message.data.hex())
|
||||
|
||||
@Subgroup(0x0004)
|
||||
def parameter_query(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] ICBM__PARAMENTER_QUERY')
|
||||
|
||||
response_msg = SNACMessage(0x0004, 0x0005)
|
||||
|
||||
# Response from NINA's servers:
|
||||
# ==
|
||||
# 0000 2a 02 00 0a 00 1a 00 04 00 05 00 00 00 00 00 04 *...............
|
||||
# [FLAP Header....] [SNAC Header................]
|
||||
# 0010 00 05 00 00 00 03 02 00 03 84 03 e7 00 00 03 e8 ................
|
||||
#
|
||||
# The response data are not TLVs and are instead WORD/DWORDs so I cannot fit
|
||||
# the names below the hex data. See https://wiki.nina.chat/wiki/Protocols/OSCAR/SNAC/ICBM_PARAMETER_REPLY and
|
||||
# https://wiki.nina.chat/wiki/Protocols/OSCAR/SNAC/ICBM_ADD_PARAMETERS for more information.
|
||||
response_msg.write_u16(5) # maxSlots
|
||||
response_msg.write_u32(0x00003) # icbmFlags (default)
|
||||
response_msg.write_u16(512) # maxIncomingICBMLen
|
||||
response_msg.write_u16(999) # maxSourceEvil
|
||||
response_msg.write_u16(999) # maxDestinationEvil
|
||||
response_msg.write_u32(1000) # minInterICBMInterval
|
||||
|
||||
self.logger.info('[Server] ICBM__PARAMENTER_REPLY')
|
||||
client.send_snac(response_msg)
|
||||
|
||||
@Subgroup(0x0006)
|
||||
def channel_msg_tohost(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
# Client packet:
|
||||
# ===
|
||||
# 0000 44 42 38 35 30 35 00 00 00 01 04 74 65 73 74 00 DB8505.....test.
|
||||
# 0010 02 00 3F 05 01 00 03 01 01 02 01 01 00 34 00 00 ..?..........4..
|
||||
# 0020 00 00 3C 48 54 4D 4C 3E 3C 42 4F 44 59 20 42 47 ..<HTML><BODY BG
|
||||
# 0030 43 4F 4C 4F 52 3D 22 23 66 66 66 66 66 66 22 3E COLOR="#ffffff">
|
||||
# 0040 74 65 73 74 3C 2F 42 4F 44 59 3E 3C 2F 48 54 4D test</BODY></HTM
|
||||
# 0050 4C 3E 00 03 00 00 L>....
|
||||
#
|
||||
# [OSCAR] (ID: c245) [Client] ICBM__CHANNEL_MSG_TOHOST
|
||||
# [OSCAR] (ID: c245) [Client] Cookie: b'E67FA1\x00\x00'
|
||||
# [OSCAR] (ID: c245) [Client] Channel: 0x1
|
||||
# [OSCAR] (ID: c245) [Client] Message reciever: test
|
||||
# [OSCAR] (ID: c245) [Server] TLV 2 - b'\x05\x01\x00\x03\x01\x01\x02\x01\x01\x006\x00\x00\x00\x00<HTML><BODY BGCOLOR="#ffffff">123456</BODY></HTML>'
|
||||
cookie = message.read_bytes(8)
|
||||
channel = message.read_u16()
|
||||
reciever = message.read_string_u8()
|
||||
tlvs = unmarshal_tlvs(message.data)
|
||||
|
||||
self.logger.info('[Client] ICBM__CHANNEL_MSG_TOHOST')
|
||||
self.logger.info('[Client] Cookie:', cookie)
|
||||
self.logger.info('[Client] Channel:', hex(channel))
|
||||
self.logger.info('[Client] Message reciever:', reciever)
|
||||
|
||||
for tlv in tlvs:
|
||||
self.logger.info('[Client] TLV', hex(tlv.type), '-', tlv.data)
|
||||
|
||||
def messagedata_to_icbm(cookie: bytes, data: MessageData, user: User):
|
||||
# THIS SHIT DOESN'T WORK. WHY??? HAS I EVER!?!?!?
|
||||
if 'icbm' not in data.front_cache:
|
||||
type = data.type
|
||||
text = data.text
|
||||
sender = data.sender
|
||||
if type == MessageType.Chat:
|
||||
msg = SNACMessage(0x0004, 0x0007)
|
||||
msg.write_bytes(cookie)
|
||||
msg.write_u16(ICBMChannel.AOLIM)
|
||||
|
||||
msg.write_string_u8(user.username)
|
||||
msg.write_u16(0)
|
||||
# we should not hardcode these
|
||||
capabilities = [
|
||||
"{09461345-4C7F-11D1-8222-444553540000}", # Direct ICBM
|
||||
"{094601FF-4C7F-11D1-8222-444553540000}", # Smart caps
|
||||
"{748F2420-6287-11D1-8222-444553540000}", # Chat
|
||||
"{09461343-4C7F-11D1-8222-444553540000}", # File transfer
|
||||
"{09461341-4C7F-11D1-8222-444553540000}", # Voice chat
|
||||
"{09460104-4C7F-11D1-8222-444553540000}", # RTC audio
|
||||
"{09460105-4C7F-11D1-8222-444553540000}", # Unknown
|
||||
"{09460102-4C7F-11D1-8222-444553540000}", # Camera
|
||||
"{09460103-4C7F-11D1-8222-444553540000}", # Microphone
|
||||
"{09460101-4C7F-11D1-8222-444553540000}", # RTC video
|
||||
"{0946134A-4C7F-11D1-8222-444553540000}", # Games
|
||||
"{09461346-4C7F-11D1-8222-444553540000}" # BART
|
||||
]
|
||||
capabilities_bytes = b''
|
||||
|
||||
for capability in capabilities:
|
||||
capabilities_bytes += bytes.fromhex(capability
|
||||
.lstrip('{')
|
||||
.rstrip('}')
|
||||
.replace('-', ''))
|
||||
|
||||
date_created_unix = int(time.mktime(user.date_created.timetuple()))
|
||||
date_login_unix = int(time.mktime(user.date_login.timetuple())) if user.date_login else 0
|
||||
|
||||
# usually would include 0x001D (BART info), but CrossTalk doesn't support that yet
|
||||
pre = marshal_tlvs([
|
||||
TLV(0x3000, struct.pack('>L', 0x6719674C)), # Unknown
|
||||
TLV(0x000D, capabilities_bytes), # Capability info
|
||||
TLV(0x0001, struct.pack('>H', 0x0008)), # User class (bitfield)
|
||||
TLV(0x0003, struct.pack('>L', date_login_unix)), # Account signon time (unix time_t)
|
||||
TLV(0x000F, struct.pack('>L', 0)), # Session length
|
||||
TLV(0x0005, struct.pack('>L', date_created_unix)), # Account creation time (unix time_t)
|
||||
])
|
||||
|
||||
|
||||
im_text = data.text.encode('ascii')
|
||||
im_tag = 0x0101
|
||||
im_len = 2 + 2 + len(im_text)
|
||||
imdata_bytes = struct.pack('>HHHH', im_tag, im_len, 0, 0) + im_text
|
||||
|
||||
msg.write_tlv_block([
|
||||
*unmarshal_tlvs(pre),
|
||||
TLV(0x0501, struct.pack('>L', 1)),
|
||||
TLV(0x0101, imdata_bytes)
|
||||
])
|
||||
|
||||
return msg
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import struct
|
||||
|
||||
from util.misc import Logger
|
||||
|
||||
from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup
|
||||
from ..proto.tlv import TLV
|
||||
|
||||
@Foodgroup(0x0002)
|
||||
class LocateFoodgroup:
|
||||
logger: Logger
|
||||
|
||||
@Subgroup(0x0002)
|
||||
def client_versions(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] LOCATE__RIGHTS_QUERY')
|
||||
|
||||
response_msg = SNACMessage(0x0002, 0x0003)
|
||||
|
||||
# Response from NINA's servers:
|
||||
# ==
|
||||
# 0000 2a 02 00 07 00 28 00 02 00 03 00 00 00 00 00 02 *....(..........
|
||||
# [FLAP Header....] [SNAC Header................]
|
||||
# 0010 00 01 00 02 10 00 00 02 00 02 00 80 00 03 00 02 ................
|
||||
# [TLV 0x0001.....] [TLV 0x0002.....] [TLV 0x0003
|
||||
# 0020 00 1e 00 04 00 02 10 00 00 05 00 02 00 80 ..............
|
||||
# ....] [TLV 0x0004.....] [TLV 0x0005.....]
|
||||
#
|
||||
# TLV 0x0001: client max profile len (data is 0x1000 = 256)
|
||||
# TLV 0x0002: max capabilities (CLSIDs) (data is 0x0080 = 128)
|
||||
# TLV 0x0003: unknown (data is 0x001E = 30)
|
||||
# TLV 0x0004: unknown (data is 0x1000 = 256)
|
||||
# TLV 0x0005: unknown (data is 0x0080 = 128)
|
||||
#
|
||||
response_msg.write_tlvs([
|
||||
TLV(0x0001, struct.pack('>H', 4096)), # client max profile len
|
||||
TLV(0x0002, struct.pack('>H', 128)), # max capabilities (CLSIDs)
|
||||
TLV(0x0003, struct.pack('>H', 0x001E)), # unknown
|
||||
TLV(0x0004, struct.pack('>H', 0x1000)), # unknown
|
||||
TLV(0x0005, struct.pack('>H', 0x0080)) # unknown
|
||||
])
|
||||
|
||||
self.logger.info('[Server] LOCATE__RIGHTS_REPLY')
|
||||
client.send_snac(response_msg)
|
||||
|
||||
@Subgroup(0x0004)
|
||||
def set_info(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] LOCATE__SET_INFO (not implemented)')
|
||||
self.logger.info('[Client]', message.data.hex())
|
||||
|
||||
@Subgroup(0x000B)
|
||||
def get_dir_info(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] LOCATE__GET_DIR_INFO')
|
||||
|
||||
screen_name = message.read_string_u8()
|
||||
self.logger.info('[Client] Getting info for:', screen_name)
|
||||
|
||||
# Response from NINA's servers:
|
||||
# ==
|
||||
# 0000 2A 02 00 0F 00 1A 00 02 00 0C 00 00 00 01 00 0B *...............
|
||||
# [FLAP Header....] [SNAC Header................]
|
||||
# 0010 00 01 00 01 00 1C 00 08 75 73 2D 61 73 63 69 69 ........us-ascii
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
#
|
||||
# I have no idea what any of the values in the SNAC body mean, and the lack of documentation on NINA's wiki
|
||||
# don't help either: https://wiki.nina.chat/wiki/Protocols/OSCAR/SNAC/LOCATE_GET_DIR_REPLY
|
||||
response_msg = SNACMessage(0x0002, 0x000C)
|
||||
response_msg.write_u16(0x0001)
|
||||
response_msg.write_u16(0x0001)
|
||||
response_msg.write_u16(0x001C)
|
||||
response_msg.write_string_u16('us-ascii')
|
||||
|
||||
self.logger.info('[Server] LOCATE__GET_DIR_REPLY')
|
||||
client.send_snac(response_msg)
|
||||
@@ -0,0 +1,194 @@
|
||||
import os
|
||||
import calendar
|
||||
import struct
|
||||
import settings
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from util.misc import Logger
|
||||
|
||||
from ..proto.backend import FOODGROUP_VERSIONS, bos_cookies
|
||||
from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup
|
||||
from ..proto.tlv import TLV, unmarshal_tlvs
|
||||
|
||||
|
||||
@Foodgroup(0x0001)
|
||||
class OSERVICEFoodgroup:
|
||||
logger: Logger
|
||||
|
||||
@Subgroup(0x0017)
|
||||
def client_versions(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] OSERVICE__CLIENT_VERSIONS')
|
||||
|
||||
response_msg = SNACMessage(0x0001, 0x0018, 0x0000, 0x0000)
|
||||
for foodgroup, version in FOODGROUP_VERSIONS.items():
|
||||
response_msg.write_u16(foodgroup)
|
||||
response_msg.write_u16(version)
|
||||
|
||||
self.logger.info('[Server] OSERVICE__HOST_VERSIONS')
|
||||
client.send_snac(response_msg)
|
||||
|
||||
@Subgroup(0x0006)
|
||||
def rate_params_query(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] OSERVICE__RATE_PARAMS_QUERY')
|
||||
|
||||
hex = """
|
||||
00 05 00 01 00 00 00 50 00 00 09 C4 00 00 07 D0
|
||||
00 00 05 DC 00 00 03 20 00 00 0D 69 00 00 17 70
|
||||
00 00 00 00 00 00 02 00 00 00 50 00 00 0B B8 00
|
||||
00 07 D0 00 00 05 DC 00 00 03 E8 00 00 17 70 00
|
||||
00 17 70 00 00 F9 0B 00 00 03 00 00 00 14 00 00
|
||||
13 EC 00 00 13 88 00 00 0F A0 00 00 0B B8 00 00
|
||||
11 47 00 00 17 70 00 00 5C D8 00 00 04 00 00 00
|
||||
14 00 00 15 7C 00 00 14 B4 00 00 10 68 00 00 0B
|
||||
B8 00 00 17 70 00 00 1F 40 00 00 F9 0B 00 00 05
|
||||
00 00 00 0A 00 00 15 7C 00 00 14 B4 00 00 10 68
|
||||
00 00 0B B8 00 00 17 70 00 00 1F 40 00 00 F9 0B
|
||||
00 00 01 00 91 00 01 00 01 00 01 00 02 00 01 00
|
||||
03 00 01 00 04 00 01 00 05 00 01 00 06 00 01 00
|
||||
07 00 01 00 08 00 01 00 09 00 01 00 0A 00 01 00
|
||||
0B 00 01 00 0C 00 01 00 0D 00 01 00 0E 00 01 00
|
||||
0F 00 01 00 10 00 01 00 11 00 01 00 12 00 01 00
|
||||
13 00 01 00 14 00 01 00 15 00 01 00 16 00 01 00
|
||||
17 00 01 00 18 00 01 00 19 00 01 00 1A 00 01 00
|
||||
1B 00 01 00 1C 00 01 00 1D 00 01 00 1E 00 01 00
|
||||
1F 00 01 00 20 00 01 00 21 00 02 00 01 00 02 00
|
||||
02 00 02 00 03 00 02 00 04 00 02 00 06 00 02 00
|
||||
07 00 02 00 08 00 02 00 0A 00 02 00 0C 00 02 00
|
||||
0D 00 02 00 0E 00 02 00 0F 00 02 00 10 00 02 00
|
||||
11 00 02 00 12 00 02 00 13 00 02 00 14 00 02 00
|
||||
15 00 03 00 01 00 03 00 02 00 03 00 03 00 03 00
|
||||
06 00 03 00 07 00 03 00 08 00 03 00 09 00 03 00
|
||||
0A 00 03 00 0B 00 03 00 0C 00 04 00 01 00 04 00
|
||||
02 00 04 00 03 00 04 00 04 00 04 00 05 00 04 00
|
||||
07 00 04 00 08 00 04 00 09 00 04 00 0A 00 04 00
|
||||
0B 00 04 00 0C 00 04 00 0D 00 04 00 0E 00 04 00
|
||||
0F 00 04 00 10 00 04 00 11 00 04 00 12 00 04 00
|
||||
13 00 04 00 14 00 06 00 01 00 06 00 02 00 06 00
|
||||
03 00 08 00 01 00 08 00 02 00 09 00 01 00 09 00
|
||||
02 00 09 00 03 00 09 00 04 00 09 00 09 00 09 00
|
||||
0A 00 09 00 0B 00 0A 00 01 00 0A 00 02 00 0A 00
|
||||
03 00 0B 00 01 00 0B 00 02 00 0B 00 03 00 0B 00
|
||||
04 00 0C 00 01 00 0C 00 02 00 0C 00 03 00 13 00
|
||||
01 00 13 00 02 00 13 00 03 00 13 00 04 00 13 00
|
||||
05 00 13 00 06 00 13 00 07 00 13 00 08 00 13 00
|
||||
09 00 13 00 0A 00 13 00 0B 00 13 00 0C 00 13 00
|
||||
0D 00 13 00 0E 00 13 00 0F 00 13 00 10 00 13 00
|
||||
11 00 13 00 12 00 13 00 13 00 13 00 14 00 13 00
|
||||
15 00 13 00 16 00 13 00 17 00 13 00 18 00 13 00
|
||||
19 00 13 00 1A 00 13 00 1B 00 13 00 1C 00 13 00
|
||||
1D 00 13 00 1E 00 13 00 1F 00 13 00 20 00 13 00
|
||||
21 00 13 00 22 00 13 00 23 00 13 00 24 00 13 00
|
||||
25 00 13 00 26 00 13 00 27 00 13 00 28 00 15 00
|
||||
01 00 15 00 02 00 15 00 03 00 02 00 06 00 03 00
|
||||
04 00 03 00 05 00 09 00 05 00 09 00 06 00 09 00
|
||||
07 00 09 00 08 00 03 00 02 00 02 00 05 00 04 00
|
||||
06 00 04 00 02 00 02 00 09 00 02 00 0B 00 05 00
|
||||
00
|
||||
"""
|
||||
cleaned_hex = (hex
|
||||
.strip()
|
||||
.replace(" ", "")
|
||||
.replace("\r\n", "\n")
|
||||
.replace("\n", ""))
|
||||
|
||||
response_msg = SNACMessage(0x0001, 0x0007, 0x0000, 0x0000, bytes.fromhex(cleaned_hex))
|
||||
|
||||
self.logger.info('[Server] OSERVICE__RATE_PARAMS_REPLY')
|
||||
client.send_snac(response_msg)
|
||||
|
||||
@Subgroup(0x0008)
|
||||
def rate_params_sub_add(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] OSERVICE__RATE_PARAMS_SUB_ADD (not implemented)')
|
||||
|
||||
# since i don't have rate limits properly implemented *yet*, i won't do anything here
|
||||
|
||||
@Subgroup(0x000E)
|
||||
def user_info_query(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] OSERVICE__USER_INFO_QUERY')
|
||||
|
||||
response_msg = SNACMessage(0x0001, 0x000F)
|
||||
response_msg.write_string_u8(context.bs.user.username) # Screen name
|
||||
response_msg.write_u16(0) # Warning level
|
||||
|
||||
date_created = context.bs.user.date_created
|
||||
date_login = context.bs.user.date_login
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
|
||||
date_created_unix = int(calendar.timegm(date_created.timetuple()))
|
||||
date_login_unix = int(calendar.timegm(date_login.timetuple())) if date_login else 0
|
||||
session_len = int(now.timestamp()) - date_login_unix if date_login else 0
|
||||
|
||||
self.logger.info('Date created:', date_created)
|
||||
self.logger.info('Date created (UNIX time):', date_created_unix)
|
||||
self.logger.info('Date logged in:', date_login or 'Never')
|
||||
|
||||
self.logger.info('Date logged in (UNIX time):', date_login_unix)
|
||||
self.logger.info('Current time:', int(now.timestamp()))
|
||||
self.logger.info('Session length:', session_len)
|
||||
|
||||
self.logger.info('Client IP:', client.get_ip())
|
||||
|
||||
response_msg.write_tlv_block([
|
||||
TLV(0x0001, struct.pack('>L', 0x0100)), # User class (bitfield)
|
||||
TLV(0x0003, struct.pack('>L', date_login_unix)), # Account signon time (unix time_t)
|
||||
TLV(0x0005, struct.pack('>L', date_created_unix)), # Accout creation time (unix time_t)
|
||||
TLV(0x000F, struct.pack('>L', session_len)) # Session length
|
||||
])
|
||||
|
||||
self.logger.info('[Server] OSERVICE__USER_INFO_UPDATE')
|
||||
client.send_snac(response_msg)
|
||||
|
||||
#if settings.OSCAR_MOTD_ENABLED:
|
||||
# self.logger.info('[Server] OSERVICE__MOTD')
|
||||
# motd = SNACMessage(0x0001, 0x0013, 0x0000, 0x0000)
|
||||
# motd.write_u8(4)
|
||||
# motd.write_u8(0) # filler
|
||||
# motd.write_tlv(TLV(0x000B, "Welcome to CrossTalk! OSCAR support is currently in pre-alpha. You should use MSN Messenger or Yahoo Messenger for now."))
|
||||
# client.send_snac(motd)
|
||||
|
||||
@Subgroup(0x0011)
|
||||
def idle_notification(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] OSERVICE__IDLE_NOTIFICATION')
|
||||
|
||||
idle_time = message.read_u32()
|
||||
if idle_time == 0:
|
||||
self.logger.info(context.bs.user.username, 'is no longer idle')
|
||||
else:
|
||||
self.logger.info(context.bs.user.username, 'has been idle for', str(idle_time), 'seconds')
|
||||
|
||||
# This SNAC as seen from NINA's servers:
|
||||
# ==
|
||||
# 0000 2a 02 00 0e 00 0e 00 0b 00 02 00 00 e7 19 67 3c *.............g<
|
||||
# [FLAP Header....] [SNAC Header................]
|
||||
# 0010 00 01 00 00 ....
|
||||
# ^^^^^^^^^^^
|
||||
msg = SNACMessage(0x000B, 0x0002)
|
||||
msg.write_u32(0x010000)
|
||||
|
||||
self.logger.info('[Server] STATS__SET_MIN_REPORT_INTERVAL')
|
||||
client.send_snac(msg)
|
||||
|
||||
@Subgroup(0x0004)
|
||||
def service_request(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] OSERVICE__SERVICE_REQUEST')
|
||||
|
||||
foodgroup = message.read_u16()
|
||||
self.logger.info('[Client] Foodgroup:', hex(foodgroup))
|
||||
|
||||
# generate BOS cookie and add it to array
|
||||
bos_cookie = os.urandom(256)
|
||||
bos_cookies.append({
|
||||
bos_cookie: context.bs
|
||||
})
|
||||
|
||||
response_msg = SNACMessage(0x0001, 0x0005, 0x0000, message.request_id)
|
||||
response_msg.write_tlvs([
|
||||
TLV(0x0001, struct.pack('>H', 3)),
|
||||
TLV(0x0005, f'{settings.TARGET_IP}:5190'),
|
||||
TLV(0x0006, bos_cookie),
|
||||
TLV(0x000D, struct.pack('>H', foodgroup)),
|
||||
TLV(0x008E, struct.pack('>B', 0))
|
||||
])
|
||||
|
||||
client.send_snac(response_msg)
|
||||
@@ -0,0 +1,17 @@
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from util.misc import Logger
|
||||
|
||||
from ..proto.common import system_message
|
||||
from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup
|
||||
from ..proto.tlv import TLV, unmarshal_tlvs
|
||||
|
||||
def popup_display(self, alert_url: Optional[str], alert_txt: str):
|
||||
response_msg = SNACMessage(0x0008, 0x0002, 0x0000, 0x00000000)
|
||||
|
||||
response_msg.write_bytes(system_message(alert_url, alert_txt))
|
||||
# TODO(HIDEN): pipe to ctrl.send_snac()
|
||||
print('TLVs:', unmarshal_tlvs(response_msg.data))
|
||||
#client.send_snac(response_msg)
|
||||
return response_msg
|
||||
@@ -0,0 +1,17 @@
|
||||
from util.misc import Logger
|
||||
|
||||
from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup
|
||||
|
||||
|
||||
@Foodgroup(0x000B)
|
||||
class StatsFoodgroup:
|
||||
logger: Logger
|
||||
|
||||
@Subgroup(0x0003)
|
||||
def report_events(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
|
||||
self.logger.info('[Client] STATS__REPORT_EVENTS')
|
||||
|
||||
# we don't really care for the info in the SNAC message's body for now, so just send
|
||||
# a STATS__REPORT_ACK
|
||||
self.logger.info('[Server] STATS__REPORT_ACK')
|
||||
client.send_snac(SNACMessage(0x000B, 0x0004))
|
||||
@@ -0,0 +1,239 @@
|
||||
import os
|
||||
import struct
|
||||
import settings
|
||||
|
||||
from array import array
|
||||
from core import event
|
||||
from core.backend import Chat, ChatSession, BackendSession
|
||||
from core.models import Contact, Substatus, Circle, CircleRole, User, TextWithData, OIM, LoginOption
|
||||
from enum import IntEnum
|
||||
from util.misc import Logger
|
||||
from urllib.parse import quote
|
||||
from typing import Optional, Any, Dict
|
||||
|
||||
from ..foodgroups.buddy import build_presence_notif
|
||||
from ..foodgroups.popup import popup_display
|
||||
from ..proto.snac import OSCARContext
|
||||
from ..proto.tlv import TLV, marshal_tlvs, find_tlv
|
||||
from ..proto.chat import ChatEventHandler
|
||||
|
||||
# TODO(subpurple): move this to a config of some sort
|
||||
FOODGROUP_VERSIONS: {int, int} = {
|
||||
0x0001: 4, # OSERVICE
|
||||
0x0002: 1, # LOCATE
|
||||
0x0003: 1, # BUDDY
|
||||
0x0004: 1, # ICBM
|
||||
0x0006: 1, # INVITE
|
||||
0x0008: 1, # POPUP
|
||||
0x0009: 1, # BOS
|
||||
0x000A: 1, # USER_LOOKUP
|
||||
0x000B: 1, # STATS
|
||||
0x000C: 1, # TRANSLATE
|
||||
0x0013: 6, # FEEDBAG
|
||||
0x0015: 2, # ICQ
|
||||
0x0022: 1, # PLUGIN
|
||||
0x0024: 1, # UNNAMED (possibly NACHOS?)
|
||||
0x0025: 1 # MDIR
|
||||
}
|
||||
|
||||
ERROR_URLS: {int, str} = {
|
||||
0x0001: 'http://www.aim.aol.com/errors/UNREGISTERED_SCREENNAME.html', # Unregistered screen name
|
||||
0x0005: 'http://www.aim.aol.com/errors/MISMATCH_PASSWD.html', # Incorrect password
|
||||
0x0011: 'http://www.aim.aol.com/errors/SUSPENDED.html', # Suspended
|
||||
}
|
||||
|
||||
PW_CHANGE_URL_FORMAT = 'http://aim.aol.com/redirects/password/change_password.adp?ScreenName={}&ccode={}&lang={}'
|
||||
|
||||
|
||||
bos_cookies: array[{bytes, OSCARContext}] = []
|
||||
|
||||
|
||||
class BackendEventHandler(event.BackendEventHandler):
|
||||
__slots__ = ('ctrl', 'bs')
|
||||
|
||||
ctrl: Any
|
||||
|
||||
def __init__(self, ctrl: Any) -> None:
|
||||
self.ctrl = ctrl
|
||||
|
||||
def on_maintenance_message(self, *args: Any, **kwargs: Any) -> None:
|
||||
self.ctrl.logger.info("on_maintenance message dispatched")
|
||||
if args[1] is not None and args[1] > 0:
|
||||
message = """CrossTalk will be down for maintenance in around {} minute(s). \
|
||||
Now is a good time to wrap up any conversations.""".format(args[1])
|
||||
self.ctrl.send_snac(popup_display(self, "https://crosstalk.im/maintenance", message))
|
||||
|
||||
def on_client_alert(self, icon_url: Optional[str], url: Optional[str], message: str = '') -> None:
|
||||
self.ctrl.logger.info("on_client_alert dispatched")
|
||||
self.ctrl.send_snac(popup_display(self, url, message))
|
||||
|
||||
def on_maintenance_boot(self) -> None:
|
||||
# TODO(HIDEN): can we pass an informative message to the user?
|
||||
self.ctrl.close()
|
||||
return
|
||||
|
||||
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:
|
||||
self.ctrl.logger.info('on_presence_notification dispatched')
|
||||
self.ctrl.logger.info('update_status:', update_status)
|
||||
self.ctrl.logger.info('update_info_other:', update_info_other)
|
||||
self.ctrl.logger.info('status:', ctc.status.substatus.name)
|
||||
|
||||
# don't want any non-status updates
|
||||
if not update_status:
|
||||
self.ctrl.logger.info('fell into update_status check')
|
||||
return
|
||||
|
||||
# for now, we only send presence notifications if the buddy changing status is offline(-ish) or online
|
||||
if not ctc.status.is_offlineish() and ctc.status.substatus != Substatus.Online:
|
||||
self.ctrl.logger.info('fell into status check')
|
||||
return
|
||||
|
||||
self.ctrl.logger.info('sending presence notif')
|
||||
self.ctrl.send_snac(build_presence_notif(ctc))
|
||||
|
||||
def on_presence_self_notification(self, old_substatus: Substatus, *, update_status: bool = True,
|
||||
update_info: bool = True) -> None:
|
||||
self.ctrl.logger.info('on_presence_self_notification')
|
||||
pass
|
||||
|
||||
def on_chat_invite(
|
||||
self, chat: Chat, inviter: User, *, circle: bool = False, inviter_id: Optional[str] = None,
|
||||
invite_msg: str = '',
|
||||
) -> None:
|
||||
self.ctrl.logger.info('on_chat_invite')
|
||||
if chat is None:
|
||||
chat = self.backend.chat_create()
|
||||
chat.add_id('oscar0', chat.ids['main'])
|
||||
evt = ChatEventHandler(self.ctrl.backend.loop, self.ctrl, self.bs)
|
||||
cs = chat.join('oscar0', self.bs, evt)
|
||||
chat.send_participant_joined(cs)
|
||||
self.bs.front_data['oscar0_chats'][inviter.uuid] = (cs, evt)
|
||||
return
|
||||
|
||||
def on_declined_chat_invite(self, chat: Chat, circle: bool = False) -> None:
|
||||
self.ctrl.logger.info('on_declined_chat_invite')
|
||||
pass
|
||||
|
||||
def on_added_me(self, user: User, *, adder_id: Optional[str] = None,
|
||||
message: Optional[TextWithData] = None) -> None:
|
||||
self.ctrl.logger.info('on_added_me')
|
||||
pass
|
||||
|
||||
def on_removed_me(self, user: User) -> None:
|
||||
self.ctrl.logger.info('on_removed_me')
|
||||
pass
|
||||
|
||||
def on_contact_request_denied(self, user_added: User, message: Optional[str], *,
|
||||
contact_id: Optional[str] = None) -> None:
|
||||
self.ctrl.logger.info('on_contact_request_denied')
|
||||
pass
|
||||
|
||||
def on_oim_sent(self, oim: OIM) -> None:
|
||||
self.ctrl.logger.info('on_oim_sent')
|
||||
pass
|
||||
|
||||
def on_login_elsewhere(self, option: LoginOption) -> None:
|
||||
self.ctrl.logger.info('on_login_elsewhere')
|
||||
self.ctrl.close()
|
||||
return
|
||||
|
||||
# circles are not a thing in AIM
|
||||
def on_circle_invite_revoked(self, chat_id: str) -> None:
|
||||
pass
|
||||
|
||||
def on_accepted_circle_invite(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_circle_role_updated(self, chat_id: str, role: CircleRole) -> None:
|
||||
pass
|
||||
|
||||
def on_circle_created(self, circle: Circle) -> None:
|
||||
pass
|
||||
|
||||
def on_close(self) -> None:
|
||||
self.ctrl.close()
|
||||
|
||||
class LoginError(IntEnum):
|
||||
UnregisteredScreenname = 0x0001,
|
||||
IncorrectPassword = 0x0005,
|
||||
NotATester = 0x0009,
|
||||
Suspended = 0x0011
|
||||
|
||||
|
||||
def login(logger: Logger,
|
||||
context: OSCARContext,
|
||||
tlvs: array[TLV],
|
||||
uuid: Optional[str],
|
||||
error_code: Optional[int]) -> bytes:
|
||||
|
||||
# get user if found
|
||||
user = context.backend.user_service.get(uuid) if uuid else None
|
||||
|
||||
# find screen name TLV and use it if not found in DB
|
||||
screen_name_tlv = find_tlv(tlvs, 0x0001)
|
||||
screen_name = user.username if user else screen_name_tlv.data.decode()
|
||||
|
||||
# additionally check if user is suspended here
|
||||
if error_code is None and user.suspended:
|
||||
error_code = LoginError.Suspended
|
||||
|
||||
logger.info(screen_name, 'tried to sign on but is suspended!')
|
||||
|
||||
if error_code is None and not user.is_tester:
|
||||
error_code = LoginError.NotATester
|
||||
|
||||
logger.info(screen_name, 'tried to sign on but is not a tester!')
|
||||
|
||||
if error_code is None:
|
||||
logger.info(screen_name, 'signed on successfully!')
|
||||
|
||||
# generate BOS cookie and add it to array
|
||||
bos_cookie = os.urandom(256)
|
||||
bos_cookies.append({
|
||||
bos_cookie: uuid
|
||||
})
|
||||
|
||||
# get user email
|
||||
email = user.email
|
||||
|
||||
# find client's country TLV for use in password change URL
|
||||
country_tlv = find_tlv(tlvs, 0x000E)
|
||||
country = country_tlv.data if country_tlv else 'us'
|
||||
|
||||
# find client's language TLV for use in password change URL
|
||||
language_tlv = find_tlv(tlvs, 0x000F)
|
||||
language = language_tlv.data if language_tlv else 'en'
|
||||
|
||||
# format password change URL with found client country and language
|
||||
pw_change_url = PW_CHANGE_URL_FORMAT.format(
|
||||
quote(screen_name),
|
||||
country,
|
||||
language
|
||||
)
|
||||
|
||||
return marshal_tlvs([
|
||||
TLV(0x0001, screen_name), # Screen name
|
||||
TLV(0x0005, f'{settings.TARGET_IP}:5190'), # BOS address
|
||||
TLV(0x0006, bos_cookie), # BOS authorization cookie
|
||||
TLV(0x0011, email), # User's e-mail address
|
||||
TLV(0x0013, struct.pack('>H', 1)), # Registration status
|
||||
TLV(0x0054, pw_change_url), # Password change URL
|
||||
TLV(0x008E, struct.pack('>B', 0)) # Unknown
|
||||
])
|
||||
|
||||
return marshal_tlvs([
|
||||
TLV(0x0001, screen_name), # Screen name
|
||||
TLV(0x0008, struct.pack('>H', error_code)), # Error code
|
||||
TLV(0x0004, ERROR_URLS[error_code]), # Error URL
|
||||
])
|
||||
@@ -0,0 +1,249 @@
|
||||
import struct
|
||||
|
||||
from array import array
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .tlv import TLV, marshal_tlvs, unmarshal_tlvs
|
||||
|
||||
|
||||
@dataclass
|
||||
class Buffer:
|
||||
data: bytes
|
||||
|
||||
def __init__(self, data: bytes = b''):
|
||||
self.data = data
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.data)
|
||||
|
||||
########################
|
||||
# read_xxx() functions #
|
||||
########################
|
||||
def read_bytes(self, length: int) -> bytes:
|
||||
"""Reads the specified amount of bytes from the buffer.
|
||||
|
||||
Args:
|
||||
length: bytes to read
|
||||
|
||||
Returns:
|
||||
bytes: the bytes read
|
||||
"""
|
||||
value = self.data[:length]
|
||||
|
||||
self.data = self.data[length:]
|
||||
|
||||
return value
|
||||
|
||||
def read_u8(self) -> int:
|
||||
"""Reads a byte (8 bits) from the buffer.
|
||||
|
||||
Returns:
|
||||
int: The read byte from the buffer.
|
||||
"""
|
||||
value, = struct.unpack('>B', self.data[:1])
|
||||
|
||||
self.data = self.data[1:]
|
||||
|
||||
return value
|
||||
|
||||
def read_u16(self) -> int:
|
||||
"""Reads an unsigned short (16 bits) from the buffer.
|
||||
|
||||
Returns:
|
||||
int: The read unsigned short from the buffer.
|
||||
"""
|
||||
value, = struct.unpack('>H', self.data[:2])
|
||||
|
||||
self.data = self.data[2:]
|
||||
|
||||
return value
|
||||
|
||||
def read_u32(self) -> int:
|
||||
"""Reads an unsigned int (32 bits) from the buffer.
|
||||
|
||||
Returns:
|
||||
int: The read unsigned int from the buffer.
|
||||
"""
|
||||
value, = struct.unpack('>L', self.data[:4])
|
||||
|
||||
self.data = self.data[4:]
|
||||
|
||||
return value
|
||||
|
||||
def read_string(self, length: int) -> str:
|
||||
"""Reads a string with the specified length from the buffer.
|
||||
|
||||
Args:
|
||||
length: The length of the string to read.
|
||||
|
||||
Returns:
|
||||
str: The read string from the buffer."""
|
||||
str_bytes = self.data[:length]
|
||||
|
||||
self.data = self.data[length:]
|
||||
|
||||
return str_bytes.decode('utf-8')
|
||||
|
||||
def read_string_u8(self) -> str:
|
||||
"""Reads a string prepended with a u8 showing the length of the string.
|
||||
|
||||
Returns:
|
||||
str: The read string from the buffer.
|
||||
"""
|
||||
return self.read_string(self.read_u8())
|
||||
|
||||
def read_string_u16(self) -> str:
|
||||
"""Reads a string prepended with a u16 showing the length of the string.
|
||||
|
||||
Returns:
|
||||
str: The read string from the buffer.
|
||||
"""
|
||||
return self.read_string(self.read_u16())
|
||||
|
||||
def read_string_u32(self) -> str:
|
||||
"""Reads a string prepended with a u32 showing the length of the string.
|
||||
|
||||
Returns:
|
||||
str: The read string from the buffer.
|
||||
"""
|
||||
return self.read_string(self.read_u32())
|
||||
|
||||
def read_tlv_block(self) -> array[TLV]:
|
||||
"""Reads a TLV list prepended with a u16 showing the number of TLVs in the list, from the buffer.
|
||||
|
||||
Returns:
|
||||
array[TLV]: The TLVs found in the block.
|
||||
"""
|
||||
length = self.read_u16()
|
||||
tlvs = []
|
||||
|
||||
for _ in range(length):
|
||||
type, length = struct.unpack('>HH', self.data[0:4])
|
||||
value = self.data[4:length + 4]
|
||||
|
||||
# make sure length is not too long
|
||||
assert len(self) > length - 4
|
||||
|
||||
tlvs.append(TLV(type, value))
|
||||
self.data = self.data[length + 4:]
|
||||
|
||||
return tlvs
|
||||
|
||||
def read_tlv_l_block(self) -> array[TLV]:
|
||||
"""Reads a list of TLVs prepended with a u16 showing the length of the total TLV bytes, from the buffer.
|
||||
|
||||
Returns:
|
||||
array[TLV]: The TLVs found in the block.
|
||||
"""
|
||||
length = self.read_u16()
|
||||
tlvs = unmarshal_tlvs(self.data[:length])
|
||||
|
||||
self.data = self.data[length:]
|
||||
|
||||
return tlvs
|
||||
|
||||
#########################
|
||||
# write_xxx() functions #
|
||||
#########################
|
||||
def write_bytes(self, value: bytes) -> None:
|
||||
"""Writes the specified bytes into the buffer.
|
||||
|
||||
Args:
|
||||
value: The bytes to write into the buffer.
|
||||
"""
|
||||
self.data += value
|
||||
|
||||
def write_u8(self, value: int) -> None:
|
||||
"""Writes a byte (8 bits) into the buffer.
|
||||
|
||||
Args:
|
||||
value: The byte to write into the buffer.
|
||||
"""
|
||||
self.data += struct.pack('>B', value)
|
||||
|
||||
def write_u16(self, value: int) -> None:
|
||||
"""Writes a unsigned short (16 bits) into the buffer.
|
||||
|
||||
Args:
|
||||
value: The unsigned short to write into the buffer.
|
||||
"""
|
||||
self.data += struct.pack('>H', value)
|
||||
|
||||
def write_u32(self, value: int) -> None:
|
||||
"""Writes a unsigned int (32 bits) into the buffer.
|
||||
|
||||
Args:
|
||||
value: The unsigned int to write into the buffer.
|
||||
"""
|
||||
self.data += struct.pack('>L', value)
|
||||
|
||||
def write_string(self, value: str) -> None:
|
||||
"""Writes a string imto the buffer.
|
||||
|
||||
Args:
|
||||
value: The string to write into the buffer.
|
||||
"""
|
||||
self.data += value.encode('utf-8')
|
||||
|
||||
def write_string_u8(self, value: str) -> None:
|
||||
"""Writes a string prepended with a u8 showing the length of the string into the buffer.
|
||||
|
||||
Args:
|
||||
value: The string to write into the buffer.
|
||||
"""
|
||||
self.write_u8(len(value))
|
||||
self.write_string(value)
|
||||
|
||||
def write_string_u16(self, value: str) -> None:
|
||||
"""Writes a string prepended with a u16 showing the length of the string into the buffer.
|
||||
|
||||
Args:
|
||||
value: The string to write into the buffer.
|
||||
"""
|
||||
self.write_u16(len(value))
|
||||
self.write_string(value)
|
||||
|
||||
def write_string_u32(self, value: str) -> None:
|
||||
"""Writes a string prepended with a u32 showing the length of the string into the buffer.
|
||||
|
||||
Args:
|
||||
value: The string to write into the buffer.
|
||||
"""
|
||||
self.write_u32(len(value))
|
||||
self.write_string(value)
|
||||
|
||||
def write_tlv(self, tlv: TLV) -> None:
|
||||
"""Writes the specified TLV into the buffer.
|
||||
|
||||
Args:
|
||||
tlv: The TLV to write into the buffer.
|
||||
"""
|
||||
self.data += tlv.marshal()
|
||||
|
||||
def write_tlvs(self, tlvs: array[TLV]) -> None:
|
||||
"""Writes a list of TLVs into the buffer.
|
||||
|
||||
Args:
|
||||
tlvs: The list of TLVs to write into the buffer.
|
||||
"""
|
||||
self.data += marshal_tlvs(tlvs)
|
||||
|
||||
def write_tlv_block(self, tlvs: array[TLV]) -> None:
|
||||
"""Writes a list of TLVs prepended with a u16 describing the TLV count in the list, into the buffer.
|
||||
|
||||
Args:
|
||||
tlvs: The list of TLVs to write into the buffer.
|
||||
"""
|
||||
self.write_u16(len(tlvs))
|
||||
self.write_tlvs(tlvs)
|
||||
|
||||
def write_tlv_l_block(self, tlvs: array[TLV]) -> None:
|
||||
"""Writes a list of TLVs prepended with a u16 describing the length of the total TLV bytes, into the buffer.
|
||||
|
||||
Args:
|
||||
tlvs: The list of TLVs to write into the buffers.
|
||||
"""
|
||||
marshalled_tlvs = marshal_tlvs(tlvs)
|
||||
|
||||
self.write_u16(len(marshalled_tlvs))
|
||||
self.write_bytes(marshalled_tlvs)
|
||||
@@ -0,0 +1,61 @@
|
||||
import asyncio
|
||||
import secrets
|
||||
|
||||
from typing import Any, Optional
|
||||
from core import event
|
||||
from core.backend import BackendSession, ChatSession, Chat
|
||||
from core.models import User, Substatus, MessageData, MessageType
|
||||
|
||||
from ..proto.snac import SNACMessage
|
||||
from ..proto.tlv import TLV
|
||||
from ..proto.buffer import Buffer
|
||||
from ..foodgroups.icbm import ICBMChannel, messagedata_to_icbm
|
||||
|
||||
class ChatEventHandler(event.ChatEventHandler):
|
||||
__slots__ = ('loop', 'ctrl', 'bs', 'cs', 'cookie')
|
||||
|
||||
loop: asyncio.AbstractEventLoop
|
||||
ctrl: Any
|
||||
bs: BackendSession
|
||||
cs: ChatSession
|
||||
cookie: bytes
|
||||
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop, ctrl: Any, bs: BackendSession) -> None:
|
||||
self.loop = loop
|
||||
self.ctrl = ctrl
|
||||
self.bs = bs
|
||||
self.cookie = secrets.token_bytes(8)
|
||||
|
||||
def on_participant_joined(self, cs_other: 'ChatSession', first_pop: bool, initial_join: bool) -> None:
|
||||
self.ctrl.logger.info('on_participant_joined')
|
||||
pass
|
||||
|
||||
def on_participant_left(self, cs_other: 'ChatSession', last_pop: bool) -> None:
|
||||
self.ctrl.logger.info('on_participant_left')
|
||||
pass
|
||||
|
||||
def on_chat_invite_declined(self, chat: 'Chat', invitee: User, *, invitee_id: Optional[str] = None,
|
||||
message: Optional[str] = None, circle: bool = False) -> None:
|
||||
self.ctrl.logger.info('on_chat_invite_declined')
|
||||
pass
|
||||
|
||||
def on_chat_updated(self) -> None:
|
||||
self.ctrl.logger.info('on_chat_updated')
|
||||
pass
|
||||
|
||||
def on_chat_roster_updated(self) -> None:
|
||||
self.ctrl.logger.info('on_chat_roster_updated')
|
||||
pass
|
||||
|
||||
def on_participant_status_updated(self, cs_other: 'ChatSession', first_pop: bool, initial: bool,
|
||||
old_substatus: Substatus) -> None:
|
||||
self.ctrl.logger.info('on_participant_status_updated')
|
||||
pass
|
||||
|
||||
def on_message(self, data: MessageData) -> None:
|
||||
|
||||
self.ctrl.logger.info('Got a message from', data.sender.username, 'saying:')
|
||||
self.ctrl.logger.info(data.text.encode())
|
||||
|
||||
self.ctrl.send_snac(messagedata_to_icbm(self.cookie, data, self.bs.user))
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import struct
|
||||
|
||||
from typing import Optional
|
||||
from util.misc import Logger
|
||||
|
||||
from ..proto.tlv import TLV, marshal_tlvs
|
||||
|
||||
def system_message(alert_url: Optional[str], alert_txt: str) -> bytes:
|
||||
return marshal_tlvs([
|
||||
TLV(0x0003, struct.pack('>H', 0x00D9)), # message window width
|
||||
TLV(0x0004, struct.pack('>H', 0x0096)), # message window length
|
||||
TLV(0x0005, struct.pack('>H', 0x001E)), # autohide delay
|
||||
TLV(0x0002, alert_url), # alert URL
|
||||
TLV(0x0001, alert_txt), # alert text
|
||||
])
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
from core.backend import Backend, BackendSession
|
||||
from core.client import Client
|
||||
from core.models import User
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Callable, Any
|
||||
|
||||
from .buffer import Buffer
|
||||
|
||||
foodgroups = {}
|
||||
|
||||
|
||||
@dataclass
|
||||
class SNACMessage(Buffer):
|
||||
__slots__ = ('foodgroup', 'subgroup', 'flags', 'request_id', 'data')
|
||||
|
||||
foodgroup: int
|
||||
subgroup: int
|
||||
flags: int
|
||||
request_id: int
|
||||
data: bytes
|
||||
|
||||
def __init__(self, foodgroup: int = 0x0000, subgroup: int = 0x0000, flags: int = 0x0000,
|
||||
request_id: int = 0x00000000, data: bytes = b'') -> None:
|
||||
super().__init__()
|
||||
|
||||
self.foodgroup = foodgroup
|
||||
self.subgroup = subgroup
|
||||
self.flags = flags
|
||||
self.request_id = request_id
|
||||
self.data = data
|
||||
|
||||
def marshal(self) -> bytes:
|
||||
# we use a new Buffer here because even though we are a subclass of Buffer, the methods are reserved
|
||||
# for SNAC data (everything after the header)
|
||||
buf = Buffer()
|
||||
buf.write_u16(self.foodgroup)
|
||||
buf.write_u16(self.subgroup)
|
||||
buf.write_u16(self.flags)
|
||||
buf.write_u32(self.request_id)
|
||||
buf.write_bytes(self.data)
|
||||
|
||||
return buf.data
|
||||
|
||||
def unmarshal(self, flap_data: bytes) -> None:
|
||||
buf = Buffer(flap_data)
|
||||
|
||||
self.foodgroup = buf.read_u16()
|
||||
self.subgroup = buf.read_u16()
|
||||
self.flags = buf.read_u16()
|
||||
self.request_id = buf.read_u32()
|
||||
self.data = buf.data
|
||||
|
||||
|
||||
class Foodgroup:
|
||||
__slots__ = ('value', 'cls')
|
||||
|
||||
value: int
|
||||
cls: Any
|
||||
|
||||
def __init__(self, value) -> None:
|
||||
self.value = value
|
||||
self.cls = None
|
||||
|
||||
def __call__(self, *args) -> None:
|
||||
self.cls = args[0]
|
||||
|
||||
foodgroups[self.value] = self.cls()
|
||||
|
||||
|
||||
class Subgroup:
|
||||
__slots__ = ('value', 'mode', 'func')
|
||||
|
||||
value: int
|
||||
mode: str
|
||||
func: Optional[Callable]
|
||||
|
||||
def __init__(self, value) -> None:
|
||||
self.value = value
|
||||
self.mode = 'decorating'
|
||||
self.func = None
|
||||
|
||||
def __call__(self, *args) -> Any:
|
||||
if self.mode == 'decorating':
|
||||
self.func = args[0]
|
||||
self.mode = 'calling'
|
||||
return self
|
||||
|
||||
return self.func(*args)
|
||||
|
||||
def __set_name__(self, owner, name) -> None:
|
||||
if not hasattr(owner, 'subgroups'):
|
||||
owner.subgroups = {}
|
||||
|
||||
owner.subgroups[self.value] = self.func
|
||||
|
||||
self.func.class_name = owner.__name__
|
||||
setattr(owner, name, self.func)
|
||||
|
||||
|
||||
# OSCARClient and OSCARContext
|
||||
class OSCARClient:
|
||||
__slots__ = 'ctrl'
|
||||
|
||||
ctrl: Any # Any because of circular imports - TODO(subpurple): make this less hacky
|
||||
|
||||
def __init__(self, ctrl: Any) -> None:
|
||||
self.ctrl = ctrl
|
||||
|
||||
def send_snac(self, msg: SNACMessage) -> None:
|
||||
self.ctrl.send_specific_frame(0x02, msg.marshal())
|
||||
|
||||
def get_ip(self) -> str:
|
||||
ip, *_ = self.ctrl.transport.get_extra_info('peername')
|
||||
return ip
|
||||
|
||||
|
||||
@dataclass
|
||||
class OSCARContext:
|
||||
__slots__ = ('backend', 'bs', 'client', 'user')
|
||||
|
||||
backend: Backend
|
||||
bs: Optional[BackendSession]
|
||||
client: Client
|
||||
|
||||
# These slots are equivalent to .bs.* and only exist for the sake of convenience
|
||||
user: Optional[User]
|
||||
|
||||
def __init__(self, backend: Backend, client: Client) -> None:
|
||||
self.backend = backend
|
||||
self.bs = None
|
||||
self.client = client
|
||||
self.user = None
|
||||
@@ -0,0 +1,54 @@
|
||||
from . import buffer # because of circular imports, would've prefered to do `from .buffer import ...` but oh well
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class TLV:
|
||||
__slots__ = ('type', 'data')
|
||||
|
||||
type: int
|
||||
data: bytes
|
||||
|
||||
def __init__(self, type: int, data: bytes | str = b''):
|
||||
self.type = type
|
||||
|
||||
if isinstance(data, str):
|
||||
self.data = data.encode()
|
||||
else:
|
||||
self.data = data
|
||||
|
||||
def marshal(self) -> bytes:
|
||||
buf = buffer.Buffer()
|
||||
buf.write_u16(self.type)
|
||||
buf.write_u16(len(self.data))
|
||||
buf.write_bytes(self.data)
|
||||
|
||||
return buf.data
|
||||
|
||||
|
||||
def marshal_tlvs(tlvs: list[TLV]) -> bytes:
|
||||
return b''.join([tlv.marshal() for tlv in tlvs])
|
||||
|
||||
|
||||
def unmarshal_tlvs(data: bytes) -> list[TLV]:
|
||||
buf = buffer.Buffer(data)
|
||||
tlvs = []
|
||||
|
||||
while len(buf) > 4:
|
||||
type = buf.read_u16()
|
||||
length = buf.read_u16()
|
||||
value = buf.read_bytes(length)
|
||||
|
||||
tlvs.append(TLV(type, value))
|
||||
|
||||
return tlvs
|
||||
|
||||
|
||||
def find_tlv(tlvs: list[TLV], type: int) -> Optional[TLV]:
|
||||
for tlv in tlvs:
|
||||
if tlv.type == type:
|
||||
return tlv
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user