From 55beccb53ea299c87dbc34076d58fb6afd5d8022 Mon Sep 17 00:00:00 2001 From: zhxy-CN Date: Fri, 22 Jul 2022 20:39:14 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=E6=94=AF=E6=8C=81=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E7=82=B9=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨支持资源点查询 --- model/helpers.py | 4 +- plugins/map.py | 71 ++++++++ requirements.txt | 3 +- resources/icon/0.png | Bin 0 -> 13882 bytes resources/icon/box.png | Bin 0 -> 9703 bytes resources/icon/box_alpha.png | Bin 0 -> 2986 bytes service/map.py | 313 +++++++++++++++++++++++++++++++++++ 7 files changed, 388 insertions(+), 3 deletions(-) create mode 100644 plugins/map.py create mode 100644 resources/icon/0.png create mode 100644 resources/icon/box.png create mode 100644 resources/icon/box_alpha.png create mode 100644 service/map.py diff --git a/model/helpers.py b/model/helpers.py index 4b16cadf..8313d489 100644 --- a/model/helpers.py +++ b/model/helpers.py @@ -16,7 +16,7 @@ from service.cache import RedisCache USER_AGENT: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " \ "Chrome/90.0.4430.72 Safari/537.36" -headers: dict = {'User-Agent': USER_AGENT} +REQUEST_HEADERS: dict = {'User-Agent': USER_AGENT} current_dir = os.getcwd() cache_dir = os.path.join(current_dir, "cache") if not os.path.exists(cache_dir): @@ -56,7 +56,7 @@ async def url_to_file(url: str, prefix: str = "file://") -> str: temp_file_name = url_sha1 + extension file_dir = os.path.join(cache_dir, temp_file_name) if not os.path.exists(file_dir): - async with httpx.AsyncClient(headers=headers) as client: + async with httpx.AsyncClient(headers=REQUEST_HEADERS) as client: try: data = await client.get(url) except UnsupportedProtocol as error: diff --git a/plugins/map.py b/plugins/map.py new file mode 100644 index 00000000..adbdbb4c --- /dev/null +++ b/plugins/map.py @@ -0,0 +1,71 @@ +from os import sep + +from PIL import Image +from telegram import Update +from telegram.constants import ChatAction +from telegram.ext import CommandHandler, MessageHandler, filters + +from logger import Log +from manager import listener_plugins_class +from plugins.base import BasePlugins, restricts +from plugins.errorhandler import conversation_error_handler +from service.map import MapHelper +from utils.base import PaimonContext + + +@listener_plugins_class() +class Map(BasePlugins): + + def __init__(self): + self.init_resource_map = False + self.map_helper = MapHelper() + + @classmethod + def create_handlers(cls) -> list: + map_res = cls() + return [ + CommandHandler("map", map_res.command_start, block=False), + MessageHandler(filters.Regex(r"^资源点查询(.*)"), map_res.command_start, block=True) + ] + + async def init_point_list_and_map(self): + Log.info("正在初始化地图资源节点") + if not self.init_resource_map: + await self.map_helper.init_point_list_and_map() + self.init_resource_map = True + + @conversation_error_handler + @restricts(restricts_time=20) + async def command_start(self, update: Update, context: PaimonContext): + message = update.message + args = context.args + user = update.effective_user + if not self.init_resource_map: + await self.init_point_list_and_map() + await message.reply_chat_action(ChatAction.TYPING) + if len(args) >= 1: + resource_name = args[0] + else: + Log.info(f"用户: {user.full_name} [{user.id}] 使用了 map 命令") + await message.reply_text("请输入要查找的资源,或私聊派蒙发送 `/map list` 查看资源列表", parse_mode="Markdown") + return + if resource_name in ("list", "列表"): + if filters.ChatType.GROUPS.filter(message): + reply_message = await message.reply_text("请私聊派蒙使用该命令") + self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 300) + self._add_delete_message_job(context, message.chat_id, message.message_id, 300) + return + Log.info(f"用户: {user.full_name} [{user.id}] 使用 map 命令查询了 资源列表") + text = self.map_helper.get_resource_list_mes() + await message.reply_text(text) + return + Log.info(f"用户: {user.full_name} [{user.id}] 使用 map 命令查询了 {resource_name}") + text = await self.map_helper.get_resource_map_mes(resource_name) + if "不知道" in text or "没有找到" in text: + await message.reply_text(text, parse_mode="Markdown") + return + img = Image.open(f"cache{sep}map.jpg") + if img.size[0] > 2048 or img.size[1] > 2048: + await message.reply_document(open(f"cache{sep}map.jpg", mode='rb+'), caption=text) + else: + await message.reply_photo(open(f"cache{sep}map.jpg", mode='rb+'), caption=text) diff --git a/requirements.txt b/requirements.txt index 5d70cbc2..7c90f692 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,5 @@ lxml>=4.9.0 fakeredis>=1.8.1 aiohttp<=3.8.1 python-telegram-bot==20.0a2 -pytz>=2021.3 \ No newline at end of file +pytz>=2021.3 +Pillow \ No newline at end of file diff --git a/resources/icon/0.png b/resources/icon/0.png new file mode 100644 index 0000000000000000000000000000000000000000..bdf2090bc9e19355111b3f839cb09c157e6a6e2d GIT binary patch literal 13882 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 literal 0 HcmV?d00001 diff --git a/resources/icon/box_alpha.png b/resources/icon/box_alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..0c634fa2905065a7af818d62f6444bd983bc1e84 GIT binary patch literal 2986 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 NYw*ZKr(W9;{a+{<%0vJF literal 0 HcmV?d00001 diff --git a/service/map.py b/service/map.py new file mode 100644 index 00000000..db99ee70 --- /dev/null +++ b/service/map.py @@ -0,0 +1,313 @@ +import os +import time +from io import BytesIO +from typing import Optional, List + +import httpx +import ujson +from PIL import Image, ImageMath + +from model.helpers import REQUEST_HEADERS + +Image.MAX_IMAGE_PIXELS = None + +ZOOM = 0.5 +RESOURCE_ICON_OFFSET = (-int(150 * 0.5 * ZOOM), -int(150 * ZOOM)) + + +class MapHelper: + LABEL_URL = 'https://api-static.mihoyo.com/common/blackboard/ys_obc/v1/map/label/tree?app_sn=ys_obc' + POINT_LIST_URL = 'https://api-static.mihoyo.com/common/blackboard/ys_obc/v1/map/point/list?map_id=2&app_sn=ys_obc' + MAP_URL = 'https://api-static.mihoyo.com/common/map_user/ys_obc/v1/map/info?map_id=2&app_sn=ys_obc&lang=zh-cn' + + def __init__(self, cache_dir_name: str = "cache"): + self._current_dir = os.getcwd() + self._output_dir = os.path.join(self._current_dir, cache_dir_name) + self._resources_icon_dir = os.path.join(self._current_dir, "resources", "icon") + self._cache_dir = os.path.join(self._current_dir, "cache") + self._map_dir = os.path.join(self._cache_dir, "map_icon.jpg") + self.client = httpx.AsyncClient(headers=REQUEST_HEADERS, timeout=10.0) + self.all_resource_type: dict = {} + """这个字典保存所有资源类型 + + "1": { + "id": 1, + "name": "传送点", + "icon": "", + "parent_id": 0, + "depth": 1, + "node_type": 1, + "jump_type": 0, + "jump_target_id": 0, + "display_priority": 0, + "children": [] + } + """ + self.can_query_type_list: dict = {} + """这个字典保存所有可以查询的资源类型名称和ID,这个字典只有名称和ID + + 上边字典里"depth": 2的类型才可以查询,"depth": 1的是1级目录,不能查询 + + "七天神像":"2" + + "风神瞳":"5" + """ + self.all_resource_point_list: list = [] + """这个列表保存所有资源点的数据 + + { + "id": 2740, + "label_id": 68, + "x_pos": -1789, + "y_pos": 2628, + "author_name": "作者名称", + "ctime": "更新时间", + "display_state": 1 + } + """ + self.date: str = "" + """记录上次更新"all_resource_point_list"的日期 + """ + + self.center: Optional[List[float]] = None + """center + """ + + self.map_icon: Optional[Image] = None + """map_icon + """ + + async def download_icon(self, url): + """下载图片 返回Image对象 + :param url: + :return: + """ + resp = await self.client.get(url=url) + if resp.status_code != 200: + raise ValueError(f"获取图片数据失败,错误代码 {resp.status_code}") + icon = resp.content + return Image.open(BytesIO(icon)) + + async def download_json(self, url): + """ + 获取资源数据,返回 JSON + :param url: + :return: dict + """ + resp = await self.client.get(url=url) + if resp.status_code != 200: + raise RuntimeError(f"获取资源点数据失败,错误代码 {resp.status_code}") + return resp.json() + + async def init_point_list_and_map(self): + await self.up_label_and_point_list() + await self.up_map() + + async def up_map(self): + """更新地图文件 并按照资源点的范围自动裁切掉不需要的地方 + 裁切地图需要最新的资源点位置,所以要先调用 up_label_and_point_list 再更新地图 + :return: None + """ + map_info = await self.download_json(self.MAP_URL) + map_info = map_info["data"]["info"]["detail"] + map_info = ujson.loads(map_info) + + map_url_list = map_info['slices'][0] + origin = map_info["origin"] + + x_start = map_info['total_size'][1] + y_start = map_info['total_size'][1] + x_end = 0 + y_end = 0 + for resource_point in self.all_resource_point_list: + x_pos = resource_point["x_pos"] + origin[0] + y_pos = resource_point["y_pos"] + origin[1] + x_start = min(x_start, x_pos) + y_start = min(y_start, y_pos) + x_end = max(x_end, x_pos) + y_end = max(y_end, y_pos) + + x_start -= 200 + y_start -= 200 + x_end += 200 + y_end += 200 + + self.center = [origin[0] - x_start, origin[1] - y_start] + x = int(x_end - x_start) + y = int(y_end - y_start) + self.map_icon = Image.new("RGB", (x, y)) + x_offset = 0 + for i in map_url_list: + map_url = i["url"] + map_icon = await self.download_icon(map_url) + self.map_icon.paste(map_icon, (int(-x_start) + x_offset, int(-y_start))) + x_offset += map_icon.size[0] + + async def up_label_and_point_list(self): + """更新label列表和资源点列表 + :return: + """ + label_data = await self.download_json(self.LABEL_URL) + for label in label_data["data"]["tree"]: + self.all_resource_type[str(label["id"])] = label + for sublist in label["children"]: + self.all_resource_type[str(sublist["id"])] = sublist + self.can_query_type_list[sublist["name"]] = str(sublist["id"]) + await self.up_icon_image(sublist) + label["children"] = [] + test = await self.download_json(self.POINT_LIST_URL) + self.all_resource_point_list = test["data"]["point_list"] + self.date = time.strftime("%d") + + async def up_icon_image(self, sublist: dict): + """检查是否有图标,没有图标下载保存到本地 + :param sublist: + :return: + """ + icon_id = sublist["id"] + icon_path = os.path.join(self._cache_dir, f"{icon_id}.png") + + if not os.path.exists(icon_path): + icon_url = sublist["icon"] + icon = await self.download_icon(icon_url) + icon = icon.resize((150, 150)) + + box_alpha = Image.open( + os.path.join(os.path.dirname(__file__), os.path.pardir, + "resources", "icon", "box_alpha.png")).getchannel("A") + box = Image.open(os.path.join(os.path.dirname(__file__), os.path.pardir, "resources", "icon", "box.png")) + + try: + icon_alpha = icon.getchannel("A") + icon_alpha = ImageMath.eval("convert(a*b/256, 'L')", a=icon_alpha, b=box_alpha) + except ValueError: + # 米游社的图有时候会没有alpha导致报错,这时候直接使用box_alpha当做alpha就行 + icon_alpha = box_alpha + + icon2 = Image.new("RGBA", (150, 150), "#00000000") + icon2.paste(icon, (0, -10)) + + bg = Image.new("RGBA", (150, 150), "#00000000") + bg.paste(icon2, mask=icon_alpha) + bg.paste(box, mask=box) + + with open(icon_path, "wb") as icon_file: + bg.save(icon_file) + + async def get_resource_map_mes(self, name): + if self.date != time.strftime("%d"): + await self.init_point_list_and_map() + if name not in self.can_query_type_list: + return f"派蒙还不知道 {name} 在哪里呢,可以发送 `/map list` 查看资源列表" + resource_id = self.can_query_type_list[name] + map_res = ResourceMap(self.all_resource_point_list, self.map_icon, self.center, resource_id) + count = map_res.get_resource_count() + if not count: + return f"派蒙没有找到 {name} 的位置,可能米游社wiki还没更新" + map_res.gen_jpg() + return f"派蒙一共找到 {name} 的 {count} 个位置点\n* 数据来源于米游社wiki" + + def get_resource_list_mes(self): + temp = {list_id: [] for list_id in self.all_resource_type if self.all_resource_type[list_id]["depth"] == 1} + + for list_id in self.all_resource_type: + # 再找2级目录 + if self.all_resource_type[list_id]["depth"] == 2: + temp[str(self.all_resource_type[list_id]["parent_id"])].append(list_id) + mes = "当前资源列表如下:\n" + + for resource_type_id, value in temp.items(): + if resource_type_id in ["1", "12", "50", "51", "95", "131"]: + # 在游戏里能查到的数据这里就不列举了,不然消息太长了 + continue + mes += f"{self.all_resource_type[resource_type_id]['name']}:" + for resource_id in value: + mes += f"{self.all_resource_type[resource_id]['name']}," + mes += "\n" + return mes + + +class ResourceMap: + + def __init__(self, all_resource_point_list: List[dict], map_icon: Image, center: List[float], resource_id: int): + self.all_resource_point_list = all_resource_point_list + self.resource_id = resource_id + self.center = center + self.map_image = map_icon.copy() + self.map_size = self.map_image.size + # 地图要要裁切的左上角和右下角坐标 + # 这里初始化为地图的大小 + self.x_start = self.map_size[0] + self.y_start = self.map_size[1] + self.x_end = 0 + self.y_end = 0 + resource_icon = Image.open(self.get_icon_path()) + self.resource_icon = resource_icon.resize((int(150 * ZOOM), int(150 * ZOOM))) + self.resource_xy_list = self.get_resource_point_list() + + def get_icon_path(self): + # 检查有没有图标,有返回正确图标,没有返回默认图标 + icon_path = os.path.join(os.path.dirname(__file__), os.path.pardir, + "resources", "icon", f"{self.resource_id}.png") + if os.path.exists(icon_path): + return icon_path + return os.path.join(os.path.dirname(__file__), os.path.pardir, "resources", "icon", "0.png") + + def get_resource_point_list(self): + temp_list = [] + for resource_point in self.all_resource_point_list: + if str(resource_point["label_id"]) == self.resource_id: + # 获取xy坐标,然后加上中心点的坐标完成坐标转换 + x = resource_point["x_pos"] + self.center[0] + y = resource_point["y_pos"] + self.center[1] + temp_list.append((int(x), int(y))) + return temp_list + + def paste(self): + for x, y in self.resource_xy_list: + # 把资源图片贴到地图上 + # 这时地图已经裁切过了,要以裁切后的地图左上角为中心再转换一次坐标 + x -= self.x_start + y -= self.y_start + self.map_image.paste(self.resource_icon, (x + RESOURCE_ICON_OFFSET[0], y + RESOURCE_ICON_OFFSET[1]), + self.resource_icon) + + def crop(self): + # 把大地图裁切到只保留资源图标位置 + for x, y in self.resource_xy_list: + # 找出4个方向最远的坐标,用于后边裁切 + self.x_start = min(x, self.x_start) + self.y_start = min(y, self.y_start) + self.x_end = max(x, self.x_end) + self.y_end = max(y, self.y_end) + + # 先把4个方向扩展150像素防止把资源图标裁掉 + self.x_start -= 150 + self.y_start -= 150 + self.x_end += 150 + self.y_end += 150 + + # 如果图片裁切得太小会看不出资源的位置在哪,检查图片裁切的长和宽看够不够1000,不到1000的按1000裁切 + if (self.x_end - self.x_start) < 1000: + center = int((self.x_end + self.x_start) / 2) + self.x_start = center - 500 + self.x_end = center + 500 + if (self.y_end - self.y_start) < 1000: + center = int((self.y_end + self.y_start) / 2) + self.y_start = center - 500 + self.y_end = center + 500 + + self.map_image = self.map_image.crop((self.x_start, self.y_start, + self.x_end, self.y_end)) + + def gen_jpg(self): + if not self.resource_xy_list: + return "没有这个资源的信息" + if not os.path.exists("cache"): + os.mkdir("cache") # 查找 cache 目录 (缓存目录) 是否存在,如果不存在则创建 + self.crop() + self.paste() + self.map_image.save(f'cache{os.sep}map.jpg', format='JPEG') + + def get_resource_count(self): + return len(self.resource_xy_list)