This commit is contained in:
Athena Funderburg
2026-05-25 07:05:17 +00:00
commit 4b463a3432
682 changed files with 47796 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
__all__ = [
'oservice',
'locate',
'buddy',
'icbm',
'bos',
'stats',
'feedbag',
'popup',
'bucp'
]
+26
View File
@@ -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)
+88
View File
@@ -0,0 +1,88 @@
from core.client import Client
from util.misc import Logger
from util.hash import gen_salt
from ..proto.backend import login, LoginError
from ..proto.buffer import Buffer
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()
major_tlv = find_tlv(tlvs, 0x0017)
minor_tlv = find_tlv(tlvs, 0x0018)
build_tlv = find_tlv(tlvs, 0x001A)
major = Buffer(major_tlv.data).read_u16() if major_tlv else 0
minor = Buffer(minor_tlv.data).read_u16() if minor_tlv else 0
build = Buffer(build_tlv.data).read_u16() if build_tlv else 0
version_str = '{}.{}.{}'.format(major, minor, build)
context.client = Client('aim', version_str, context.client.via)
self.logger.info('Screen Name (client-given):', screen_name)
self.logger.info('Client version:', version_str)
self.logger.info('Password (hashed):', hashed_pw_tlv.data)
response_msg = SNACMessage(0x0017, 0x0003)
error_code = None
uuid = None
if (uuid := context.backend.util_get_uuid_from_username(screen_name)) is None:
error_code = LoginError.UnregisteredScreenname
self.logger.info('Unregistered screenname')
else:
client_hash = hashed_pw_tlv.data
pw_bucp1 = context.backend.user_service.aim_get_md5_password(screen_name)
pw_bucp2 = context.backend.user_service.aim_get_md5_password_bucp2(screen_name)
auth_ok = (pw_bucp1 is not None and pw_bucp1 == client_hash) or (pw_bucp2 is not None and pw_bucp2 == client_hash)
if not auth_ok:
error_code = LoginError.IncorrectPassword
self.logger.info('Incorrect password')
else:
self.logger.info('Authenticated via', 'AIM5.x' if (pw_bucp2 == client_hash) else 'AIM3.5-4.8')
response_msg.write_bytes(login(self.logger, context, tlvs, uuid, error_code))
client.send_snac(response_msg)
+111
View File
@@ -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)
+587
View File
@@ -0,0 +1,587 @@
import time
import struct
from array import array
from core import error
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://ninawiki.preloading.dev/wiki/Protocols/OSCAR/Foodgroups/FEEDBAG/Items#Class:_FEEDBAG_CLASS_IDS
class FeedbagClass(IntEnum):
Buddy = 0x0000,
Group = 0x0001,
Permit = 0x0002,
Deny = 0x0003,
PdInfo = 0x0004,
BuddyPrefs = 0x0005
class FeedbagAttributes(IntEnum):
Order = 0x00C8,
PdMode = 0x00CA,
WirelessPdMode = 0x00D0,
WirelessIgnoreMode = 0x00D1,
FishPdMode = 0x00D2,
FishIgnoreMode = 0x00D3,
PdMask = 0x00CB,
BuddyPrefs = 0x00C9
class FeedbagStatus(IntEnum):
Success = 0x0000,
NotFound = 0x0002,
AlreadyExists = 0x0003,
BadRequest = 0x000A,
OverLimit = 0x000C,
BadLoginID = 0x0010,
@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
def _send_feedbag_status(client: OSCARClient, request_id: int, statuses: list) -> None:
msg = SNACMessage(0x0013, 0x000E, 0x0000, request_id)
for code in statuses:
msg.write_u16(code)
client.send_snac(msg)
def _resolve_group_backend_id(context: OSCARContext, client_group_id: int) -> Optional[str]:
detail = context.user.detail
if detail is None:
return None
if str(client_group_id) in detail._groups_by_id:
return str(client_group_id)
mapping = context.bs.front_data.get("oscar0_group_id_map", {})
return mapping.get(client_group_id)
def build_buddy_added_notif(adder_username: str) -> SNACMessage:
msg = SNACMessage(0x0013, 0x001C)
username_bytes = adder_username.encode('ascii')
msg.write_u8(len(username_bytes))
msg.write_bytes(username_bytes)
return msg
@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://ninawiki.preloading.dev/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")
# 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)
current_items = get_items(context)
if len(current_items) == cached_feedbag_num_items:
response_msg = SNACMessage(0x0013, 0x000F, 0x0000, message.request_id)
response_msg.write_u32(cached_feedbag_timestamp)
response_msg.write_u16(cached_feedbag_num_items)
self.logger.info("[Server] FEEDBAG__REPLY_NOT_MODIFIED")
client.send_snac(response_msg)
else:
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")
bs = context.bs
user = context.user
detail = user.detail
items = unmarshal_items(message.data)
statuses = []
for item in items:
if item.class_id == FeedbagClass.Group:
if item.group_id == 0 and item.item_id == 0:
statuses.append(FeedbagStatus.Success)
continue
existing_names = {g.name.lower() for g in detail._groups_by_id.values()}
if item.name.lower() in existing_names:
statuses.append(FeedbagStatus.AlreadyExists)
continue
try:
group = bs.me_group_add(item.name)
mapping = bs.front_data.setdefault("oscar0_group_id_map", {})
mapping[item.group_id] = group.id
self.logger.info("[Client] Added group:", item.name)
statuses.append(FeedbagStatus.Success)
except Exception:
statuses.append(FeedbagStatus.BadRequest)
elif item.class_id == FeedbagClass.Buddy:
if not item.name:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact_uuid = context.backend.util_get_uuid_from_username(item.name)
if contact_uuid is None:
self.logger.info("[Client] Unknown buddy:", item.name)
statuses.append(FeedbagStatus.BadLoginID)
continue
contact = detail.contacts.get(contact_uuid)
if contact is not None and contact.lists & ContactList.FL:
statuses.append(FeedbagStatus.AlreadyExists)
continue
try:
bs.me_contact_add(contact_uuid, ContactList.FL | ContactList.AL)
if item.group_id != 0:
backend_group_id = _resolve_group_backend_id(context, item.group_id)
if backend_group_id is not None:
bs.me_group_contact_add(backend_group_id, contact_uuid)
self.logger.info("[Client] Added buddy:", item.name)
statuses.append(FeedbagStatus.Success)
except error.ContactListIsFull:
statuses.append(FeedbagStatus.OverLimit)
except Exception:
statuses.append(FeedbagStatus.BadRequest)
elif item.class_id == FeedbagClass.Deny:
if not item.name:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact_uuid = context.backend.util_get_uuid_from_username(item.name)
if contact_uuid is None:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact = detail.contacts.get(contact_uuid)
if contact is not None and contact.lists & ContactList.BL:
statuses.append(FeedbagStatus.AlreadyExists)
continue
try:
bs.me_contact_add(contact_uuid, ContactList.BL)
self.logger.info("[Client] Added deny entry:", item.name)
statuses.append(FeedbagStatus.Success)
except Exception:
statuses.append(FeedbagStatus.BadRequest)
elif item.class_id == FeedbagClass.Permit:
if not item.name:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact_uuid = context.backend.util_get_uuid_from_username(item.name)
if contact_uuid is None:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact = detail.contacts.get(contact_uuid)
if contact is not None and contact.lists & ContactList.AL:
statuses.append(FeedbagStatus.AlreadyExists)
continue
try:
bs.me_contact_add(contact_uuid, ContactList.AL)
self.logger.info("[Client] Added permit entry:", item.name)
statuses.append(FeedbagStatus.Success)
except Exception:
statuses.append(FeedbagStatus.BadRequest)
else:
statuses.append(FeedbagStatus.Success)
self.logger.info("[Server] FEEDBAG__STATUS")
_send_feedbag_status(client, message.request_id, statuses)
@Subgroup(0x0009)
def update_item(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
self.logger.info("[Client] FEEDBAG__UPDATE_ITEM")
bs = context.bs
user = context.user
detail = user.detail
items = unmarshal_items(message.data)
statuses = []
for item in items:
if item.class_id == FeedbagClass.Group:
if item.group_id == 0 and item.item_id == 0:
statuses.append(FeedbagStatus.Success)
continue
backend_group_id = _resolve_group_backend_id(context, item.group_id)
if backend_group_id is None:
statuses.append(FeedbagStatus.NotFound)
continue
group = detail._groups_by_id.get(backend_group_id)
if group is None:
statuses.append(FeedbagStatus.NotFound)
continue
if item.name and item.name != group.name:
try:
bs.me_group_edit(backend_group_id, new_name=item.name)
self.logger.info("[Client] Renamed group to:", item.name)
statuses.append(FeedbagStatus.Success)
except Exception:
statuses.append(FeedbagStatus.BadRequest)
else:
statuses.append(FeedbagStatus.Success)
else:
statuses.append(FeedbagStatus.Success)
self.logger.info("[Server] FEEDBAG__STATUS")
_send_feedbag_status(client, message.request_id, statuses)
@Subgroup(0x000A)
def delete_item(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
self.logger.info("[Client] FEEDBAG__DELETE_ITEM")
bs = context.bs
user = context.user
detail = user.detail
items = unmarshal_items(message.data)
statuses = []
for item in items:
if item.class_id == FeedbagClass.Group:
if item.group_id == 0 and item.item_id == 0:
statuses.append(FeedbagStatus.BadRequest)
continue
backend_group_id = _resolve_group_backend_id(context, item.group_id)
if backend_group_id is None:
statuses.append(FeedbagStatus.NotFound)
continue
try:
bs.me_group_remove(backend_group_id)
mapping = bs.front_data.get("oscar0_group_id_map", {})
for k in [k for k, v in mapping.items() if v == backend_group_id]:
del mapping[k]
self.logger.info("[Client] Removed group:", item.name)
statuses.append(FeedbagStatus.Success)
except Exception:
statuses.append(FeedbagStatus.NotFound)
elif item.class_id == FeedbagClass.Buddy:
if not item.name:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact_uuid = context.backend.util_get_uuid_from_username(item.name)
if contact_uuid is None:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact = detail.contacts.get(contact_uuid)
if contact is None or not (contact.lists & ContactList.FL):
statuses.append(FeedbagStatus.NotFound)
continue
try:
if item.group_id != 0:
backend_group_id = _resolve_group_backend_id(context, item.group_id)
if backend_group_id is not None:
bs.me_group_contact_remove(backend_group_id, contact_uuid)
if not contact._groups:
bs.me_contact_remove(contact_uuid, ContactList.FL)
self.logger.info("[Client] Removed buddy:", item.name)
statuses.append(FeedbagStatus.Success)
except Exception:
statuses.append(FeedbagStatus.BadRequest)
elif item.class_id == FeedbagClass.Deny:
if not item.name:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact_uuid = context.backend.util_get_uuid_from_username(item.name)
if contact_uuid is None:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact = detail.contacts.get(contact_uuid)
if contact is None or not (contact.lists & ContactList.BL):
statuses.append(FeedbagStatus.NotFound)
continue
try:
bs.me_contact_remove(contact_uuid, ContactList.BL)
self.logger.info("[Client] Removed deny entry:", item.name)
statuses.append(FeedbagStatus.Success)
except Exception:
statuses.append(FeedbagStatus.BadRequest)
elif item.class_id == FeedbagClass.Permit:
if not item.name:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact_uuid = context.backend.util_get_uuid_from_username(item.name)
if contact_uuid is None:
statuses.append(FeedbagStatus.BadLoginID)
continue
contact = detail.contacts.get(contact_uuid)
if contact is None or not (contact.lists & ContactList.AL):
statuses.append(FeedbagStatus.NotFound)
continue
try:
bs.me_contact_remove(contact_uuid, ContactList.AL)
self.logger.info("[Client] Removed permit entry:", item.name)
statuses.append(FeedbagStatus.Success)
except Exception:
statuses.append(FeedbagStatus.BadRequest)
else:
statuses.append(FeedbagStatus.Success)
self.logger.info("[Server] FEEDBAG__STATUS")
_send_feedbag_status(client, message.request_id, statuses)
+218
View File
@@ -0,0 +1,218 @@
import time, struct
import re
from html import unescape
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 find_tlv, unmarshal_tlvs, marshal_tlvs, TLV
class ICBMChannel(IntEnum):
AOLIM = 0x0001
Rendezvous = 0x0002
Mime = 0x0003
ICQ = 0x0004
CoBrowser = 0x0005
class ICBMEvent(IntEnum):
Finished = 0x0000
Typed = 0x0001
Typing = 0x0002
Closed = 0x000F
@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://ninawiki.preloading.dev/wiki/Protocols/OSCAR/SNAC/ICBM_PARAMETER_REPLY and
# https://ninawiki.preloading.dev/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:
cookie = message.read_bytes(8)
channel = message.read_u16()
receiver = 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 receiver:', receiver)
for tlv in tlvs:
self.logger.info('[Client] TLV', hex(tlv.type), '-', tlv.data)
if channel != ICBMChannel.AOLIM:
self.logger.info('[Client] Unsupported channel:', hex(channel))
return
im_data_tlv = find_tlv(tlvs, 0x0002)
if im_data_tlv is None:
self.logger.info('[Client] Missing IM data TLV')
return
im_tlvs = unmarshal_tlvs(im_data_tlv.data)
im_text_tlv = find_tlv(im_tlvs, 0x0101)
if im_text_tlv is None:
self.logger.info('[Client] Missing IM text TLV inside IM data')
return
text = im_text_tlv.data[4:].decode('ascii', errors='replace')
self.logger.info('[Client] Message text:', text)
receiver_uuid = context.backend.util_get_uuid_from_username(receiver)
if receiver_uuid is None:
self.logger.info('[Client] Unknown receiver:', receiver)
return
try:
cs, evt = client.ctrl._get_private_chat_with(receiver_uuid)
except Exception as ex:
self.logger.info('[Client] Could not create private chat:', str(ex))
return
ack = SNACMessage(0x0004, 0x000C)
ack.request_id = message.request_id
ack.write_bytes(cookie)
ack.write_u16(channel)
ack.write_string_u8(receiver)
self.logger.info('[Server] ICBM__HOST_ACK')
client.send_snac(ack)
md = MessageData(sender=context.user, type=MessageType.Chat, text=strip_html_tags(text))
md.front_cache['icbm_cookie'] = cookie
md.front_cache['icbm_html'] = text
evt._send_when_user_joins(receiver_uuid, md)
@Subgroup(0x0014)
def client_event(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None:
cookie = message.read_bytes(8)
channel = message.read_u16()
receiver = message.read_string_u8()
event = message.read_u16()
self.logger.info('[Client] ICBM__CLIENT_EVENT')
self.logger.info('[Client] Receiver:', receiver, '| Event:', hex(event))
if channel != ICBMChannel.AOLIM:
self.logger.info('[Client] Unsupported channel:', hex(channel))
return
receiver_uuid = context.backend.util_get_uuid_from_username(receiver)
if receiver_uuid is None:
self.logger.info('[Client] Unknown receiver:', receiver)
return
# map OSCAR events to respective MessageTypes
if event == ICBMEvent.Typing:
msg_type = MessageType.Typing
elif event in (ICBMEvent.Finished, ICBMEvent.Typed, ICBMEvent.Closed):
msg_type = MessageType.TypingDone
else:
self.logger.info('[Client] Unknown event type:', hex(event))
return
try:
cs, evt = client.ctrl._get_private_chat_with(receiver_uuid)
except Exception as ex:
self.logger.info('[Client] Could not get private chat:', str(ex))
return
md = MessageData(sender=context.user, type=msg_type, text='')
md.front_cache['icbm_cookie'] = cookie
if evt._user_in_chat(receiver_uuid):
cs.send_message_to_user(receiver_uuid, md)
def strip_html_tags(text: str) -> str:
return unescape(re.compile(r'<[^>]+>').sub('', text))
def messagedata_to_icbm(cookie: bytes, data: MessageData, user: User) -> SNACMessage:
# cookie - 8 bytes, chat sess cookie
# channel - u16, 0x0001 for plain text IM
# sender - string08 (u8 length prefix), sender's screen name
# warning level - u16, always 0
# userinfo TLVs - write_tlv_block (count-prefixed) containing:
# TLV(0x0001) user class
# TLV(0x0003) account signon time (unix time_t)
# TLV(0x000F) session length in seconds
# TLV(0x0005) account creation time (unix time_t)
# TLV(0x0002) - IM data block containing nested TLVs:
# TLV(0x0501) capabilities (1 byte = 0x01 for plain text)
# TLV(0x0101) text: u16 encoding + u16 language + bytes
sender = data.sender
text = data.front_cache.get('icbm_html', data.text or '')
date_created_unix = int(time.mktime(sender.date_created.timetuple()))
date_login_unix = int(time.mktime(sender.date_login.timetuple())) if sender.date_login else 0
msg = SNACMessage(0x0004, 0x0007)
msg.write_bytes(cookie)
msg.write_u16(ICBMChannel.AOLIM)
msg.write_string_u8(sender.username)
msg.write_u16(0) # warning level
# count-prefixed userinfo TLV block
# usually would include TLV(0x001D) (BART info), but we don't support that yet
msg.write_tlv_block([
TLV(0x0001, struct.pack('>H', 0x0008)), # user class
TLV(0x0003, struct.pack('>L', date_login_unix)), # account signon time
TLV(0x000F, struct.pack('>L', 0)), # session length
TLV(0x0005, struct.pack('>L', date_created_unix)), # account creation time
])
im_text = (text or '').encode('ascii', errors='replace')
# IM data: TLV(0x0002) wrapping nested capability + text TLVs
im_data = marshal_tlvs([
TLV(0x0501, b'\x01'), # capabilities: plain text (1 byte)
TLV(0x0101, struct.pack('>HH', 0, 0) + im_text), # encoding=ASCII, language=0, text
])
msg.write_tlv(TLV(0x0002, im_data))
return msg
def messagedata_to_icbm_event(data: MessageData, receiver: User) -> SNACMessage:
if data.type is MessageType.Typing:
event = ICBMEvent.Typing
else:
event = ICBMEvent.Finished
msg = SNACMessage(0x0004, 0x0014)
msg.write_bytes(b'\x00' * 8)
msg.write_u16(ICBMChannel.AOLIM)
msg.write_string_u8(data.sender.username)
msg.write_u16(event)
return msg
+72
View File
@@ -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://ninawiki.preloading.dev/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)
+194
View File
@@ -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)
+17
View File
@@ -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
+17
View File
@@ -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))