mirror of
https://git.ugnet.gay/CrossTalk/azul.git
synced 2026-05-27 22:59:49 +00:00
1967 lines
67 KiB
Python
1967 lines
67 KiB
Python
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])
|
|
|
|
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 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 _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
|
|
|
|
|
|
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 or contact_uuid == user.uuid:
|
|
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 buddy from 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 someone from 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_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 or contact_uuid == user.uuid:
|
|
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]
|
|
|
|
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
|
|
contact_list = [c.head.username for c in cs_fl if not c._groups]
|
|
if contact_list:
|
|
contact_group_list.append('(No Group):' + ','.join(contact_list) + '\n')
|
|
|
|
contact_list_format = ''.join(contact_group_list)
|
|
|
|
ignore_list = [c.head.username for c in cs if c.lists & ContactList.BL]
|
|
ignore_list_format = ','.join(ignore_list)
|
|
|
|
list_reply_kvs = MultiDict() # type: MultiDict[bytes, bytes]
|
|
|
|
if len(contact_list_format) > 815:
|
|
contact_chunks = split_to_chunks(contact_list_format, 815)
|
|
for contact_chunk in contact_chunks:
|
|
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:
|
|
ignore_chunks = split_to_chunks(ignore_list_format, 815)
|
|
for ignore_chunk in ignore_chunks:
|
|
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)
|
|
|
|
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 _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.0125)
|
|
|
|
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 password from the database
|
|
# NOTE: The MD5 hash of the password is unsalted.
|
|
pass_md5 = Y64.Y64Encode(self.backend.user_service.yahoo_get_md5_password(uuid) or b'')
|
|
# Retrieve MD5-crypt(3)'d hash of the user's password from the database
|
|
pass_md5crypt = Y64.Y64Encode(md5(self.backend.user_service.yahoo_get_md5crypt_password(uuid) or b'').digest())
|
|
|
|
seed_val = (chal[15] % 8) % 5
|
|
|
|
if seed_val == 0:
|
|
checksum = bytes([chal[chal[7] % 16]])
|
|
hash_p = checksum + pass_md5 + arbitrary_encode(yahoo_id) + chal
|
|
hash_c = checksum + pass_md5crypt + arbitrary_encode(yahoo_id) + chal
|
|
elif seed_val == 1:
|
|
checksum = bytes([chal[chal[9] % 16]])
|
|
hash_p = checksum + arbitrary_encode(yahoo_id) + chal + pass_md5
|
|
hash_c = checksum + arbitrary_encode(yahoo_id) + chal + pass_md5crypt
|
|
elif seed_val == 2:
|
|
checksum = bytes([chal[chal[15] % 16]])
|
|
hash_p = checksum + chal + pass_md5 + arbitrary_encode(yahoo_id)
|
|
hash_c = checksum + chal + pass_md5crypt + arbitrary_encode(yahoo_id)
|
|
elif seed_val == 3:
|
|
checksum = bytes([chal[chal[1] % 16]])
|
|
hash_p = checksum + arbitrary_encode(yahoo_id) + pass_md5 + chal
|
|
hash_c = checksum + arbitrary_encode(yahoo_id) + pass_md5crypt + chal
|
|
elif seed_val == 4:
|
|
checksum = bytes([chal[chal[3] % 16]])
|
|
hash_p = checksum + pass_md5 + chal + arbitrary_encode(yahoo_id)
|
|
hash_c = checksum + pass_md5crypt + chal + arbitrary_encode(yahoo_id)
|
|
|
|
resp_6_server = Y64.Y64Encode(md5(hash_p).digest())
|
|
resp_96_server = Y64.Y64Encode(md5(hash_c).digest())
|
|
|
|
return resp_6 == resp_6_server and resp_96 == resp_96_server
|
|
|
|
def _refresh_cookies(self) -> 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 == '<ding>':
|
|
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'<ding>' 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)
|