from typing import Optional, Dict, Any, Tuple import datetime, asyncio, time, settings, base64, util, secrets, json from urllib.parse import parse_qs from disposable_email_domains import blocklist as disposable_emails from util.misc import Logger, gen_uuid, MultiDict, arbitrary_decode, arbitrary_encode from core import event, error from core.backend import Backend, BackendSession, Chat, ChatSession from core.models import ( Substatus, ContactList, User, Contact, Circle, CircleRole, TextWithData, MessageData, MessageType, UserStatus, LoginOption, OIM, ) from core.client import Client from core.auth import GenTokenStr from .ymsg_ctrl import YMSGCtrlBase from .misc import YMSGService, YMSGStatus, split_to_chunks from . import misc, Y64 # "Pre" because it's needed before BackendSession is created. # Comment out session ID stuff as that'll have to be reworked with account usernames being a thing #PRE_SESSION_ID: Dict[str, int] = {} class YMSGCtrlPager(YMSGCtrlBase): __slots__ = ('backend', 'dialect', 'sess_id', 'challenge', 't_cookie_token', 'bs', 'client') backend: Backend dialect: int sess_id: int challenge: Optional[bytes] t_cookie_token: Optional[str] bs: Optional[BackendSession] client: Client def __init__(self, logger: Logger, via: str, backend: Backend) -> None: super().__init__(logger) self.backend = backend self.dialect = 0 self.sess_id = 0 self.challenge = None self.t_cookie_token = None self.bs = None self.client = Client('ymsg', '?', via) def _on_close(self, remove_sess_id: bool = True) -> None: if self.bs: self.bs.close() # State = Auth def _y_004c(self, *args: Any) -> None: # SERVICE_HANDSHAKE (0x4c); acknowledgement of the server self.client = Client('ymsg', 'YMSG{}'.format(str(args[0])), self.client.via) self.dialect = int(args[0]) self.send_reply(YMSGService.Handshake, YMSGStatus.BRB, 0, None) def _y_0057(self, *args: Any) -> None: # SERVICE_AUTH (0x57); send a challenge string for the client to craft two response strings with backend = self.backend # Yahoo for Vista & 9+ doesn't send SERVICE_HANDSHAKE apparently, so I'm adding this here as well self.client = Client('ymsg', 'YMSG{}'.format(str(args[0])), self.client.via) self.dialect = int(args[0]) print(f'Shit: {self.dialect}') yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = yahoo_data.get(b'1') self.sess_id = secrets.randbelow(4294967294) + 1 auth_dict = MultiDict([ (b'1', yahoo_id), ]) # type: MultiDict[bytes, bytes] if settings.STRESS_TEST_ACTIVE: send_system_message(self, "We're currently doing a stress test of the CrossTalk service. Put load on the server, and tell your friends!") if settings.FORCE_HOSTS_UPDATE: send_system_message(self, "You are required to update your patches before continuing. Go here to learn more: http://diet.crosstalk.im/moving-hosts") authresp_error = int(YMSGStatus.Bad) self.close() return if self.dialect <= 12: self.challenge = generate_challenge_v1() else: auth_dict.add(b'13', b'2') self.challenge = base64.b64encode(util.misc.generate_random_string(chars=48)) auth_dict.add(b'94', self.challenge) self.send_reply(YMSGService.Auth, YMSGStatus.BRB, self.sess_id, auth_dict) def _y_0001(self, *args: Any) -> None: backend = self.backend y = None t = None authresp_error = None status = args[2] if status is YMSGStatus.WebLogin: status = YMSGStatus.Available yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = yahoo_data.get(b'0') or b'' active_yid = yahoo_data.get(b'1') or b'' resp_6 = yahoo_data.get(b'6') resp_96 = yahoo_data.get(b'96') resp_307 = yahoo_data.get(b'307') if b'' in (active_yid,yahoo_id): authresp_error = int(YMSGStatus.NotAtHome) yahoo_id_str = arbitrary_decode(yahoo_id) uuid = backend.util_get_uuid_from_username(yahoo_id_str) if uuid is None: authresp_error = int(YMSGStatus.NotAtHome) version = arbitrary_decode(yahoo_data.get(b'135') or b'') or 'unknown' self.client = Client('ymsg', version, self.client.via) assert self.challenge is not None if authresp_error is None: if self.dialect >= 13: if b'277' in yahoo_data and b'278' in yahoo_data: y = arbitrary_decode(yahoo_data.get(b'277')) t = arbitrary_decode(yahoo_data.get(b'278')) self.logger.debug(f'Y Cookie sent by client: {y}') self.logger.debug(f'T Cookie sent by client: {t}') token = None if t: qs = parse_qs(t, keep_blank_values=True, strict_parsing=False) if 'z' in qs and qs['z']: token = qs['z'][0] elif 'sk' in qs and qs['sk']: token = qs['sk'][0] elif 'd' in qs and qs['d']: token = qs['d'][0] if not token: self.logger.error('Could not extract tokenfrom T cookie') authresp_error = int(YMSGStatus.Bad) else: tpl = self.backend.login_auth_service.get_token('ymsg/cookie', token) if tpl is not None: bs = self.backend.login(uuid, self.client, BackendEventHandler(self.backend.loop, self), option=LoginOption.BootOthers) if bs is None: authresp_error = int(YMSGStatus.Bad) else: self.bs = bs user = bs.user self._util_authresp_final(status, cached_y=y, cached_t=t) else: authresp_error = int(YMSGStatus.Bad) else: is_resp_correct = self._verify_challenge_v1(arbitrary_decode(yahoo_id), resp_6, resp_96) if is_resp_correct: assert uuid is not None bs = self.backend.login(uuid, self.client, BackendEventHandler(self.backend.loop, self), option = LoginOption.BootOthers) if bs is None: authresp_error = int(YMSGStatus.Bad) else: self.bs = bs user = bs.user self._util_authresp_final(status, cached_y = y, cached_t = t) else: authresp_error = int(YMSGStatus.Bad) if authresp_error is not None: self.send_reply(YMSGService.AuthResp, YMSGStatus.LoginError, self.sess_id, MultiDict([ (b'1', active_yid), (b'66', str(authresp_error).encode('utf-8')), (b'0', yahoo_id), ])) self.close() def _y_0054(self, *args: Any) -> None: backend = self.backend y = None t = None authresp_error = None status = args[2] if status is YMSGStatus.WebLogin: status = YMSGStatus.Available yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = yahoo_data.get(b'0') or b'' active_yid = yahoo_data.get(b'1') or b'' resp_6 = yahoo_data.get(b'6') resp_96 = yahoo_data.get(b'96') resp_307 = yahoo_data.get(b'307') if b'' in (active_yid,yahoo_id): authresp_error = int(YMSGStatus.NotAtHome) yahoo_id_str = arbitrary_decode(yahoo_id) uuid = backend.util_get_uuid_from_username(yahoo_id_str) if uuid is None: authresp_error = int(YMSGStatus.NotAtHome) version = arbitrary_decode(yahoo_data.get(b'135') or b'') or 'unknown' self.client = Client('ymsg', version, self.client.via) assert self.challenge is not None if authresp_error is None: if self.dialect >= 13: if b'277' in yahoo_data and b'278' in yahoo_data: y = arbitrary_decode(yahoo_data.get(b'277')) t = arbitrary_decode(yahoo_data.get(b'278')) self.logger.debug(f'Y Cookie sent by client: {y}') self.logger.debug(f'T Cookie sent by client: {t}') token = None if t: qs = parse_qs(t, keep_blank_values=True, strict_parsing=False) if 'z' in qs and qs['z']: token = qs['z'][0] elif 'sk' in qs and qs['sk']: token = qs['sk'][0] elif 'd' in qs and qs['d']: token = qs['d'][0] if not token: self.logger.info('Could not extract token from T cookie') authresp_error = int(YMSGStatus.Bad) else: tpl = self.backend.login_auth_service.get_token('ymsg/cookie', token) if tpl is not None: bs = self.backend.login(uuid, self.client, BackendEventHandler(self.backend.loop, self), option=LoginOption.BootOthers) if bs is None: authresp_error = int(YMSGStatus.Bad) else: self.bs = bs user = bs.user self._util_authresp_final(status, cached_y=y, cached_t=t) else: authresp_error = int(YMSGStatus.Bad) else: is_resp_correct = self._verify_challenge_v1(arbitrary_decode(yahoo_id), resp_6, resp_96) if is_resp_correct: assert uuid is not None bs = self.backend.login(uuid, self.client, BackendEventHandler(self.backend.loop, self), option = LoginOption.BootOthers) if bs is None: authresp_error = int(YMSGStatus.Bad) else: self.bs = bs user = bs.user self._util_authresp_final(status, cached_y = y, cached_t = t) else: authresp_error = int(YMSGStatus.Bad) if authresp_error is not None: self.send_reply(YMSGService.AuthResp, YMSGStatus.LoginError, self.sess_id, MultiDict([ (b'1', active_yid), (b'66', str(authresp_error).encode('utf-8')), (b'0', yahoo_id), ])) self.close() def _util_authresp_final(self, status: YMSGStatus, *, cached_y: Optional[str] = None, cached_t: Optional[str] = None) -> None: bs = self.bs assert bs is not None user = bs.user email = user.email self.t_cookie_token = (cached_t[4:24] if cached_y and cached_t else GenTokenStr()) bs.front_data['ymsg'] = True bs.front_data['ymsg_private_chats'] = {} bs.front_data['ymsg_chat_sessions'] = {} if user.suspended: send_system_message(self, "Your CrossTalk account has been suspended. You may not connect to the service.") authresp_error = int(YMSGStatus.Bad) self.close() return #if not user.verified_to_login: # send_system_message(self, "You need to verify the e-mail address attached to this account belongs to you before you can log in. Log in to the CrossTalk site and do so in your Account Settings.") # # authresp_error = int(YMSGStatus.Bad) # self.close() # return with open('config/restricted-emails.json', 'r') as file: restricted_emails = json.load(file) email_domain = email.lower().split('@')[-1] if email_domain in restricted_emails or email_domain in disposable_emails: send_system_message(self, "Your e-mail address contains a disallowed domaiin and needs to be changed before you can log in. Go to https://crosstalk.im/alias-prep to learn more. Change your e-mail in your CrossTalk account settings.") authresp_error = int(YMSGStatus.Bad) self.close() return self._update_buddy_list(cached_y = cached_y, cached_t = cached_t, on_login = True) if self.dialect >= 10: kvs = MultiDict([ (b'143', b'60'), (b'144', b'13') ]) # type: MultiDict[bytes, bytes] self.send_reply(YMSGService.PingConfiguration, YMSGStatus.Available, self.sess_id, kvs) self._get_oims(user) if self.backend.notify_maintenance: bs.evt.on_maintenance_message(None, self.backend.maintenance_mins) # State = Live def _y_0004(self, *args: Any) -> None: # SERVICE_ISBACK (0x04); notify contacts of online presence bs = self.bs assert bs is not None new_status = YMSGStatus(int(args[2])) me_status_update(bs, new_status) def _y_0003(self, *args: Any) -> None: # SERVICE_ISAWAY (0x03); notify contacts of FYI idle presence bs = self.bs assert bs is not None yahoo_data = args[4] # type: MultiDict[bytes, bytes] new_status = YMSGStatus(int(yahoo_data.get(b'10') or b'')) message = arbitrary_decode(yahoo_data.get(b'19') or b'') is_away_message = (yahoo_data.get(b'47') == b'1') me_status_update(bs, new_status, message = message, is_away_message = is_away_message) def _y_0012(self, *args: Any) -> None: # SERVICE_PINGCONFIGURATION (0x12); set the "ticks" and "tocks" of a ping sent kvs = MultiDict([ (b'143', b'60'), (b'144', b'13') ]) # type: MultiDict[bytes, bytes] self.send_reply(YMSGService.PingConfiguration, YMSGStatus.Available, self.sess_id, kvs) def _y_0016(self, *args: Any) -> None: # SERVICE_CLIENTHOSTSTATS (0x16); collects OS version, processor, and time zone # # 1: YahooId # 25: unknown ('C=0[0x01]F=1,P=0,C=0,H=0,W=0,B=0,O=0,G=0[0x01]M=0,P=0,C=0,S=0,L=3,D=1,N=0,G=0,F=0,T=0') # 146: Base64-encoded string of host OS (e.g.: 'V2luZG93cyAyMDAwLCBTZXJ2aWNlIFBhY2sgNA==' = 'Windows 2000, Service Pack 4') # 145: Base64-encoded string of processor type (e.g.: 'SW50ZWwgUGVudGl1bSBQcm8gb3IgUGVudGl1bQ==' = 'Intel Pentium Pro or Pentium') # 147: Base64-encoded string of time zone (e.g.: 'RWFzdGVybiBTdGFuZGFyZCBUaW1l' = 'Eastern Standard Time') return def _y_0015(self, *args: Any) -> None: # SERVICE_SKINNAME (0x15); used for IMVironments # Also happens when enabling/disabling Yahoo Helper. return def _y_0083(self, *args: Any) -> None: # SERVICE_FRIENDADD (0x83); add a friend to your contact list backend = self.backend bs = self.bs assert bs is not None user = bs.user yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') contact_yahoo_id = arbitrary_decode(yahoo_data.get(b'7') or b'') message = arbitrary_decode(yahoo_data.get(b'14') or b'') buddy_group = arbitrary_decode(yahoo_data.get(b'65') or b'') utf8 = arbitrary_decode(yahoo_data.get(b'97') or b'') group = None old_lists = None action_group_refresh = False add_request_response = MultiDict([ (b'1', arbitrary_encode(yahoo_id)), (b'7', arbitrary_encode(contact_yahoo_id)), (b'65', arbitrary_encode(buddy_group)) ]) # type: MultiDict[bytes, bytes] # Yahoo! Messenger has a function that lets you add people by email address (a.k.a. stripping the "@domain.tld" part of the address and # filling that out in the "Yahoo! ID" section of the contact add dialog). Treat as is. contact_uuid = backend.util_get_uuid_from_username(contact_yahoo_id) if contact_uuid is None: add_request_response.add(b'66', b'3') # User doesn't exist in database self.send_reply(YMSGService.FriendAdd, YMSGStatus.BRB, self.sess_id, add_request_response) return bs = self.bs assert bs is not None user = bs.user detail = user.detail assert detail is not None contacts = detail.contacts contact = contacts.get(contact_uuid) if contact is not None: old_lists = contact.lists if contact.lists & ContactList.FL: if contact._groups: for group_other in contact._groups.copy(): if detail._groups_by_id[group_other.id].name == buddy_group: add_request_response.add(b'66', b'2') # Contact already in group self.send_reply(YMSGService.FriendAdd, YMSGStatus.BRB, self.sess_id, add_request_response) return else: if buddy_group == '(No Group)': add_request_response.add(b'66', b'2') self.send_reply(YMSGService.FriendAdd, YMSGStatus.BRB, self.sess_id, add_request_response) return if buddy_group != '(No Group)': for grp in detail._groups_by_id.values(): if grp.name == buddy_group: group = grp break if group is None: group = bs.me_group_add(buddy_group) ctc_head = self.backend._load_user_record(contact_uuid) assert ctc_head is not None try: contact_new = bs.me_contact_add( ctc_head.uuid, ContactList.FL | ContactList.AL, message = (TextWithData(message, utf8) if message is not None else None), adder_id = yahoo_id, needs_notify = True, )[0] except error.ContactListIsFull: add_request_response.add(b'66', b'6') # Contact list full self.send_reply(YMSGService.FriendAdd, YMSGStatus.BRB, self.sess_id, add_request_response) return except: add_request_response.add(b'66', b'1') # General failure self.send_reply(YMSGService.FriendAdd, YMSGStatus.BRB, self.sess_id, add_request_response) return if (contact_new is not None and contact is None) or (old_lists is not None and (not old_lists & ContactList.FL and not old_lists & ContactList.BL)): contact_struct = MultiDict([ (b'8', (b'1' if not contact_new.status.is_offlineish() else b'0')), ]) # type: MultiDict[bytes, bytes] add_contact_status_to_data(contact_struct, ctc_head.status, ctc_head) self.send_reply(YMSGService.ContactNew, YMSGStatus.BRB, self.sess_id, contact_struct) add_request_response.add(b'66', b'0') # Success self.send_reply(YMSGService.FriendAdd, YMSGStatus.BRB, self.sess_id, add_request_response) try: # TODO: Moving/copying contacts to groups if len(contact_new._groups) >= 1 or (contact_new._groups and buddy_group == '(No Group)'): action_group_refresh = True if buddy_group == '(No Group)': for group_other in contact_new._groups.copy(): group_full = False bs.me_group_contact_remove(group_other.id, contact_new.head.uuid) for ctc_other in detail.contacts.values(): if ctc_other is contact_new: continue for group_ctc in ctc_other._groups.copy(): if group_ctc.id is group_other.id: group_full = True break if group_full: break if group is not None: bs.me_group_contact_add(group.id, contact_new.head.uuid) if action_group_refresh: self._update_buddy_list() except error.ContactAlreadyOnContactList: # Ignore, because this condition was checked earlier, so the only way this # can happen is if the the contact list gets in an inconsistent state. # (I.e. contact is not on FL, but still part of groups.) pass def _y_0086(self, *args: Any) -> None: # SERVICE_CONTACTDENY (0x86); deny a contact request backend = self.backend yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') adder_to_deny = arbitrary_decode(yahoo_data.get(b'7') or b'') deny_message = arbitrary_decode(yahoo_data.get(b'14') or b'') adder_uuid = backend.util_get_uuid_from_username(adder_to_deny) assert adder_uuid is not None bs = self.bs assert bs is not None bs.me_contact_deny(adder_uuid, deny_message, addee_id = yahoo_id) def _y_0089(self, *args: Any) -> None: # SERVICE_GROUPRENAME (0x89); rename a contact group yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') group_name = arbitrary_decode(yahoo_data.get(b'65') or b'') new_group_name = arbitrary_decode(yahoo_data.get(b'67') or b'') bs = self.bs assert bs is not None user = bs.user detail = user.detail assert detail is not None group = None # "(No Group)" is used for displaying group-less contacts; ignore any requests to rename the "group" if '(No Group)' not in (group_name,new_group_name): for grp in detail._groups_by_id.values(): if grp.name == group_name: group = grp if group is not None: try: bs.me_group_edit(group.id, new_name = new_group_name) except: pass group_rename_response = MultiDict([ (b'1', arbitrary_encode(yahoo_id or '')), (b'66', b'0'), (b'67', arbitrary_encode(new_group_name or '')), (b'65', arbitrary_encode(group_name or '')), ]) # type: MultiDict[bytes, bytes] self.send_reply(YMSGService.GroupRename, YMSGStatus.BRB, self.sess_id, group_rename_response) self._update_buddy_list() def _y_0084(self, *args: Any) -> None: # SERVICE_FRIENDREMOVE (0x84); remove a buddyfrom your list backend = self.backend yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') contact_id = arbitrary_decode(yahoo_data.get(b'7') or b'') buddy_group = arbitrary_decode(yahoo_data.get(b'65') or b'') bs = self.bs assert bs is not None user = bs.user detail = user.detail assert detail is not None group = None contact_uuid = backend.util_get_uuid_from_username(contact_id) if contact_uuid is None: return contact = detail.contacts.get(contact_uuid) if contact is None: return if contact._groups: for group_other in contact._groups.copy(): if detail._groups_by_id[group_other.id].name == buddy_group: group = detail._groups_by_id[group_other.id] break if group is None and buddy_group != '(No Group)': return if group is not None: bs.me_group_contact_remove(group.id, contact.head.uuid) if not contact._groups: bs.me_contact_remove(contact_uuid, ContactList.FL) remove_buddy_response = MultiDict([ (b'1', arbitrary_encode(yahoo_id)), (b'66', b'0'), (b'7', arbitrary_encode(contact_id)), (b'65', arbitrary_encode(buddy_group)), ]) # type: MultiDict[bytes, bytes] self.send_reply(YMSGService.FriendRemove, YMSGStatus.BRB, self.sess_id, remove_buddy_response) self._update_buddy_list() def _y_0085(self, *args: Any) -> None: # SERVICE_IGNORE (0x85); add/remove someonefrom your ignore list yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') ignored_yahoo_id = arbitrary_decode(yahoo_data.get(b'7') or b'') ignore_mode = arbitrary_decode(yahoo_data.get(b'13') or b'') ignore_reply_response = MultiDict([ (b'0', arbitrary_encode(yahoo_id)), (b'7', arbitrary_encode(ignored_yahoo_id)), (b'13', arbitrary_encode(ignore_mode)) ]) backend = self.backend bs = self.bs assert bs is not None user = bs.user detail = user.detail assert detail is not None contacts = detail.contacts ignored_uuid = backend.util_get_uuid_from_username(ignored_yahoo_id) if ignored_uuid is None: ignore_reply_response.add(b'66', b'3') self.send_reply(YMSGService.Ignore, YMSGStatus.BRB, self.sess_id, ignore_reply_response) return if int(ignore_mode) == 1: contact = contacts.get(ignored_uuid) if contact is not None: if contact.lists & ContactList.BL: ignore_reply_response.add(b'66', b'2') self.send_reply(YMSGService.Ignore, YMSGStatus.BRB, self.sess_id, ignore_reply_response) return bs.me_contact_add(ignored_uuid, ContactList.BL, name = ignored_yahoo_id) elif int(ignore_mode) == 2: bs.me_contact_remove(ignored_uuid, ContactList.BL) else: return self.send_reply(YMSGService.AddIgnore, YMSGStatus.BRB, self.sess_id, None) ignore_reply_response.add(b'66', b'0') self._update_buddy_list() self.send_reply(YMSGService.Ignore, YMSGStatus.BRB, self.sess_id, ignore_reply_response) def _y_000a(self, *args: Any) -> None: # SERVICE_USERSTAT (0x0a); synchronize logged on user's status bs = self.bs assert bs is not None user = bs.user self.send_reply( YMSGService.UserStat, bs.front_data.get('ymsg_status') or YMSGStatus.FromSubstatus(user.status.substatus), self.sess_id, self._gen_multi_user_status_dict(user), ) def _y_00d2(self, *args: Any) -> None: # photo sharing invite, stub for now pass def _y_00dc(self, *args: Any) -> None: # P2P file transfers on newer version, stub for now pass def _y_00bd(self, *args: Any) -> None: pass def _y_00c1(self, *args: Any) -> None: pass def _y_00c7(self, *args: Any) -> None: # for changing avatars, ignore for now pass def _y_0055(self, *args: Any) -> None: # SERVICE_LIST (0x55); send a user's buddy list self._update_buddy_list(after_login = True) def _y_00c5(self, *args: any) -> None: # SERVICE_ISINVISIBLE (0xc5); mark as invisible bs = self.bs assert bs is not None status = None yahoo_data = args[4] new_status = YMSGStatus(int(yahoo_data.get(b'13') or b'')) if new_status == int(2): status = YMSGStatus.Invisible elif new_status == int(1): status = YMSGStatus.Available me_status_update(bs, status) def _y_00c6(self, *args: Any) -> None: # SERVICE_STATUSUPDATE (0xc6); essentially the same as 0x03. For whatever reason, it was changed in YMSG12 bs = self.bs assert bs is not None yahoo_data = args[4] new_status = YMSGStatus(int(yahoo_data.get(b'10') or b'')) message = arbitrary_decode(yahoo_data.get(b'19') or b'') is_away_message = (yahoo_data.get(b'47') == b'1') me_status_update(bs, new_status, message = message, is_away_message = is_away_message) def _y_008a(self, *args: Any) -> None: # SERVICE_PING (0x8a); send a response ping after the client pings bs = self.bs assert bs is not None self.send_reply(YMSGService.Ping, YMSGStatus.Available, self.sess_id, MultiDict([ (b'1', arbitrary_encode(bs.user.username)), ])) def _y_0008(self, *args: Any) -> None: # SERVICE_IDDEACTIVATE (0x08); deactivate an alias return def _y_0007(self, *args: Any) -> None: # SERVICE_IDACTIVATE (0x07); activate an alias return def _y_004f(self, *args: Any) -> None: # SERVICE_PEERTOPEER (0x4f); see if P2P messaging is possible. Possibly also used for other P2P actions backend = self.backend # Formality; make sure unregistered sessions aren't able to send these packets bs = self.bs assert bs is not None yahoo_data = args[4] recipient_uuid = backend.util_get_uuid_from_username(arbitrary_decode(yahoo_data.get(b'5'))) if recipient_uuid is None: return recipient_head = backend._load_user_record(recipient_uuid) if recipient_head is None: return for bs_other in backend.util_get_sessions_by_user(recipient_head): bs_other.evt.ymsg_on_p2p_msg_request(self.sess_id, yahoo_data) def _y_004b(self, *args: Any) -> None: # SERVICE_NOTIFY (0x4b); notify a contact of an action (typing, games, etc.) backend = self.backend yahoo_data = args[4] yahoo_id = yahoo_data.get('1') if yahoo_id is not None: yahoo_id = arbitrary_decode(yahoo_id) notify_type = yahoo_data.get(b'49') # typing, games, etc. typing_flag = yahoo_data.get(b'13') if typing_flag is not None: typing_flag = arbitrary_decode(typing_flag) contact_yahoo_id = yahoo_data.get(b'5') if contact_yahoo_id is not None: contact_yahoo_id = arbitrary_decode(contact_yahoo_id) contact_uuid = backend.util_get_uuid_from_username(contact_yahoo_id) if contact_uuid is None: return try: cs, _ = self._get_private_chat_with(contact_uuid) if cs is not None: cs.preferred_name = yahoo_id cs.send_message_to_everyone(messagedata_from_ymsg(cs.user, yahoo_data, notify_type = notify_type, typing_flag = typing_flag)) except error.ContactNotOnline: pass def _y_00d4(self, *args: Any) -> None: # SERVICE_CHATSESSION (0xD4); start a chat session pass def _y_00e7(self, *args: Any) -> None: # gotta figure out what the fuck this one is, but it's used in YMSG14/15 for group management backend = self.backend bs = self.bs assert bs is not None user = bs.user yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') contact_yahoo_id = arbitrary_decode(yahoo_data.get(b'7') or b'') new_buddy_group = arbitrary_decode(yahoo_data.get(b'264') or b'') old_buddy_group = arbitrary_decode(yahoo_data.get(b'224') or b'') group_new = None group_old = None action_group_refresh = False contact_uuid = backend.util_get_uuid_from_username(contact_yahoo_id) if contact_uuid is None: return bs = self.bs assert bs is not None user = bs.user detail = user.detail assert detail is not None contacts = detail.contacts contact = contacts.get(contact_uuid) if contact is None: return if new_buddy_group != '(No Group)': for grp in detail._groups_by_id.values(): if grp.name == new_buddy_group: group_new = grp break if group_new is None: group_new = bs.me_group_add(new_buddy_group) if old_buddy_group and old_buddy_group != '(No Group)': for grp in detail._groups_by_id.values(): if grp.name == old_buddy_group: group_old = grp break try: if len(contact._groups) >= 1 or (contact._groups and new_buddy_group == '(No Group)'): action_group_refresh = True if old_buddy_group == '(No Group)': for group_other in contact._groups.copy(): group_full = False bs.me_group_contact_remove(group_other.id, contact.head.uuid) for ctc_other in detail.contacts.values(): if ctc_other is contact: continue for group_ctc in ctc_other._groups.copy(): if group_ctc.id is group_other.id: group_full = True break if group_full: break else: if group_old is not None: bs.me_group_contact_remove(group_old.id, contact.head.uuid) if group_new is not None: bs.me_group_contact_add(group_new.id, contact.head.uuid) if action_group_refresh: self._update_buddy_list() except error.ContactAlreadyOnContactList: pass def _y_0006(self, *args: Any) -> None: # SERVICE_MESSAGE (0x06); send a message to a user (older YMSG) yahoo_data = args[4] # type: MultiDict[bytes, bytes] #yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') contact_yahoo_id = arbitrary_decode(yahoo_data.get(b'5') or b'') self._message_common(yahoo_data, contact_yahoo_id) def _y_0017(self, *args: Any) -> None: # SERVICE_MASSMESSAGE (0x17); send a message to multiple users yahoo_data = args[4] # type: MultiDict[bytes, bytes] #yahoo_id = yahoo_data.get(b'1') contact_yahoo_ids = [ arbitrary_decode(contact_yahoo_id) for contact_yahoo_id in (yahoo_data.getall(b'5') or []) ] for contact_yahoo_id in contact_yahoo_ids: self._message_common(yahoo_data, contact_yahoo_id) def _y_0050(self, *args: Any) -> None: # SERVICE_VIDEOCHAT (0x50); create a webcam token for authentication bs = self.bs assert bs is not None yahoo_data = args[4] # type: MultiDict[bytes, bytes] if not yahoo_data.get(b'1'): return webcam_token = self.backend.auth_service.create_token('ymsg/webcam', yahoo_data.get(b'1'), lifetime = 86400) self.send_reply(YMSGService.VideoChat, YMSGStatus.BRB, self.sess_id, MultiDict([ (b'1', yahoo_data.get(b'1')), (b'5', yahoo_data.get(b'1')), (b'61', webcam_token), ])) return def _y_004d(self, *args: Any) -> None: # SERVICE_P2PFILEXFER (0x4d); initiate P2P file transfer. Due to this service being present # in 3rd-party libraries; we can implement it here yahoo_data = args[4] # type: MultiDict[bytes, bytes] backend = self.backend bs = self.bs assert bs is not None contact_id = arbitrary_decode(yahoo_data.get(b'5') or b'') contact_uuid = backend.util_get_uuid_from_username(contact_id) if contact_uuid is None: return for bs_other in backend._sc.iter_sessions(): if bs_other.user.uuid == contact_uuid: bs_other.evt.ymsg_on_xfer_init(self.sess_id, yahoo_data) def _y_0018(self, *args: Any) -> None: # SERVICE_CONFINVITE (0x18); send a conference invite to one or more people backend = self.backend yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') conf_roster = [ arbitrary_decode(conf_member) for conf_member in (yahoo_data.getall(b'52') or []) ] # Comma-separated yahoo ids conf_roster_2 = yahoo_data.get(b'51') if conf_roster_2 is not None: conf_roster.extend(arbitrary_decode(conf_roster_2).split(',')) conf_id = arbitrary_decode(yahoo_data.get(b'57') or b'') invite_msg = arbitrary_decode(yahoo_data.get(b'58') or b'') voice_chat = arbitrary_decode(yahoo_data.get(b'13') or b'') chat = self._get_chat_by_id('ymsg/conf', conf_id, create = True) assert chat is not None cs = self._get_chat_session(yahoo_id, chat, create = True) assert cs is not None chat.front_data['ymsg_voice_chat'] = voice_chat for conf_user_yahoo_id in conf_roster: conf_user_uuid = backend.util_get_uuid_from_username(conf_user_yahoo_id) if conf_user_uuid is None: continue conf_user = self.backend._load_user_record(conf_user_uuid) if conf_user is None: continue cs.invite(conf_user, invite_msg = invite_msg) def _y_001c(self, *args: Any) -> None: # SERVICE_CONFADDINVITE (0x1c); send a conference invite to an existing conference to one or more people backend = self.backend yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') conf_new_roster_str = yahoo_data.get(b'51') if conf_new_roster_str is None: return conf_new_roster = arbitrary_decode(conf_new_roster_str).split(',') conf_roster = [ arbitrary_decode(conf_member) for conf_member in (yahoo_data.getall(b'52') or yahoo_data.getall(b'53') or []) ] conf_id = arbitrary_decode(yahoo_data.get(b'57') or b'') if not conf_id: return invite_msg = arbitrary_decode(yahoo_data.get(b'58') or b'') voice_chat = arbitrary_decode(yahoo_data.get(b'13') or b'') chat = self._get_chat_by_id('ymsg/conf', conf_id) assert chat is not None cs = self._get_chat_session(yahoo_id, chat) assert cs is not None chat.front_data['ymsg_voice_chat'] = voice_chat for conf_user_yahoo_id in conf_new_roster: conf_user_uuid = backend.util_get_uuid_from_username(conf_user_yahoo_id) if conf_user_uuid is None: continue conf_user = self.backend._load_user_record(conf_user_uuid) if conf_user is None: continue cs.invite(conf_user, invite_msg = invite_msg) def _y_0019(self, *args: Any) -> None: # SERVICE_CONFLOGON (0x19); request for me to join a conference yahoo_data = args[4] # type: MultiDict[bytes, bytes] inviter_ids = yahoo_data.getall(b'3') if inviter_ids is None: return yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') conf_id = arbitrary_decode(yahoo_data.get(b'57') or b'') if not conf_id: return chat = self._get_chat_by_id('ymsg/conf', conf_id) assert chat is not None cs = self._get_chat_session(yahoo_id, chat, create = True) assert cs is not None def _y_001a(self, *args: Any) -> None: # SERVICE_CONFDECLINE (0x1a); decline a request to join a conference yahoo_data = args[4] # type: MultiDict[bytes, bytes] yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') bs = self.bs assert bs is not None inviter_ids = [ arbitrary_decode(inviter_id).lower() for inviter_id in (yahoo_data.getall(b'3') or []) ] if not inviter_ids: return conf_id = arbitrary_decode(yahoo_data.get(b'57') or b'') deny_msg = arbitrary_decode(yahoo_data.get(b'14') or b'') chat = self._get_chat_by_id('ymsg/conf', conf_id) if chat is None: return for cs in chat.get_roster(): if cs.user.username.lower() not in inviter_ids: continue cs.evt.on_chat_invite_declined(chat, bs.user, invitee_id = yahoo_id, message = deny_msg) def _y_001d(self, *args: Any) -> None: # SERVICE_CONFMSG (0x1d); send a message in a conference yahoo_data = args[4] # type: MultiDict[bytes, bytes] conf_user_ids = yahoo_data.getall(b'53') if conf_user_ids is None: return yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') conf_id = arbitrary_decode(yahoo_data.get(b'57') or b'') chat = self._get_chat_by_id('ymsg/conf', conf_id) assert chat is not None cs = self._get_chat_session(yahoo_id, chat) assert cs is not None cs.preferred_name = yahoo_id cs.send_message_to_everyone(messagedata_from_ymsg(cs.user, yahoo_data)) def _y_001b(self, *args: Any) -> None: # SERVICE_CONFLOGOFF (0x1b); leave a conference yahoo_data = args[4] # type: MultiDict[bytes, bytes] conf_roster = yahoo_data.getall(b'3') if conf_roster is None: return yahoo_id = arbitrary_decode(yahoo_data.get(b'1') or b'') conf_id = arbitrary_decode(yahoo_data.get(b'57') or b'') chat = self._get_chat_by_id('ymsg/conf', conf_id) if chat is None: return cs = self._get_chat_session(yahoo_id, chat) if cs is not None: cs.close() # Other functions def _message_common(self, yahoo_data: MultiDict[bytes, bytes], contact_yahoo_id: Optional[str]) -> None: backend = self.backend bs = self.bs assert bs is not None user = bs.user if contact_yahoo_id is not None and contact_yahoo_id.lower() == 'yahoohelper': yhlper_msg_dict = MultiDict([ (b'5', yahoo_data.get(b'1') or b''), (b'4', b'YahooHelper'), (b'14', YAHOO_HELPER_MSG.encode('utf-8')), ]) # type: MultiDict[bytes, bytes] if yahoo_data.get(b'63') is not None: yhlper_msg_dict.add(b'63', yahoo_data.get(b'63') or b'') if yahoo_data.get(b'64') is not None: yhlper_msg_dict.add(b'64', yahoo_data.get(b'64') or b'') yhlper_msg_dict.add(b'97', b'1') if yahoo_data.get(b'206') is not None: yhlper_msg_dict.add(b'206', yahoo_data.get(b'206') or b'') self.send_reply(YMSGService.Message, YMSGStatus.BRB, self.sess_id, yhlper_msg_dict) return contact_uuid = backend.util_get_uuid_from_username(contact_yahoo_id or '') if contact_uuid is None: return try: cs, evt = self._get_private_chat_with(contact_uuid) if None not in (cs, evt): evt._send_when_user_joins(contact_uuid, messagedata_from_ymsg(cs.user, yahoo_data)) except error.ContactNotOnline: contact_user = self.backend._load_user_record(contact_uuid) if contact_user is None: return contact_detail = self.backend._load_detail(contact_user) if contact_detail is None: return ctc_self = contact_detail.contacts.get(user.uuid) if ctc_self is not None: if ctc_self.lists & ContactList.BL: return elif ctc_self is None and contact_user.settings.get('BLP', 'AL') == 'BL': return md = messagedata_from_ymsg(contact_user, yahoo_data) if md.type is MessageType.Chat: (ip, _) = self.peername from_user_id = None key1_val = md.front_cache['ymsg'].get(b'1') if key1_val is not None: from_user_id = arbitrary_decode(key1_val) self.backend.user_service.save_oim( bs, contact_uuid, gen_uuid(), ip, md.text or '', False if md.front_cache['ymsg'].get(b'97') == b'0' else True, from_user_id =from_user_id, ) except error.ContactNotOnContactList: pass def _get_private_chat_with(self, other_user_uuid: str) -> Tuple[ChatSession, 'ChatEventHandler']: bs = self.bs assert bs is not None user = bs.user detail = user.detail assert detail is not None other_user = self.backend._load_user_record(other_user_uuid) if other_user is None: raise error.ContactNotOnContactList() other_user_ctc = detail.contacts.get(other_user.uuid) if other_user_ctc is not None and other_user_ctc.lists & ContactList.BL: raise error.ContactNotOnContactList() if other_user_uuid not in bs.front_data['ymsg_private_chats'] and not other_user.status.is_offlineish(): other_user_detail = self.backend._load_detail(other_user) if other_user_detail is None: raise error.ContactNotOnline() ctc_self = other_user_detail.contacts.get(user.uuid) if ctc_self is not None: if ctc_self.lists & ContactList.BL: raise error.ContactNotOnline() chat = self.backend.chat_create() chat.front_data['ymsg_twoway_only'] = True # `user` joins evt = ChatEventHandler(self.backend.loop, self, bs) cs = chat.join('yahoo', bs, evt) bs.front_data['ymsg_private_chats'][other_user_uuid] = (cs, evt) cs.invite(other_user) self.backend.loop.create_task(self._check_private_chat(chat, other_user, bs)) elif other_user.status.is_offlineish(): raise error.ContactNotOnline() return bs.front_data['ymsg_private_chats'].get(other_user_uuid) def _get_chat_by_id(self, scope: str, id: str, *, create: bool = False) -> Optional[Chat]: chat = self.backend.chat_get(scope, id) if chat is None and create: chat = self.backend.chat_create() chat.add_id(scope, id) return chat def _get_chat_session(self, yahoo_id: Optional[str], chat: Chat, *, create: bool = False) -> Optional[ChatSession]: bs = self.bs assert bs is not None cs = bs.front_data['ymsg_chat_sessions'].get(chat) if cs is None and create: evt = ChatEventHandler(self.backend.loop, self, bs) cs = chat.join('yahoo', bs, evt, preferred_name = yahoo_id) bs.front_data['ymsg_chat_sessions'][chat] = cs chat.send_participant_joined(cs) return cs def _update_buddy_list(self, cached_y: Optional[str] = None, cached_t: Optional[str] = None, on_login: bool = False, after_login: bool = False) -> None: backend = self.backend bs = self.bs assert bs is not None user = bs.user detail = user.detail assert detail is not None contacts = detail.contacts cs = list(contacts.values()) cs_fl = [c for c in cs if c.lists & ContactList.FL and not c.lists & ContactList.BL] ignore_list = [c for c in cs if c.lists & ContactList.BL] if self.dialect >= 16: self.generate_buddy_list_assnew(user, detail, cs_fl, ignore_list) else: self.generate_buddy_list_assold(backend, user, detail, cs_fl, ignore_list, cached_y, cached_t) if on_login: me_status_update(bs, YMSGStatus.Available) if not after_login: self.send_reply(YMSGService.LogOn, YMSGStatus.Available, self.sess_id, self._gen_multi_user_status_dict(user)) def generate_buddy_list_assnew(self, user: User, detail: Any, cs_fl: list, ignore_list: list) -> None: kvs = MultiDict() # type: MultiDict[bytes, bytes] kvs.add(b'1', arbitrary_encode(user.username)) kvs.add(b'4', arbitrary_encode(user.username)) kvs.add(b'302', b'318') for grp in detail._groups_by_id.values(): buddies_in_group = [] for c in cs_fl: for group in c._groups.copy(): if group.id == grp.id: buddies_in_group.append(c) kvs.add(b'300', b'318') kvs.add(b'65', arbitrary_encode(grp.name)) kvs.add(b'302', b'319') for c in buddies_in_group: kvs.add(b'300', b'319') kvs.add(b'241', b'0') kvs.add(b'7', arbitrary_encode(c.head.username)) kvs.add(b'317', b'1') kvs.add(b'301', b'319') kvs.add(b'303', b'319') kvs.add(b'301', b'318') no_group_buddies = [c for c in cs_fl if not c._groups] if no_group_buddies: kvs.add(b'300', b'318') kvs.add(b'65', b'(No Group)') kvs.add(b'302', b'319') for c in no_group_buddies: kvs.add(b'300', b'319') kvs.add(b'241', b'0') kvs.add(b'7', arbitrary_encode(c.head.username)) kvs.add(b'317', b'1') kvs.add(b'301', b'319') kvs.add(b'303', b'319') kvs.add(b'301', b'318') kvs.add(b'303', b'318') kvs.add(b'302', b'320') for c in ignore_list: kvs.add(b'300', b'320') kvs.add(b'7', arbitrary_encode(c.head.username)) kvs.add(b'301', b'320') kvs.add(b'303', b'320') # Service 0xF1 (241) is the YMSG16 structured buddy list command. self.send_reply(YMSGService(0xF1), YMSGStatus.Available, self.sess_id, kvs) def generate_buddy_list_assold( self, backend: Backend, user: User, detail: Any, cs_fl: list, ignore_list: list, cached_y: Optional[str], cached_t: Optional[str], ) -> None: # YMSG <= 15: flat text encoding on keys 87 (contacts) and 88 (ignored). contact_group_list = [] for grp in detail._groups_by_id.values(): contact_list = [] for c in cs_fl: for group in c._groups.copy(): if group.id == grp.id: contact_list.append(c.head.username) if contact_list: contact_group_list.append(grp.name + ':' + ','.join(contact_list) + '\n') # Handle contacts that aren't part of any groups no_group = [c.head.username for c in cs_fl if not c._groups] if no_group: contact_group_list.append('(No Group):' + ','.join(no_group) + '\n') contact_list_format = ''.join(contact_group_list) ignore_list_format = ','.join(c.head.username for c in ignore_list) list_reply_kvs = MultiDict() # type: MultiDict[bytes, bytes] if len(contact_list_format) > 815: for contact_chunk in split_to_chunks(contact_list_format, 815): self.send_reply(YMSGService.List, YMSGStatus.NotInOffice, self.sess_id, MultiDict([ (b'87', arbitrary_encode(contact_chunk)), ])) else: list_reply_kvs.add(b'87', arbitrary_encode(contact_list_format)) if len(ignore_list_format) > 815: for ignore_chunk in split_to_chunks(ignore_list_format, 815): self.send_reply(YMSGService.List, YMSGStatus.NotInOffice, self.sess_id, MultiDict([ (b'88', ignore_chunk.encode('utf-8')), ])) else: list_reply_kvs.add(b'88', ignore_list_format.encode('utf-8')) list_reply_kvs.add(b'89', arbitrary_encode(user.username)) if ( cached_y is not None and cached_t is not None and backend.auth_service.get_token('ymsg/cookie', cached_y) and backend.auth_service.get_token('ymsg/cookie', cached_t) ): list_reply_kvs.add(b'59', arbitrary_encode(cached_y)) list_reply_kvs.add(b'59', arbitrary_encode(cached_t)) else: tpl = self._refresh_cookies() if tpl is None: list_reply_kvs.add(b'59', b'Y\t') list_reply_kvs.add(b'59', b'T\t') else: (y_cookie, t_cookie, y_expiry, t_expiry) = tpl domain = settings.TARGET_HOST list_reply_kvs.add(b'59', arbitrary_encode('Y\t{}; expires={}; path=/; domain={}'.format(y_cookie, y_expiry, domain))) list_reply_kvs.add(b'59', arbitrary_encode('T\t{}; expires={}; path=/; domain={}'.format(t_cookie, t_expiry, domain))) list_reply_kvs.add(b'59', b'C\tmg=1') list_reply_kvs.add(b'3', arbitrary_encode(user.username)) list_reply_kvs.add(b'90', b'1') list_reply_kvs.add(b'100', b'0') list_reply_kvs.add(b'101', b'') list_reply_kvs.add(b'102', b'') list_reply_kvs.add(b'93', b'86400') self.send_reply(YMSGService.List, YMSGStatus.Available, self.sess_id, list_reply_kvs) def _gen_multi_user_status_dict(self, user: User) -> MultiDict[bytes, bytes]: detail = user.detail assert detail is not None cs = list(detail.contacts.values()) cs_fl_online = [c for c in cs if c.lists & ContactList.FL and not c.lists & ContactList.BL and not c.status.is_offlineish()] logon_payload = MultiDict([ (b'0', arbitrary_encode(user.username)), (b'1', arbitrary_encode(user.username)), (b'8', str(len(cs_fl_online)).encode('utf-8')), ]) # type: MultiDict[bytes, bytes] for c in cs_fl_online: add_contact_status_to_data(logon_payload, c.status, c.head) return logon_payload async def _check_private_chat(self, chat: Chat, other_user: User, bs: BackendSession) -> None: two_users = False while True: await asyncio.sleep(0.04) if 'ymsg_twoway_only' in chat.front_data: if len(list(chat.get_roster_single())) == 2: two_users = True if len(list(chat.get_roster_single())) > 2 and 'ymsg_twoway_only' in chat.front_data: del chat.front_data['ymsg_twoway_only'] if 'ymsg/conf' not in chat.ids: chat.add_id('ymsg/conf', chat.ids['main']) for cs_other in chat.get_roster(): if cs_other.user in (bs.user, other_user) and cs_other.bs.front_data.get('ymsg'): target_user = None if cs_other.user is bs.user: target_user = other_user elif cs_other.user is other_user: target_user = bs.user if 'ymsg_private_chats' in cs_other.bs.front_data: if target_user: del cs_other.bs.front_data['ymsg_private_chats'][target_user.uuid] bs.front_data['ymsg_chat_sessions'][chat] = cs_other if target_user: cs_other.bs.evt.on_chat_invite(chat, target_user, invite_msg='Two-way chat automatically converted to conference.') break elif len(list(chat.get_roster_single())) == 1 and two_users: for cs_other in chat.get_roster(): if cs_other.user in (bs.user, other_user) and cs_other.bs.front_data.get('ymsg'): if cs_other.user is bs.user: target_user = other_user elif cs_other.user is other_user: target_user = bs.user if target_user: del cs_other.bs.front_data['ymsg_private_chats'][target_user.uuid] cs_other.close() break def _get_oims(self, user: User) -> None: oims = self.backend.user_service.get_oim_batch(user) oim_msg_dict = MultiDict() # type: MultiDict[bytes, bytes] for oim in oims: oim_msg_dict.add(b'31', b'6') oim_msg_dict.add(b'32', b'6') oim_msg_dict.add(b'4', arbitrary_encode(oim.from_username or oim.from_user_id)) oim_msg_dict.add(b'5', arbitrary_encode(user.username)) oim_msg_dict.add(b'14', arbitrary_encode(oim.message)) oim_msg_dict.add(b'15', str(int(oim.sent.timestamp())).encode('utf-8')) oim_msg_dict.add(b'97', b'1' if oim.utf8 else b'0') self.backend.user_service.delete_oim(user.uuid, oim.uuid) if len(list(oim_msg_dict.items())) > 0: self.send_reply(YMSGService.Message, YMSGStatus.NotInOffice, self.sess_id, oim_msg_dict) def _verify_challenge_v1(self, yahoo_id: Optional[str], resp_6: Optional[bytes], resp_96: Optional[bytes]) -> bool: from hashlib import md5 backend = self.backend if not yahoo_id: return False if not resp_6: return False #if not resp_96: # return False chal = self.challenge if chal is None: return False uuid = backend.util_get_uuid_from_username(yahoo_id) if uuid is None: return False # Retrieve Yahoo64-encoded MD5 hash of the user's passwordfrom the database # NOTE: The MD5 hash of the password is unsa Tuple[str, str, str, str]: # Creates the cookies if they don't exist assert self.t_cookie_token is not None bs = self.bs assert bs is not None user = bs.user auth_service = self.backend.auth_service y_cookie = Y_COOKIE_TEMPLATE.format(encodedname=_encode_yahoo_id(user.username)) t_cookie = T_COOKIE_TEMPLATE.format(token=self.t_cookie_token) y_expiry = 0 t_expiry = 0 try: _, y_expiry = auth_service.create_token('ymsg/cookie', user.username, token=y_cookie, lifetime=86400) _, t_expiry = auth_service.create_token('ymsg/cookie', self.bs, token=t_cookie, lifetime=86400) except: return None return ( y_cookie, t_cookie, _format_cookie_expiry(datetime.datetime.utcfromtimestamp(y_expiry)), _format_cookie_expiry(datetime.datetime.utcfromtimestamp(t_expiry)) ) Y_COOKIE_TEMPLATE = 'v=1&n=&l={encodedname}&p=&r=&lg=&intl=&np=' T_COOKIE_TEMPLATE = 'z={token}&a=&sk={token}&ks={token}&kt=&ku=&d={token}' YAHOO_HELPER_MSG = settings.YAHOOHELPER_MSG def _format_cookie_expiry(expiry: datetime.datetime) -> str: return expiry.strftime('%a, %d %b %Y %H:%M:%S GMT') def _encode_yahoo_id(yahoo_id: str) -> str: return ''.join( YAHOO_ID_ENCODING.get(c) or c for c in yahoo_id ) YAHOO_ID_ENCODING = { 'k': 'a', 'l': 'b', 'm': 'c', 'n': 'd', 'o': 'e', 'p': 'f', 'q': 'g', 'r': 'h', 's': 'i', 't': 'j', 'u': 'k', 'v': 'l', 'w': 'm', 'x': 'n', 'y': 'o', 'z': 'p', '0': 'q', '1': 'r', '2': 's', '3': 't', '4': 'u', '5': 'v', '7': 'x', '8': 'y', '9': 'z', '6': 'w', 'a': '0', 'b': '1', 'c': '2', 'd': '3', 'e': '4', 'f': '5', 'g': '6', 'h': '7', 'i': '8', 'j': '9', } def add_contact_status_to_data( data: Any, status: UserStatus, contact: User, *, old_substatus: Substatus = Substatus.Offline, message: Optional[str] = None, exclude_psm: bool = False, sess_id: Optional[int] = None, ) -> None: is_offlineish = status.is_offlineish() # `static var YMSG_FLD_SESSION_ID = 11;` # Yahoo! was weird sometimes :p #if user_yahoo_id in PRE_SESSION_ID: # key_11_val = binascii.hexlify(struct.pack('!I', PRE_SESSION_ID[user_yahoo_id])).decode().upper() #elif sess_id is not None: # key_11_val = binascii.hexlify(struct.pack('!I', sess_id)).decode().upper() #else: # key_11_val = '0' data.add(b'7', arbitrary_encode(contact.username)) if not message: message = status.message if is_offlineish or not message or exclude_psm: data.add(b'10', str(int(YMSGStatus.Available if is_offlineish else YMSGStatus.FromSubstatus(status.substatus))).encode('utf-8')) #data.add(b'11', key_11_val.encode('utf-8')) data.add(b'11', b'0') else: data.add(b'10', str(int(YMSGStatus.Custom)).encode('utf-8')) #data.add(b'11', key_11_val.encode('utf-8')) data.add(b'11', b'0') data.add(b'19', arbitrary_encode(message)) is_away_message = (status.substatus is not Substatus.Online) data.add(b'47', str(int(is_away_message)).encode('utf-8')) data.add(b'17', b'0') data.add(b'13', (b'0' if is_offlineish else b'1')) class BackendEventHandler(event.BackendEventHandler): __slots__ = ('loop', 'ctrl', 'dialect', 'sess_id', 'bs') loop: asyncio.AbstractEventLoop ctrl: YMSGCtrlPager dialect: int sess_id: int bs: BackendSession def __init__(self, loop: asyncio.AbstractEventLoop, ctrl: YMSGCtrlPager) -> None: self.loop = loop self.ctrl = ctrl self.dialect = ctrl.dialect self.sess_id = ctrl.sess_id def on_maintenance_message(self, *args: Any, **kwargs: Any) -> None: if args[1] is not None and args[1] > 0: msg = """CrossTalk will be down for maintenance in around {} minute(s). \ Now is a good time to wrap up any conversations.""".format(args[1]) kvs = MultiDict([ (b'14', msg.encode('utf-8')), (b'15', str(time.time()).encode('utf-8')), ]) # type: MultiDict[bytes, bytes] self.ctrl.send_reply(YMSGService.SystemMessage, YMSGStatus.BRB, self.sess_id, kvs) def on_client_alert(self, icon_url: Optional[str], url: Optional[str], message: str = '') -> None: msg = message kvs = MultiDict([ (b'14', msg.encode('utf-8')), (b'15', str(time.time()).encode('utf-8')), ]) # type: MultiDict[bytes, bytes] self.ctrl.send_reply(YMSGService.SystemMessage, YMSGStatus.BRB, self.sess_id, kvs) def on_maintenance_boot(self) -> None: # No maintenance-specific booting packets known as of now. Use generic booting procedure. self.ctrl.send_reply(YMSGService.LogOff, YMSGStatus.Available, 0, None) self.on_close() def on_presence_notification( self, ctc: Contact, on_contact_add: bool, old_substatus: Substatus, *, trid: Optional[str] = None, update_status: bool = True, update_info_other: bool = True, send_status_on_bl: bool = False, sess_id: Optional[int] = None, updated_phone_info: Optional[Dict[str, Any]] = None, ) -> None: bs = self.bs assert bs is not None user = bs.user if on_contact_add: return if update_status or update_info_other: if not ctc.lists & ContactList.FL: return if ctc.status.is_offlineish() and not old_substatus.is_offlineish(): service = YMSGService.LogOff elif not old_substatus.is_offlineish() and ctc.status.substatus is Substatus.Online and not ctc.status.message: service = YMSGService.IsBack else: if self.ctrl.dialect < 15: service = YMSGService.IsAway else: service = YMSGService.StatusUpdate2 if not (ctc.status.is_offlineish() and old_substatus.is_offlineish()): yahoo_data = MultiDict() # type: MultiDict[bytes, bytes] if service is not YMSGService.LogOff: yahoo_data.add(b'0', arbitrary_encode(user.username)) if old_substatus.is_offlineish() and not ctc.status.is_offlineish(): add_contact_status_to_data(yahoo_data, ctc.status, ctc.head, old_substatus = old_substatus, exclude_psm = True, sess_id = sess_id) self.ctrl.send_reply(YMSGService.LogOn, YMSGStatus.BRB, self.sess_id, yahoo_data) if ctc.status.message: message = ctc.status.message yahoo_data = MultiDict() yahoo_data.add(b'0', arbitrary_encode(user.username)) add_contact_status_to_data(yahoo_data, ctc.status, ctc.head, old_substatus = old_substatus, message = message, sess_id = sess_id) self.ctrl.send_reply(YMSGService.IsAway, YMSGStatus.BRB, self.sess_id, yahoo_data) else: add_contact_status_to_data(yahoo_data, ctc.status, ctc.head, old_substatus = old_substatus, sess_id = sess_id) self.ctrl.send_reply(service, YMSGStatus.BRB, self.sess_id, yahoo_data) def on_presence_self_notification(self, old_substatus: Substatus, *, update_status: bool = True, update_info: bool = True) -> None: pass def on_contact_request_denied(self, user_added: User, message: str, *, contact_id: Optional[str] = None) -> None: bs = self.bs assert bs is not None user = bs.user self.ctrl.send_reply(YMSGService.ContactNew, YMSGStatus.OnVacation, self.sess_id, MultiDict([ (b'1', arbitrary_encode(user.username)), (b'3', arbitrary_encode(contact_id or user_added.username)), (b'14', arbitrary_encode(message)), ])) def on_oim_sent(self, oim: 'OIM') -> None: bs = self.ctrl.bs assert bs is not None user = bs.user backend = bs.backend message_dict = MultiDict([ (b'5', arbitrary_encode(user.username)), (b'4', arbitrary_encode(oim.from_username or oim.from_user_id)), (b'14', arbitrary_encode(oim.message or '')), (b'63', b';0'), (b'64', b'0'), (b'97', (b'1' if oim.utf8 else b'0')), ]) # type: MultiDict[bytes, bytes] self.ctrl.send_reply(YMSGService.Message, YMSGStatus.BRB, self.ctrl.sess_id, message_dict) backend.user_service.delete_oim(user.uuid, oim.uuid) def ymsg_on_p2p_msg_request(self, sess_id: int, yahoo_data: MultiDict[bytes, bytes]) -> None: for y in misc.build_p2p_msg_packet(self.bs, sess_id, yahoo_data): self.ctrl.send_reply(y[0], y[1], self.sess_id, y[2]) def ymsg_on_xfer_init(self, sess_id: int, yahoo_data: MultiDict[bytes, bytes]) -> None: for y in misc.build_ft_packet(self.bs, sess_id, yahoo_data): self.ctrl.send_reply(y[0], y[1], self.sess_id, y[2]) def ymsg_on_upload_file_ft(self, recipient: str, message: str) -> None: self.ctrl.send_reply(YMSGService.FileTransfer, YMSGStatus.NotAtDesk, self.sess_id, MultiDict([ (b'1', self.bs.user.username.encode('utf-8')), (b'5', arbitrary_encode(recipient)), (b'4', self.bs.user.username.encode('utf-8')), (b'14', arbitrary_encode(message)), ])) def ymsg_on_sent_ft_http(self, yahoo_id_sender: str, url_path: str, upload_time: float, message: str) -> None: for y in misc.build_http_ft_packet(self.bs, yahoo_id_sender, url_path, upload_time, message): self.ctrl.send_reply(y[0], y[1], self.sess_id, y[2]) def on_circle_created(self, circle: Circle) -> None: pass def on_circle_updated(self, circle: Circle) -> None: pass def on_left_circle(self, circle: Circle) -> None: pass def on_accepted_circle_invite(self, circle: Circle) -> None: pass def on_circle_invite_revoked(self, chat_id: str) -> None: pass def on_circle_role_updated(self, chat_id: str, role: CircleRole) -> None: pass def on_chat_invite( self, chat: Chat, inviter: User, *, circle: bool = False, inviter_id: Optional[str] = None, invite_msg: str = '', ) -> None: bs = self.bs assert bs is not None if circle: return roster = list(chat.get_roster_single()) if 'ymsg_twoway_only' not in chat.front_data and len(roster) == 2: chat.add_id('ymsg_twoway_only', chat.ids['main']) elif 'ymsg_twoway_only' in chat.front_data and len(roster) >= 3: del chat.front_data['ymsg_twoway_only'] if 'ymsg/conf' not in chat.ids: chat.add_id('ymsg/conf', chat.ids['main']) if chat.front_data.get('ymsg_twoway_only') or len(roster) < 2: # A Yahoo! non-conference chat; auto-accepted invite evt = ChatEventHandler(self.loop, self.ctrl, self.bs) cs = chat.join('yahoo', self.bs, evt) chat.send_participant_joined(cs) self.bs.front_data['ymsg_private_chats'][inviter.uuid] = (cs, evt) return conf_invite_dict = MultiDict([ (b'1', arbitrary_encode(self.bs.user.username)), (b'57', arbitrary_encode(chat.ids['ymsg/conf'])), (b'50', arbitrary_encode(inviter_id or inviter.username)), (b'58', arbitrary_encode(invite_msg)), ]) # type: MultiDict[bytes, bytes] for cs in roster: if cs.user.uuid == inviter.uuid: continue conf_invite_dict.add(b'52', arbitrary_encode(cs.preferred_name or cs.user.username)) conf_invite_dict.add(b'53', arbitrary_encode(cs.preferred_name or cs.user.username)) conf_invite_dict.add(b'13', arbitrary_encode(chat.front_data.get('ymsg_voice_chat') or '0')) self.ctrl.send_reply( YMSGService.ConfAddInvite if len(roster) > 1 else YMSGService.ConfInvite, YMSGStatus.BRB, self.ctrl.sess_id, conf_invite_dict, ) def on_declined_chat_invite(self, chat: Chat, circle: bool = False) -> None: pass def on_added_me(self, user: User, *, adder_id: Optional[str] = None, message: Optional[TextWithData] = None) -> None: bs = self.bs assert bs is not None user_me = bs.user detail = user_me.detail assert detail is not None contacts = detail.contacts ctc = contacts.get(user.uuid) if ctc is not None: if ctc.lists & ContactList.BL: return if ctc.pending: bs.me_contact_remove(ctc.head.uuid, ContactList.PL) contact_request_data = MultiDict([ (b'1', arbitrary_encode(user_me.username)), (b'3', arbitrary_encode(adder_id or user.username)), ]) # type: MultiDict[bytes, bytes] if message is not None: contact_request_data.add(b'14', arbitrary_encode(message.text)) if message.yahoo_utf8 is not None: contact_request_data.add(b'97', arbitrary_encode(message.yahoo_utf8)) contact_request_data.add(b'15', arbitrary_encode(str(time.time()))) self.ctrl.send_reply(YMSGService.ContactNew, YMSGStatus.NotAtHome, self.sess_id, contact_request_data) def on_removed_me(self, user: User) -> None: pass def on_login_elsewhere(self, option: LoginOption) -> None: self.ctrl.send_reply(YMSGService.LogOff, YMSGStatus.LoginError, self.sess_id, None) self.ctrl.close() def on_close(self) -> None: self.ctrl.close() class ChatEventHandler(event.ChatEventHandler): __slots__ = ('loop', 'ctrl', 'bs', 'cs') loop: asyncio.AbstractEventLoop ctrl: YMSGCtrlPager bs: BackendSession cs: ChatSession def __init__(self, loop: asyncio.AbstractEventLoop, ctrl: YMSGCtrlPager, bs: BackendSession) -> None: self.loop = loop self.ctrl = ctrl self.bs = bs def on_close(self) -> None: assert self.bs is not None self.bs.front_data['ymsg_chat_sessions'].pop(self.cs.chat, None) def on_participant_joined(self, cs_other: ChatSession, first_pop: bool, initial_join: bool) -> None: bs = self.bs assert bs is not None user = bs.user if self.cs.chat.front_data.get('ymsg_twoway_only') or not first_pop: return else: if 'ymsg/conf' not in self.cs.chat.ids: self.cs.chat.add_id('ymsg/conf', self.cs.chat.ids['main']) self.ctrl.send_reply(YMSGService.ConfLogon, YMSGStatus.BRB, self.ctrl.sess_id, MultiDict([ (b'1', arbitrary_encode(user.username)), (b'57', arbitrary_encode(cs_other.chat.ids['ymsg/conf'])), (b'53', arbitrary_encode(cs_other.preferred_name or cs_other.user.username)), ])) def on_participant_left(self, cs_other: ChatSession, last_pop: bool) -> None: bs = self.bs assert bs is not None user = bs.user if 'ymsg/conf' not in cs_other.chat.ids: # Yahoo only receives this event in "conferences" return if not last_pop: return self.ctrl.send_reply(YMSGService.ConfLogoff, YMSGStatus.BRB, self.ctrl.sess_id, MultiDict([ (b'1', arbitrary_encode(user.username)), (b'57', arbitrary_encode(cs_other.chat.ids['ymsg/conf'])), (b'56', arbitrary_encode(cs_other.preferred_name or cs_other.user.username)), ])) def on_chat_invite_declined( self, chat: Chat, invitee: User, *, invitee_id: Optional[str] = None, message: Optional[str] = None, circle: bool = False, ) -> None: bs = self.bs assert bs is not None user = bs.user if circle: return self.ctrl.send_reply(YMSGService.ConfDecline, YMSGStatus.BRB, self.ctrl.sess_id, MultiDict([ (b'1', user.username.encode('utf-8')), (b'57', arbitrary_encode(chat.ids['ymsg/conf'])), (b'54', arbitrary_encode(invitee_id or invitee.username)), (b'14', arbitrary_encode(message or '')), ])) def on_chat_updated(self) -> None: pass def on_chat_roster_updated(self) -> None: pass def on_participant_status_updated(self, cs_other: ChatSession, first_pop: bool, initial: bool, old_substatus: Substatus) -> None: pass def on_message(self, data: MessageData) -> None: ctrl = self.ctrl bs = self.bs assert bs is not None user = bs.user sender = data.sender yahoo_data = messagedata_to_ymsg(data) roster = list(self.cs.chat.get_roster_single()) if data.type in (MessageType.Chat,MessageType.Nudge): if self.cs.chat.front_data.get('ymsg_twoway_only') or len(roster) <= 2: message_to_dict = MultiDict([ (b'5', yahoo_data.get(b'5') or arbitrary_encode(user.username)), (b'4', yahoo_data.get(b'1') or arbitrary_encode(sender.username)), (b'14', yahoo_data.get(b'14') or arbitrary_encode(data.text or '')), ]) # type: MultiDict[bytes, bytes] if yahoo_data.get(b'63') is not None: message_to_dict.add(b'63', yahoo_data.get(b'63') or b'') if yahoo_data.get(b'64') is not None: message_to_dict.add(b'64', yahoo_data.get(b'64') or b'') if yahoo_data.get(b'97') is not None: message_to_dict.add(b'97', yahoo_data.get(b'97') or b'') self.ctrl.send_reply(YMSGService.Message, YMSGStatus.BRB, self.ctrl.sess_id, message_to_dict) else: if data.type is not MessageType.Nudge: conf_message_dict = MultiDict([ (b'1', arbitrary_encode(user.username)), (b'57', arbitrary_encode(self.cs.chat.ids['ymsg/conf'])), (b'3', yahoo_data.get(b'1') or arbitrary_encode(sender.username)), (b'14', yahoo_data.get(b'14') or arbitrary_encode(data.text or '')), ]) if yahoo_data.get(b'97') is not None: conf_message_dict.add(b'97', yahoo_data.get(b'97') or b'') self.ctrl.send_reply(YMSGService.ConfMsg, YMSGStatus.BRB, self.ctrl.sess_id, conf_message_dict) elif data.type in (MessageType.Typing,MessageType.TypingDone): if self.cs.chat.front_data.get('ymsg_twoway_only') or len(roster) <= 2: self.ctrl.send_reply(YMSGService.Notify, YMSGStatus.BRB if self.ctrl.dialect <= 12 else YMSGStatus.Typing, self.ctrl.sess_id, MultiDict([ (b'5', yahoo_data.get(b'5') or arbitrary_encode(user.username)), (b'4', yahoo_data.get(b'1') or arbitrary_encode(sender.username)), (b'49', b'TYPING'), (b'14', yahoo_data.get(b'14') or arbitrary_encode(data.text or ' ')), (b'13', yahoo_data.get(b'13') or (b'0' if data.type is MessageType.TypingDone else b'1')) ])) elif data.type is MessageType.Webcam: kvs = MultiDict([ (b'5', yahoo_data.get(b'5') or arbitrary_encode(user.username)), (b'4', yahoo_data.get(b'1') or arbitrary_encode(sender.username)), (b'49', b'WEBCAMINVITE'), (b'14', yahoo_data.get(b'14') or arbitrary_encode(data.text or ' ')), ]) # type: MultiDict[bytes, bytes] self.ctrl.send_reply(YMSGService.Notify, YMSGStatus.BRB, self.ctrl.sess_id, kvs) def _send_when_user_joins(self, user_uuid: str, data: MessageData) -> None: # Send to everyone currently in chat self.cs.send_message_to_everyone(data) if self._user_in_chat(user_uuid): return # If `user_uuid` hasn't joined yet, send it later self.loop.create_task(self._send_delayed(user_uuid, data)) async def _send_delayed(self, user_uuid: str, data: MessageData) -> None: delay = 0.1 for _ in range(3): await asyncio.sleep(delay) delay *= 3 if self._user_in_chat(user_uuid): self.cs.send_message_to_user(user_uuid, data) return def _user_in_chat(self, user_uuid: str) -> bool: for cs_other in self.cs.chat.get_roster(): if cs_other.user.uuid == user_uuid: return True return False def messagedata_from_ymsg( sender: User, data: MultiDict[bytes, bytes], *, notify_type: Optional[bytes] = None, typing_flag: Optional[str] = None, ) -> MessageData: text = arbitrary_decode(data.get(b'14') or b'') if notify_type is None: if text == '': type = MessageType.Nudge text = '' else: type = MessageType.Chat elif notify_type == b'TYPING': if typing_flag == '0': type = MessageType.TypingDone else: type = MessageType.Typing elif notify_type == b'WEBCAMINVITE': type = MessageType.Webcam else: # TODO: other `notify_type`s raise Exception("Unknown notify_type", notify_type) message = MessageData(sender = sender, type = type, text = text) message.front_cache['ymsg'] = data return message def messagedata_to_ymsg(data: MessageData) -> MultiDict[bytes, bytes]: if 'ymsg' not in data.front_cache: data.front_cache['ymsg'] = MultiDict([ (b'14', (b'' if data.type is MessageType.Nudge else arbitrary_encode(data.text or ''))), (b'63', b';0'), (b'64', b'0'), (b'97', b'1'), (b'206', b'2') ]) return data.front_cache['ymsg'] def me_status_update(bs: BackendSession, status_new: YMSGStatus, *, message: str = '', is_away_message: bool = False) -> None: bs.front_data['ymsg_status'] = status_new if status_new is YMSGStatus.Custom: substatus = (Substatus.Busy if is_away_message else Substatus.Online) else: substatus = YMSGStatus.ToSubstatus(status_new) bs.me_update({ 'message': message, 'substatus': substatus, 'notify_status': True, 'notify_info': (True if message else False), }) def send_system_message(self, message): msg = f"{message}" kvs = MultiDict([ (b'14', msg.encode('utf-8')), (b'15', str(time.time()).encode('utf-8')), ]) # type: MultiDict[bytes, bytes] self.send_reply(YMSGService.SystemMessage, YMSGStatus.BRB, self.sess_id, kvs) def generate_challenge_v1() -> bytes: from uuid import uuid4 # Yahoo64-encode the raw 16 bytes of a UUID return Y64.Y64Encode(uuid4().bytes)