mirror of
https://git.ugnet.gay/CrossTalk/azul.git
synced 2026-05-27 22:59:49 +00:00
production init
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
Y64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._'
|
||||
|
||||
def Y64Encode(string_encode: bytes) -> bytes:
|
||||
limit = len(string_encode) - (len(string_encode) % 3)
|
||||
out = bytearray()
|
||||
buff = [0] * len(string_encode)
|
||||
|
||||
for i in range(len(string_encode)):
|
||||
buff[i] = string_encode[i] & 0xff
|
||||
|
||||
for i in range(0, limit, 3):
|
||||
out.extend([Y64[buff[i] >> 2],
|
||||
Y64[((buff[i] << 4) & 0x30) | (buff[i + 1] >> 4)],
|
||||
Y64[((buff[i + 1] << 2) & 0x3c) | (buff[i + 2] >> 6)],
|
||||
Y64[buff[i + 2] & 0x3f]])
|
||||
|
||||
remaining = len(string_encode) - limit
|
||||
if remaining == 1:
|
||||
out.extend([Y64[buff[limit] >> 2],
|
||||
Y64[(buff[limit] << 4) & 0x30],
|
||||
ord('-'),
|
||||
ord('-')])
|
||||
elif remaining == 2:
|
||||
out.extend([Y64[buff[limit] >> 2],
|
||||
Y64[((buff[limit] << 4) & 0x30) | (buff[limit + 1] >> 4)],
|
||||
Y64[(buff[limit + 1] << 2) & 0x3c],
|
||||
ord('-')])
|
||||
|
||||
return bytes(out)
|
||||
@@ -0,0 +1 @@
|
||||
from .entry import register
|
||||
@@ -0,0 +1,68 @@
|
||||
from typing import Optional, Callable
|
||||
import asyncio, settings
|
||||
|
||||
from aiohttp import web
|
||||
from core.backend import Backend
|
||||
from util.misc import Logger
|
||||
|
||||
from .ymsg_ctrl import YMSGCtrlBase
|
||||
|
||||
def register(loop: asyncio.AbstractEventLoop, backend: Backend, http_app: web.Application, *, devmode: bool = False) -> None:
|
||||
from util.misc import ProtocolRunner
|
||||
from . import pager, http, videochat, voicechat
|
||||
|
||||
backend.add_runner(ProtocolRunner('0.0.0.0', 5050, ListenerYMSG, args = ['Yahoo', backend, pager.YMSGCtrlPager], service = "YMSG Pager"))
|
||||
# Funny that Yahoo! used the FTP transfer, Telnet, SMTP, and NNTP (Usenet) ports as the fallback ports.
|
||||
backend.add_runner(ProtocolRunner('0.0.0.0', 20, ListenerYMSG, args = ['Yahoo', backend, pager.YMSGCtrlPager], service = "YMSG Pager"))
|
||||
backend.add_runner(ProtocolRunner('0.0.0.0', 23, ListenerYMSG, args = ['Yahoo', backend, pager.YMSGCtrlPager], service = "YMSG Pager"))
|
||||
#backend.add_runner(ProtocolRunner('0.0.0.0', 25, ListenerYMSG, args = ['Yahoo', backend, pager.YMSGCtrlPager], service = "YMSG Pager"))
|
||||
#backend.add_runner(ProtocolRunner('0.0.0.0', 119, ListenerYMSG, args = ['Yahoo', backend, pager.YMSGCtrlPager], service = "YMSG Pager"))
|
||||
# Yahoo! also utilized port 80 for YMSG communication via TCP, but that interferes with the port 80 binded to the HTTP
|
||||
# services when the server is run in dev mode.
|
||||
#backend.add_runner(ProtocolRunner('0.0.0.0', 80, ListenerYMSG, args = ['Yahoo', backend, pager.YMSGCtrlPager], service = "YMSG Pager"))
|
||||
backend.add_runner(ProtocolRunner('0.0.0.0', 8001, ListenerYMSG, args = ['Yahoo', backend, pager.YMSGCtrlPager], service = "YMSG Pager"))
|
||||
backend.add_runner(ProtocolRunner('0.0.0.0', 8002, ListenerYMSG, args = ['Yahoo', backend, pager.YMSGCtrlPager], service = "YMSG Pager"))
|
||||
http.register(http_app, devmode = devmode)
|
||||
voicechat.register(backend)
|
||||
videochat.register(backend)
|
||||
|
||||
class ListenerYMSG(asyncio.Protocol):
|
||||
logger: Logger
|
||||
backend: Backend
|
||||
controller: YMSGCtrlBase
|
||||
transport: Optional[asyncio.WriteTransport]
|
||||
|
||||
def __init__(self, logger_prefix: str, backend: Backend, controller_factory: Callable[[Logger, str, Backend], YMSGCtrlBase]) -> None:
|
||||
super().__init__()
|
||||
self.logger = Logger(logger_prefix, self)
|
||||
self.backend = backend
|
||||
self.controller = controller_factory(self.logger, 'direct', backend)
|
||||
self.controller.close_callback = self._on_close
|
||||
self.transport = None
|
||||
|
||||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
assert isinstance(transport, asyncio.WriteTransport)
|
||||
self.transport = transport
|
||||
self.logger.log_connect()
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
self.controller.close()
|
||||
self.logger.log_disconnect()
|
||||
self.transport = None
|
||||
|
||||
def data_received(self, data: bytes) -> None:
|
||||
transport = self.transport
|
||||
assert transport is not None
|
||||
if self.backend.maintenance_mode:
|
||||
transport.close()
|
||||
return
|
||||
self.controller.transport = None
|
||||
if self.controller.transport is None:
|
||||
self.controller.transport = self.transport
|
||||
self.controller.data_received(data)
|
||||
transport.write(self.controller.flush())
|
||||
self.controller.transport = transport
|
||||
|
||||
def _on_close(self) -> None:
|
||||
if self.transport is None: return
|
||||
self.transport.close()
|
||||
@@ -0,0 +1,484 @@
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from aiohttp import web
|
||||
import asyncio, shutil, time, uuid, datetime, util.misc, settings
|
||||
from markupsafe import Markup
|
||||
from urllib.parse import unquote, unquote_plus, quote
|
||||
from pathlib import Path
|
||||
|
||||
from core.backend import Backend, BackendSession
|
||||
from core.models import Contact, Substatus, ContactList
|
||||
from util.hash import gen_salt
|
||||
from util.misc import Logger
|
||||
from .ymsg_ctrl import YMSGCtrlBase, _try_decode_ymsg
|
||||
from .misc import YMSGService
|
||||
from .pager import _encode_yahoo_id, Y_COOKIE_TEMPLATE, T_COOKIE_TEMPLATE
|
||||
|
||||
YAHOO_TMPL_DIR = 'front/ymsg/tmpl'
|
||||
# https://github.com/ifwe/digsby/blob/f5fe00244744aa131e07f09348d10563f3d8fa99/digsby/src/yahoo/yahooutil.py#L33
|
||||
FILE_STORE_PATH = '/storage/file/{filename}'
|
||||
_tasks_by_token = {} # type: Dict[str, asyncio.Task[None]]
|
||||
|
||||
def register(app: web.Application, *, devmode: bool = False) -> None:
|
||||
util.misc.add_to_jinja_env(app, 'ymsg', YAHOO_TMPL_DIR)
|
||||
|
||||
# HTTP auth
|
||||
app.router.add_get('/config/ncclogin', handle_ncclogin)
|
||||
app.router.add_get('/config/pwtoken_get', handle_gettoken)
|
||||
app.router.add_get('/config/pwtoken_login', handle_login)
|
||||
|
||||
# Yahoo! Insider
|
||||
app.router.add_get('/ycontent/', handle_insider_ycontent)
|
||||
|
||||
# Yahoo! Chat/Ads
|
||||
app.router.add_get('/us.yimg.com/i/msgr/chat/conf-banner.html', handle_chat_banad)
|
||||
app.router.add_get('/c/msg/tabs.html', handle_chat_tabad)
|
||||
app.router.add_get('/etc/yahoo-tab-ad', handle_chat_tabad)
|
||||
app.router.add_get('/c/msg/chat.html', handle_chat_notice)
|
||||
app.router.add_get('/c/msg/alerts.html', handle_chat_alertad)
|
||||
app.router.add_get('/etc/yahoo-placeholder', handle_placeholder)
|
||||
app.router.add_get('/external/client_ad.php', handle_banneradredir)
|
||||
|
||||
# Yahoo!'s redirector to cookie-based services
|
||||
#app.router.add_get('/config/reset_cookies', handle_cookies_redirect)
|
||||
|
||||
# Yahoo!'s redirect service (rd.yahoo.com)
|
||||
app.router.add_get('/messenger/search/', handle_rd_yahoo)
|
||||
app.router.add_get('/messenger/client/', handle_rd_yahoo)
|
||||
|
||||
# Yahoo HTTP file transfer fallback
|
||||
app.router.add_post('/notifyft', handle_ft_http)
|
||||
app.router.add_get(FILE_STORE_PATH, handle_yahoo_filedl)
|
||||
|
||||
# Misc stuff
|
||||
app.router.add_get('/capacity', handle_capacity)
|
||||
|
||||
async def handle_insider_ycontent(req: web.Request) -> web.Response:
|
||||
backend = req.app['backend']
|
||||
|
||||
yab_received = False
|
||||
yab_set = False
|
||||
config_xml = []
|
||||
for query_xml in req.query.keys():
|
||||
# Ignore any `chatroom_##########` requests for now
|
||||
if query_xml in IGNORED_QUERIES or query_xml.startswith('chatroom_'): continue
|
||||
if query_xml in ('ab2','addab2'):
|
||||
(_, bs) = _parse_cookies(req, backend)
|
||||
if bs is not None:
|
||||
user = bs.user
|
||||
detail = user.detail
|
||||
if detail is not None:
|
||||
ab2_tmpl = req.app['jinja_env'].get_template('ymsg:Yinsider/Yinsider.ab2.xml')
|
||||
if query_xml == 'ab2':
|
||||
if yab_received or yab_set: continue
|
||||
ctcs = detail.contacts.values()
|
||||
|
||||
records = []
|
||||
|
||||
for ctc in ctcs:
|
||||
records.append(_gen_yab_record(ctc))
|
||||
config_xml.append(ab2_tmpl.render(epoch = round(time.time()), records = Markup('\n'.join(records))))
|
||||
if query_xml == 'addab2':
|
||||
edit_mode = False
|
||||
|
||||
if yab_set or yab_received: continue
|
||||
if req.query.get('ee') == '1' and req.query.get('ow') == '1':
|
||||
edit_mode = True
|
||||
|
||||
if edit_mode:
|
||||
if req.query.get('id') is None:
|
||||
continue
|
||||
|
||||
target_ctc = None
|
||||
|
||||
entry_id = str(req.query['id'])
|
||||
for ctc in detail.contacts.values():
|
||||
if ctc.detail.index_id == entry_id:
|
||||
target_ctc = ctc
|
||||
|
||||
if not target_ctc:
|
||||
continue
|
||||
|
||||
if req.query.get('pp') is not None:
|
||||
if str(req.query['pp']) not in ('0','1','2'):
|
||||
continue
|
||||
|
||||
new_first_name = req.query.get('fn')
|
||||
new_last_name = req.query.get('ln')
|
||||
# Yahoo! will set the email/YID as the first name when editing contact details;
|
||||
# if new_first_name == email and last name isn't set, don't set first name
|
||||
if new_first_name != target_ctc.head.email and new_last_name:
|
||||
target_ctc.detail.first_name = new_first_name
|
||||
target_ctc.detail.last_name = new_last_name
|
||||
target_ctc.detail.nickname = req.query.get('nn')
|
||||
target_ctc.detail.personal_email = req.query.get('e')
|
||||
target_ctc.detail.home_phone = req.query.get('hp')
|
||||
target_ctc.detail.work_phone = req.query.get('wp')
|
||||
target_ctc.detail.mobile_phone = req.query.get('mb')
|
||||
|
||||
backend._mark_modified(user)
|
||||
else:
|
||||
continue
|
||||
|
||||
config_xml.append(ab2_tmpl.render(epoch = round(time.time()), records = Markup(_gen_yab_record(target_ctc))))
|
||||
continue
|
||||
tmpl = req.app['jinja_env'].get_template('ymsg:Yinsider/Yinsider.' + query_xml + '.xml')
|
||||
config_xml.append(tmpl.render())
|
||||
|
||||
return render(req, 'ymsg:Yinsider/Yinsider.xml', {
|
||||
'epoch': round(time.time()),
|
||||
'configxml': Markup('\n'.join(config_xml)),
|
||||
})
|
||||
|
||||
# 'intl', 'os', 'ver', 'fn', 'ln', 'yid', 'nn', 'e', 'hp', 'wp', 'mp', 'pp', 'ee',
|
||||
# 'ow', and 'id' are NOT queries to retrieve config XML files;
|
||||
# 'getwc' and 'getgp' are undocumented as of now
|
||||
# Other queries most likely are just not implemented
|
||||
IGNORED_QUERIES = {
|
||||
'intl', 'os', 'ver',
|
||||
'imv', 'sms', 'getimv', 'getwc', 'getgp',
|
||||
'fn', 'ln', 'yid',
|
||||
'nn', 'e', 'hp',
|
||||
'wp', 'mb', 'pp',
|
||||
'ee', 'ow', 'id',
|
||||
}
|
||||
|
||||
def _gen_yab_record(ctc: Contact) -> str:
|
||||
fname = None
|
||||
lname = None
|
||||
nname = None
|
||||
email = None
|
||||
hphone = None
|
||||
wphone = None
|
||||
mphone = None
|
||||
if ctc.detail.first_name is not None:
|
||||
fname = ' fname="{}"'.format(ctc.detail.first_name)
|
||||
if ctc.detail.last_name is not None:
|
||||
lname = ' lname="{}"'.format(ctc.detail.last_name)
|
||||
if ctc.detail.nickname is not None:
|
||||
nname = ' nname="{}"'.format(ctc.detail.nickname)
|
||||
if ctc.detail.personal_email is not None:
|
||||
email = ' email="{}"'.format(ctc.detail.personal_email)
|
||||
if ctc.detail.home_phone is not None:
|
||||
hphone = ' hphone="{}"'.format(ctc.detail.home_phone)
|
||||
if ctc.detail.work_phone is not None:
|
||||
wphone = ' wphone="{}"'.format(ctc.detail.work_phone)
|
||||
if ctc.detail.mobile_phone is not None:
|
||||
mphone = ' mphone="{}"'.format(ctc.detail.mobile_phone)
|
||||
|
||||
return '<record userid="{yid}"{fname}{lname}{nname}{email}{hphone}{wphone}{mphone} dbid="{contact_id}"/>'.format(
|
||||
yid = ctc.head.username,
|
||||
fname = fname or '', lname = lname or '', nname = nname or '',
|
||||
email = email or '', hphone = hphone or '', wphone = wphone or '', mphone = mphone or '',
|
||||
contact_id = ctc.detail.index_id,
|
||||
)
|
||||
|
||||
async def handle_chat_banad(req: web.Request) -> web.Response:
|
||||
return render(req, 'ymsg:placeholders/banad.html')
|
||||
|
||||
async def handle_chat_tabad(req: web.Request) -> web.Response:
|
||||
query = req.query
|
||||
|
||||
return render(req, 'ymsg:placeholders/adsmall.html', {
|
||||
'adtitle': 'banner ad',
|
||||
'spaceid': (query.get('spaceid') or 0),
|
||||
})
|
||||
|
||||
async def handle_chat_alertad(req: web.Request) -> web.Response:
|
||||
query = req.query
|
||||
|
||||
return render(req, 'ymsg:placeholders/adsmall.html', {
|
||||
'adtitle': 'alert ad usmsgr',
|
||||
'spaceid': (query.get('spaceid') or 0),
|
||||
})
|
||||
|
||||
async def handle_placeholder(req: web.Request) -> web.Response:
|
||||
return render(req, 'ymsg:placeholders/generic.html')
|
||||
|
||||
async def handle_chat_notice(req: web.Request) -> web.Response:
|
||||
return render(req, 'ymsg:placeholders/generic.html')
|
||||
|
||||
async def handle_rd_yahoo(req: web.Request) -> web.Response:
|
||||
return web.HTTPFound(req.query_string.replace(' ', '+'))
|
||||
|
||||
async def handle_ft_http(req: web.Request) -> web.Response:
|
||||
body = await req.read()
|
||||
|
||||
# Look for incomplete key-value field `29`
|
||||
stream_loc = body.find(b'29\xC0\x80')
|
||||
stream = body[(stream_loc + 4):]
|
||||
|
||||
# Parse the rest of the YMSG packet
|
||||
raw_ymsg_data = body[:stream_loc]
|
||||
|
||||
# Now change the length field as fit to get the YMSG parser to gobble it up
|
||||
import struct
|
||||
|
||||
raw_ymsg_part_pre = raw_ymsg_data[0:8]
|
||||
raw_ymsg_part_post = raw_ymsg_data[10:]
|
||||
|
||||
raw_ymsg_data = raw_ymsg_part_pre + struct.pack('!H', len(raw_ymsg_part_post[10:])) + raw_ymsg_part_post
|
||||
|
||||
backend = req.app['backend']
|
||||
|
||||
try:
|
||||
y_ft_pkt = _try_decode_ymsg(raw_ymsg_data, 0)[0]
|
||||
except Exception:
|
||||
return web.HTTPInternalServerError(text = '')
|
||||
|
||||
try:
|
||||
# check version and vendorId
|
||||
if y_ft_pkt[1] > 16 or y_ft_pkt[2] not in (0, 100):
|
||||
return web.HTTPInternalServerError(text = '')
|
||||
except Exception:
|
||||
return web.HTTPInternalServerError(text = '')
|
||||
|
||||
if y_ft_pkt[0] is not YMSGService.FileTransfer:
|
||||
return web.HTTPInternalServerError(text = '')
|
||||
|
||||
ymsg_data = y_ft_pkt[5]
|
||||
|
||||
yahoo_id_sender = util.misc.arbitrary_decode(ymsg_data.get(b'0') or b'')
|
||||
(yahoo_id, bs) = _parse_cookies(req, backend)
|
||||
if None in (bs,yahoo_id):
|
||||
return web.HTTPInternalServerError(text = '')
|
||||
assert bs is not None
|
||||
|
||||
yahoo_id_recipient = util.misc.arbitrary_decode(ymsg_data.get(b'5') or b'')
|
||||
recipient_uuid = backend.util_get_uuid_from_username(yahoo_id_recipient)
|
||||
if recipient_uuid is None:
|
||||
return web.HTTPInternalServerError(text = '')
|
||||
recipient_head = backend._load_user_record(recipient_uuid)
|
||||
if recipient_head is None or recipient_head.status.substatus is Substatus.Offline:
|
||||
return web.HTTPInternalServerError(text = '')
|
||||
|
||||
message = util.misc.arbitrary_decode(ymsg_data.get(b'14') or b'')
|
||||
|
||||
file_path_raw = ymsg_data.get(b'27') # type: Optional[bytes]
|
||||
file_len = util.misc.arbitrary_decode(ymsg_data.get(b'28') or b'0')
|
||||
|
||||
# https://github.com/ifwe/digsby/blob/master/digsby/src/yahoo/yfiletransfer.py#L7
|
||||
# Looks like the HTTP file transfer server had its own size limits (10 MB)
|
||||
if file_path_raw is None or str(len(stream)) != file_len or len(stream) > (2 ** 20):
|
||||
return web.HTTPInternalServerError(text = '')
|
||||
|
||||
file_path = util.misc.arbitrary_decode(file_path_raw)
|
||||
|
||||
try:
|
||||
filename = Path(file_path).name
|
||||
except:
|
||||
return web.HTTPInternalServerError(text = '')
|
||||
|
||||
token = gen_salt(length = 30)
|
||||
path = _get_tmp_file_storage_path(token)
|
||||
path.mkdir(exist_ok = True, parents = True)
|
||||
|
||||
file_tmp_path = path / unquote_plus(filename)
|
||||
file_tmp_path.write_bytes(stream)
|
||||
|
||||
upload_time = time.time()
|
||||
|
||||
expiry_task = req.app.loop.create_task(_store_tmp_file_until_expiry(file_tmp_path))
|
||||
_tasks_by_token[token] = expiry_task
|
||||
|
||||
for bs_other in bs.backend._sc.iter_sessions():
|
||||
if bs_other.user.uuid != recipient_uuid:
|
||||
continue
|
||||
bs_other.evt.ymsg_on_sent_ft_http(
|
||||
yahoo_id_sender, '{}?{}'.format(FILE_STORE_PATH.format(filename = quote(file_tmp_path.name)), token),
|
||||
upload_time, message,
|
||||
)
|
||||
|
||||
bs.evt.ymsg_on_upload_file_ft(yahoo_id_recipient, message)
|
||||
|
||||
return web.HTTPOk(text = '')
|
||||
|
||||
async def _store_tmp_file_until_expiry(path: Path) -> None:
|
||||
await asyncio.sleep(86400)
|
||||
# When a day passes, delete the file (unless it has already been deleted by
|
||||
# the downloader handler; it will cancel the according task then)
|
||||
shutil.rmtree(str(path), ignore_errors = True)
|
||||
|
||||
async def handle_yahoo_filedl(req: web.Request) -> web.Response:
|
||||
filename = req.match_info['filename']
|
||||
token = None
|
||||
query_keys = list(req.query.keys())
|
||||
if query_keys:
|
||||
token = list(req.query.keys())[0]
|
||||
|
||||
if token is None:
|
||||
return web.HTTPNotFound(text = '')
|
||||
|
||||
file_storage_path = _get_tmp_file_storage_path(token)
|
||||
file_path = file_storage_path / unquote(filename)
|
||||
try:
|
||||
file_stream = file_path.read_bytes()
|
||||
except FileNotFoundError:
|
||||
return web.HTTPNotFound(text = '')
|
||||
# Only delete temporary file if request is specifically `GET`
|
||||
if req.method == 'GET':
|
||||
_tasks_by_token[token].cancel()
|
||||
del _tasks_by_token[token]
|
||||
shutil.rmtree(file_storage_path, ignore_errors = True)
|
||||
return web.Response(status = 200, headers = {
|
||||
'Content-Disposition': 'attachment; filename="{}"'.format(filename),
|
||||
}, body = file_stream)
|
||||
|
||||
async def handle_capacity(req: web.Request) -> web.Response:
|
||||
return web.Response(text="COLO_CAPACITY=1\nCS_IP_ADDRESS={}".format(settings.TARGET_IP))
|
||||
|
||||
async def handle_gettoken(req: web.Request) -> web.Response:
|
||||
backend = req.app['backend']
|
||||
|
||||
params = req.rel_url.query
|
||||
username = params.get('login')
|
||||
encoded_pwd = params.get('passwd')
|
||||
pwd = unquote(encoded_pwd) if encoded_pwd is not None else None
|
||||
error_int = None
|
||||
|
||||
uuid_val = backend.util_get_uuid_from_username(username)
|
||||
user = backend.user_service.get(uuid_val) if uuid_val else None
|
||||
|
||||
if not username or not encoded_pwd:
|
||||
error_int = "100"
|
||||
elif not user:
|
||||
error_int = "1235"
|
||||
elif user.suspended:
|
||||
error_int = "1218"
|
||||
else:
|
||||
token_tpl = _login(req, username, pwd, lifetime=86400)
|
||||
if token_tpl is None:
|
||||
error_int = "1212"
|
||||
else:
|
||||
token, _ = token_tpl
|
||||
|
||||
if error_int:
|
||||
return web.Response(text=error_int)
|
||||
else:
|
||||
return web.Response(text="0\r\nymsgr={}\r\npartnerid=0".format(token))
|
||||
|
||||
async def handle_login(req: web.Request) -> web.Response:
|
||||
backend = req.app['backend']
|
||||
|
||||
params = req.rel_url.query
|
||||
token = params.get('token')
|
||||
|
||||
tpl = backend.login_auth_service.get_token('ymsg/cookie', token)
|
||||
|
||||
crumb = util.misc.generate_random_string(chars=32)
|
||||
crumbstr = crumb.decode('utf-8')
|
||||
|
||||
y_cookie = Y_COOKIE_TEMPLATE.format(encodedname=_encode_yahoo_id(token))
|
||||
t_cookie = T_COOKIE_TEMPLATE.format(token=token)
|
||||
import uuid
|
||||
ssl_cookie = str(uuid.uuid4())
|
||||
b_cookie = str(uuid.uuid4())
|
||||
|
||||
if tpl is not None:
|
||||
resp = render(req, 'ymsg:auth/ok.tmpl', {
|
||||
'crumb': crumbstr,
|
||||
't_cookie': t_cookie,
|
||||
'y_cookie': y_cookie,
|
||||
'ssl_cookie': ssl_cookie,
|
||||
'b_cookie': b_cookie
|
||||
})
|
||||
resp.set_cookie('T', t_cookie, path='/')
|
||||
resp.set_cookie('Y', y_cookie, path='/',)
|
||||
resp.set_cookie('SSL', ssl_cookie, path='/')
|
||||
resp.set_cookie('B', b_cookie, path='/')
|
||||
return resp
|
||||
else:
|
||||
return web.Response(text="100")
|
||||
|
||||
async def handle_ncclogin(req: web.Request) -> web.Response:
|
||||
backend = req.app['backend']
|
||||
params = req.rel_url.query
|
||||
|
||||
username = params.get('login')
|
||||
encoded_pwd = params.get('passwd')
|
||||
pwd = unquote(encoded_pwd)
|
||||
uuid = backend.util_get_uuid_from_username(username)
|
||||
user = backend.user_service.get(uuid) if uuid else None
|
||||
detail = backend._load_detail(user)
|
||||
|
||||
if settings.DEBUG:
|
||||
print(f"Username: {username}")
|
||||
print(f"Encoded Password: {encoded_pwd}")
|
||||
print(f"Decoded Password: {pwd}")
|
||||
print(f"UUID: {uuid}")
|
||||
print(f"User: {user}")
|
||||
|
||||
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(f"{grp.name}:{','.join(contact_list)}\n")
|
||||
|
||||
# Handle contacts that aren't part of any groups
|
||||
no_group_contacts = [c.head.username for c in cs_fl if not c._groups]
|
||||
if no_group_contacts:
|
||||
contact_group_list.append(f"(No Group):{','.join(no_group_contacts)}\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)
|
||||
|
||||
if user is None:
|
||||
return web.Response(status=500, text="Internal Server Error")
|
||||
else:
|
||||
return web.Response(text=(
|
||||
f"OK\r\n"
|
||||
f"BEGIN BUDDYLIST\r\n{contact_list_format}END BUDDYLIST\r\n"
|
||||
f"BEGIN IGNORELIST\r\n{ignore_list_format}\r\nEND IGNORELIST\r\n"
|
||||
f"BEGIN IDENTITIES\r\n{username}\r\nEND IDENTITIES\r\n"
|
||||
f"Mail=1\r\nLogin={username}"
|
||||
))
|
||||
|
||||
async def handle_banneradredir(req: web.Request) -> web.Response:
|
||||
return web.HTTPFound(f'http://{settings.TARGET_HOST}/ads/banner')
|
||||
|
||||
def _get_tmp_file_storage_path(token: str) -> Path:
|
||||
return Path('storage/file') / token
|
||||
|
||||
def _parse_cookies(
|
||||
req: web.Request, backend: Backend, y: Optional[str] = None, t: Optional[str] = None,
|
||||
) -> Tuple[Optional[str], Optional[BackendSession]]:
|
||||
cookies = req.cookies
|
||||
|
||||
if None in (y,t):
|
||||
y_cookie = cookies.get('Y')
|
||||
t_cookie = cookies.get('T')
|
||||
else:
|
||||
y_cookie = y
|
||||
t_cookie = t
|
||||
|
||||
return (backend.auth_service.get_token('ymsg/cookie', y_cookie or ''), backend.auth_service.get_token('ymsg/cookie', t_cookie or ''))
|
||||
|
||||
def _login(req: web.Request, username: str, pwd: str, lifetime: int = 86400) -> Optional[Tuple[str, datetime]]:
|
||||
backend: Backend = req.app['backend']
|
||||
uuid_val = backend.user_service.login_with_username(username, pwd)
|
||||
if uuid_val is None:
|
||||
return None
|
||||
|
||||
token_tuple = backend.login_auth_service.create_token('ymsg/cookie', [uuid_val, None], lifetime=lifetime)
|
||||
if token_tuple is None:
|
||||
return None
|
||||
|
||||
token, expiry = token_tuple
|
||||
return (token, expiry)
|
||||
|
||||
def render(req: web.Request, tmpl_name: str, ctxt: Optional[Dict[str, Any]] = None, status: int = 200) -> web.Response:
|
||||
if tmpl_name.endswith('.xml'):
|
||||
content_type = 'text/xml'
|
||||
else:
|
||||
content_type = 'text/html'
|
||||
tmpl = req.app['jinja_env'].get_template(tmpl_name)
|
||||
content = tmpl.render(**(ctxt or {})).replace('\n', '\r\n')
|
||||
return web.Response(status = status, content_type = content_type, text = content)
|
||||
@@ -0,0 +1,223 @@
|
||||
from typing import Tuple, Any, Iterable, List
|
||||
from enum import IntEnum
|
||||
|
||||
from util.misc import DefaultDict, MultiDict, arbitrary_encode
|
||||
|
||||
from core.backend import BackendSession
|
||||
from core.models import Substatus
|
||||
|
||||
import settings
|
||||
|
||||
class YMSGService(IntEnum):
|
||||
LogOn = 0x01
|
||||
LogOff = 0x02
|
||||
IsAway = 0x03
|
||||
IsBack = 0x04
|
||||
Message = 0x06
|
||||
IDActivate = 0x07
|
||||
IDDeactivate = 0x08
|
||||
UserStat = 0x0A
|
||||
ContactNew = 0x0F
|
||||
AddIgnore = 0x11
|
||||
PingConfiguration = 0x12
|
||||
SystemMessage = 0x14
|
||||
SkinName = 0x15
|
||||
ClientHostStats = 0x16
|
||||
MassMessage = 0x17
|
||||
ConfInvite = 0x18
|
||||
ConfLogon = 0x19
|
||||
ConfDecline = 0x1A
|
||||
ConfLogoff = 0x1B
|
||||
ConfAddInvite = 0x1C
|
||||
ConfMsg = 0x1D
|
||||
MessageV2 = 0x27
|
||||
AvatarOld = 0xBD
|
||||
Avatar = 0xC7
|
||||
FileTransfer = 0x46
|
||||
VoiceChat = 0x4A
|
||||
Notify = 0x4B
|
||||
Handshake = 0x4C
|
||||
P2PFileXfer = 0x4D
|
||||
P2PFileXfer8 = 0xDC
|
||||
P2PPhotoSharing = 0xD2
|
||||
PeerToPeer = 0x4F
|
||||
VideoChat = 0x50
|
||||
AuthResp = 0x54
|
||||
List = 0x55
|
||||
Auth = 0x57
|
||||
FriendAdd = 0x83
|
||||
FriendRemove = 0x84
|
||||
Ignore = 0x85
|
||||
ContactDeny = 0x86
|
||||
GroupRename = 0x89
|
||||
Ping = 0x8A
|
||||
ChatJoin = 0x96
|
||||
IsInvisible = 0xC5
|
||||
StatusUpdate = 0xC6
|
||||
StatusUpdate2 = 0xF0
|
||||
# `static var YES_CHAT_PING = 161;` 161 = 0xA1
|
||||
# Yahoo! Messenger 9.0's `desktopHub` SWF seems to list a lot of YMSG service codes and field defs in its code. :p
|
||||
ChatPing = 0xA1
|
||||
ChatSession = 0xD4
|
||||
ContactRegroup = 0xE7
|
||||
# Documented by the Yahsmosis project (https://www.autoitscript.com/forum/topic/142448-help-with-yahomosis/) - this appears to be used when a protocol-level error occurs (protocol version "cloaking", bad `Y`/`T` cookies, invalid Yahoo ID, invalid key/value pairs, etc.). Unsure what protocol version this was first placed into or how far back in the protocol version Yahoo's servers decided to send this on (A Wireshark capture indicates as far back as YMSG12)
|
||||
ProtocolError = 0x07D1
|
||||
|
||||
class YMSGStatus(IntEnum):
|
||||
# Available/Client Request
|
||||
Available = 0x00000000
|
||||
# BRB/Server Response
|
||||
BRB = 0x00000001
|
||||
Busy = 0x00000002
|
||||
# "Not at Home"/BadUsername
|
||||
NotAtHome = 0x00000003
|
||||
NotAtDesk = 0x00000004
|
||||
# "Not in Office"/OfflineMessage/MultiPacket
|
||||
NotInOffice = 0x00000005
|
||||
OnPhone = 0x00000006
|
||||
OnVacation = 0x00000007
|
||||
OutToLunch = 0x00000008
|
||||
SteppedOut = 0x00000009
|
||||
# Dunno when this is used, but the `PeerToPeer` service sends this according to Pidgin
|
||||
P2P = 0x0000000B
|
||||
Invisible = 0x0000000C
|
||||
Bad = 0x0000000D
|
||||
Locked = 0x0000000E
|
||||
Typing = 0x00000016
|
||||
Custom = 0x00000063
|
||||
Idle = 0x000003E7
|
||||
WebLogin = 0x5A55AA55
|
||||
Offline = 0x5A55AA56
|
||||
LoginError = 0xFFFFFFFF
|
||||
|
||||
@classmethod
|
||||
def ToSubstatus(cls, ymsg_status: 'YMSGStatus') -> Substatus:
|
||||
return _ToSubstatus[ymsg_status]
|
||||
|
||||
@classmethod
|
||||
def FromSubstatus(cls, substatus: Substatus) -> 'YMSGStatus':
|
||||
return _FromSubstatus[substatus]
|
||||
|
||||
_ToSubstatus = DefaultDict(Substatus.Busy, {
|
||||
YMSGStatus.Offline: Substatus.Offline,
|
||||
YMSGStatus.Available: Substatus.Online,
|
||||
YMSGStatus.BRB: Substatus.BRB,
|
||||
YMSGStatus.Busy: Substatus.Busy,
|
||||
YMSGStatus.Idle: Substatus.Idle,
|
||||
YMSGStatus.Invisible: Substatus.Invisible,
|
||||
YMSGStatus.NotAtHome: Substatus.NotAtHome,
|
||||
YMSGStatus.NotAtDesk: Substatus.NotAtDesk,
|
||||
YMSGStatus.NotInOffice: Substatus.NotInOffice,
|
||||
YMSGStatus.OnPhone: Substatus.OnPhone,
|
||||
YMSGStatus.OutToLunch: Substatus.OutToLunch,
|
||||
YMSGStatus.SteppedOut: Substatus.SteppedOut,
|
||||
YMSGStatus.OnVacation: Substatus.OnVacation,
|
||||
YMSGStatus.Locked: Substatus.Away,
|
||||
YMSGStatus.LoginError: Substatus.Offline,
|
||||
YMSGStatus.Bad: Substatus.Offline,
|
||||
})
|
||||
_FromSubstatus = DefaultDict(YMSGStatus.Bad, {
|
||||
Substatus.Offline: YMSGStatus.Offline,
|
||||
Substatus.Online: YMSGStatus.Available,
|
||||
Substatus.Busy: YMSGStatus.Busy,
|
||||
Substatus.Idle: YMSGStatus.Idle,
|
||||
Substatus.BRB: YMSGStatus.BRB,
|
||||
Substatus.Away: YMSGStatus.NotAtHome,
|
||||
Substatus.OnPhone: YMSGStatus.OnPhone,
|
||||
Substatus.OutToLunch: YMSGStatus.OutToLunch,
|
||||
Substatus.Invisible: YMSGStatus.Invisible,
|
||||
Substatus.NotAtHome: YMSGStatus.NotAtHome,
|
||||
Substatus.NotAtDesk: YMSGStatus.NotAtDesk,
|
||||
Substatus.NotInOffice: YMSGStatus.NotInOffice,
|
||||
Substatus.OnVacation: YMSGStatus.OnVacation,
|
||||
Substatus.SteppedOut: YMSGStatus.SteppedOut,
|
||||
})
|
||||
|
||||
KVSType = MultiDict[bytes, bytes]
|
||||
EncodedYMSG = Tuple[YMSGService, YMSGStatus, KVSType]
|
||||
|
||||
def build_p2p_msg_packet(bs: BackendSession, sess_id: int, p2p_dict: KVSType) -> Iterable[EncodedYMSG]:
|
||||
user_to = bs.user
|
||||
|
||||
p2p_conn_dict = MultiDict([
|
||||
(b'4', p2p_dict.get(b'4') or b''),
|
||||
(b'5', arbitrary_encode(user_to.username)),
|
||||
])
|
||||
|
||||
#p2p_conn_dict.add(b'11', binascii.hexlify(struct.pack('!I', sess_id)).decode().upper().encode('utf-8'))
|
||||
p2p_conn_dict.add(b'11', b'0')
|
||||
if p2p_dict.get(b'12') is not None: p2p_conn_dict.add(b'12', p2p_dict.get(b'12') or b'')
|
||||
if p2p_dict.get(b'13') is not None: p2p_conn_dict.add(b'13', p2p_dict.get(b'13') or b'')
|
||||
p2p_conn_dict.add(b'49', p2p_dict.get(b'49') or b'')
|
||||
if p2p_dict.get(b'61') is not None: p2p_conn_dict.add(b'61', p2p_dict.get(b'61') or b'')
|
||||
|
||||
yield (YMSGService.PeerToPeer, YMSGStatus.BRB, p2p_conn_dict)
|
||||
|
||||
def build_ft_packet(bs: BackendSession, sess_id: int, xfer_dict: KVSType) -> Iterable[EncodedYMSG]:
|
||||
user_to = bs.user
|
||||
|
||||
ft_dict = MultiDict([
|
||||
(b'5', arbitrary_encode(user_to.username)),
|
||||
(b'4', xfer_dict.get(b'1') or xfer_dict.get(b'4') or b'')
|
||||
])
|
||||
|
||||
ft_type = xfer_dict.get(b'13')
|
||||
if ft_type is not None: ft_dict.add(b'13', ft_type)
|
||||
if ft_type == b'1':
|
||||
if xfer_dict.get(b'27') is not None: ft_dict.add(b'27', xfer_dict.get(b'27') or b'')
|
||||
if xfer_dict.get(b'28') is not None: ft_dict.add(b'28', xfer_dict.get(b'28') or b'')
|
||||
|
||||
if xfer_dict.get(b'20') is not None: ft_dict.add(b'20', xfer_dict.get(b'20') or b'')
|
||||
if xfer_dict.get(b'53') is not None: ft_dict.add(b'53', xfer_dict.get(b'53') or b'')
|
||||
if xfer_dict.get(b'14') is not None: ft_dict.add(b'14', xfer_dict.get(b'14') or b'')
|
||||
if xfer_dict.get(b'54') is not None: ft_dict.add(b'54', xfer_dict.get(b'54') or b'')
|
||||
if ft_type in (b'2',b'3'):
|
||||
# For shared files
|
||||
if xfer_dict.get(b'27') is not None: ft_dict.add(b'27', xfer_dict.get(b'27') or b'')
|
||||
if xfer_dict.get(b'53') is not None: ft_dict.add(b'53', xfer_dict.get(b'53') or b'')
|
||||
|
||||
# For P2P messaging
|
||||
if xfer_dict.get(b'2') is not None: ft_dict.add(b'2', xfer_dict.get(b'2') or b'')
|
||||
if xfer_dict.get(b'11') is not None:
|
||||
#ft_dict.add(b'11', binascii.hexlify(struct.pack('!I', sess_id)).decode().upper().encode('utf-8'))
|
||||
ft_dict.add(b'11', b'0')
|
||||
if xfer_dict.get(b'12') is not None: ft_dict.add(b'12', xfer_dict.get(b'12') or b'')
|
||||
if xfer_dict.get(b'60') is not None: ft_dict.add(b'60', xfer_dict.get(b'60') or b'')
|
||||
if xfer_dict.get(b'61') is not None: ft_dict.add(b'61', xfer_dict.get(b'61') or b'')
|
||||
if ft_type == b'5':
|
||||
if xfer_dict.get(b'54') is not None: ft_dict.add(b'54', xfer_dict.get(b'54') or b'')
|
||||
if ft_type == b'6':
|
||||
if xfer_dict.get(b'20') is not None: ft_dict.add(b'20', xfer_dict.get(b'20') or b'')
|
||||
if xfer_dict.get(b'53') is not None: ft_dict.add(b'53', xfer_dict.get(b'53') or b'')
|
||||
if xfer_dict.get(b'54') is not None: ft_dict.add(b'54', xfer_dict.get(b'54') or b'')
|
||||
if ft_type == b'9':
|
||||
if xfer_dict.get(b'53') is not None: ft_dict.add(b'53', xfer_dict.get(b'53') or b'')
|
||||
if xfer_dict.get(b'49') is not None: ft_dict.add(b'49', xfer_dict.get(b'49') or b'')
|
||||
|
||||
yield (YMSGService.P2PFileXfer, YMSGStatus.BRB, ft_dict)
|
||||
|
||||
def build_http_ft_packet(bs: BackendSession, sender: str, url_path: str, upload_time: float, message: str) -> Iterable[Any]:
|
||||
user = bs.user
|
||||
|
||||
yield (YMSGService.FileTransfer, YMSGStatus.BRB, MultiDict([
|
||||
(b'1', arbitrary_encode(user.username)),
|
||||
(b'5', arbitrary_encode(sender)),
|
||||
(b'4', arbitrary_encode(user.username)),
|
||||
(b'14', arbitrary_encode(message)),
|
||||
(b'38', str(upload_time + 86400).encode('utf-8')),
|
||||
(b'20', arbitrary_encode('http://{}{}'.format(settings.USERSTORAGE_HOST, url_path))),
|
||||
]))
|
||||
|
||||
def split_to_chunks(s: str, count: int) -> List[str]:
|
||||
i = 0
|
||||
j = 0
|
||||
final = []
|
||||
|
||||
while i < len(s):
|
||||
j += count
|
||||
if j > len(s):
|
||||
j = len(s)
|
||||
final.append(s[i:j])
|
||||
i += count
|
||||
|
||||
return final
|
||||
@@ -0,0 +1,52 @@
|
||||
--- YMSG8 ---
|
||||
http://web.archive.org/web/20010801212305/http://www.venkydude.com:80/articles/yahoo.htm (Archive of http://www.venkydude.com/articles/yahoo.htm from 2001)
|
||||
|
||||
--- YMSG9/10 ---
|
||||
http://libyahoo2.sourceforge.net/ymsg-9.txt (YMSG9)
|
||||
http://www.engr.mun.ca/~sircar/ymsg9.htm (YMSG9)
|
||||
http://web.archive.org/web/20020806213426/http://www.venkydude.com:80/articles/yahoo.htm (YMSG9, Archive of http://www.venkydude.com/articles/yahoo.htm from 2002)
|
||||
http://web.archive.org/web/20021203013158/http://www.cse.iitb.ac.in/varunk/YahooProtocol.php (YMSG9)
|
||||
http://web.archive.org/web/20030811172816/http://venkydude.com:80/articles/yahoo.htm (YMSG10, Archive of http://www.venkydude.com/articles/yahoo.htm from 2003)
|
||||
|
||||
--- YMSG11 ---
|
||||
http://web.archive.org/web/20031205031525/venkydude.com/articles/yahoo.htm (Archive of http://www.venkydude.com/articles/yahoo.htm from 2003, first revision)
|
||||
https://web.archive.org/web/20100615081758/http://www.venkydude.com/articles/yahoo.htm (Last archive of http://www.venkydude.com/articles/yahoo.htm from 2010, second revision)
|
||||
|
||||
--- YMSG12 ---
|
||||
http://web.archive.org/web/20070910203807/http://www.ycoderscookbook.com/index.html
|
||||
|
||||
--- YMSG16 ---
|
||||
https://kb.imfreedom.org/protocols/yahoo/
|
||||
http://web.archive.org/web/20090623064155/carbonize.co.uk/ymsg16.html (Site no longer exists)
|
||||
http://web.archive.org/web/20090912133315/http://www.adrensoftware.com/tools/yahoo_v16_protocol.php
|
||||
|
||||
--- YMSG18 ---
|
||||
http://web.archive.org/web/20120626082942/http://www.adrensoftware.com/tools/yahoo_v16_protocol.php
|
||||
|
||||
--- Misc. ---
|
||||
https://github.com/ifwe/digsby/blob/master/digsby/src/yahoo/
|
||||
https://bitbucket.org/pidgin/main/src/soc.2006.msnp13/src/protocols/yahoo/
|
||||
|
||||
--- Yahoo! Messenger client archives ---
|
||||
http://www.oldversion.com/windows/yahoo-messenger/
|
||||
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
Return codes for YMSG service 0x7D1 (Protocol-level Failure; taken from Yahsmosis `YMSGLib.INI`):
|
||||
|
||||
1004=Protocol Mismatch
|
||||
1005=Unknown Data/Invalid Field number
|
||||
1006=Incompatible software (or Cloaking)
|
||||
; ^ happens when sending the old packets (eg: when using cloak)
|
||||
1007=Invalid Protocol Version or Authorization
|
||||
1011=Cookies Expired or Invalid
|
||||
1013=Username format not acceptable.
|
||||
: ^ ?
|
||||
1014=Session expired or terminated
|
||||
; ^ occurs after 52
|
||||
1015=Session expired or invalid
|
||||
;^ occurs after 1011 or 42 or 1051
|
||||
1017=Authorized failed, a session was already active
|
||||
1020=Invalid VendorID?
|
||||
1051=Cookies Expired?
|
||||
+1966
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
<addressbook time="{{ epoch }}">
|
||||
<farm id="1646"/>
|
||||
{{ records }}
|
||||
</addressbook>
|
||||
@@ -0,0 +1,75 @@
|
||||
<audiblemenu signature="YwYF9wY51P0MW+oG9U17PKb70MPP9BjlKlIkVuCThYVzcs2+OB2dK4f1jDy9vhj334eJbansjC8S+N5xSEi/IZh6/34KcwzQPhB1/bSBKVCoRBi+qA0DlLE/I1KpYyiYNWhNohLlEqJRU8Glw8YN/Wwq3U8ZI99XFMrzZd3F/Tf6EiloTZYGDcv2mgFeXTcVORpClBWAfbpGZ8lKNt8xRixHH3u/AGzTE3C1p+EwNXBtzO6NpF3GdqsLv8Z8W9KUP0WNvdn3YN/9kd7OG2ZWlHni0Tf8zXxVvz0t8xpfegHl1YfMbJIeoGyffPIyJQVyqL3teqrq2X4TlsX8DZs6DA==">
|
||||
<Menu text="See all Audibles">
|
||||
<set id="base.us.hello" img="1" text="Hellos"/>
|
||||
<set id="base.us.goodbyes" img="1" text="Goodbyes"/>
|
||||
<set id="base.us.emoticons2" img="1" text="Emoticons"/>
|
||||
<set id="base.us.flirts" img="1" text="Flirts" ver="1.2"/>
|
||||
<set id="base.us.work" img="1" text="Work" ver="1.1"/>
|
||||
<set id="base.us.insults" img="1" text="Insults"/>
|
||||
<set id="base.us.yodel" img="1" text="Yahoo! Yodels" ver="4.0"/>
|
||||
<set id="base.us.emoticats" img="1" text="Emoticats" ver="5.0"/>
|
||||
<set id="base.us.halloween" img="1" text="Halloween" ver="1.0"/>
|
||||
<Menu text="Game Audibles">
|
||||
<set id="base.us.losing" img="1" text="Game - Losing" ver="1.2"/>
|
||||
<set id="base.us.taunt" img="1" text="Game - Taunts"/>
|
||||
<set id="base.us.winning" img="1" text="Game - Winning"/>
|
||||
</Menu>
|
||||
<set id="base.us.htf" img="1" text="Happy Tree Friends"/>
|
||||
<set id="base.us.contender" img="1" text="The Contender"/>
|
||||
<set id="base.us.madonna" img="1" text="Madonna"/>
|
||||
<set id="base.us.chilis" img="1" text="Chili's"/>
|
||||
<set id="base.us.valentines" img="1" text="Valentines" ver="1.0"/>
|
||||
<Menu text="International">
|
||||
<Menu text="Chinese">
|
||||
<set id="base.cn.emotion" img="1" text="China"/>
|
||||
<set id="base.tw.smiley" img="1" text="Taiwan"/>
|
||||
</Menu>
|
||||
<Menu text="English">
|
||||
<set id="base.uk.general" img="1" text="UK"/>
|
||||
</Menu>
|
||||
<set id="base.de.siedler5" img="1" text="German"/>
|
||||
<Menu text="Indic Languages">
|
||||
<set id="base.in.gujarati" img="1" text="Gujarati"/>
|
||||
<set id="base.in.malayalam" img="1" text="Malayalam"/>
|
||||
<set id="base.in.marathi" img="1" text="Marathi"/>
|
||||
<set id="base.in.masti" img="1" text="Masti"/>
|
||||
<set id="base.in.punjabi" img="1" text="Punjabi"/>
|
||||
<set id="base.in.tamil" img="1" text="Tamil"/>
|
||||
</Menu>
|
||||
<Menu text="Indonesia">
|
||||
<set id="base.id.changcuters" img="1" text="Changcuters"/>
|
||||
<set id="base.aa.indonesia2" img="1" text="Indonesian"/>
|
||||
</Menu>
|
||||
<set id="base.aa.malaysia2" img="1" text="Malay"/>
|
||||
<Menu text="Spanish">
|
||||
<Menu text="Neutral">
|
||||
<set id="base.es.generic" img="1" text="Genéricos"/>
|
||||
<set id="base.es.flirts" img="1" text="Coqueteo"/>
|
||||
<set id="base.es.hello" img="1" text="Hola"/>
|
||||
<set id="base.es.goodbyes" img="1" text="Despedidas"/>
|
||||
<set id="base.es.winning" img="1" text="Ganando"/>
|
||||
<set id="base.es.losing" img="1" text="Perdiendo"/>
|
||||
</Menu>
|
||||
<Menu text="Argentina">
|
||||
<set id="base.ar.valentines2" img="1" text="Enamorados"/>
|
||||
<set id="base.ar.friends" img="1" text="Amigos"/>
|
||||
<set id="base.ar.hello" img="1" text="¡Hola!"/>
|
||||
<set id="base.ar.goodbye" img="1" text="Chau"/>
|
||||
<set id="base.ar.yesno" img="1" text="¿Sí o no?"/>
|
||||
<set id="base.ar.moods" img="1" text="Estados de ánimo"/>
|
||||
<set id="base.ar.games" img="1" text="Jugando"/>
|
||||
<set id="base.ar.springtime2" img="1" text="Primavera"/>
|
||||
<set id="base.ar.winter" img="1" text="Invierno"/>
|
||||
<set id="base.ar.generic" img="1" text="Variados"/>
|
||||
</Menu>
|
||||
</Menu>
|
||||
<set id="base.aa.philippines2" img="1" text="Tagalog"/>
|
||||
<Menu text="Vietnam">
|
||||
<set id="base.vn.yahooinvietnam" img="1" text="Yahoo! in Vietnam" ver="1.1"/>
|
||||
<set id="base.aa.vietnam4" img="1" text="Vietnamese" ver="1.1"/>
|
||||
</Menu>
|
||||
</Menu>
|
||||
</Menu>
|
||||
<separator/>
|
||||
<set id="RecentAudibles" img="1" text="Recently used Audibles"/>
|
||||
</audiblemenu>
|
||||
@@ -0,0 +1,2 @@
|
||||
<chatCategories intl="us">
|
||||
</chatCategories>
|
||||
@@ -0,0 +1,2 @@
|
||||
<countries>
|
||||
</countries>
|
||||
@@ -0,0 +1,2 @@
|
||||
<countries>
|
||||
</countries>
|
||||
@@ -0,0 +1,48 @@
|
||||
<filters signature="gkwxLK0AFQJLQ7OOCdw2waHSj+Be+vC21j1FavJ/aiK6v2P073+nY8htit8T22Pa3jdpbSaZe/9qmEXNBhMy4Uy1fF9QxoopUsLglUTDAlgR3UkL/r9P9Zytoh+gQoABeEf3Q/Us2O/AEkXeviW541qdfAEnMd4KqQem3d6b19wj5BtXAaFD4QG7cY7SwfogDpXj9sCudnDBGg19udHvIsE62ottiILdk5+qvEF6DgL/Z54+PYIeebk8qoWXFX1njmnXTX603+FwSrnY0M6SyIIIXsZy88A6ZXJrp+pJYu5VaTj9RYCekAY2ogTlhanatdefILS0CCnIOfgGsQhYVw==">
|
||||
<replace field="135" flags="3" pattern=".*[<>"'&\/].*|^\s*$" with=""/>
|
||||
<replace field="310" flags="3" pattern=".*[<>"'&\/].*|^\s*$" with=""/>
|
||||
<replace field="265" flags="3" pattern=".*[<>"'&\/].*|^\s*$" with=""/>
|
||||
<replace field="11" flags="3" pattern=".*[<>"'&\/].*" with=""/>
|
||||
<replace field="180" flags="3" pattern=".*[<>"'&\/].*" with=""/>
|
||||
<replace field="230" flags="3" pattern=".*[<>"'&\/].*" with=""/>
|
||||
<replace field="231" flags="3" pattern=".*[<>"'&\/].*" with=""/>
|
||||
<replace field="232" flags="3" pattern=".*[<>"'&\/].*" with=""/>
|
||||
<replace field="182" flags="3" pattern=".*[<>"'&\/].*" with=""/>
|
||||
<replace command="208" field="231" flags="3" pattern="http.*|www..*" with=" "/>
|
||||
<replace field="14" flags="3" pattern="<search[^>]*>.*<search[^>]*>.*<search[^>]*>.*<search[^>]*>" with=" "/>
|
||||
<replace field="14" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="14" flags="3" pattern="ymsgr:-kill" with=" "/>
|
||||
<replace field="14" flags="3" pattern="geocities.com/a[0-9]{4,9}" with=" "/>
|
||||
<replace field="14" flags="3" pattern="rmarriso.blogspot.com" with=" "/>
|
||||
<replace field="280" flags="3" pattern="&#39;" with="'"/>
|
||||
<replace field="280" flags="3" pattern="&quot;" with="""/>
|
||||
<replace field="280" flags="3" pattern="&." with=" "/>
|
||||
<replace field="254" flags="3" pattern="[<>]." with=" "/>
|
||||
<replace field="216" flags="3" pattern="[<>]." with=" "/>
|
||||
<replace field="19" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="63" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="117" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="57" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="58" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="105" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="231" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="27" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace command="220" field="27" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="4" flags="3" pattern="msg:" with="msg: "/>
|
||||
<replace field="5" flags="3" pattern="<" with=" "/>
|
||||
<replace field="4" flags="3" pattern="<" with=" "/>
|
||||
<replace field="14" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="19" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="63" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="117" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="57" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="58" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="105" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="231" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="27" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="4" flags="3" pattern="y-source.com" with=" "/>
|
||||
<replace field="14" flags="3" pattern="geocities.com/a[0-9]{4,9}" with=" "/>
|
||||
<replace field="14" flags="3" pattern="thecoolpics.com" with=" "/>
|
||||
<replace field="14" flags="3" pattern="^s*s:" with=" s;"/>
|
||||
<replace command="233" field="59" flags="3" pattern="[,"<>]" with=" "/>
|
||||
</filters>
|
||||
@@ -0,0 +1,2 @@
|
||||
<games>
|
||||
</games>
|
||||
@@ -0,0 +1,147 @@
|
||||
<imvironments signature="E88BOvcU+WllkxR80dQOa0Cyu4++8tVzFzMTmnW7Uk+6KjwsFxBbNBKLvNp1jbNKVOx5uA5Ybzsb9T8xjSChH5mhSu3GS3LI+vGonIGZ0CJeAPkzmVedGeP9Tg//XB4I9SsTP5J6JtdS8x+DTdBPu+tywYBp05yLaewn1do4Xfx2mHDDJ+gOA/jxsAs/UTS4GpCKyS9v15Vw/c/dkX0C3A70UF4ufGxn30r2vB0EjJd33kCIqGxBX/P6Tmk+PFnIC/LZCsqv0LtlsurrnzQm2JtEg0ncVeEYvnQPxgC5khz6kNcfFNY/VxFy842S5QlJ4lLWGymA1xFnGF7o/3eQdA==">
|
||||
<imvgallerypromo>
|
||||
<headertext>Featured IMVironments</headertext>
|
||||
<ctatext>Try it now!</ctatext>
|
||||
</imvgallerypromo>
|
||||
<imv gallery="1" id="hearts" name="Falling Hearts" ver="107">
|
||||
<category>Love and Friendship</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/imv_hearts.gif</picture>
|
||||
<description>What better way to show you care, than with an IMVironment that comes from the heart. For that extra special someone, use the buzz feature (Ctrl + G) to give your loved one a virtual kiss.</description>
|
||||
<galleryName>Falling Hearts</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" id="leaves" name="Autumn Leaves" ver="105">
|
||||
<category>Animals and Nature</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/imv_leaves.gif</picture>
|
||||
<description>Watch autumn leaves fall with this IMVironment, and make them blow with the wind when you Buzz your friends (Ctrl + G).</description>
|
||||
<galleryName>Autumn Leaves</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" id="precious" name="Precious Moments" ver="105">
|
||||
<category>Love and Friendship</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/imv_precious.gif</picture>
|
||||
<description>Send a smile to friends and family with this adorable Precious Moments IMVironment. Scroll through five totally cute Precious Moment backgrounds, then hit Ctrl-G to share a giggle and some instant message fun.</description>
|
||||
<galleryName>Precious Moments</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" id="dilbert" name="Dilbert" ver="104">
|
||||
<category>Comics and Characters</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/dilbert3_gallery.gif</picture>
|
||||
<description>Dilbert is still stuck in his cubicle, but now he's back with a new Messenger IMV! Dilbert works on his desktop computer as Dogbert looks on. Be sure to buzz your friends (Ctrl+G) and see what Dogbert really thinks of Dilbert.</description>
|
||||
<galleryName>Dilbert</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" id="peanuts" name="Peanuts" ver="108">
|
||||
<category>Comics and Characters</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/peanuts3_gallery.gif</picture>
|
||||
<description>The gang from Peanuts is back in a whole new IMVironment. Charlie Brown eagerly checks his mail, and be sure to buzz your friend to see what's waiting for him. Click to visit the Peanuts official site, or get Peanuts in your e-mail box.</description>
|
||||
<galleryName>Peanuts</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" id="garfieldgames" name="Garfield" ver="105">
|
||||
<category>Comics and Characters</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/imv_garfield2.gif</picture>
|
||||
<description>Look who's raiding the refrigerator... it's your old pal Garfield! Andy our friend can help him rummage for a tasty snack simply by typing a message to you. Be sure to buzz a friend (Ctrl + G) to see another snacktime visitor.</description>
|
||||
<galleryName>Garfield</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" id="fishtank" name="Fishtank" ver="105">
|
||||
<category>Animals and Nature</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/imv_fish.gif</picture>
|
||||
<description>Looking for a virtual fish tank to calm your day? Download our Fishtank IMVironment and watch the colorful fishes swim around effortlessly. A great way to relieve stress as you chat with a friend or have it open in your desktop while you work.</description>
|
||||
<galleryName>Fishtank</galleryName>
|
||||
</imv>
|
||||
<imv gallery="1" id="snowflake" name="Snowflakes" ver="108">
|
||||
<category>Animals and Nature</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/imv_snow.gif</picture>
|
||||
<description>Start a virtual snowball fight with your friends using the Snowflake IMV! Just open it up, hit buzz (Control + G) and watch the snowballs fly!</description>
|
||||
<galleryName>Snowflake</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" id="doodle" name="Doodle" ver="109">
|
||||
<category>Interactive Fun</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/insider/doodle_gallery.gif</picture>
|
||||
<description>Everyone has a little bit of an artistic streak in them. Express yourself, and create a great work of art with your friends with this neat IMVironment! After you're done, you can print your work of art to show off to your friends and family. Or if drawing isn't your thing, there are some neat games to try.</description>
|
||||
<galleryName>Doodle</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" id="doodle" name="Doodle" ver="109">
|
||||
<category>Yahoo Tools</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/insider/doodle_gallery.gif</picture>
|
||||
<description>Everyone has a little bit of an artistic streak in them. Express yourself, and create a great work of art with your friends with this neat IMVironment! After you're done, you can print your work of art to show off to your friends and family. Or if drawing isn't your thing, there are some neat games to try.</description>
|
||||
<galleryName>Doodle</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" id="search" name="Yahoo Search" ver="104">
|
||||
<category>Yahoo Tools</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/search_gallery.gif</picture>
|
||||
<description>Looking for something on the Internet with your friends? Why paste links back and forth when you can search together in an IMVironment? Search, then share the links with your friends, all in one message window!</description>
|
||||
<galleryName>Yahoo Search</galleryName>
|
||||
<feature>buzz</feature>
|
||||
</imv>
|
||||
<imv gallery="1" gallerypromo="5" id="emoticats" name="Emoticats" ver="15">
|
||||
<promo ver="2">https://s.yimg.com/pu/dl/imv/promo/emoticats.cab</promo>
|
||||
<category>Yahoo Tools</category>
|
||||
<topFive>1</topFive>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/emoticats_gallery.jpg</picture>
|
||||
<description>Love emoticons? Now you can adopt an "Emoticat&reg;" as your display image or just insert one into your conversation. Open it up and start turning the cat photos into emoticons by zooming in on each cat!</description>
|
||||
<galleryName>Emoticats&reg;</galleryName>
|
||||
</imv>
|
||||
<imv gallery="1" id="emoticats" name="Emoticats" ver="15">
|
||||
<category>Animals and Nature</category>
|
||||
<topFive>1</topFive>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/emoticats_gallery.jpg</picture>
|
||||
<description>Love emoticons? Now you can adopt an "Emoticat&reg;" as your display image or just insert one into your conversation. Open it up and start turning the cat photos into emoticons by zooming in on each cat!</description>
|
||||
<galleryName>Emoticats&reg;</galleryName>
|
||||
</imv>
|
||||
<imv gallery="1" gallerypromo="4" id="footballkick" name="Football Kick" ver="4">
|
||||
<category>Yahoo Tools</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/footballkick_gallery.jpg</picture>
|
||||
<description>Think you've got a winning foot? Challenge your friend to a game of Football Kick to determine a champion. Use the arrow keys to adjust the direction of the ball, and then hold the spacebar down to let it fly. The first player to score 3 points wins!</description>
|
||||
<galleryName>Football Kick</galleryName>
|
||||
</imv>
|
||||
<imv gallery="1" id="footballkick" name="Football Kick" ver="4">
|
||||
<category>Interactive Fun</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/footballkick_gallery.jpg</picture>
|
||||
<description>Think you've got a winning foot? Challenge your friend to a game of Football Kick to determine a champion. Use the arrow keys to adjust the direction of the ball, and then hold the spacebar down to let it fly. The first player to score 3 points wins!</description>
|
||||
<galleryName>Football Kick</galleryName>
|
||||
</imv>
|
||||
<imv gallery="1" id="footballkick" name="Football Kick" ver="4">
|
||||
<category>Sports and Games</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/footballkick_gallery.jpg</picture>
|
||||
<description>Think you've got a winning foot? Challenge your friend to a game of Football Kick to determine a champion. Use the arrow keys to adjust the direction of the ball, and then hold the spacebar down to let it fly. The first player to score 3 points wins!</description>
|
||||
<galleryName>Football Kick</galleryName>
|
||||
</imv>
|
||||
<imv gallery="1" gallerypromo="3" id="purpleleaves" name="Purple Leaves" ver="3">
|
||||
<category>Animals and Nature</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/purpleleaves_gallery.jpg</picture>
|
||||
<description>Fall is here and Purple is this season's biggest color. Celebrate fall by watching these gently falling purple leaves flutter in the breeze. Be stylish and stress-free at the same time.</description>
|
||||
<galleryName>Purple Leaves</galleryName>
|
||||
</imv>
|
||||
<imv gallery="1" id="purpleleaves" name="Purple Leaves" ver="3">
|
||||
<category>Purple</category>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/purpleleaves_gallery.jpg</picture>
|
||||
<description>Fall is here and Purple is this season's biggest color. Celebrate fall by watching these gently falling purple leaves flutter in the breeze. Be stylish and stress-free at the same time.</description>
|
||||
<galleryName>Purple Leaves</galleryName>
|
||||
</imv>
|
||||
<imv gallery="1" gallerypromo="2" id="flickr" name="Flickr" ver="12">
|
||||
<promo ver="4">https://s.yimg.com/pu/dl/imv/promo/flickr.cab</promo>
|
||||
<category>Yahoo Tools</category>
|
||||
<topFive>3</topFive>
|
||||
<picture>https://s.yimg.com/lq/i/mesg/imv/flickr_gallery2.jpg</picture>
|
||||
<description>From snowy landscapes in the winter, to sunny beaches in the summer, spice up your conversation with the some of the most interesting seasonal photos on Flickr.</description>
|
||||
<galleryName>Flickr</galleryName>
|
||||
</imv>
|
||||
<imv gallery="1" gallerypromo="1" id="thethread" name="The Thread" ver="53">
|
||||
<promo ver="30">https://s.yimg.com/pu/dl/imv/promo/thethreadgeneric.cab</promo>
|
||||
<category>Beauty and Fashion</category>
|
||||
<topFive>2</topFive>
|
||||
<picture>https://s.yimg.com/pu/asset/imv/thethreadgeneric_gallery.jpg</picture>
|
||||
<description>Do you love celebrity style? Then watch the Thread while you chat. The Thread brings you the hottest styles from your favorite TV shows, movies and music videos.</description>
|
||||
<galleryName>The Thread</galleryName>
|
||||
</imv>
|
||||
<imv id="ourworld" name="ourWorld" ver="10">
|
||||
<promo ver="1">https://s.yimg.com/pu/dl/imv/promo/ourworld.cab</promo>
|
||||
<category>Interactive Fun</category>
|
||||
</imv>
|
||||
</imvironments>
|
||||
@@ -0,0 +1,112 @@
|
||||
<sms signature="Q7ZR0QLgniIrASEJlPHHPeeaRNSjWbSEBDyhqSlQT8A7GmXsQnFJkD17nnevfcNfHFmQcxmCHIAELuMlBpiQuLKUgTBrYMRcdZczBCpgJF+02kVPZmlb1TdY5CnrP1rtKHUDWrgZGMn4YhPZzo8LhS1qCfbXc65/y3Q5Lc6xguf7kkYZ2uwpisv+0ofmUIyn9JF8A/8GSXtxvbKttsdL1nejOyVRCu4QPbtgDUqc9TcvQdU3TLF+cuI2mWSJr9xZFWGJVIu9LtmEcPpVz/fYKA+clrC4qXTZDuZhzLy+UWFiKOCRMTRKZczzg8u6e3RTdIG6Qi4BMbA1gizl0ojHUw==">
|
||||
<countries>
|
||||
<intl code="1" id="us" maxdigits="10" mindigits="10"/>
|
||||
<intl code="1" id="ca" maxdigits="10" mindigits="10"/>
|
||||
<intl code="91" id="in" maxdigits="10" mindigits="10"/>
|
||||
<intl code="60" maxdigits="9" mindigits="9" name="Malaysia"/>
|
||||
<intl code="965" maxdigits="7" mindigits="7" name="Kuwait"/>
|
||||
<intl code="962" maxdigits="9" mindigits="9" name="Jordan"/>
|
||||
<intl code="974" maxdigits="7" mindigits="7" name="Qatar"/>
|
||||
<intl code="94" maxdigits="9" mindigits="9" name="Sri Lanka"/>
|
||||
<intl code="66" maxdigits="9" mindigits="8" name="Thailand"/>
|
||||
<intl code="65" id="sg" maxdigits="8" mindigits="8"/>
|
||||
<intl code="62" maxdigits="11" mindigits="8" name="Indonesia"/>
|
||||
<intl code="63" maxdigits="10" mindigits="10" name="Philippines"/>
|
||||
<intl code="880" maxdigits="10" mindigits="10" name="Bangladesh"/>
|
||||
<intl code="234" maxdigits="8" mindigits="8" name="Nigeria"/>
|
||||
<intl code="84" maxdigits="12" mindigits="9" name="Vietnam"/>
|
||||
</countries>
|
||||
<carrierslist>
|
||||
<carrier id="pcsms.in.acl" maxchars="159" name="ACL India"/>
|
||||
<carrier id="pcsms.us.boost" maxchars="159" name="Sprint Boost"/>
|
||||
<carrier id="pcsms.us.att" maxchars="159" name="AT&T"/>
|
||||
<carrier id="pcsms.us.cingular" maxchars="156" name="Cingular"/>
|
||||
<carrier id="pcsms.us.verizon" maxchars="159" name="Verizon"/>
|
||||
<carrier id="pcsms.us.virgin" maxchars="159" name="Virgin"/>
|
||||
<carrier id="pcsms.us.tmobile" maxchars="159" name="T-Mobile"/>
|
||||
<carrier id="pcsms.us.alltel" maxchars="159" name="Alltel"/>
|
||||
<carrier id="pcsms.us.sprintpcs" maxchars="159" name="Sprint PCS"/>
|
||||
<carrier id="pcsms.us.sprint_iden" maxchars="159" name="Sprint IDEN(Nextel)"/>
|
||||
<carrier id="pcsms.us.uscell" maxchars="159" name="US Cellular"/>
|
||||
<carrier id="pcsms.ca.rogers" maxchars="159" name="Rogers Wireless"/>
|
||||
<carrier id="hutchinson.in" maxchars="159" name="Orange Mumbai"/>
|
||||
<carrier id="hutchinson.in.mum" maxchars="159" name="Orange Mumbai (Pre-Paid)"/>
|
||||
<carrier id="hutchinson.in.del" maxchars="159" name="Hutch Delhi"/>
|
||||
<carrier id="hutchinson.in.guj" maxchars="159" name="Hutch Gujarat"/>
|
||||
<carrier id="hutchinson.in.kol" maxchars="159" name="Hutch Kolkata"/>
|
||||
<carrier id="hutchinson.in.south" maxchars="159" name="Hutch South"/>
|
||||
<carrier id="hutchinson.in.south2" maxchars="159" name="Hutch South"/>
|
||||
<carrier id="hutchinson.in.adil" maxchars="159" name="Hutch ADIL"/>
|
||||
<carrier id="escotel.in.up" maxchars="159" name="Escotel UP"/>
|
||||
<carrier id="escotel.in.har" maxchars="159" name="Escotel Haryana"/>
|
||||
<carrier id="escotel.in.ker" maxchars="159" name="Escotel Kerala"/>
|
||||
<carrier id="spice.in.kk" maxchars="159" name="Spice Karnataka"/>
|
||||
<carrier id="spice.in.pun" maxchars="159" name="Spice Punjab"/>
|
||||
<carrier id="aircel.in.north" maxchars="159" name="Aircel North"/>
|
||||
<carrier id="airtel.in.del" maxchars="159" name="Airtel Delhi"/>
|
||||
<carrier id="airtel.in.pun" maxchars="159" name="Airtel Punjab"/>
|
||||
<carrier id="airtel.in.ap" maxchars="159" name="Airtel AP"/>
|
||||
<carrier id="airtel.in.kk" maxchars="159" name="Airtel Karnataka"/>
|
||||
<carrier id="airtel.in.mum" maxchars="159" name="Airtel Mumbai"/>
|
||||
<carrier id="airtel.in.kol" maxchars="159" name="Airtel Kolkata"/>
|
||||
<carrier id="airtel.in.chen" maxchars="159" name="Airtel Chennai"/>
|
||||
<carrier id="idea.in" maxchars="159" name="Idea India"/>
|
||||
<carrier id="idea.in.ap" maxchars="159" name="Idea AP"/>
|
||||
<carrier id="idea.in.mp" maxchars="159" name="Idea MP"/>
|
||||
<carrier id="idea.in.guj" maxchars="159" name="Idea Gujarat"/>
|
||||
<carrier id="idea.in.del" maxchars="159" name="Idea Delhi"/>
|
||||
<carrier id="idea.in.mah" maxchars="159" name="Idea Maharashtra"/>
|
||||
<carrier id="reliance.in" maxchars="159" name="Reliance India"/>
|
||||
<carrier id="tatatele.in" maxchars="159" name="TataTele India"/>
|
||||
<carrier id="tatagsm.in" maxchars="159" name="Tata GSM India"/>
|
||||
<carrier id="rpg.in" maxchars="159" name="RPG India"/>
|
||||
<carrier id="oasis.in" maxchars="159" name="Oasis India"/>
|
||||
<carrier id="mtnl.in.mum" maxchars="159" name="MTNL Mumbai"/>
|
||||
<carrier id="mtnl.in.del" maxchars="159" name="MTNL Delhi"/>
|
||||
<carrier id="reliance" maxchars="159" name="Reliance"/>
|
||||
<carrier id="bpl.in.mum" maxchars="159" name="BPL Mumbai"/>
|
||||
<carrier id="bpl.in.mah" maxchars="159" name="BPL Maharashtra"/>
|
||||
<carrier id="bpl.in.ker" maxchars="159" name="BPL Kerala"/>
|
||||
<carrier id="bpl.in.tn" maxchars="159" name="BPL Tamil Nadu"/>
|
||||
<carrier id="bsnl.in.punjab" maxchars="159" name="BSNL Punjab"/>
|
||||
<carrier id="reliance.in.infocomm" maxchars="159" name="Reliance Infocomm"/>
|
||||
<carrier id="reliance.in.infocomm6" maxchars="159" name="Reliance Infocomm6"/>
|
||||
<carrier id="kuwataniya" maxchars="159" name="Wataniya Kuwait"/>
|
||||
<carrier id="pcsms.th.ais" maxchars="159" name="AIS Thailand"/>
|
||||
<carrier id="pcsms.th.dtac" maxchars="159" name="DTAC Thailand"/>
|
||||
<carrier id="pcsms.ph.smart" maxchars="159" name="Smart Philippines"/>
|
||||
<carrier id="globe.ph" maxchars="159" name="Globe Philippines"/>
|
||||
<carrier id="telkomsel.id" maxchars="159" name="Telkomsel Indonesia"/>
|
||||
<carrier id="excelcom.id" maxchars="159" name="Excelcom Indonesia"/>
|
||||
<carrier id="pcsms.id.excelcom" maxchars="159" name="Excelcom Indonesia2"/>
|
||||
<carrier id="pcsms.id.hutch3" maxchars="159" name="Hutch3 Indonesia"/>
|
||||
<carrier id="pcsms.id.indosat" maxchars="159" name="Indosat Indonesia"/>
|
||||
<carrier id="pcsms.my.celcom" maxchars="159" name="Celcom Malaysia"/>
|
||||
<carrier id="pcsms.my.akn_celcom" maxchars="159" name="AKN Celcom Malaysia"/>
|
||||
<carrier id="pcsms.my.akn_digi" maxchars="159" name="MyDigi Malaysia"/>
|
||||
<carrier id="pcsms.vn.gapit_vinaphone" maxchars="159" name="Vinaphone Vietnam"/>
|
||||
<carrier id="pcsms.vn.gapit_mobifone" maxchars="159" name="Mobifone Vietnam"/>
|
||||
<carrier id="pcsms.vn.gapit_viettel" maxchars="159" name="Viettel Vietnam"/>
|
||||
<carrier id="pcsms.vn.htmobile" maxchars="159" name="Htmobile Vietnam"/>
|
||||
<carrier id="pcsms.vn.evntelecom" maxchars="159" name="Evntelecom Vietnam"/>
|
||||
<carrier id="pcsms.vn.sfone" maxchars="159" name="Sfone Vietnam"/>
|
||||
<carrier id="pcsms.yahoo.test" maxchars="159" name="Yahoo Test Carrier"/>
|
||||
<carrier id="reliancegsm.in" maxchars="159" name="Reliance GSM"/>
|
||||
<carrier id="airtel.in" maxchars="159" name="Airtel"/>
|
||||
<carrier id="aircel.in.south" maxchars="159" name="Aircel"/>
|
||||
<carrier id="idea.in.pun" maxchars="159" name="Idea"/>
|
||||
<carrier id="idea.in.ker" maxchars="159" name="Idea"/>
|
||||
<carrier id="reliancecdma.in" maxchars="159" name="Reliance CDMA"/>
|
||||
<carrier id="bpl.in" maxchars="159" name="BPL"/>
|
||||
<carrier id="pcsms.id.mobile8" maxchars="159" name="Mobile 8"/>
|
||||
<carrier id="pcsms.my.maxis" maxchars="159" name="Maxis"/>
|
||||
<carrier id="pcsms.ca.telus" maxchars="159" name="CA Telus"/>
|
||||
<carrier id="pcsms.id.bakrie" maxchars="159" name="ID Bakrie"/>
|
||||
<carrier id="pcsms.ph.sun" maxchars="159" name="Sun Cellular"/>
|
||||
<carrier id="pcsms.us.test" maxchars="159" name="Yahoo US Test Carrier"/>
|
||||
<carrier id="pcsms.id.smart" maxchars="159" name="ID Smart"/>
|
||||
<carrier id="pcsms.vn.gtel" maxchars="159" name="Gtel"/>
|
||||
<carrier id="tatacdma.in" maxchars="159" name="TataCDMA"/>
|
||||
<carrier id="pcsms.ca.bell" maxchars="159" name="Bell Cannada"/>
|
||||
</carrierslist>
|
||||
</sms>
|
||||
@@ -0,0 +1,30 @@
|
||||
<system signature="Sb6CAnTIeMO6Tn6e6K+IJPM+3NDuoQ0aHddsgSOC5qRMPVe+F9DIWjYLIcxKpd3/Y34ymZPoWUfU7dKPWHJs8U6xYRgVPgEAXYVun36gkb3WgkmI/ys729iXayQ0RGOgzJ3NTKSuHOm6snlmh+5jdTvm//n+JbhisiICYklrPTcoVIofKcmUtlwtCXTMk+AjmD7sQlviJn87tSv21zIm6t4GY2zZwykyvF+n3v7sfmPQjIfhXKXgzmjG+ZYbw4EDzJ5laW5L7fNTF2XAiUAdEmqHJ3xyXTK+iPj9nS7HafyTbpL8w4Otc/nGD3CHHtQ11DDjJ8lGqMJqjipEe/4J7Q==">
|
||||
<feature enabled="0" name="guestmsg"/>
|
||||
<feature enabled="1" name="sms"/>
|
||||
<feature enabled="1" name="login2mobile"/>
|
||||
<feature enabled="1" name="imtbphoto"/>
|
||||
<feature enabled="0" name="yahoo360"/>
|
||||
<feature enabled="1" name="launchcast"/>
|
||||
<feature enabled="1" name="UserChatRooms"/>
|
||||
<feature enabled="1" name="chat"/>
|
||||
<feature enabled="0" name="pstn"/>
|
||||
<feature enabled="0" name="pstnin"/>
|
||||
<feature enabled="1" name="call_forwarding"/>
|
||||
<feature enabled="1" name="ymsgrads"/>
|
||||
<feature enabled="1" name="gallery"/>
|
||||
<feature enabled="0" name="InteropPromo"/>
|
||||
<feature enabled="1" name="cookie_token"/>
|
||||
<feature enabled="1" name="sumo"/>
|
||||
<feature enabled="0" name="vitality"/>
|
||||
<feature enabled="0" name="pingbox"/>
|
||||
<feature enabled="0" name="keywordtooltips"/>
|
||||
<feature enabled="1" name="langpacks"/>
|
||||
<feature enabled="1" name="mail_alert"/>
|
||||
<feature enabled="1" name="avatar"/>
|
||||
<appdata>
|
||||
<flickr>
|
||||
<d1>f_Drbm3OeQq1hDzuUozZXxogIdw-</d1>
|
||||
<d2>DaftNW2VLF3JjhoE7cOFSc33SKSw_EeJhROFQlU5C7gJ9F06</d2>
|
||||
</flickr>
|
||||
</appdata>
|
||||
</system>
|
||||
@@ -0,0 +1,3 @@
|
||||
<content time="{{ epoch }}">
|
||||
{{ configxml }}
|
||||
</content>
|
||||
@@ -0,0 +1,12 @@
|
||||
OK
|
||||
BEGIN BUDDYLIST
|
||||
{{ buddylist }}
|
||||
END BUDDYLIST
|
||||
BEGIN IGNORELIST
|
||||
{{ blocklist }}
|
||||
END IGNORELIST
|
||||
BEGIN IDENTITIES
|
||||
{{ username }}
|
||||
END IDENTITIES
|
||||
Mail=1
|
||||
Login={{ username }}
|
||||
@@ -0,0 +1,6 @@
|
||||
0
|
||||
crumb={{ crumb }}
|
||||
Y={{ y_cookie }}
|
||||
T={{ t_cookie }}
|
||||
SSL={{ ssl_cookie }}
|
||||
cookievalidfor=86400
|
||||
@@ -0,0 +1,20 @@
|
||||
<html><head></head>
|
||||
<body scroll="no" topmargin="0" leftmargin="0">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Trebuchet MS', sans-serif;
|
||||
background: #1d1b52;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<center>
|
||||
<br>
|
||||
<strong><p>This is a placeholder.</p></strong>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<html><head><title>banner ad</title></head>
|
||||
<body scroll="no" topmargin="0" leftmargin="0">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Trebuchet MS', sans-serif;
|
||||
background: #1d1b52;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<center>
|
||||
<br>
|
||||
<h1>This is a placeholder.</h1>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<html><head></head>
|
||||
<body scroll="no" topmargin="0" leftmargin="0">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Trebuchet MS', sans-serif;
|
||||
background: #1d1b52;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<center>
|
||||
<br>
|
||||
<h1>This is a placeholder.</h1>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
from core.backend import Backend
|
||||
|
||||
def register(backend: Backend) -> None:
|
||||
from util.misc import ProtocolRunner
|
||||
|
||||
backend.add_runner(ProtocolRunner('0.0.0.0', 5100, ListenerVideoChat, service = 'YMSG Video'))
|
||||
|
||||
class ListenerVideoChat(asyncio.Protocol):
|
||||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
print("Video chat connection_made")
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
print("Video chat connection_lost")
|
||||
|
||||
def data_received(self, data: bytes) -> None:
|
||||
print("Video chat data_received", data)
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
from core.backend import Backend
|
||||
|
||||
def register(backend: Backend) -> None:
|
||||
from util.misc import ProtocolRunner
|
||||
|
||||
# TODO: Implement UDP ports
|
||||
# https://wiki.imfreedom.org/index.php/Yahoo#Network
|
||||
backend.add_runner(ProtocolRunner('0.0.0.0', 5000, ListenerVoiceChat, service = 'YMSG Voice'))
|
||||
backend.add_runner(ProtocolRunner('0.0.0.0', 5001, ListenerVoiceChat, service = 'YMSG Voice'))
|
||||
|
||||
class ListenerVoiceChat(asyncio.Protocol):
|
||||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
print("Voice chat connection_made")
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
print("Voice chat connection_lost")
|
||||
|
||||
def data_received(self, data: bytes) -> None:
|
||||
print("Voice chat data_received", data)
|
||||
@@ -0,0 +1,210 @@
|
||||
import io, asyncio, binascii, struct, settings
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Tuple, Any, Optional, Callable, Iterable
|
||||
|
||||
from core import error
|
||||
from util.misc import Logger, MultiDict
|
||||
from .misc import YMSGStatus, YMSGService
|
||||
|
||||
KVS = MultiDict[bytes, bytes]
|
||||
|
||||
class YMSGCtrlBase(metaclass = ABCMeta):
|
||||
__slots__ = ('logger', 'decoder', 'encoder', 'peername', 'closed', 'close_callback', 'transport', 'session_id')
|
||||
|
||||
logger: Logger
|
||||
decoder: 'YMSGDecoder'
|
||||
encoder: 'YMSGEncoder'
|
||||
peername: Tuple[str, int]
|
||||
close_callback: Optional[Callable[[], None]]
|
||||
closed: bool
|
||||
transport: Optional[asyncio.WriteTransport]
|
||||
session_id: int
|
||||
|
||||
def __init__(self, logger: Logger) -> None:
|
||||
self.logger = logger
|
||||
self.decoder = YMSGDecoder(logger)
|
||||
self.encoder = YMSGEncoder(logger)
|
||||
self.peername = ('0.0.0.0', 5050)
|
||||
self.closed = False
|
||||
self.close_callback = None
|
||||
self.transport = None
|
||||
self.session_id = 0
|
||||
|
||||
def data_received(self, data: bytes, *, transport: Optional[asyncio.BaseTransport] = None) -> None:
|
||||
if transport is None:
|
||||
transport = self.transport
|
||||
assert transport is not None
|
||||
self.peername = transport.get_extra_info('peername')
|
||||
for y in self.decoder.data_received(data):
|
||||
try:
|
||||
# check version and vendorId
|
||||
if y[1] > 18 or y[2] not in (0, 1, 100):
|
||||
continue
|
||||
if y[4]:
|
||||
self.session_id = y[4]
|
||||
f = getattr(self, '_y_{}'.format(binascii.hexlify(struct.pack('!H', y[0])).decode()))
|
||||
f(*y[1:])
|
||||
except Exception as ex:
|
||||
self.logger.error(ex)
|
||||
|
||||
def send_reply(self, service: YMSGService, status: YMSGStatus, session_id: int, kvs: Optional[KVS] = None) -> None:
|
||||
if session_id == 0:
|
||||
session_id = self.session_id
|
||||
try:
|
||||
self.encoder.encode(service, status, session_id, kvs)
|
||||
except error.DataTooLargeToSend:
|
||||
return
|
||||
transport = self.transport
|
||||
if transport is not None:
|
||||
transport.write(self.flush())
|
||||
|
||||
def flush(self) -> bytes:
|
||||
return self.encoder.flush()
|
||||
|
||||
def close(self, **kwargs: Any) -> None:
|
||||
if self.closed: return
|
||||
self.closed = True
|
||||
|
||||
if self.close_callback:
|
||||
self.close_callback()
|
||||
self._on_close(**kwargs)
|
||||
|
||||
@abstractmethod
|
||||
def _on_close(self, remove_sess_id: bool = True) -> None: pass
|
||||
|
||||
class YMSGEncoder:
|
||||
__slots__ = ('_logger', '_buf')
|
||||
|
||||
_logger: Logger
|
||||
_buf: io.BytesIO
|
||||
|
||||
def __init__(self, logger: Logger) -> None:
|
||||
self._logger = logger
|
||||
self._buf = io.BytesIO()
|
||||
|
||||
def encode(self, service: YMSGService, status: YMSGStatus, session_id: int, kvs: Optional[KVS] = None) -> None:
|
||||
payload_list = []
|
||||
if kvs is not None:
|
||||
k = None # type: Optional[bytes]
|
||||
v = None # type: Optional[bytes]
|
||||
for k, v in kvs.items():
|
||||
payload_list.extend([k, SEP, v, SEP])
|
||||
payload = b''.join(payload_list)
|
||||
|
||||
# TODO: Yahoo!'s servers used to split large payloads into packet chunks,
|
||||
# but there's little information on how it was exactly handled.
|
||||
# Just drop packets if they're too big (for the length field to handle unfortunately) until we can find a solution.
|
||||
|
||||
if len(payload) > 0xffff:
|
||||
raise error.DataTooLargeToSend()
|
||||
|
||||
w = self._buf.write
|
||||
w(PRE)
|
||||
# version number and vendor id are replaced with 0x00000000
|
||||
w(b'\x00\x00\x00\x00')
|
||||
|
||||
# Have to call `int` on these because they might be an IntEnum, which
|
||||
# get `repr`'d to `EnumName.ValueName`. Grr.
|
||||
w(struct.pack('!HHII', len(payload), int(service), int(status), session_id))
|
||||
w(payload)
|
||||
|
||||
self._logger.debug('[Server]', service, status, session_id)
|
||||
if kvs:
|
||||
_truncated_kvs(service, kvs)
|
||||
|
||||
def flush(self) -> bytes:
|
||||
data = self._buf.getvalue()
|
||||
if data:
|
||||
#self._logger.info('<<<', data)
|
||||
self._buf = io.BytesIO()
|
||||
return data
|
||||
|
||||
DecodedYMSG = Tuple[YMSGService, int, int, YMSGStatus, int, KVS]
|
||||
|
||||
class YMSGDecoder:
|
||||
__slots__ = ('logger', '_data', '_i')
|
||||
|
||||
logger: Logger
|
||||
_data: bytes
|
||||
_i: int
|
||||
|
||||
def __init__(self, logger: Logger) -> None:
|
||||
self.logger = logger
|
||||
self._data = b''
|
||||
self._i = 0
|
||||
|
||||
def data_received(self, data: bytes) -> Iterable[DecodedYMSG]:
|
||||
if self._data:
|
||||
self._data += data
|
||||
else:
|
||||
self._data = data
|
||||
while self._data:
|
||||
y = self._ymsg_read()
|
||||
if y is None: break
|
||||
yield y
|
||||
|
||||
def _ymsg_read(self) -> Optional[DecodedYMSG]:
|
||||
try:
|
||||
y, e = _try_decode_ymsg(self._data, self._i)
|
||||
except Exception:
|
||||
print("ERR _ymsg_read", self._data)
|
||||
raise
|
||||
|
||||
self._data = self._data[e:]
|
||||
self._i = 0
|
||||
self.logger.debug('[Client]', 'YMSG{}'.format(str(y[1])), y[0], y[3], y[4])
|
||||
_truncated_kvs(y[0], y[5])
|
||||
return y
|
||||
|
||||
def _try_decode_ymsg(d: bytes, i: int) -> Tuple[DecodedYMSG, int]:
|
||||
kvs = MultiDict() # type: KVS
|
||||
|
||||
e = 20
|
||||
assert len(d[i:]) >= e
|
||||
assert d[i:i+4] == PRE
|
||||
header = d[i+4:i+e]
|
||||
if header[:2] in (b'\x08\x00',b'\x09\x00',b'\x0a\x00'):
|
||||
version = struct.unpack('<H', header[:2])[0] # type: int
|
||||
else:
|
||||
version = struct.unpack('!H', header[:2])[0]
|
||||
(vendor_id, n, service, status, session_id) = struct.unpack('!HHHII', header[2:]) # type: Tuple[int, int, int, int, int]
|
||||
assert version in YMSG_DIALECTS
|
||||
assert e+n <= len(d[i:])
|
||||
payload = d[e:e+n]
|
||||
if payload:
|
||||
parts = payload.split(SEP)
|
||||
del parts[-1]
|
||||
assert len(parts) % 2 == 0
|
||||
for j in range(1, len(parts), 2):
|
||||
kvs.add(parts[j-1], parts[j])
|
||||
e += n
|
||||
return ((YMSGService(service), version, vendor_id, YMSGStatus(status), session_id, kvs), e)
|
||||
|
||||
def _truncated_kvs(service: YMSGService, kvs: KVS) -> None:
|
||||
restricted_keys = set()
|
||||
|
||||
if service in (YMSGService.AuthResp, YMSGService.List):
|
||||
restricted_keys.add(b'59')
|
||||
if service in (
|
||||
YMSGService.Message, YMSGService.MassMessage, YMSGService.ContactNew, YMSGService.FriendAdd,
|
||||
YMSGService.ContactDeny, YMSGService.ConfDecline, YMSGService.ConfMsg,YMSGService.P2PFileXfer, YMSGService.FileTransfer
|
||||
):
|
||||
restricted_keys.add(b'14')
|
||||
if service in (YMSGService.ConfInvite, YMSGService.ConfAddInvite):
|
||||
restricted_keys.add(b'58')
|
||||
if service in (YMSGService.P2PFileXfer, YMSGService.FileTransfer):
|
||||
restricted_keys.add(b'20')
|
||||
|
||||
if settings.DEBUG:
|
||||
for k, v in kvs.items():
|
||||
print('{!r} -> {}'.format(k, (v)))
|
||||
|
||||
PRE = b'YMSG'
|
||||
SEP = b'\xC0\x80'
|
||||
|
||||
YMSG_DIALECTS = [
|
||||
# Not actually supported
|
||||
18, 17, 16, 8,
|
||||
# Actually supported
|
||||
15, 14, 13, 12, 11, 10, 9
|
||||
]
|
||||
Reference in New Issue
Block a user