From 960ada7e897bb762baf4191c428be026a4c9a3c7 Mon Sep 17 00:00:00 2001 From: hemengyang Date: Wed, 27 Nov 2019 19:02:52 +0800 Subject: [PATCH] fflogs update (#34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 更新 CoolQBot-env 到 0.2.4 * 初步支持从 FFlogs API 计算输出百分比 * 更新伊甸的数据,并支持 adps * 添加每天的缓存,并支持 pdps * 定时提前缓存数据 * 添加异常处理 --- CHANGELOG.md | 5 +- Dockerfile | 2 +- requirements.txt | 4 +- src/plugins/fflogs/__init__.py | 43 ++++++--- src/plugins/fflogs/api.py | 146 +++++++++++++++++++++++++------ src/plugins/fflogs/data.py | 62 +++++++------ src/plugins/fflogs/exceptions.py | 14 +++ 7 files changed, 207 insertions(+), 69 deletions(-) create mode 100644 src/plugins/fflogs/exceptions.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 45caa2dc..70cd70ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## [Unreleased] +## [0.10.0] - 2019-11-28 + ### Added - 添加了查询 fflogs 输出百分比的功能 @@ -234,7 +236,8 @@ - 正常工作的版本。 -[Unreleased]: https://github.com/he0119/CoolQBot/compare/v0.9.1...HEAD +[Unreleased]: https://github.com/he0119/CoolQBot/compare/v0.10.0...HEAD +[0.10.0]: https://github.com/he0119/CoolQBot/compare/v0.9.1...v0.10.0 [0.9.1]: https://github.com/he0119/CoolQBot/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/he0119/CoolQBot/compare/v0.8.1...v0.9.0 [0.8.1]: https://github.com/he0119/CoolQBot/compare/v0.8.0...v0.8.1 diff --git a/Dockerfile b/Dockerfile index 48f955e2..5008ffe3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM he0119/coolqbot-env:v0.2.3 +FROM he0119/coolqbot-env:v0.2.4 # 安装依赖 COPY requirements.txt /home/user/coolqbot/requirements.txt diff --git a/requirements.txt b/requirements.txt index 69193f0e..ba3dd090 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ nonebot==1.3.1 requests==2.22.0 -apscheduler==3.6.1 +apscheduler==3.6.3 jieba==0.39 -python-dateutil==2.8.0 +python-dateutil==2.8.1 msgpack==0.6.2 diff --git a/src/plugins/fflogs/__init__.py b/src/plugins/fflogs/__init__.py index e85a9891..946c30e4 100644 --- a/src/plugins/fflogs/__init__.py +++ b/src/plugins/fflogs/__init__.py @@ -3,11 +3,31 @@ 从网站 https://cn.fflogs.com/ 获取输出数据 文档网址 https://cn.fflogs.com/v1/docs """ +import asyncio + from nonebot import CommandSession, on_command from coolqbot import bot from .api import API +from .data import boss_list, job_list + +HOUR = int(API.data.config_get('cache', 'hour', fallback='4')) +MINUTE = int(API.data.config_get('cache', 'minute', fallback='30')) +SECOND = int(API.data.config_get('cache', 'second', fallback='0')) + + +@bot.scheduler.scheduled_job( + 'cron', hour=HOUR, minute=MINUTE, second=SECOND, id='fflogs_cache' +) +async def fflogs_cache(): + """ 定时缓存数据 + """ + for (boss_id, difficulty), boss_nickname in boss_list.items(): + for job_id, job_nickname in job_list.items(): + await API.dps(boss_nickname[0], job_nickname[0]) + bot.logger.debug(f'{boss_nickname[0]} {job_nickname[0]}的数据缓存完成。') + await asyncio.sleep(30) @on_command('dps', aliases=['输出'], only_to_me=False, shell_like=True) @@ -15,23 +35,26 @@ async def dps(session: CommandSession): """ 查询 DPS """ # 设置 Token - if len(session.argv) == 2 and session.argv[0] == 'token': - API.set_token(session.argv[1]) + if session.argv[0] == 'token' and len(session.argv) == 2: + API.token = session.argv[1] session.finish('Token 设置完成。') # 检查 Token 是否设置 - # 现在不需要用到 token 所以注释掉这个检查 - # if not API.token: - # session.finish( - # '对不起,Token 未设置,无法查询数据。\n请先使用命令\n/dps token \n配置好 Token 后再尝试查询数据。' - # ) + if not API.token: + session.finish( + '对不起,Token 未设置,无法查询数据。\n请先使用命令\n/dps token \n配置好 Token 后再尝试查询数据。' + ) if session.argv[0] == 'token' and len(session.argv) == 1: session.finish(f'当前的 Token 为 {API.token}') - # if session.argv[0] == 'zones': - # reply = await API.zones() - # session.finish(str(reply[int(session.argv[1])])) + if session.argv[0] == 'classes' and len(session.argv) == 1: + reply = await API.classes() + session.finish(str(reply)) + + if session.argv[0] == 'zones' and len(session.argv) == 2: + reply = await API.zones() + session.finish(str(reply[int(session.argv[1])])) if len(session.argv) > 1 and len(session.argv) < 4: reply = await API.dps(*session.argv) diff --git a/src/plugins/fflogs/api.py b/src/plugins/fflogs/api.py index 6185f3fc..49602a83 100644 --- a/src/plugins/fflogs/api.py +++ b/src/plugins/fflogs/api.py @@ -1,12 +1,17 @@ """ API + +文档网址 https://cn.fflogs.com/v1/docs """ import json -import re +import math +from datetime import datetime, timedelta import aiohttp from coolqbot import PluginData + from .data import get_boss_info, get_job_name +from .exceptions import AuthException, DataException class FFlogs: @@ -14,27 +19,27 @@ def __init__(self): self.base_url = 'https://cn.fflogs.com/v1' self.data = PluginData('fflogs', config=True) - self.token = self.get_token() - # 当前是 5.0 版本 - self.version = self.data.config_get('fflogs', 'version', '0') - # 默认为两周的数据 - self.range = self.data.config_get('fflogs', 'range', '14') + # 默认从两周的数据中计算排名百分比 + self.range = int(self.data.config_get('fflogs', 'range', '14')) - def set_token(self, token): - self.token = token - self.data.config_set('fflogs', 'token', self.token) - - def get_token(self): + @property + def token(self): try: return self.data.config_get('fflogs', 'token') except: return None - async def _http(self, url, is_json=True): + @token.setter + def token(self, token): + self.data.config_set('fflogs', 'token', token) + + async def _http(self, url, is_json=True, headers=None): try: # 使用 aiohttp 库发送最终的请求 async with aiohttp.ClientSession() as sess: - async with sess.get(url) as response: + async with sess.get(url, headers=headers) as response: + if response.status == 401: + raise AuthException('Token 有误,无法获取数据') if response.status != 200: # 如果 HTTP 响应状态码不是 200,说明调用失败 return None @@ -48,6 +53,73 @@ async def _http(self, url, is_json=True): # 抛出上面任何异常,说明调用失败 return None + async def _get_one_day_ranking( + self, boss, difficulty, job, date: datetime + ): + """ 获取指定 boss,指定职业,指定一天中的排名数据 + """ + # 查看是否有缓存 + cache_name = f'{boss}_{difficulty}_{job}_{date.strftime("%Y%m%d")}' + if self.data.exists(f'{cache_name}.pkl'): + return self.data.load_pkl(cache_name) + + page = 1 + hasMorePages = True + rankings = [] + + end_date = date + timedelta(days=1) + # 转换成 API 支持的时间戳格式 + start_timestamp = int(date.timestamp()) * 1000 + end_timestamp = int(end_date.timestamp()) * 1000 + + while hasMorePages: + rankings_url = f'{self.base_url}/rankings/encounter/{boss}?metric=rdps&difficulty={difficulty}&spec={job}&page={page}&filter=date.{start_timestamp}.{end_timestamp}&api_key={self.token}' + + res = await self._http(rankings_url) + + if not res: + raise DataException('服务器没有正确返回数据') + + hasMorePages = res['hasMorePages'] + rankings += res['rankings'] + page += 1 + + # 如果获取数据的日期不是当天,则缓存数据 + # 因为今天的数据可能还会增加,不能先缓存 + if end_date < datetime.now(): + self.data.save_pkl(rankings, cache_name) + + return rankings + + async def _get_whole_ranking( + self, boss, difficulty, job, dps_type: str, date: datetime + ): + date = datetime(year=date.year, month=date.month, day=date.day) + + rankings = [] + for _ in range(self.range): + rankings += await self._get_one_day_ranking( + boss, difficulty, job, date + ) + date -= timedelta(days=1) + + # 根据 DPS 类型进行排序,并提取数据 + if dps_type == 'rdps': + rankings.sort(key=lambda x: x['total'], reverse=True) + rankings = [i['total'] for i in rankings] + + if dps_type == 'adps': + rankings.sort( + key=lambda x: x['other_per_second_amount'], reverse=True + ) + rankings = [i['other_per_second_amount'] for i in rankings] + + if dps_type == 'pdps': + rankings.sort(key=lambda x: x['raw_dps'], reverse=True) + rankings = [i['raw_dps'] for i in rankings] + + return rankings + async def zones(self): """ 副本 """ @@ -55,31 +127,49 @@ async def zones(self): data = await self._http(url) return data + async def classes(self): + """ 职业 + """ + url = f'{self.base_url}/classes?api_key={self.token}' + data = await self._http(url) + return data + async def dps(self, boss, job, dps_type='rdps'): """ 查询 DPS 百分比排名 """ - (boss_bucket, boss_id), boss_name = get_boss_info(boss) - if not boss_bucket: + boss_id, difficulty, boss_name = get_boss_info(boss) + if not boss_id: return f'找不到 {boss} 的数据,请换个名字试试' - job_name_en, job_name_cn = get_job_name(job) - if not job_name_en: + job_id, job_name = get_job_name(job) + if not job_id: return f'找不到 {job} 的数据,请换个名字试试' - fflogs_url = f'https://cn.fflogs.com/zone/statistics/table/{boss_bucket}/dps/{boss_id}/100/8/5/100/1/{self.range}/{self.version}/Global/{job_name_en}/All/0/normalized/single/0/-1/?keystone=15&dpstype={dps_type}' - - res = await self._http(fflogs_url, is_json=False) + if dps_type not in ['adps', 'rdps', 'pdps']: + return f'找不到类型为 {dps_type} 的数据,只支持 adps rdps pdps' + # 排名从前一天开始排,因为今天的数据并不全 + date = datetime.now() - timedelta(days=1) + try: + rankings = await self._get_whole_ranking( + boss_id, difficulty, job_id, dps_type, date + ) + except DataException as e: + return f'{e},请稍后再试' + except AuthException as e: + return f'{e},请检查 Token' + + reply = f'{boss_name} {job_name} 的数据({dps_type})' + + total = len(rankings) + reply += f'\n数据总数:{total} 条' + # 计算百分比的 DPS percentage_list = [100, 99, 95, 75, 50, 25, 10] - reply = f'{boss_name} {job_name_cn} 的数据({dps_type})' for perc in percentage_list: - if perc == 100: - re_str = ('series' + r'.data.push\((\d*\.?\d*)\)') - else: - re_str = (f'series{perc}' + r'.data.push\((\d*\.?\d*)\)') - ptn = re.compile(re_str) - find_res = float(ptn.findall(res)[0]) - reply += f'\n{perc}% : {find_res:.2f}' + number = math.floor(total * 0.01 * (100 - perc)) + dps = float(rankings[number]) + reply += f'\n{perc}% : {dps:.2f}' + return reply diff --git a/src/plugins/fflogs/data.py b/src/plugins/fflogs/data.py index f7cec05b..65c3c19a 100644 --- a/src/plugins/fflogs/data.py +++ b/src/plugins/fflogs/data.py @@ -1,45 +1,53 @@ """ 一些数据 """ boss_list = { - (28, 1045): ['缇坦妮雅', '妖精', '极妖精', '妖灵王', '妖精王', '老婆', '10王'], - (28, 1046): ['无瑕灵君', '肥宅', '极肥宅', '全能王'], - (28, 1049): ['哈迪斯', '老公'] + (1045, 0): ['提坦妮雅歼殛战', '缇坦妮雅', '妖精', '极妖精', '妖灵王', '妖精王', '老婆', '10王'], + (1046, 0): ['无瑕灵君', '肥宅', '极肥宅', '全能王'], + (1049, 0): ['哈迪斯', '老公'], + (65, 100): ['至尊伊甸', 'E1', 'e1'], + (66, 100): ['虚空行者','E2', 'e2'], + (67, 100): ['利维亚桑','E3', 'e3'], + (68, 100): ['泰坦','E4', 'e4'], + (65, 0): ['至尊伊甸','E1S', 'e1s', 'E1s', 'e1S'], + (66, 0): ['虚空行者','E2S', 'e2s', 'E2s', 'e2S'], + (67, 0): ['利维亚桑','E3S', 'e3s', 'E3s', 'e3S'], + (68, 0): ['泰坦','E4S', 'e4s', 'E4s', 'e4S'], } # yapf: disable job_list = { - 'Astrologian': ['占星术士', '占星'], - 'Bard': ['吟游诗人', '诗人'], - 'BlackMage': ['黑魔法师', '黑魔', '伏地魔', '永动机'], - 'Dancer': ['舞者', '舞娘'], - 'DarkKnight': ['暗黑骑士', '黑骑', '暗骑', 'DK'], - 'Dragoon': ['龙骑士', '龙骑', '躺尸龙', '擦炮工'], - 'Gunbreaker': ['绝枪战士', '绝枪', '枪刃', '枪决战士'], - 'Machinist': ['机工士', '机工'], - 'Monk': ['武僧', '扫地僧', '猴子', '和尚'], - 'Ninja': ['忍者', '兔忍', '火影'], - 'Paladin': ['骑士', '圣骑', '奶骑'], - 'RedMage': ['赤魔法师', '赤魔', '吃馍', '红色治疗'], - 'Samurai': ['武士', '侍'], - 'Scholar': ['学者', '小仙女', '死炎法师'], - 'Summoner': ['召唤师', '召唤'], - 'Warrior': ['战士', '战爹'], - 'WhiteMage': ['白魔法师', '白魔', '白膜', '投石机'], + 1: ['占星术士', '占星'], + 2: ['吟游诗人', '诗人'], + 3: ['黑魔法师', '黑魔', '伏地魔', '永动机'], + 4: ['暗黑骑士', '黑骑', '暗骑', 'DK'], + 5: ['龙骑士', '龙骑', '躺尸龙', '擦炮工'], + 6: ['机工士', '机工'], + 7: ['武僧', '扫地僧', '猴子', '和尚'], + 8: ['忍者', '兔忍', '火影'], + 9: ['骑士', '圣骑', '奶骑'], + 10: ['学者', '小仙女', '死炎法师'], + 11: ['召唤师', '召唤'], + 12: ['战士', '战爹'], + 13: ['白魔法师', '白魔', '白膜', '投石机'], + 14: ['赤魔法师', '赤魔', '吃馍', '红色治疗'], + 15: ['武士', '侍'], + 16: ['舞者', '舞娘'], + 17: ['绝枪战士', '绝枪', '枪刃', '枪决战士'], } # yapf: disable def get_boss_info(name): - """ 根据昵称获取 boss 的 bucket 和 id + """ 根据昵称获取 boss 的 ID 同时返回正式名称 """ - for boss, nickname in boss_list.items(): + for (boss_id, difficulty), nickname in boss_list.items(): if name in nickname: - return boss, nickname[0] - return (None, None), None + return boss_id, difficulty, nickname[0] + return None, None, None def get_job_name(name): - """ 将中文称呼转换成英文称呼 + """ 将中文昵称转换成具体的 ID 同时返回正式名称 """ - for name_en, nickname in job_list.items(): + for job_id, nickname in job_list.items(): if name in nickname: - return name_en, nickname[0] + return job_id, nickname[0] return None, None diff --git a/src/plugins/fflogs/exceptions.py b/src/plugins/fflogs/exceptions.py new file mode 100644 index 00000000..bf16b243 --- /dev/null +++ b/src/plugins/fflogs/exceptions.py @@ -0,0 +1,14 @@ +""" 异常 +""" + + +class DataException(Exception): + """ 数据异常 + """ + pass + + +class AuthException(Exception): + """ 认证异常 + """ + pass