From 914a41700e9d9daa92e4fb8566a2db36bae10807 Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 5 Jul 2023 00:02:28 +0800 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 414258724b456628a2e361a11de102dbdc5a2fe4 Author: John Smith Date: Tue Jul 4 23:58:59 2023 +0800 修正代码风格和wbi签名实现 commit 34a6546bd6ac00a893d36a65c20d479b113e3f53 Author: Bryan不可思议 Date: Thu Jun 22 15:52:51 2023 +0800 Update services/avatar.py Co-authored-by: Hao Guan <10684225+hguandl@users.noreply.github.com> commit f9dd56a13c296ff938560bcd161570f000855b35 Author: ProgramRipper Date: Tue Jun 13 03:33:15 2023 +0800 fix: syntax error commit a0354f2480a7c561e6dcc223fa9d4834310540b5 Author: ProgramRipper Date: Tue Jun 13 03:08:55 2023 +0800 fix: get_avatar_url --- services/avatar.py | 86 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/services/avatar.py b/services/avatar.py index cccf5a6..1cbd585 100644 --- a/services/avatar.py +++ b/services/avatar.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- import asyncio import datetime +import hashlib import logging import re +import urllib.parse from typing import * import aiohttp @@ -29,6 +31,14 @@ _uid_queue_to_fetch: Optional[asyncio.Queue] = None # 上次被B站ban时间 _last_fetch_banned_time: Optional[datetime.datetime] = None +# wbi密码表 +WBI_KEY_INDEX_TABLE = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, + 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13 +] +# wbi鉴权口令 +_wbi_key = '' + def init(): cfg = config.get_config() @@ -146,21 +156,21 @@ async def _get_avatar_url_from_web_coroutine(user_id, future): async def _do_get_avatar_url_from_web(user_id): + global _wbi_key + if _wbi_key == '': + # TODO 判断一下是否正在获取 + _wbi_key = await _get_wbi_key() + try: async with utils.request.http_session.get( - 'https://api.bilibili.com/x/space/acc/info', + 'https://api.bilibili.com/x/space/wbi/acc/info', headers={ **utils.request.BILIBILI_COMMON_HEADERS, 'Origin': 'https://space.bilibili.com', 'Referer': f'https://space.bilibili.com/{user_id}/' }, cookies=utils.request.BILIBILI_COMMON_COOKIES, - params={ - 'mid': user_id, - 'token': '', - 'platform': 'web', - 'jsonp': 'jsonp' - } + params=_add_wbi_sign({'mid': user_id}), ) as r: if r.status != 200: logger.warning('Failed to fetch avatar: status=%d %s uid=%d', r.status, r.reason, user_id) @@ -176,6 +186,8 @@ async def _do_get_avatar_url_from_web(user_id): if data['code'] != 0: # 这里虽然失败但不会被ban一段时间 logger.info('Failed to fetch avatar: code=%d %s uid=%d', data['code'], data['message'], user_id) + if data['code'] == -403: + _wbi_key = '' return None avatar_url = process_avatar_url(data['data']['face']) @@ -183,6 +195,66 @@ async def _do_get_avatar_url_from_web(user_id): return avatar_url +async def _get_wbi_key(): + try: + async with utils.request.http_session.get( + 'https://api.bilibili.com/nav', + headers=utils.request.BILIBILI_COMMON_HEADERS, + ) as r: + if r.status != 200: + logger.warning('Failed to get wbi key: status=%d %s', r.status, r.reason) + return '' + data = await r.json() + except (aiohttp.ClientConnectionError, asyncio.TimeoutError): + logger.exception('Failed to get wbi key:') + return '' + + try: + wbi_img = data['data']['wbi_img'] + img_key = wbi_img['img_url'].rpartition('/')[2].partition('.')[0] + sub_key = wbi_img['sub_url'].rpartition('/')[2].partition('.')[0] + except KeyError: + logger.warning('Failed to get wbi key: data=%s', data) + return '' + + shuffled_key = img_key + sub_key + wbi_key = [] + for index in WBI_KEY_INDEX_TABLE: + if index < len(shuffled_key): + wbi_key.append(shuffled_key[index]) + return ''.join(wbi_key) + + +def _add_wbi_sign(params: dict): + if _wbi_key == '': + return params + + wts = str(int(datetime.datetime.now().timestamp())) + params_to_sign = {**params, 'wts': wts} + + # 按key字典序排序 + params_to_sign = { + key: params_to_sign[key] + for key in sorted(params_to_sign.keys()) + } + # 过滤一些字符 + for key, value in params_to_sign.items(): + value = ''.join( + ch + for ch in str(value) + if ch not in "!'()*" + ) + params_to_sign[key] = value + + str_to_sign = urllib.parse.urlencode(params_to_sign) + _wbi_key + w_rid = hashlib.md5(str_to_sign.encode('utf-8')).hexdigest() + return { + **params, + 'wts': wts, + 'w_rid': w_rid + } + + def process_avatar_url(avatar_url): # 去掉协议,兼容HTTP、HTTPS m = re.fullmatch(r'(?:https?:)?(.*)', avatar_url)