import time import struct from array import array from core.models import ContactList, Substatus from dataclasses import dataclass from enum import IntEnum from typing import Optional from util.misc import Logger from .buddy import build_presence_notif # TODO(subpurple): move build_presence_notif out of buddy? from ..proto.tlv import TLV from ..proto.snac import OSCARClient, OSCARContext, SNACMessage, Foodgroup, Subgroup from ..proto.buffer import Buffer # I only put the classes I need in this IntEnum # # For a complete list of feedbag classes, see https://wiki.nina.chat/wiki/Protocols/OSCAR/Foodgroups/FEEDBAG/Items#Class:_FEEDBAG_CLASS_IDS class FeedbagClass(IntEnum): Buddy = 0x0000, Group = 0x0001, PdInfo = 0x0004, BuddyPrefs = 0x0005 class FeedbagAttributes(IntEnum): Order = 0x00C8, PdMode = 0x00CA, WirelessPdMode = 0x00D0, WirelessIgnoreMode = 0x00D1, FishPdMode = 0x00D2, FishIgnoreMode = 0x00D3, PdMask = 0x00CB, BuddyPrefs = 0x00C9 @dataclass class FeedbagItem: name: str group_id: int item_id: int class_id: int attributes: array[TLV] def __init__(self, name: str, group_id: int, item_id: int, class_id: int, attributes: Optional[array[TLV]] = None) -> None: self.name = name self.group_id = group_id self.item_id = item_id self.class_id = class_id self.attributes = attributes or [] def marshal(self) -> bytes: buf = Buffer() buf.write_string_u16(self.name) buf.write_u16(self.group_id) buf.write_u16(self.item_id) buf.write_u16(self.class_id) buf.write_tlv_l_block(self.attributes) return buf.data def get_items(context: OSCARContext) -> array[FeedbagItem]: user = context.user detail = user.detail contacts = list({ *detail.get_contacts_by_list(ContactList.AL), *detail.get_contacts_by_list(ContactList.FL) }) group_feedbag = [] group_order = b'' contact_feedbag = [] for id, group in detail._groups_by_id.items(): order = b'' for contact in contacts: user = contact.head for grp in contact._groups.copy(): if grp.id == id: contact_feedbag.append(FeedbagItem(user.username, int(group.id), user.id, FeedbagClass.Buddy)) order += struct.pack('>H', user.id) group_feedbag.append(FeedbagItem(group.name, int(group.id), 0x0000, FeedbagClass.Group, [ TLV(FeedbagAttributes.Order, order) ])) group_order += struct.pack('>H', int(group.id)) # deal with any ungrouped contacts left if ungrouped_contacts := [contact for contact in contacts if not contact._groups]: no_group_gid = len(group_feedbag) + 1 order = b'' for contact in ungrouped_contacts: user = contact.head contact_feedbag.append(FeedbagItem(user.username, no_group_gid, user.id, FeedbagClass.Buddy)) order += struct.pack('>H', user.id) group_feedbag.append(FeedbagItem('(No Group)', no_group_gid, 0x0000, FeedbagClass.Group, [ TLV(FeedbagAttributes.Order, order) ])) return [ FeedbagItem('', 0x0000, 0x0000, FeedbagClass.Group, [ TLV(FeedbagAttributes.Order, group_order) ]), FeedbagItem('', 0x0000, 0x0E63, FeedbagClass.PdInfo, [ TLV(FeedbagAttributes.PdMode, struct.pack('>H', 0x0004)), TLV(FeedbagAttributes.WirelessPdMode, struct.pack('>B', 0x0001)), TLV(FeedbagAttributes.WirelessIgnoreMode, struct.pack('>B', 0x0001)), TLV(FeedbagAttributes.FishPdMode, struct.pack('>B', 0x0001)), TLV(FeedbagAttributes.FishIgnoreMode, struct.pack('>B', 0x0001)), TLV(FeedbagAttributes.PdMask, struct.pack('>H', 0xFFFF)) ]), FeedbagItem('', 0x0000, 0x4B1D, FeedbagClass.BuddyPrefs, [ TLV(FeedbagAttributes.BuddyPrefs, struct.pack('>L', 0x00000400)) ]), *group_feedbag, *contact_feedbag ] def marshal_items(items: array[FeedbagItem]) -> bytes: return b''.join([item.marshal() for item in items]) def unmarshal_items(item_bytes: bytes) -> array[FeedbagItem]: buf = Buffer(item_bytes) items = [] # Minimum feedbag item size (in bytes): # Name = +2 = 2 # Group ID = +2 = 4 # Item ID = +2 = 6 # Class ID = +2 = 8 # Attributes = +2 = 10 # while len(buf) > 10: name = buf.read_string_u16() group_id = buf.read_u16() item_id = buf.read_u16() class_id = buf.read_u16() attributes = buf.read_tlv_l_block() items.append(FeedbagItem(name, group_id, item_id, class_id, attributes)) return items @Foodgroup(0x0013) class FeedbagFoodgroup: logger: Logger @Subgroup(0x0002) def rights_query(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None: self.logger.info('[Client] FEEDBAG__RIGHTS_QUERY') response_msg = SNACMessage(0x0013, 0x0003) # https://wiki.nina.chat/wiki/Protocols/OSCAR/SNAC/FEEDBAG_RIGHTS_REPLY#TLV_Class:_FEEDBAG_RIGHTS_REPLY_TAGS response_msg.write_tlvs([ TLV(0x0002, struct.pack('>H', 254)), # max class attrs TLV(0x0003, struct.pack('>H', 1698)), # max item attrs # max items TLV(0x0004, struct.pack(f'>{'H' * 67}', 1000, # max num of contacts 100, # max num of groups 1000, # max num of visible contacts 1000, # max num of invisible contacts 1, # max vis/invis bitmasks 1, # max presence info fields 150, # limit for item type 06 12, # limit for item type 07 12, # limit for item type 08 3, # limit for item type 09 50, # limit for item type 0a 50, # limit for item type 0b 0, # limit for item type 0c 128, # limit for item type 0d 1000, # max ignore list entries 20, # limit for item type 0f 200, # limit for item 10 1, # limit for item 11 100, # limit for item 12 1, # limit for item 13 25, # limit for item 14 # These values are unknown but are here in the sake of keeping response # parity with NINA: 1, 40, 1, 10, 200, 1, 60, 200, 1, 8, 20, 1, 10000, 1000, 1000, 50, 1, 5, 500, 1, 8, 10000, 1, 1, 1, 10000, 0, 0, 1, 2000, 0, 60, 24, 10, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1000, 1, 1)), TLV(0x0005, struct.pack('>H', 100)), # max client items TLV(0x0006, struct.pack('>H', 97)), # max item name len TLV(0x0007, struct.pack('>H', 2000)), # max recent buddies TLV(0x0008, struct.pack('>H', 10)), # interaction buddies TLV(0x0009, struct.pack('>L', 432000)), # interaction half life - in 2^(-age/half_life) in seconds TLV(0x000A, struct.pack('>L', 14)), # interaction max score TLV(0x000B, struct.pack('>H', 0)), # unknown TLV(0x000C, struct.pack('>H', 600)), # max buddies per group TLV(0x000D, struct.pack('>H', 200)), # max allowed bot buddies TLV(0x000E, struct.pack('>H', 32)) # max smart groups ]) self.logger.info('[Server] FEEDBAG__RIGHTS_REPLY') client.send_snac(response_msg) @Subgroup(0x0004) def query(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None: self.logger.info('[Client] FEEDBAG__QUERY') items = get_items(context) response_msg = SNACMessage(0x0013, 0x0006, 0x0000, message.request_id) response_msg.write_u8(0) # feedbag protocol version - always 0 response_msg.write_u16(len(items)) # no. of feedbag items response_msg.write_bytes(marshal_items(items)) # list of feedbag items response_msg.write_u32(int(time.time())) # feedbag last change time - TODO: should pull from db self.logger.info('[Server] FEEDBAG__REPLY') client.send_snac(response_msg) @Subgroup(0x0005) def query_if_modified(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None: self.logger.info('[Client] FEEDBAG__QUERY_IF_MODIFIED (not implemented)') # 66 51 29 47 00 0D # # 66 51 29 47 - u32 for unix timestamp of cached client-side feedbag # 00 0D - u16 for number of items in cached client-side feedbag cached_feedbag_timestamp = message.read_u32() cached_feedbag_num_items = message.read_u16() self.logger.info('[Client] Cached feedbag timestamp:', cached_feedbag_timestamp) self.logger.info('[Client] Cached feedbag items num:', cached_feedbag_num_items) # TODO(subpurple): do check self.query(client, context, message) @Subgroup(0x0007) def use(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None: self.logger.info('[Client] FEEDBAG__USE') # set our status to Online context.bs.me_update({ 'substatus': Substatus.Online }) # notify the client if any contacts are online user = context.user detail = user.detail contacts = list({ *detail.get_contacts_by_list(ContactList.AL) }) for contact in contacts: if not contact.status.is_offlineish(): client.send_snac(build_presence_notif(contact)) # stubs because I cba to implement more of feedbag rn @Subgroup(0x0011) def start_cluster(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None: self.logger.info('[Client] FEEDBAG__START_CLUSTER (not implemented)') @Subgroup(0x0012) def end_cluster(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None: self.logger.info('[Client] FEEDBAG__END_CLUSTER (not implemented)') @Subgroup(0x0008) def insert_item(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None: self.logger.info('[Client] FEEDBAG__INSERT_ITEM (not implemented)') self.logger.info('[Client]', unmarshal_items(message.data)) @Subgroup(0x0009) def update_item(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None: self.logger.info('[Client] FEEDBAG__UPDATE_ITEM (not implemented)') self.logger.info('[Client]', unmarshal_items(message.data)) @Subgroup(0x000A) def delete_item(self, client: OSCARClient, context: OSCARContext, message: SNACMessage) -> None: self.logger.info('[Client] FEEDBAG__DELETE_ITEM (not implemented)') self.logger.info('[Client]', unmarshal_items(message.data))