from typing import Optional, Tuple, Any, Iterable from hashlib import md5, sha1 import hmac, struct, base64, binascii, random from pytz import timezone from datetime import datetime from quopri import encodestring as quopri_encode from enum import Enum from util.misc import first_in_iterable, date_format, DefaultDict from typing import Optional from core import error from core.backend import Backend, BackendSession from core.models import User, OIM, Substatus, NetworkID, CircleState, Circle def build_presence_notif( trid: Optional[str], old_substatus: Optional[Substatus], ctc_head: User, user_me: User, dialect: int, backend: Backend, iln_sent: bool, update_info: bool, *, self_presence: bool = False, update_status: bool = True, circle: Optional['Circle'] = None, circle_owner: bool = False, ) -> Iterable[Tuple[Any, ...]]: detail = user_me.detail assert detail is not None if not iln_sent: return nfy_rst = '' if not circle: if not self_presence and ctc_head is not user_me: ctc = detail.contacts.get(ctc_head.uuid) assert ctc is not None status = ctc.status head = ctc.head else: head = user_me status = head.status else: head = ctc_head status = head.status is_offlineish = status.is_offlineish() if is_offlineish and trid is not None: return ctc_sess = None # type: Optional['BackendSession'] ctc_sess = first_in_iterable(backend.util_get_sessions_by_user(head)) head_display_email = backend.util_get_display_email(head) user_me_display_email = backend.util_get_display_email(user_me) # TODO: This is insanely ugly. if dialect >= 19: cm = None # type: Optional[str] pop_id_ctc = None # type: Optional[str] substatus = status.substatus if is_offlineish and head is not user_me: # In case `ctc` is going `HDN`; make sure other people don't receive `HDN` as status substatus = Substatus.Offline if substatus is not Substatus.Offline: assert ctc_sess is not None cm = NFY_PUT_PRESENCE_USER_S_CM.format(cm = encode_xml_he(status.media or '', dialect)) nfy_rst += NFY_PUT_PRESENCE_USER_S_PE.format( msnobj = encode_xml_he(ctc_sess.front_data.get('msn_msnobj') or '', dialect), name = status.name or head_display_email, message = status.message, ddp = encode_xml_he(ctc_sess.front_data.get('msn_msnobj_ddp') or '', dialect), colorscheme = encode_xml_he(ctc_sess.front_data.get('msn_colorscheme') or '', dialect), scene = encode_xml_he(ctc_sess.front_data.get('msn_msnobj_scene') or '', dialect), sigsound = encode_xml_he(ctc_sess.front_data.get('msn_sigsound') or '', dialect), ) if ctc_sess.front_data.get('msn_pop_id') is not None: pop_id_ctc = '{' + ctc_sess.front_data['msn_pop_id'] + '}' nfy_rst += NFY_PUT_PRESENCE_USER_SEP_IM.format( epid_attrib = (NFY_PUT_PRESENCE_USER_SEP_EPID.format(mguid = pop_id_ctc or '') if pop_id_ctc is not None else ''), capabilities = encode_capabilities_capabilitiesex( (ctc_sess.front_data.get('msn_capabilities') if ctc_sess.front_data.get('msn') is True else MAX_CAPABILITIES_BASIC), ctc_sess.front_data.get('msn_capabilitiesex') or 0, ), ) if ctc_sess.front_data.get('msn_PE'): pe_data = '' pe_data += NFY_PUT_PRESENCE_USER_SEP_PE_VER.format(ver = ctc_sess.front_data.get('msn_PE_VER') or '') pe_data += NFY_PUT_PRESENCE_USER_SEP_PE_TYP.format(typ = ctc_sess.front_data.get('msn_PE_TYP') or '') pe_data += NFY_PUT_PRESENCE_USER_SEP_PE_CAP.format( pe_capabilities = encode_capabilities_capabilitiesex( ctc_sess.front_data.get('msn_PE_capabilities') or 0, ctc_sess.front_data.get('msn_PE_capabilitiesex') or 0, ) ) nfy_rst += NFY_PUT_PRESENCE_USER_SEP_PE.format( epid_attrib = (NFY_PUT_PRESENCE_USER_SEP_EPID.format(mguid = pop_id_ctc or '') if pop_id_ctc is not None else ''), pe_data = pe_data, ) if pop_id_ctc is not None: nfy_rst += NFY_PUT_PRESENCE_USER_SEP_PD.format( mguid = pop_id_ctc, ped_data = _list_private_endpoint_data(ctc_sess), ) for ctc_sess_other in backend.util_get_sessions_by_user(ctc_sess.user): if ctc_sess_other is ctc_sess: continue if ctc_sess_other.front_data.get('msn_pop_id') is None: continue nfy_rst += NFY_PUT_PRESENCE_USER_SEP_IM.format( epid_attrib = NFY_PUT_PRESENCE_USER_SEP_EPID.format(mguid = '{' + ctc_sess_other.front_data['msn_pop_id'] + '}'), capabilities = encode_capabilities_capabilitiesex( ( (ctc_sess_other.front_data.get('msn_capabilities') or 0) if ctc_sess_other.front_data.get('msn') is True else MAX_CAPABILITIES_BASIC ), ctc_sess_other.front_data.get('msn_capabilitiesex') or 0, ), ) if ctc_sess_other.front_data.get('msn_PE'): pe_data = '' pe_data += NFY_PUT_PRESENCE_USER_SEP_PE_VER.format(ver = ctc_sess_other.front_data.get('msn_PE_VER') or '') pe_data += NFY_PUT_PRESENCE_USER_SEP_PE_TYP.format(typ = ctc_sess_other.front_data.get('msn_PE_TYP') or '') pe_data += NFY_PUT_PRESENCE_USER_SEP_PE_CAP.format( pe_capabilities = encode_capabilities_capabilitiesex( ctc_sess_other.front_data.get('msn_PE_capabilities') or 0, ctc_sess_other.front_data.get('msn_PE_capabilitiesex') or 0, ), ) nfy_rst += NFY_PUT_PRESENCE_USER_SEP_PE.format( epid_attrib = NFY_PUT_PRESENCE_USER_SEP_EPID.format(mguid = '{' + ctc_sess_other.front_data['msn_pop_id'] + '}'), pe_data = pe_data, ) nfy_rst += NFY_PUT_PRESENCE_USER_SEP_PD.format( mguid = '{' + ctc_sess_other.front_data['msn_pop_id'] + '}', ped_data = _list_private_endpoint_data(ctc_sess_other) ) msn_status = MSNStatus.FromSubstatus(substatus) nfy_presence_body = NFY_PUT_PRESENCE_USER.format( substatus = msn_status.name, cm = cm or '', rst = nfy_rst, ) nfy_payload = encode_payload(NFY_PUT_PRESENCE, to = user_me_display_email, from_email = head_display_email, cl = len(nfy_presence_body), payload = nfy_presence_body, ) yield ('NFY', 'PUT', nfy_payload) return if status.substatus is not old_substatus: if (is_offlineish or (circle is not None and circle.memberships[head.uuid].blocking)) and head is not user_me: if dialect >= 18: reply = ('FLN', encode_email_networkid(head_display_email, None, circle = circle)) # type: Tuple[Any, ...] else: reply = ('FLN', head_display_email, (int(NetworkID.WINDOWS_LIVE) if 14 <= dialect <= 17 else None)) if 14 <= dialect <= 15: reply += ('0',) elif dialect >= 16: reply += ('0:0',) yield reply return if ctc_sess is None: return if status.substatus is not old_substatus or update_status: msn_status = MSNStatus.FromSubstatus(status.substatus) if trid and dialect < 18: frst = ('ILN', trid) # type: Tuple[Any, ...] else: frst = ('NLN',) rst = [] if 8 <= dialect <= 15: rst.append(((ctc_sess.front_data.get('msn_capabilities') or 0) if ctc_sess.front_data.get('msn') is True else MAX_CAPABILITIES_BASIC)) elif dialect >= 16: rst.append(( '0:0' if circle is not None and circle_owner else encode_capabilities_capabilitiesex( ((ctc_sess.front_data.get('msn_capabilities') or 0) if ctc_sess.front_data.get('msn') is True else MAX_CAPABILITIES_BASIC), ctc_sess.front_data.get('msn_capabilitiesex') or 0, ) )) if dialect >= 9: rst.append(MSNObj(ctc_sess.front_data.get('msn_msnobj') or '')) if dialect >= 18: yield (*frst, msn_status.name, encode_email_networkid(head_display_email, None, circle = circle), status.name, *rst) else: yield (*frst, msn_status.name, head_display_email, (int(NetworkID.WINDOWS_LIVE) if 14 <= dialect <= 17 else None), status.name, *rst) # MSNP16/18 requires `UBX`s when a user changes any sort of state (this makes MPoP and Circles work effectively) if dialect < 11 or (not update_info and dialect < 16): return ubx_payload = '{}{}{}'.format( (encode_xml_he(status.message, dialect) if dialect >= 13 else encode_xml_ne(status.message)) or '', (encode_xml_he(status.media, dialect) if dialect >= 13 else encode_xml_ne(status.media)) or '', extend_ubx_payload(dialect, backend, user_me, ctc_sess), ).encode('utf-8') if dialect >= 18: yield ('UBX', encode_email_networkid(head_display_email, None, circle = circle), ubx_payload) else: yield ('UBX', head_display_email, (int(NetworkID.WINDOWS_LIVE) if 14 <= dialect <= 17 else None), ubx_payload) def encode_email_networkid(email: str, networkid: Optional[NetworkID], *, circle: Optional['Circle'] = None) -> str: result = '{}:{}'.format(int(networkid or NetworkID.WINDOWS_LIVE), email) if circle: result = '{};via=9:00000000-0000-0000-0009-{}@live.com'.format(result, circle.chat_id) return result def decode_email_networkid(email_networkid: str) -> Tuple[NetworkID, str]: parts = email_networkid.split(':', 1) networkid = NetworkID(int(parts[0])) return networkid, parts[1] def encode_xml_he(data: Optional[str], dialect: int) -> Optional[str]: if data is None: return None encoded = data.replace('&', '&') if dialect >= 16: encoded = encoded.replace('<', '<').replace('>', '>').replace('=', '=').replace('\\', '\') return encoded def encode_xml_ne(data: Optional[str]) -> Optional[str]: if data is None: return None encoded = data.replace('&', '&').replace('<', '<').replace('>', '>').replace('\'', ''').replace('"', '"') return encoded def encode_capabilities_capabilitiesex(capabilities: int, capabilitiesex: int) -> str: return '{}:{}'.format(capabilities, capabilitiesex) def decode_capabilities_capabilitiesex(capabilities_encoded: str) -> Optional[Tuple[int, int]]: if capabilities_encoded.find(':') > 0: a, b = capabilities_encoded.split(':', 1) return int(a), int(b) return int(capabilities_encoded), 0 def cid_format(uuid: str, *, decimal: bool = False) -> str: cid = (uuid[19:23] + uuid[24:36]).lower() if not decimal: return cid # convert to decimal string return str(struct.unpack(' Tuple[int, int]: import uuid u = uuid.UUID(uuid_str) high = u.time_low % (1<<32) low = u.node % (1<<32) return (high, low) def puid_format(uuid_str: str) -> str: high, low = uuid_to_high_low(uuid_str) n = (high * (2 ** 32)) + low return binascii.hexlify(struct.pack('>Q', n)).decode('utf-8').upper() def encode_email_pop(email: str, pop_id: Optional[str]) -> str: result = email if pop_id: result = '{};{}'.format(result, '{' + pop_id + '}') return result def decode_email_pop(s: str) -> Tuple[str, Optional[str]]: # Split `foo@email.com;{uuid}` into (email, pop_id) parts = s.split(';', 1) if len(parts) < 2: pop_id = None else: pop_id = parts[1] return (parts[0], pop_id) def normalize_pop_id(pop_id: str) -> str: return ''.join(pop_id.replace('{', '', 1).rsplit('}', 1)) def extend_ubx_payload(dialect: int, backend: Backend, user: User, ctc_sess: 'BackendSession') -> str: response = '' ctc_machineguid = ctc_sess.front_data.get('msn_machineguid') pop_id_ctc = ctc_sess.front_data.get('msn_pop_id') if dialect >= 13 and ctc_machineguid: response += '{}'.format(ctc_machineguid) if dialect >= 16: response += '{}{}{}'.format( ('{}'.format(encode_xml_he(ctc_sess.front_data.get('msn_msnobj_ddp'), dialect) or '') if dialect >= 18 else ''), encode_xml_he(ctc_sess.front_data.get('msn_sigsound'), dialect) or '', ( '{}{}'.format( encode_xml_he(ctc_sess.front_data.get('msn_msnobj_scene'), dialect) or '', ctc_sess.front_data.get('msn_colorscheme') or '', ) if dialect >= 18 else '' ), ) if pop_id_ctc: response += EPDATA_PAYLOAD.format( mguid = '{' + pop_id_ctc + '}', capabilities = encode_capabilities_capabilitiesex( ((ctc_sess.front_data.get('msn_capabilities') or 0) if ctc_sess.front_data.get('msn') is True else MAX_CAPABILITIES_BASIC), ctc_sess.front_data.get('msn_capabilitiesex') or 0, ), ) for ctc_sess_other in backend.util_get_sessions_by_user(ctc_sess.user): pop_id = ctc_sess_other.front_data.get('msn_pop_id') or '' if pop_id.lower() == pop_id_ctc.lower(): continue response += EPDATA_PAYLOAD.format( mguid = '{' + pop_id + '}', capabilities = encode_capabilities_capabilitiesex( ((ctc_sess.front_data.get('msn_capabilities') or 0) if ctc_sess.front_data.get('msn') is True else MAX_CAPABILITIES_BASIC), ctc_sess_other.front_data.get('msn_capabilitiesex') or 0, ) ) if ctc_sess.user is user: for ctc_sess_other in backend.util_get_sessions_by_user(ctc_sess.user): if ctc_sess_other.front_data.get('msn_pop_id') is None: continue response += PRIVATEEPDATA_PAYLOAD.format( mguid = '{' + (ctc_sess_other.front_data.get('msn_pop_id') or '') + '}', ped_data = _list_private_endpoint_data(ctc_sess_other), ) return response def _list_private_endpoint_data(ctc_sess: 'BackendSession') -> str: ped_data = '' if ctc_sess.front_data.get('msn_epname'): ped_data += PRIVATEEPDATA_EPNAME_PAYLOAD.format(epname = ctc_sess.front_data['msn_epname']) if ctc_sess.front_data.get('msn_endpoint_idle'): ped_data += PRIVATEEPDATA_IDLE_PAYLOAD.format(idle = ('true' if ctc_sess.front_data['msn_endpoint_idle'] else 'false')) if ctc_sess.front_data.get('msn_client_type'): ped_data += PRIVATEEPDATA_CLIENTTYPE_PAYLOAD.format(ct = ctc_sess.front_data['msn_client_type']) if ctc_sess.front_data.get('msn_ep_state'): ped_data += PRIVATEEPDATA_STATE_PAYLOAD.format(state = ctc_sess.front_data['msn_ep_state']) return ped_data def gen_signedticket_xml(bs: BackendSession, backend: Backend) -> str: from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding user = bs.user circleticket_sig = bs.front_data.get('msn_circleticket_sig') assert circleticket_sig is not None circles = [ CIRCLETICKET_CIRCLE.format(circle.chat_id) for circle in backend.user_service.get_circle_batch(user) if circle.memberships[user.uuid].state is CircleState.Accepted ] circleticket = encode_payload(CIRCLETICKET, circles = ''.join(circles), cid = cid_format(user.uuid, decimal = True), ) return SIGNEDTICKET.format( base64.b64encode(circleticket).decode('ascii'), base64.b64encode(circleticket_sig.sign(circleticket, padding.PKCS1v15(), hashes.SHA1())).decode('ascii'), ) def encode_payload(tmpl: str, **kwargs: Any) -> bytes: return tmpl.format(**kwargs).replace('\n', '\r\n').encode('utf-8') def gen_chal_response(chal: str, id: str, id_key: str, *, msnp11: bool = False) -> str: key_digest = md5((chal + id_key).encode('ascii')).digest() if not msnp11: return key_digest.hex() private_seed = [ struct.unpack('I', (high + private_seed[1]) % 0x7FFFFFFF))[0] low = struct.unpack('I', (low + private_seed[3]) % 0x7FFFFFFF))[0] key = struct.unpack('Q', (high << 32) | (low & 0xFFFFFFFF)))[0] result_high = struct.unpack(' bytes: hash1 = hmac.new(key, msg, sha1).digest() hash2 = hmac.new(key, (hash1 + msg), sha1).digest() hash3 = hmac.new(key, hash1, sha1).digest() hash4 = hmac.new(key, (hash3 + msg), sha1).digest() return (hash2[:20] + hash4[:4]) DES3_BLOCK_SIZE = 8 def encrypt_with_key_and_iv_tripledes_cbc(key: bytes, iv: bytes, msg: bytes) -> bytes: from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.decrepit.ciphers.algorithms import TripleDES from cryptography.hazmat.primitives.ciphers.modes import CBC from cryptography.hazmat.backends import default_backend # Use PKCS5 padding msg_padded = _pkcs5_pad_message_tripledes(msg) tripledes_cbc_cipher = Cipher(TripleDES(key), mode = CBC(iv), backend = default_backend()) # type: ignore tripledes_cbc_encryptor = tripledes_cbc_cipher.encryptor() # type: ignore final = tripledes_cbc_encryptor.update(msg_padded) final += tripledes_cbc_encryptor.finalize() return final def _pkcs5_pad_message_tripledes(msg: bytes) -> bytes: final = msg pad_value = DES3_BLOCK_SIZE - len(msg) % DES3_BLOCK_SIZE final += bytes([pad_value]) * pad_value return final def gen_mail_data( user: User, backend: Backend, *, oim: Optional[OIM] = None, just_sent: bool = False, on_ns: bool = True, e_node: bool = True, q_node: bool = True, ) -> str: md_m_pl = '' oim_collection = [] if just_sent: if oim is not None: oim_collection.append(oim) else: oim_collection = backend.user_service.get_oim_batch(user) #if on_ns and len(oim_collection) > 25: return 'too-large' for oim in oim_collection: md_m_pl += M_MAIL_DATA_PAYLOAD.format( rt = (RT_M_MAIL_DATA_PAYLOAD.format( senttime = date_format(oim.sent) ) if not just_sent else ''), oimsz = len(format_oim(oim)), frommember = oim.from_email, guid = oim.uuid, fid = ('00000000-0000-0000-0000-000000000009' if not just_sent else '.!!OIM'), fromfriendly = ( _encode_friendly(oim.from_friendly, oim.from_friendly_charset, oim.from_friendly_encoding, space = True if just_sent else False) if oim.from_friendly is not None else '' ), su = (' ' if just_sent else ''), ) return MAIL_DATA_PAYLOAD.format( e = (E_MAIL_DATA_PAYLOAD.format(inbox=0, inbox_unread=0) if e_node else ''), q = (Q_MAIL_DATA_PAYLOAD if q_node else ''), m = md_m_pl, ) def format_oim(oim: OIM) -> str: if not oim.headers: oim_headers = OIM_HEADER_BASE.format(run_id = '{' + oim.run_id + '}').replace('\n', '\r\n') else: oim_headers = '\r\n'.join(['{}: {}'.format(name, value) for name, value in oim.headers.items()]) sent_email = oim.sent.astimezone(timezone('America/Los_Angeles')) if oim.from_friendly is not None: friendly = '{} '.format(_encode_friendly(oim.from_friendly, oim.from_friendly_charset, oim.from_friendly_encoding)) else: friendly = None oim_msg = OIM_HEADER_PRE_0.format( pst1 = sent_email.strftime('%a, %d %b %Y %H:%M:%S -0800'), friendly = friendly or '', sender = oim.from_email, recipient = oim.to_email, ip = oim.origin_ip or '', ).replace('\n', '\r\n') if oim.oim_proxy: oim_msg += OIM_HEADER_PRE_1.format(oimproxy = oim.oim_proxy).replace('\n', '\r\n') oim_msg += oim_headers oim_msg += OIM_HEADER_REST.format( utc = oim.sent.strftime('%d %b %Y %H:%M:%S.%f')[:25] + ' (UTC)', ft = _datetime_to_filetime(oim.sent), pst2 = sent_email.strftime('%d %b %Y %H:%M:%S -0800'), ).replace('\n', '\r\n') oim_msg += '\r\n\r\n' + base64.b64encode(oim.message.encode('utf-8')).decode('utf-8') return oim_msg def _datetime_to_filetime(dt_time: datetime) -> str: filetime_result = round(((dt_time.timestamp() * 10000000) + 116444736000000000) + (dt_time.microsecond * 10)) # (DWORD)ll filetime_high = filetime_result & 0xFFFFFFFF filetime_low = filetime_result >> 32 filetime_high_hex = hex(filetime_high)[2:] filetime_high_hex = '0' * (8 % len(filetime_high_hex)) + filetime_high_hex filetime_low_hex = hex(filetime_low)[2:] filetime_low_hex = '0' * (8 % len(filetime_low_hex)) + filetime_low_hex return filetime_high_hex.upper() + ':' + filetime_low_hex.upper() def _encode_friendly(friendlyname: str, charset: str, encoding: str, *, space: bool = False) -> Optional[str]: data_encoded = None data = friendlyname.encode(charset) if encoding == 'B': data_encoded = base64.b64encode(data) elif encoding == 'Q': data_encoded = quopri_encode(data) if data_encoded == None: return None data_encoded_str = data_encoded.decode('utf-8') if space: data_encoded_str += ' ' return '=?{}?{}?{}?='.format(charset, encoding, data_encoded_str) MAIL_DATA_PAYLOAD = '{e}{q}{m}' E_MAIL_DATA_PAYLOAD = '{inbox}{inbox_unread}00' Q_MAIL_DATA_PAYLOAD = '409600204800' M_MAIL_DATA_PAYLOAD = '116{rt}0{oimsz}{frommember}\ {guid}{fid}{fromfriendly}{su}' RT_M_MAIL_DATA_PAYLOAD = '{senttime}' EPDATA_PAYLOAD = '{capabilities}' PRIVATEEPDATA_PAYLOAD = '{ped_data}' PRIVATEEPDATA_EPNAME_PAYLOAD = '{epname}' PRIVATEEPDATA_IDLE_PAYLOAD = '{idle}' PRIVATEEPDATA_CLIENTTYPE_PAYLOAD = '{ct}' PRIVATEEPDATA_STATE_PAYLOAD = '{state}' SIGNEDTICKET = '''{}{}''' CIRCLETICKET = ''' {circles} 0000-01-01T00:00:00 {cid} ''' CIRCLETICKET_CIRCLE = ''' ''' OIM_HEADER_PRE_0 = '''X-Message-Info: cwRBnLifKNE8dVZlNj6AiX8142B67OTjG9BFMLMyzuui1H4Xx7m3NQ== Received: from OIM-SSI02.phx.gbl ([65.54.237.206]) by oim1-f1.hotmail.com with Microsoft SMTPSVC(6.0.3790.211); {pst1} Received: from mail pickup service by OIM-SSI02.phx.gbl with Microsoft SMTPSVC; {pst1} From: {friendly}<{sender}> To: {recipient} Subject: X-OIM-originatingSource: {ip} ''' OIM_HEADER_PRE_1 = '''X-OIMProxy: {oimproxy} ''' OIM_HEADER_BASE = '''MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 X-OIM-Message-Type: OfflineMessage X-OIM-Run-Id: {run_id}''' OIM_HEADER_REST = ''' Message-ID: X-OriginalArrivalTime: {utc} FILETIME=[{ft}] Date: {pst2} Return-Path: ndr@oim.messenger.msn.com''' NFY_PUT_PRESENCE = '''Routing: 1.0 To: 1:{to} From: 1:{from_email} Reliability: 1.0 Notification: 1.0 NotifNum: 0 Uri: /user NotifType: Partial Content-Type: application/user+xml Content-Length: {cl} {payload}''' NFY_PUT_PRESENCE_USER = '{substatus}{cm}{rst}' NFY_PUT_PRESENCE_USER_S_CM = '{cm}' NFY_PUT_PRESENCE_USER_S_PE = '{msnobj}{name}\ {message}{ddp}{colorscheme}\ {scene}{sigsound}' NFY_PUT_PRESENCE_USER_SEP_IM = '{capabilities}' NFY_PUT_PRESENCE_USER_SEP_PE = '{pe_data}' NFY_PUT_PRESENCE_USER_SEP_PE_VER = '{ver}' NFY_PUT_PRESENCE_USER_SEP_PE_TYP = '{typ}' NFY_PUT_PRESENCE_USER_SEP_PE_CAP = '{pe_capabilities}' NFY_PUT_PRESENCE_USER_SEP_PD = '{ped_data}' NFY_PUT_PRESENCE_USER_SEP_EPID = ' epid="{mguid}"' MAX_CAPABILITIES_BASIC = 1073741824 class MSNObj: __slots__ = ('data',) data: Optional[str] def __init__(self, data: Optional[str]) -> None: self.data = data class MSNStatus(Enum): FLN = object() NLN = object() BSY = object() IDL = object() BRB = object() AWY = object() PHN = object() LUN = object() HDN = object() @classmethod def ToSubstatus(cls, msn_status: 'MSNStatus') -> Substatus: return _ToSubstatus[msn_status] @classmethod def FromSubstatus(cls, substatus: 'Substatus') -> 'MSNStatus': return _FromSubstatus[substatus] _ToSubstatus = DefaultDict(Substatus.Busy, { MSNStatus.FLN: Substatus.Offline, MSNStatus.NLN: Substatus.Online, MSNStatus.BSY: Substatus.Busy, MSNStatus.IDL: Substatus.Idle, MSNStatus.BRB: Substatus.BRB, MSNStatus.AWY: Substatus.Away, MSNStatus.PHN: Substatus.OnPhone, MSNStatus.LUN: Substatus.OutToLunch, MSNStatus.HDN: Substatus.Invisible, }) _FromSubstatus = DefaultDict(MSNStatus.BSY, { Substatus.Offline: MSNStatus.FLN, Substatus.Online: MSNStatus.NLN, Substatus.Busy: MSNStatus.BSY, Substatus.Idle: MSNStatus.IDL, Substatus.BRB: MSNStatus.BRB, Substatus.Away: MSNStatus.AWY, Substatus.OnPhone: MSNStatus.PHN, Substatus.OutToLunch: MSNStatus.LUN, Substatus.Invisible: MSNStatus.HDN, Substatus.NotAtHome: MSNStatus.AWY, Substatus.NotAtDesk: MSNStatus.BRB, Substatus.NotInOffice: MSNStatus.AWY, Substatus.OnVacation: MSNStatus.AWY, Substatus.SteppedOut: MSNStatus.BRB, }) class Err: InvalidParameter = 201 InvalidNetworkID = 204 InvalidUser = 205 DuplicateSession = 207 InvalidUser2 = 208 ContactListLimitReached = 210 PrincipalOnContactList = 215 PrincipalNotOnContactList = 216 PrincipalNotOnline = 217 AlreadyInMode = 218 MutuallyExclusiveList = 219 GroupInvalid = 224 PrincipalNotInGroup = 225 GroupAlreadyExists = 228 GroupNameTooLong = 229 GroupZeroUnremovable = 230 XXLEmptyDomain = 240 XXLInvalidPayload = 241 ContactListUnavailable = 402 ContactListError = 403 InvalidAccountPermissions = 420 InternalServerError = 500 CommandDisabled = 502 MemberIsSuspended = 511 ChallengeResponseFailed = 540 Overload = 712 NotExpected = 715 AuthFail = 911 NotAllowedWhileHDN = 913 InvalidDomain = 917 ChildAccountNotAuthorized = 923 NeedsHostUpdate = 929 AccountNotVerified = 926 InvalidCircleMembership = 933 @classmethod def GetCodeForException(cls, exc: Exception, dialect: int) -> int: if isinstance(exc, error.GroupAlreadyExists): return cls.GroupAlreadyExists if isinstance(exc, error.GroupNameTooLong): return cls.GroupNameTooLong if isinstance(exc, error.GroupDoesNotExist): return cls.GroupInvalid if isinstance(exc, error.CannotRemoveSpecialGroup): if dialect >= 10: return cls.GroupInvalid else: return cls.GroupZeroUnremovable if isinstance(exc, error.ContactDoesNotExist): if dialect >= 10: return cls.InvalidUser2 else: return cls.InvalidUser if isinstance(exc, error.ContactAlreadyOnContactList): return cls.PrincipalOnContactList if isinstance(exc, error.ContactNotOnContactList): return cls.PrincipalNotOnContactList if isinstance(exc, error.UserDoesNotExist): if dialect >= 10: return cls.InvalidUser2 else: return cls.InvalidUser if isinstance(exc, error.ContactNotOnline): return cls.PrincipalNotOnline if isinstance(exc, error.AuthFail): return cls.AuthFail if isinstance(exc, error.NotAllowedWhileHDN): return cls.NotAllowedWhileHDN if isinstance(exc, error.ContactListIsFull): return cls.ContactListLimitReached if isinstance(exc, error.MemberIsSuspended): return cls.MemberIsSuspended raise ValueError("Exception not convertible to MSNP error") from exc