mirror of
https://git.ugnet.gay/CrossTalk/azul.git
synced 2026-05-27 22:59:49 +00:00
init
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
__all__ = [
|
||||
'oservice',
|
||||
'locate',
|
||||
'buddy',
|
||||
'icbm',
|
||||
'bos',
|
||||
'stats',
|
||||
'feedbag',
|
||||
'popup',
|
||||
'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,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)
|
||||
@@ -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,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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user