diff --git a/assets/icon/0.png b/assets/icon/0.png new file mode 100644 index 0000000..bdf2090 Binary files /dev/null and b/assets/icon/0.png differ diff --git a/assets/icon/box.png b/assets/icon/box.png new file mode 100644 index 0000000..dadec34 Binary files /dev/null and b/assets/icon/box.png differ diff --git a/assets/icon/box_alpha.png b/assets/icon/box_alpha.png new file mode 100644 index 0000000..0c634fa Binary files /dev/null and b/assets/icon/box_alpha.png differ diff --git a/defs/artifact_rate.py b/defs/artifact_rate.py new file mode 100644 index 0000000..296cbe7 --- /dev/null +++ b/defs/artifact_rate.py @@ -0,0 +1,39 @@ +import requests +import json + +ocr_url = "https://api.genshin.pub/api/v1/app/ocr" +rate_url = "https://api.genshin.pub/api/v1/relic/rate" +head = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67 ", + "Content-Type": "application/json; charset=UTF-8", + "Connection": "close" +} + + +async def get_artifact_attr(b64_str): + upload_json = json.dumps( + { + "image": b64_str + } + ) + try: + req = requests.post(ocr_url, data=upload_json, headers=head, timeout=8) + except requests.exceptions.RequestException as e: + raise e + data = json.loads(req.text) + if req.status_code != 200: + return {"err": "未知错误", "full": data} + return data + + +async def rate_artifact(artifact_attr: dict): + upload_json_str = json.dumps(artifact_attr, ensure_ascii=False).encode('utf-8') + try: + req = requests.post(rate_url, data=upload_json_str, headers=head, timeout=8) + except requests.exceptions.RequestException as e: + raise e + data = json.loads(req.text) + if req.status_code != 200: + return {"err": "未知错误", "full": data} + return data diff --git a/defs/query_resource_points.py b/defs/query_resource_points.py new file mode 100644 index 0000000..98d87d4 --- /dev/null +++ b/defs/query_resource_points.py @@ -0,0 +1,315 @@ +from PIL import Image, ImageMath +from io import BytesIO +import json +import os +import time +import httpx +import asyncio + +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" + +header = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36' + +FILE_PATH = "assets" + +MAP_PATH = os.path.join(FILE_PATH, "icon", "map_icon.jpg") +Image.MAX_IMAGE_PIXELS = None + +CENTER = None +MAP_ICON = None + +zoom = 0.5 +resource_icon_offset = (-int(150 * 0.5 * zoom), -int(150 * zoom)) + +data = { + "all_resource_type": { + # 这个字典保存所有资源类型, + # "1": { + # "id": 1, + # "name": "传送点", + # "icon": "", + # "parent_id": 0, + # "depth": 1, + # "node_type": 1, + # "jump_type": 0, + # "jump_target_id": 0, + # "display_priority": 0, + # "children": [] + # }, + }, + "can_query_type_list": { + # 这个字典保存所有可以查询的资源类型名称和ID,这个字典只有名称和ID + # 上边字典里"depth": 2的类型才可以查询,"depth": 1的是1级目录,不能查询 + # "七天神像":"2" + # "风神瞳":"5" + + }, + "all_resource_point_list": [ + # 这个列表保存所有资源点的数据 + # { + # "id": 2740, + # "label_id": 68, + # "x_pos": -1789, + # "y_pos": 2628, + # "author_name": "✟紫灵心✟", + # "ctime": "2020-10-29 10:41:21", + # "display_state": 1 + # }, + ], + "date": "" # 记录上次更新"all_resource_point_list"的日期 +} + + +async def download_icon(url): + # 下载图片,返回Image对象 + async with httpx.AsyncClient() as client: + resp = await 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(url): + # 获取资源数据,返回 JSON + async with httpx.AsyncClient() as client: + resp = await client.get(url=url) + if resp.status_code != 200: + raise ValueError(f"获取资源点数据失败,错误代码 {resp.status_code}") + return resp.json() + + +async def up_icon_image(sublist): + # 检查是否有图标,没有图标下载保存到本地 + id = sublist["id"] + icon_path = os.path.join(FILE_PATH, "icon", f"{id}.png") + + if not os.path.exists(icon_path): + icon_url = sublist["icon"] + icon = await download_icon(icon_url) + icon = icon.resize((150, 150)) + + box_alpha = Image.open(os.path.join(FILE_PATH, "icon", "box_alpha.png")).getchannel("A") + box = Image.open(os.path.join(FILE_PATH, "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 up_label_and_point_list(): + # 更新label列表和资源点列表 + label_data = await download_json(LABEL_URL) + for label in label_data["data"]["tree"]: + data["all_resource_type"][str(label["id"])] = label + for sublist in label["children"]: + data["all_resource_type"][str(sublist["id"])] = sublist + data["can_query_type_list"][sublist["name"]] = str(sublist["id"]) + await up_icon_image(sublist) + label["children"] = [] + + test = await download_json(POINT_LIST_URL) + data["all_resource_point_list"] = test["data"]["point_list"] + data["date"] = time.strftime("%d") + + +async def up_map(): + # 更新地图文件 并按照资源点的范围自动裁切掉不需要的地方 + # 裁切地图需要最新的资源点位置,所以要先调用 up_label_and_point_list 再更新地图 + global CENTER + global MAP_ICON + map_info = await download_json(MAP_URL) + map_info = map_info["data"]["info"]["detail"] + map_info = json.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 data["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 + + CENTER = [origin[0] - x_start, origin[1] - y_start] + x = int(x_end - x_start) + y = int(y_end - y_start) + MAP_ICON = Image.new("RGB", (x, y)) + x_offset = 0 + for i in map_url_list: + map_url = i["url"] + map_icon = await download_icon(map_url) + MAP_ICON.paste(map_icon, + (int(-x_start) + x_offset, int(-y_start))) + x_offset += map_icon.size[0] + + +async def init_point_list_and_map(): + await up_label_and_point_list() + await up_map() + + +class Resource_map(object): + + def __init__(self, resource_name): + self.resource_id = str(data["can_query_type_list"][resource_name]) + + # self.map_image = Image.open(MAP_PATH) + 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 + + self.resource_icon = Image.open(self.get_icon_path()) + self.resource_icon = self.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(FILE_PATH, "icon", f"{self.resource_id}.png") + + if os.path.exists(icon_path): + return icon_path + else: + return os.path.join(FILE_PATH, "icon", "0.png") + + def get_resource_point_list(self): + temp_list = [] + for resource_point in data["all_resource_point_list"]: + if str(resource_point["label_id"]) == self.resource_id: + # 获取xy坐标,然后加上中心点的坐标完成坐标转换 + x = resource_point["x_pos"] + CENTER[0] + y = resource_point["y_pos"] + 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 "没有这个资源的信息" + + self.crop() + + self.paste() + + self.map_image.save(f'temp{os.sep}map.jpg', format='JPEG') + + def get_resource_count(self): + return len(self.resource_xy_list) + + +async def get_resource_map_mes(name): + if data["date"] != time.strftime("%d"): + await init_point_list_and_map() + + if not (name in data["can_query_type_list"]): + return f"没有 {name} 这种资源。\n发送 资源列表 查看所有资源名称" + + map = Resource_map(name) + count = map.get_resource_count() + + if not count: + return f"没有找到 {name} 资源的位置,可能米游社wiki还没更新。" + + map.gen_jpg() + mes = f"资源 {name} 的位置如图所示" + mes += f"\n\n※ {name} 一共找到 {count} 个位置点\n※ 数据来源于米游社wiki" + + return mes + + +def get_resource_list_mes(): + temp = {} + + for id in data["all_resource_type"].keys(): + # 先找1级目录 + if data["all_resource_type"][id]["depth"] == 1: + temp[id] = [] + + for id in data["all_resource_type"].keys(): + # 再找2级目录 + if data["all_resource_type"][id]["depth"] == 2: + temp[str(data["all_resource_type"][id]["parent_id"])].append(id) + + mes = "当前资源列表如下:\n" + + for resource_type_id in temp.keys(): + + if resource_type_id in ["1", "12", "50", "51", "95", "131"]: + # 在游戏里能查到的数据这里就不列举了,不然消息太长了 + continue + + mes += f"{data['all_resource_type'][resource_type_id]['name']}:" + for resource_id in temp[resource_type_id]: + mes += f"{data['all_resource_type'][resource_id]['name']}," + mes += "\n" + + return mes diff --git a/init.py b/init.py new file mode 100644 index 0000000..03a7e84 --- /dev/null +++ b/init.py @@ -0,0 +1,13 @@ +from typing import Coroutine +import asyncio +from defs.query_resource_points import init_point_list_and_map + + +def sync(coroutine: Coroutine): + loop = asyncio.get_event_loop() + return loop.run_until_complete(coroutine) + + +print("初始化资源点列表和资源点映射...") +sync(init_point_list_and_map()) +print("初始化完成") diff --git a/plugins/artifact_rate.py b/plugins/artifact_rate.py new file mode 100644 index 0000000..8bb48ee --- /dev/null +++ b/plugins/artifact_rate.py @@ -0,0 +1,44 @@ +import requests +from pyrogram import Client +from pyrogram.types import Message +from defs.artifact_rate import * +from base64 import b64encode +from os import remove + + +def get_format_sub_item(artifact_attr): + msg = "" + for i in artifact_attr["sub_item"]: + msg += f'{i["name"]:\u3000<6} | {i["value"]}\n' + return msg + + +async def artifact_rate_msg(client: Client, message: Message): + if not message.photo: + return await message.reply("图呢?\n*请命令将与截图一起发送", quote=True) + msg = await message.reply("正在下载图片。。。", quote=True) + path = await message.download() + with open(path, "rb") as f: + image_b64 = b64encode(f.read()).decode() + remove(path) + try: + artifact_attr = await get_artifact_attr(image_b64) + except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): + return await msg.edit("连接超时") + if 'err' in artifact_attr.keys(): + err_msg = artifact_attr["full"]["message"] + return await msg.edit(f"发生了点小错误:\n{err_msg}") + await msg.edit("识图成功!\n正在评分中...") + rate_result = await rate_artifact(artifact_attr) + if 'err' in rate_result.keys(): + err_msg = rate_result["full"]["message"] + return await msg.edit(f"发生了点小错误:\n{err_msg}") + format_result = f'圣遗物评分结果:\n' \ + f'主属性:{artifact_attr["main_item"]["name"]}\n' \ + f'{get_format_sub_item(artifact_attr)}'\ + f'`------------------------------`\n' \ + f'总分:{rate_result["total_percent"]}\n'\ + f'主词条:{rate_result["main_percent"]}\n' \ + f'副词条:{rate_result["sub_percent"]}\n' \ + f'评分、识图均来自 genshin.pub' + await msg.edit(format_result) diff --git a/plugins/process.py b/plugins/process.py index 02aa1fc..20cacac 100644 --- a/plugins/process.py +++ b/plugins/process.py @@ -7,6 +7,8 @@ from plugins.challenge import tf_msg, wq_msg, zb_msg from plugins.character import character_msg, mz_msg from plugins.weapons import weapon_msg from plugins.fortunate import fortunate_msg, set_fortunate_img +from plugins.artifact_rate import artifact_rate_msg +from plugins.query_resource_points import inquire_resource_points, inquire_resource_list from plugins.mys import mys_msg, promote_command from defs.log import log @@ -35,12 +37,12 @@ async def process_private_msg(client: Client, message: Message): # 天赋 if '天赋' in message.text: await tf_msg(client, message) - await log(client, message, '查询角色天赋') - # 武器查询 - if '武器资料' in message.text or '武器查询' in message.text: - await weapon_msg(client, message) - await log(client, message, '查询武器资料') - return + await log(client, message, '查询天赋副本') + # # 武器查询 + # if '武器资料' in message.text or '武器查询' in message.text: + # await weapon_msg(client, message) + # await log(client, message, '查询武器资料') + # return # 副本武器 if '武器' in message.text: await wq_msg(client, message) @@ -49,14 +51,14 @@ async def process_private_msg(client: Client, message: Message): if message.text == '周本': await zb_msg(client, message) await log(client, message, '查询周本') - # 角色查询 - if '角色资料' in message.text or '角色简介' in message.text or '角色查询' in message.text: - await character_msg(client, message) - await log(client, message, '查询角色资料') - # 命座查询 - if '命座' in message.text: - await mz_msg(client, message) - await log(client, message, '查询角色命座') + # # 角色查询 + # if '角色资料' in message.text or '角色简介' in message.text or '角色查询' in message.text: + # await character_msg(client, message) + # await log(client, message, '查询角色资料') + # # 命座查询 + # if '命座' in message.text: + # await mz_msg(client, message) + # await log(client, message, '查询角色命座') # 设置运势 if '设置运势' in message.text: await set_fortunate_img(client, message) @@ -66,6 +68,17 @@ async def process_private_msg(client: Client, message: Message): if '运势' in message.text: await fortunate_msg(client, message) await log(client, message, '查询今日运势') + # 圣遗物评分 + if '圣遗物评分' in message.text: + await message.reply("图呢?\n*请将命令与截图一起发送", quote=True) + # 资源查询 + if '哪里有' in message.text: + await inquire_resource_points(client, message) + await log(client, message, '查询地图资源') + # 资源列表 + if '资源列表' in message.text: + await inquire_resource_list(client, message) + await log(client, message, '查询资源列表') # 账号信息(cookie 过期过快 不推荐启用) # if '账号信息' in message.text or '用户信息' in message.text: # await mys_msg(client, message) @@ -78,11 +91,11 @@ async def process_group_msg(client: Client, message: Message): # 帮助消息 if msg_list[0] == '/help': await help_command(client, message) - # 武器查询 - if text.startswith('武器查询') or text.startswith('武器资料'): - await weapon_msg(client, message) - await log(client, message, '查询武器资料') - return + # # 武器查询 + # if text.startswith('武器查询') or text.startswith('武器资料'): + # await weapon_msg(client, message) + # await log(client, message, '查询武器资料') + # return # 副本武器 if text[-2:] == '武器': await wq_msg(client, message) @@ -94,19 +107,19 @@ async def process_group_msg(client: Client, message: Message): # 天赋 if text[-2:] == '天赋': await tf_msg(client, message) - await log(client, message, '查询角色天赋') + await log(client, message, '查询天赋副本') # 周本 if message.text == '周本': await zb_msg(client, message) await log(client, message, '查询周本') - # 角色查询 - if text.startswith('角色资料') or text.startswith('角色简介') or text.startswith('角色查询'): - await character_msg(client, message) - await log(client, message, '查询角色资料') - # 命座查询 - if text.startswith('命座'): - await mz_msg(client, message) - await log(client, message, '查询角色命座') + # # 角色查询 + # if text.startswith('角色资料') or text.startswith('角色简介') or text.startswith('角色查询'): + # await character_msg(client, message) + # await log(client, message, '查询角色资料') + # # 命座查询 + # if text.startswith('命座'): + # await mz_msg(client, message) + # await log(client, message, '查询角色命座') # 运势查询 if text.startswith('运势') or text.startswith('今日运势'): await fortunate_msg(client, message) @@ -115,6 +128,26 @@ async def process_group_msg(client: Client, message: Message): if text.startswith('设置运势'): await set_fortunate_img(client, message) await log(client, message, '设置运势角色') + # 圣遗物评分 + if text.startswith('圣遗物评分'): + await message.reply("图呢?\n*请将命令与截图一起发送") + # 资源查询 + if text.startswith('哪里有') or text.endswith('哪里有'): + await inquire_resource_points(client, message) + await log(client, message, '查询地图资源') + # 资源列表 + if text.startswith('资源列表'): + await inquire_resource_list(client, message) + await log(client, message, '查询资源列表') + + +@Client.on_message(Filters.photo) +async def process_photo(client: Client, message: Message): + text = message.caption if message.caption else "" + + if text.startswith('圣遗物评分'): + await artifact_rate_msg(client, message) + await log(client, message, '圣遗物评分') @Client.on_message(Filters.new_chat_members) diff --git a/plugins/query_resource_points.py b/plugins/query_resource_points.py new file mode 100644 index 0000000..ac5b3b4 --- /dev/null +++ b/plugins/query_resource_points.py @@ -0,0 +1,29 @@ +from pyrogram import Client +from pyrogram.types import Message +from os import sep +from defs.query_resource_points import get_resource_map_mes, get_resource_list_mes, init_point_list_and_map + +init_resource_map = False + + +async def inquire_resource_points(client: Client, message: Message): + global init_resource_map + if not init_resource_map: + await init_point_list_and_map() + init_resource_map = True + resource_name = message.text.replace("哪里有", "").strip() + if resource_name == "": + return await message.reply("没有想好要找的资源吗?试试 `资源列表`", quote=True) + text = await get_resource_map_mes(resource_name) + if text.find("没有") != -1: + return await message.reply(text, quote=True) + await message.reply_photo(f"temp{sep}map.jpg", caption=text, quote=True) + + +async def inquire_resource_list(client: Client, message: Message): + global init_resource_map + if not init_resource_map: + await init_point_list_and_map() + init_resource_map = True + text = get_resource_list_mes() + await message.reply(text, quote=True) diff --git a/plugins/start.py b/plugins/start.py index 41a2238..1091ca9 100644 --- a/plugins/start.py +++ b/plugins/start.py @@ -7,12 +7,12 @@ from pyrogram.types import Message async def welcome_command(client: Client, message: Message): # 发送欢迎消息 - await message.reply('你好!我是原神小助手 - 派蒙 。') + await message.reply('你好!我是原神小助手 - 派蒙 。', quote=True) async def ping_command(client: Client, message: Message): # 提醒在线状态 - await message.reply("poi~") + await message.reply("poi~", quote=True) async def leave_command(client: Client, message: Message): @@ -39,19 +39,23 @@ async def leave_command(client: Client, message: Message): async def help_command(client: Client, message: Message): - text = 'PaimonBot 0.1.1beta By Xtao-Labs\n\n' \ + text = 'PaimonBot 0.2.0beta By Xtao-Labs\n\n' \ '🔅 以下是小派蒙我学会了的功能(部分):\n' \ - '1️⃣ [武器/今日武器] 查看今日武器材料和武器\n' \ - '2️⃣ [天赋/今日天赋] 查看今日天赋材料和角色\n' \ - '3️⃣ [周本] 查看周本材料和人物\n' \ - '4️⃣ [武器查询 武器名] 查看武器资料\n' \ - ' 💠 武器查询 沐浴龙血的剑\n' \ - '5️⃣ [角色查询 名字] 查看人物简介\n' \ - ' 💠 角色查询 重云\n' \ - '6️⃣ [命座 名字] 查看人物命座\n' \ - ' 💠 命座 重云一命\n' \ - '7️⃣ [运势 (名字)] 查看今日运势\n' \ + '① [武器/今日武器] 查看今日武器材料和武器\n' \ + '② [天赋/今日天赋] 查看今日天赋材料和角色\n' \ + '③ [周本] 查看周本材料和人物\n' \ + '④ [运势 (名字)] 查看今日运势\n' \ ' 💠 运势 (重云)\n' \ ' 💠 设置运势 (重云)\n' \ - '8️⃣ [原神黄历] 查看随机生成的原神黄历' + '⑤ [原神黄历] 查看随机生成的原神黄历\n' \ + '⑥ [圣遗物评分] 我也想拥有这种分数的圣遗物(切实)\n' \ + '⑦ [哪里有 (资源名)] 查看资源的位置\n' \ + '⑧ [资源列表] 查看原神所有资源' await message.reply(text, quote=True, disable_web_page_preview=True) + +# '④ [武器查询 武器名] 查看武器资料\n' \ +# ' 💠 武器查询 沐浴龙血的剑\n' \ +# '⑤ [角色查询 名字] 查看人物简介\n' \ +# ' 💠 角色查询 重云\n' \ +# '⑥ [命座 名字] 查看人物命座\n' \ +# ' 💠 命座 重云一命\n' \ diff --git a/requirements.txt b/requirements.txt index cd3b219..4124a8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -Pyrogram>=1.2.9 -Tgcrypto>=1.2.2 +Pyrogram>=1.3.5 +Tgcrypto>=1.2.3 Pillow>=8.1.0 Redis>=3.5.3 bs4>=0.0.1 beautifulsoup4>=4.9.3 -requests>=2.25.1 +requests>=2.27.1 xpinyin>=0.7.6 lxml>=4.6.3 -httpx>=0.18.2 \ No newline at end of file +httpx>=0.21.3 \ No newline at end of file