Files
azul/front/msn/misc.py
T
Athena Funderburg 4b463a3432 init
2026-05-25 07:05:17 +00:00

747 lines
27 KiB
Python

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 '<msnobj/>'))
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 = '<Data><PSM>{}</PSM><CurrentMedia>{}</CurrentMedia>{}</Data>'.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('&', '&#x26;')
if dialect >= 16:
encoded = encoded.replace('<', '&#x3C;').replace('>', '&#x3E;').replace('=', '&#x3D;').replace('\\', '&#x5C;')
return encoded
def encode_xml_ne(data: Optional[str]) -> Optional[str]:
if data is None: return None
encoded = data.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('\'', '&apos;').replace('"', '&quot;')
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('<q', binascii.unhexlify(cid))[0])
def uuid_to_high_low(uuid_str: str) -> 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 += '<MachineGuid>{}</MachineGuid>'.format(ctc_machineguid)
if dialect >= 16:
response += '{}<SignatureSound>{}</SignatureSound>{}'.format(
('<DDP>{}</DDP>'.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 '',
(
'<Scene>{}</Scene><ColorScheme>{}</ColorScheme>'.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', key_digest[i:i+4])[0] & 0x7FFFFFFF
for i in range(0, 16, 4)
]
pub_bytes = (chal + id).encode('ascii')
pub_bytes += b'0' * (8 - (len(pub_bytes) % 8))
public_seed = [
struct.unpack('<I', pub_bytes[i:i+4])[0] & 0x7FFFFFFF
for i in range(0, len(pub_bytes), 4)
]
high = 0
low = 0
for i in range(0, len(public_seed), 2):
temp = (public_seed[i] * 0x0E79A9C1) % 0x7FFFFFFF
temp = (temp + high)
temp = (temp * private_seed[0] + private_seed[1]) % 0x7FFFFFFF
high = (public_seed[i + 1] + temp) % 0x7FFFFFFF
high = (high * private_seed[2] + private_seed[3]) % 0x7FFFFFFF
low += high + temp
high = struct.unpack('<I', struct.pack('>I', (high + private_seed[1]) % 0x7FFFFFFF))[0]
low = struct.unpack('<I', struct.pack('>I', (low + private_seed[3]) % 0x7FFFFFFF))[0]
key = struct.unpack('<Q', struct.pack('>Q', (high << 32) | (low & 0xFFFFFFFF)))[0]
result_high = struct.unpack('<Q', key_digest[0:8])[0] ^ key
result_low = struct.unpack('<Q', key_digest[8:16])[0] ^ key
return (
struct.pack('<Q', result_high).hex() +
struct.pack('<Q', result_low).hex()
)
def generate_rps_key(key: bytes, msg: bytes) -> 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 = ('<SU> </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 = '<MD>{e}{q}{m}</MD>'
E_MAIL_DATA_PAYLOAD = '<E><I>{inbox}</I><IU>{inbox_unread}</IU><O>0</O><OU>0</OU></E>'
Q_MAIL_DATA_PAYLOAD = '<Q><QTM>409600</QTM><QNM>204800</QNM></Q>'
M_MAIL_DATA_PAYLOAD = '<M><T>11</T><S>6</S>{rt}<RS>0</RS><SZ>{oimsz}</SZ><E>{frommember}</E>\
<I>{guid}</I><F>{fid}</F><N>{fromfriendly}</N></M>{su}'
RT_M_MAIL_DATA_PAYLOAD = '<RT>{senttime}</RT>'
EPDATA_PAYLOAD = '<EndpointData id="{mguid}"><Capabilities>{capabilities}</Capabilities></EndpointData>'
PRIVATEEPDATA_PAYLOAD = '<PrivateEndpointData id="{mguid}">{ped_data}</PrivateEndpointData>'
PRIVATEEPDATA_EPNAME_PAYLOAD = '<EpName>{epname}</EpName>'
PRIVATEEPDATA_IDLE_PAYLOAD = '<Idle>{idle}</Idle>'
PRIVATEEPDATA_CLIENTTYPE_PAYLOAD = '<ClientType>{ct}</ClientType>'
PRIVATEEPDATA_STATE_PAYLOAD = '<State>{state}</State>'
SIGNEDTICKET = '''<?xml version="1.0" encoding="utf-16"?><SignedTicket xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ver="1" keyVer="1"><Data>{}</Data><Sig>{}</Sig></SignedTicket>'''
CIRCLETICKET = '''<?xml version="1.0" encoding="utf-16"?>
<Ticket xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">{circles}
<TS>0000-01-01T00:00:00</TS>
<CID>{cid}</CID>
</Ticket>'''
CIRCLETICKET_CIRCLE = '''
<Circle Id="00000000-0000-0000-0009-{}" HostedDomain="live.com" />'''
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: <OIM-SSI02zDv60gxapz00061a8b@OIM-SSI02.phx.gbl>
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 = '<user><s n="IM"><Status>{substatus}</Status>{cm}</s>{rst}</user>'
NFY_PUT_PRESENCE_USER_S_CM = '<CurrentMedia>{cm}</CurrentMedia>'
NFY_PUT_PRESENCE_USER_S_PE = '<s n="PE"><UserTileLocation>{msnobj}</UserTileLocation><FriendlyName>{name}</FriendlyName>\
<PSM>{message}</PSM><DDP>{ddp}</DDP><ColorScheme>{colorscheme}</ColorScheme>\
<Scene>{scene}</Scene><SignatureSound>{sigsound}</SignatureSound></s>'
NFY_PUT_PRESENCE_USER_SEP_IM = '<sep n="IM"{epid_attrib}><Capabilities>{capabilities}</Capabilities></sep>'
NFY_PUT_PRESENCE_USER_SEP_PE = '<sep n="PE"{epid_attrib}>{pe_data}</sep>'
NFY_PUT_PRESENCE_USER_SEP_PE_VER = '<VER>{ver}</VER>'
NFY_PUT_PRESENCE_USER_SEP_PE_TYP = '<TYP>{typ}</TYP>'
NFY_PUT_PRESENCE_USER_SEP_PE_CAP = '<Capabilities>{pe_capabilities}</Capabilities>'
NFY_PUT_PRESENCE_USER_SEP_PD = '<sep n="PD" epid="{mguid}">{ped_data}</sep>'
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