|
|
@ -1,6 +1,5 @@
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import asyncio
|
|
|
|
import asyncio
|
|
|
|
import base64
|
|
|
|
|
|
|
|
import dataclasses
|
|
|
|
import dataclasses
|
|
|
|
import datetime
|
|
|
|
import datetime
|
|
|
|
import enum
|
|
|
|
import enum
|
|
|
@ -10,7 +9,6 @@ import hmac
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import logging
|
|
|
|
import random
|
|
|
|
import random
|
|
|
|
import re
|
|
|
|
|
|
|
|
from typing import *
|
|
|
|
from typing import *
|
|
|
|
|
|
|
|
|
|
|
|
import Crypto.Cipher.AES as cry_aes # noqa
|
|
|
|
import Crypto.Cipher.AES as cry_aes # noqa
|
|
|
@ -71,11 +69,7 @@ async def _do_init():
|
|
|
|
|
|
|
|
|
|
|
|
def create_translate_provider(cfg):
|
|
|
|
def create_translate_provider(cfg):
|
|
|
|
type_ = cfg['type']
|
|
|
|
type_ = cfg['type']
|
|
|
|
if type_ == 'TencentTranslateFree':
|
|
|
|
if type_ == 'TencentTranslate':
|
|
|
|
return TencentTranslateFree(
|
|
|
|
|
|
|
|
cfg['query_interval'], cfg['source_language'], cfg['target_language']
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
elif type_ == 'TencentTranslate':
|
|
|
|
|
|
|
|
return TencentTranslate(
|
|
|
|
return TencentTranslate(
|
|
|
|
cfg['query_interval'], cfg['source_language'], cfg['target_language'],
|
|
|
|
cfg['query_interval'], cfg['source_language'], cfg['target_language'],
|
|
|
|
cfg['secret_id'], cfg['secret_key'], cfg['region']
|
|
|
|
cfg['secret_id'], cfg['secret_key'], cfg['region']
|
|
|
@ -301,214 +295,6 @@ class TranslateProvider:
|
|
|
|
raise NotImplementedError
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TencentTranslateFree(TranslateProvider):
|
|
|
|
|
|
|
|
def __init__(self, query_interval, source_language, target_language):
|
|
|
|
|
|
|
|
super().__init__(query_interval)
|
|
|
|
|
|
|
|
self._be_available_event.clear() # _do_init之后才可用
|
|
|
|
|
|
|
|
self._source_language = source_language
|
|
|
|
|
|
|
|
self._target_language = target_language
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._server_time_delta = 0
|
|
|
|
|
|
|
|
self._uc_key = self._uc_iv = ''
|
|
|
|
|
|
|
|
self._qtv = self._qtk = ''
|
|
|
|
|
|
|
|
self._reinit_future = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 连续失败的次数
|
|
|
|
|
|
|
|
self._fail_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def init(self):
|
|
|
|
|
|
|
|
if not await super().init():
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
self._reinit_future = asyncio.create_task(self._reinit_coroutine())
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _do_init(self):
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
async with utils.request.http_session.get('https://fanyi.qq.com/') as r:
|
|
|
|
|
|
|
|
if r.status != 200:
|
|
|
|
|
|
|
|
logger.warning('TencentTranslateFree init request failed: status=%d %s', r.status, r.reason)
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
html = await r.text()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
server_time = r.headers['Date']
|
|
|
|
|
|
|
|
server_time = datetime.datetime.strptime(server_time, '%a, %d %b %Y %H:%M:%S GMT')
|
|
|
|
|
|
|
|
server_time = server_time.replace(tzinfo=datetime.timezone.utc).timestamp()
|
|
|
|
|
|
|
|
self._server_time_delta = int((datetime.datetime.now().timestamp() - server_time) * 1000)
|
|
|
|
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
|
|
|
|
self._server_time_delta = 0
|
|
|
|
|
|
|
|
except (aiohttp.ClientError, asyncio.TimeoutError):
|
|
|
|
|
|
|
|
logger.exception('TencentTranslateFree init error:')
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 获取token URL
|
|
|
|
|
|
|
|
m = re.search(r"""\breauthuri\s*=\s*['"](.+?)['"]""", html)
|
|
|
|
|
|
|
|
if m is None:
|
|
|
|
|
|
|
|
logger.exception('TencentTranslateFree init failed: reauthuri not found')
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
reauthuri = m[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 获取验证用的key、iv
|
|
|
|
|
|
|
|
m = re.search(r"""\s*=\s*['"]((?:\w+\|\w+-)+\w+\|\w+)['"]""", html)
|
|
|
|
|
|
|
|
if m is None:
|
|
|
|
|
|
|
|
logger.exception('TencentTranslateFree init failed: initial global variables not found')
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
uc_key = None
|
|
|
|
|
|
|
|
uc_iv = None
|
|
|
|
|
|
|
|
for item in m[1].split('-'):
|
|
|
|
|
|
|
|
key, _, value = item.partition('|')
|
|
|
|
|
|
|
|
if key == 'a137':
|
|
|
|
|
|
|
|
uc_key = value
|
|
|
|
|
|
|
|
elif key == 'E74':
|
|
|
|
|
|
|
|
uc_iv = value
|
|
|
|
|
|
|
|
if uc_key is not None and uc_iv is not None:
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 获取token
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
async with utils.request.http_session.post('https://fanyi.qq.com/api/' + reauthuri) as r:
|
|
|
|
|
|
|
|
if r.status != 200:
|
|
|
|
|
|
|
|
logger.warning('TencentTranslateFree init request failed: reauthuri=%s, status=%d %s',
|
|
|
|
|
|
|
|
reauthuri, r.status, r.reason)
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
data = await r.json()
|
|
|
|
|
|
|
|
except (aiohttp.ClientError, asyncio.TimeoutError):
|
|
|
|
|
|
|
|
logger.exception('TencentTranslateFree init error:')
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
qtv = data.get('qtv', None)
|
|
|
|
|
|
|
|
if qtv is None:
|
|
|
|
|
|
|
|
logger.warning('TencentTranslateFree init failed: qtv not found')
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
qtk = data.get('qtk', None)
|
|
|
|
|
|
|
|
if qtk is None:
|
|
|
|
|
|
|
|
logger.warning('TencentTranslateFree init failed: qtk not found')
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._uc_key = uc_key
|
|
|
|
|
|
|
|
self._uc_iv = uc_iv
|
|
|
|
|
|
|
|
self._qtv = qtv
|
|
|
|
|
|
|
|
self._qtk = qtk
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._on_availability_change()
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _reinit_coroutine(self):
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
|
|
logger.debug('TencentTranslateFree reinit')
|
|
|
|
|
|
|
|
start_time = datetime.datetime.now()
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
await self._do_init()
|
|
|
|
|
|
|
|
except Exception: # noqa
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
cost_time = (datetime.datetime.now() - start_time).total_seconds()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await asyncio.sleep(30 - cost_time)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
|
|
def is_available(self):
|
|
|
|
|
|
|
|
return '' not in (self._uc_key, self._uc_iv, self._qtv, self._qtk) and super().is_available
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _translate_wrapper(self, task: TranslateTask) -> Optional[str]:
|
|
|
|
|
|
|
|
res = await super()._translate_wrapper(task)
|
|
|
|
|
|
|
|
if res is not None:
|
|
|
|
|
|
|
|
self._fail_count = 0
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self._on_fail()
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _do_translate(self, text) -> Optional[str]:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
async with utils.request.http_session.post(
|
|
|
|
|
|
|
|
'https://fanyi.qq.com/api/translate',
|
|
|
|
|
|
|
|
headers={
|
|
|
|
|
|
|
|
'Referer': 'https://fanyi.qq.com/',
|
|
|
|
|
|
|
|
'uc': self._get_uc()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
data={
|
|
|
|
|
|
|
|
'source': self._source_language,
|
|
|
|
|
|
|
|
'target': self._target_language,
|
|
|
|
|
|
|
|
'sourceText': text,
|
|
|
|
|
|
|
|
'qtv': self._qtv,
|
|
|
|
|
|
|
|
'qtk': self._qtk
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
) as r:
|
|
|
|
|
|
|
|
if r.status != 200:
|
|
|
|
|
|
|
|
logger.warning('TencentTranslateFree request failed: status=%d %s', r.status, r.reason)
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
self._update_uc_key(r)
|
|
|
|
|
|
|
|
data = await r.json()
|
|
|
|
|
|
|
|
except (aiohttp.ClientError, asyncio.TimeoutError):
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
if data['errCode'] != 0:
|
|
|
|
|
|
|
|
logger.warning('TencentTranslateFree failed: %d %s', data['errCode'], data['errMsg'])
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
res = ''.join(record['targetText'] for record in data['translate']['records'])
|
|
|
|
|
|
|
|
if res == '' and text.strip() != '':
|
|
|
|
|
|
|
|
# qtv、qtk过期
|
|
|
|
|
|
|
|
logger.info('TencentTranslateFree result is empty %s', data)
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_uc(self):
|
|
|
|
|
|
|
|
user_actions = self._gen_user_actions()
|
|
|
|
|
|
|
|
cur_timestamp = str(int(datetime.datetime.now().timestamp() * 1000))
|
|
|
|
|
|
|
|
server_time_delta = str(self._server_time_delta)
|
|
|
|
|
|
|
|
uc = '|'.join([user_actions, cur_timestamp, server_time_delta])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aes = cry_aes.new(self._uc_key.encode('utf-8'), cry_aes.MODE_CBC, self._uc_iv.encode('utf-8'))
|
|
|
|
|
|
|
|
uc = cry_pad.pad(uc.encode('utf-8'), aes.block_size, 'pkcs7')
|
|
|
|
|
|
|
|
uc = aes.encrypt(uc)
|
|
|
|
|
|
|
|
uc = base64.b64encode(uc).decode('utf-8')
|
|
|
|
|
|
|
|
return uc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
|
def _gen_user_actions():
|
|
|
|
|
|
|
|
# 1:点击翻译;2:源输入框聚焦或失去焦点;3:点击源语言列表;4:点击交换语言;5:点击目标语言列表;6:源输入框输入、粘贴
|
|
|
|
|
|
|
|
user_actions = []
|
|
|
|
|
|
|
|
if random.randint(1, 5) == 1:
|
|
|
|
|
|
|
|
for i in range(random.randint(1, 2)):
|
|
|
|
|
|
|
|
user_actions.append('2')
|
|
|
|
|
|
|
|
user_actions.append('6')
|
|
|
|
|
|
|
|
for i in range(random.randint(0, 6)):
|
|
|
|
|
|
|
|
user_actions.append(random.choice('26'))
|
|
|
|
|
|
|
|
if random.randint(1, 5) == 1:
|
|
|
|
|
|
|
|
user_actions.append('1')
|
|
|
|
|
|
|
|
return ''.join(user_actions)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _update_uc_key(self, r):
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
hf_f = r.headers['f']
|
|
|
|
|
|
|
|
hf_ts = int(r.headers['ts'])
|
|
|
|
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cur_timestamp = int(datetime.datetime.now().timestamp() * 1000)
|
|
|
|
|
|
|
|
hf_f = base64.b64decode(hf_f.encode('utf-8')).decode('utf-8')
|
|
|
|
|
|
|
|
pos = int(hf_f[72: 72 + 4])
|
|
|
|
|
|
|
|
uc_key = hf_f[pos: pos + 16]
|
|
|
|
|
|
|
|
uc_iv = hf_f[pos + 16: pos + 16 + 16]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._server_time_delta = cur_timestamp - hf_ts
|
|
|
|
|
|
|
|
self._uc_key = uc_key
|
|
|
|
|
|
|
|
self._uc_iv = uc_iv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _on_fail(self):
|
|
|
|
|
|
|
|
self._fail_count += 1
|
|
|
|
|
|
|
|
# 为了可靠性,连续失败5次时冷却直到下次重新init
|
|
|
|
|
|
|
|
if self._fail_count >= 5:
|
|
|
|
|
|
|
|
self._cool_down()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cool_down(self):
|
|
|
|
|
|
|
|
logger.info('TencentTranslateFree is cooling down')
|
|
|
|
|
|
|
|
# 下次_do_init后恢复
|
|
|
|
|
|
|
|
self._uc_key = self._uc_iv = ''
|
|
|
|
|
|
|
|
self._qtv = self._qtk = ''
|
|
|
|
|
|
|
|
self._fail_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._on_availability_change()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TencentTranslate(TranslateProvider):
|
|
|
|
class TencentTranslate(TranslateProvider):
|
|
|
|
def __init__(self, query_interval, source_language, target_language,
|
|
|
|
def __init__(self, query_interval, source_language, target_language,
|
|
|
|
secret_id, secret_key, region):
|
|
|
|
secret_id, secret_key, region):
|
|
|
|