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,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