mirror of
https://github.com/PaiGramTeam/MibooGram.git
synced 2024-12-04 12:40:09 +00:00
✨ Support hoyolab post plugin
This commit is contained in:
parent
37272572a9
commit
2519be8d59
@ -34,8 +34,10 @@ OWNER=0
|
|||||||
# 记录错误并发送消息通知开发人员 可选配置项
|
# 记录错误并发送消息通知开发人员 可选配置项
|
||||||
# ERROR_NOTIFICATION_CHAT_ID=chat_id
|
# ERROR_NOTIFICATION_CHAT_ID=chat_id
|
||||||
|
|
||||||
# 文章推送群组 可选配置项
|
# 文章推送频道 可选配置项
|
||||||
# CHANNELS=[]
|
# CHANNELS=[]
|
||||||
|
# 文章推送群组 可选配置项
|
||||||
|
# POST_CHAT_ID=0
|
||||||
# 消息帮助频道 可选配置项
|
# 消息帮助频道 可选配置项
|
||||||
# CHANNELS_HELPER=0
|
# CHANNELS_HELPER=0
|
||||||
|
|
||||||
|
68
modules/apihelper/client/components/hoyolab.py
Normal file
68
modules/apihelper/client/components/hoyolab.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .hyperion import HyperionBase
|
||||||
|
from ..base.hyperionrequest import HyperionRequest
|
||||||
|
from ...models.genshin.hyperion import PostInfo, ArtworkImage, PostRecommend, HoYoPostMultiLang
|
||||||
|
|
||||||
|
__all__ = ("Hoyolab",)
|
||||||
|
|
||||||
|
|
||||||
|
class Hoyolab(HyperionBase):
|
||||||
|
POST_FULL_URL = "https://bbs-api-os.hoyolab.com/community/post/wapi/getPostFull"
|
||||||
|
NEW_LIST_URL = "https://bbs-api-os.hoyolab.com/community/post/wapi/getNewsList"
|
||||||
|
NEW_BG_URL = "https://bbs-api-os.hoyolab.com/community/painter/wapi/circle/info"
|
||||||
|
LANG = "zh-cn"
|
||||||
|
USER_AGENT = (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/90.0.4430.72 Safari/537.36"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.client = HyperionRequest(headers=self.get_headers(), *args, **kwargs)
|
||||||
|
|
||||||
|
def get_headers(self, lang: str = LANG):
|
||||||
|
return {
|
||||||
|
"User-Agent": self.USER_AGENT,
|
||||||
|
"Referer": "https://www.hoyolab.com/",
|
||||||
|
"X-Rpc-Language": lang,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_official_recommended_posts(
|
||||||
|
self, gids: int, page_size: int = 3, type_: int = 1
|
||||||
|
) -> List[PostRecommend]:
|
||||||
|
params = {"gids": gids, "page_size": page_size, "type": type_}
|
||||||
|
response = await self.client.get(url=self.NEW_LIST_URL, params=params)
|
||||||
|
return [
|
||||||
|
PostRecommend(
|
||||||
|
hoyolab=True,
|
||||||
|
post_id=data["post"]["post_id"],
|
||||||
|
subject=data["post"]["subject"],
|
||||||
|
multi_language_info=HoYoPostMultiLang(**data["post"]["multi_language_info"]),
|
||||||
|
)
|
||||||
|
for data in response["list"]
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_post_info(self, gids: int, post_id: int, read: int = 1, scene: int = 1, lang: str = LANG) -> PostInfo:
|
||||||
|
params = {"post_id": post_id, "read": read, "scene": scene}
|
||||||
|
response = await self.client.get(self.POST_FULL_URL, params=params, headers=self.get_headers(lang=lang))
|
||||||
|
return PostInfo.paste_data(response, hoyolab=True)
|
||||||
|
|
||||||
|
async def get_images_by_post_id(self, gids: int, post_id: int) -> List[ArtworkImage]:
|
||||||
|
post_info = await self.get_post_info(gids, post_id)
|
||||||
|
task_list = [
|
||||||
|
self._download_image(post_info.post_id, post_info.image_urls[page], page)
|
||||||
|
for page in range(len(post_info.image_urls))
|
||||||
|
]
|
||||||
|
return await self.get_images_by_post_id_tasks(task_list)
|
||||||
|
|
||||||
|
async def _download_image(self, art_id: int, url: str, page: int = 0) -> List[ArtworkImage]:
|
||||||
|
return await self.download_image(self.client, art_id, url, page)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self.client.shutdown()
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self.close()
|
@ -1,40 +1,31 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from abc import abstractmethod
|
||||||
from time import time
|
from time import time
|
||||||
from typing import List
|
from typing import List, Tuple
|
||||||
|
|
||||||
from ..base.hyperionrequest import HyperionRequest
|
from ..base.hyperionrequest import HyperionRequest
|
||||||
from ...models.genshin.hyperion import PostInfo, ArtworkImage, LiveInfo, LiveCode, LiveCodeHoYo
|
from ...models.genshin.hyperion import (
|
||||||
|
PostInfo,
|
||||||
|
ArtworkImage,
|
||||||
|
LiveInfo,
|
||||||
|
LiveCode,
|
||||||
|
LiveCodeHoYo,
|
||||||
|
PostRecommend,
|
||||||
|
PostTypeEnum,
|
||||||
|
)
|
||||||
from ...typedefs import JSON_DATA
|
from ...typedefs import JSON_DATA
|
||||||
|
|
||||||
__all__ = ("Hyperion",)
|
__all__ = (
|
||||||
|
"HyperionBase",
|
||||||
|
"Hyperion",
|
||||||
class Hyperion:
|
|
||||||
"""米忽悠bbs相关API请求
|
|
||||||
|
|
||||||
该名称来源于米忽悠的安卓BBS包名结尾,考虑到大部分重要的功能确实是在移动端实现了
|
|
||||||
"""
|
|
||||||
|
|
||||||
POST_FULL_URL = "https://bbs-api.miyoushe.com/post/wapi/getPostFull"
|
|
||||||
POST_FULL_IN_COLLECTION_URL = "https://bbs-api.miyoushe.com/post/wapi/getPostFullInCollection"
|
|
||||||
GET_NEW_LIST_URL = "https://bbs-api.miyoushe.com/post/wapi/getNewsList"
|
|
||||||
GET_OFFICIAL_RECOMMENDED_POSTS_URL = "https://bbs-api.miyoushe.com/post/wapi/getOfficialRecommendedPosts"
|
|
||||||
LIVE_INFO_URL = "https://api-takumi.mihoyo.com/event/miyolive/index"
|
|
||||||
LIVE_CODE_URL = "https://api-takumi-static.mihoyo.com/event/miyolive/refreshCode"
|
|
||||||
LIVE_CODE_HOYO_URL = "https://bbs-api-os.hoyolab.com/community/painter/wapi/circle/channel/guide/material"
|
|
||||||
|
|
||||||
USER_AGENT = (
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
||||||
"Chrome/90.0.4430.72 Safari/537.36"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.client = HyperionRequest(headers=self.get_headers(), *args, **kwargs)
|
|
||||||
|
|
||||||
|
class HyperionBase:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extract_post_id(text: str) -> int:
|
def extract_post_id(text: str) -> Tuple[int, PostTypeEnum]:
|
||||||
"""
|
"""
|
||||||
:param text:
|
:param text:
|
||||||
# https://bbs.mihoyo.com/ys/article/8808224
|
# https://bbs.mihoyo.com/ys/article/8808224
|
||||||
@ -44,20 +35,19 @@ class Hyperion:
|
|||||||
:return: post_id
|
:return: post_id
|
||||||
"""
|
"""
|
||||||
rgx = re.compile(r"(?:bbs|www\.)?(?:miyoushe|mihoyo)\.(.*)/[^.]+/article/(?P<article_id>\d+)")
|
rgx = re.compile(r"(?:bbs|www\.)?(?:miyoushe|mihoyo)\.(.*)/[^.]+/article/(?P<article_id>\d+)")
|
||||||
matches = rgx.search(text)
|
rgx2 = re.compile(r"(?:bbs|www\.)?(?:hoyolab|hoyoverse)\.(.*)/article/(?P<article_id>\d+)")
|
||||||
|
matches = rgx.search(text) or rgx2.search(text)
|
||||||
if matches is None:
|
if matches is None:
|
||||||
return -1
|
return -1, PostTypeEnum.NULL
|
||||||
entries = matches.groupdict()
|
entries = matches.groupdict()
|
||||||
if entries is None:
|
if entries is None:
|
||||||
return -1
|
return -1, PostTypeEnum.NULL
|
||||||
try:
|
try:
|
||||||
art_id = int(entries.get("article_id"))
|
art_id = int(entries.get("article_id"))
|
||||||
|
post_type = PostTypeEnum.CN if "miyoushe" in text or "mihoyo" in text else PostTypeEnum.OS
|
||||||
except (IndexError, ValueError, TypeError):
|
except (IndexError, ValueError, TypeError):
|
||||||
return -1
|
return -1, PostTypeEnum.NULL
|
||||||
return art_id
|
return art_id, post_type
|
||||||
|
|
||||||
def get_headers(self, referer: str = "https://www.miyoushe.com/ys/"):
|
|
||||||
return {"User-Agent": self.USER_AGENT, "Referer": referer}
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_list_url_params(forum_id: int, is_good: bool = False, is_hot: bool = False, page_size: int = 20) -> dict:
|
def get_list_url_params(forum_id: int, is_good: bool = False, is_hot: bool = False, page_size: int = 20) -> dict:
|
||||||
@ -89,10 +79,79 @@ class Hyperion:
|
|||||||
)
|
)
|
||||||
return {"x-oss-process": params}
|
return {"x-oss-process": params}
|
||||||
|
|
||||||
async def get_official_recommended_posts(self, gids: int) -> JSON_DATA:
|
@staticmethod
|
||||||
|
async def get_images_by_post_id_tasks(task_list: List) -> List[ArtworkImage]:
|
||||||
|
art_list = []
|
||||||
|
result_lists = await asyncio.gather(*task_list)
|
||||||
|
for result_list in result_lists:
|
||||||
|
for result in result_list:
|
||||||
|
if isinstance(result, ArtworkImage):
|
||||||
|
art_list.append(result)
|
||||||
|
|
||||||
|
def take_page(elem: ArtworkImage):
|
||||||
|
return elem.page
|
||||||
|
|
||||||
|
art_list.sort(key=take_page)
|
||||||
|
return art_list
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def download_image(client: "HyperionRequest", art_id: int, url: str, page: int = 0) -> List[ArtworkImage]:
|
||||||
|
filename = os.path.basename(url)
|
||||||
|
_, file_extension = os.path.splitext(filename)
|
||||||
|
is_image = bool(file_extension in ".jpg" or file_extension in ".png")
|
||||||
|
response = await client.get(
|
||||||
|
url, params=Hyperion.get_images_params(resize=2000) if is_image else None, de_json=False
|
||||||
|
)
|
||||||
|
return ArtworkImage.gen(
|
||||||
|
art_id=art_id, page=page, file_name=filename, file_extension=url.split(".")[-1], data=response.content
|
||||||
|
)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_official_recommended_posts(self, gids: int) -> List[PostRecommend]:
|
||||||
|
"""获取官方推荐帖子"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_post_info(self, gids: int, post_id: int, read: int = 1) -> PostInfo:
|
||||||
|
"""获取帖子信息"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_images_by_post_id(self, gids: int, post_id: int) -> List[ArtworkImage]:
|
||||||
|
"""获取帖子图片"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def close(self):
|
||||||
|
"""关闭请求会话"""
|
||||||
|
|
||||||
|
|
||||||
|
class Hyperion(HyperionBase):
|
||||||
|
"""米忽悠bbs相关API请求
|
||||||
|
|
||||||
|
该名称来源于米忽悠的安卓BBS包名结尾,考虑到大部分重要的功能确实是在移动端实现了
|
||||||
|
"""
|
||||||
|
|
||||||
|
POST_FULL_URL = "https://bbs-api.miyoushe.com/post/wapi/getPostFull"
|
||||||
|
POST_FULL_IN_COLLECTION_URL = "https://bbs-api.miyoushe.com/post/wapi/getPostFullInCollection"
|
||||||
|
GET_NEW_LIST_URL = "https://bbs-api.miyoushe.com/post/wapi/getNewsList"
|
||||||
|
GET_OFFICIAL_RECOMMENDED_POSTS_URL = "https://bbs-api.miyoushe.com/post/wapi/getOfficialRecommendedPosts"
|
||||||
|
LIVE_INFO_URL = "https://api-takumi.mihoyo.com/event/miyolive/index"
|
||||||
|
LIVE_CODE_URL = "https://api-takumi-static.mihoyo.com/event/miyolive/refreshCode"
|
||||||
|
LIVE_CODE_HOYO_URL = "https://bbs-api-os.hoyolab.com/community/painter/wapi/circle/channel/guide/material"
|
||||||
|
|
||||||
|
USER_AGENT = (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/90.0.4430.72 Safari/537.36"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.client = HyperionRequest(headers=self.get_headers(), *args, **kwargs)
|
||||||
|
|
||||||
|
def get_headers(self, referer: str = "https://www.miyoushe.com/ys/"):
|
||||||
|
return {"User-Agent": self.USER_AGENT, "Referer": referer}
|
||||||
|
|
||||||
|
async def get_official_recommended_posts(self, gids: int) -> List[PostRecommend]:
|
||||||
params = {"gids": gids}
|
params = {"gids": gids}
|
||||||
response = await self.client.get(url=self.GET_OFFICIAL_RECOMMENDED_POSTS_URL, params=params)
|
response = await self.client.get(url=self.GET_OFFICIAL_RECOMMENDED_POSTS_URL, params=params)
|
||||||
return response
|
return [PostRecommend(**data) for data in response["list"]]
|
||||||
|
|
||||||
async def get_post_full_in_collection(self, collection_id: int, gids: int = 2, order_type=1) -> JSON_DATA:
|
async def get_post_full_in_collection(self, collection_id: int, gids: int = 2, order_type=1) -> JSON_DATA:
|
||||||
params = {"collection_id": collection_id, "gids": gids, "order_type": order_type}
|
params = {"collection_id": collection_id, "gids": gids, "order_type": order_type}
|
||||||
@ -106,33 +165,14 @@ class Hyperion:
|
|||||||
|
|
||||||
async def get_images_by_post_id(self, gids: int, post_id: int) -> List[ArtworkImage]:
|
async def get_images_by_post_id(self, gids: int, post_id: int) -> List[ArtworkImage]:
|
||||||
post_info = await self.get_post_info(gids, post_id)
|
post_info = await self.get_post_info(gids, post_id)
|
||||||
art_list = []
|
|
||||||
task_list = [
|
task_list = [
|
||||||
self.download_image(post_info.post_id, post_info.image_urls[page], page)
|
self._download_image(post_info.post_id, post_info.image_urls[page], page)
|
||||||
for page in range(len(post_info.image_urls))
|
for page in range(len(post_info.image_urls))
|
||||||
]
|
]
|
||||||
result_lists = await asyncio.gather(*task_list)
|
return await self.get_images_by_post_id_tasks(task_list)
|
||||||
for result_list in result_lists:
|
|
||||||
for result in result_list:
|
|
||||||
if isinstance(result, ArtworkImage):
|
|
||||||
art_list.append(result)
|
|
||||||
|
|
||||||
def take_page(elem: ArtworkImage):
|
async def _download_image(self, art_id: int, url: str, page: int = 0) -> List[ArtworkImage]:
|
||||||
return elem.page
|
return await self.download_image(self.client, art_id, url, page)
|
||||||
|
|
||||||
art_list.sort(key=take_page)
|
|
||||||
return art_list
|
|
||||||
|
|
||||||
async def download_image(self, art_id: int, url: str, page: int = 0) -> List[ArtworkImage]:
|
|
||||||
filename = os.path.basename(url)
|
|
||||||
_, file_extension = os.path.splitext(filename)
|
|
||||||
is_image = bool(file_extension in ".jpg" or file_extension in ".png")
|
|
||||||
response = await self.client.get(
|
|
||||||
url, params=self.get_images_params(resize=2000) if is_image else None, de_json=False
|
|
||||||
)
|
|
||||||
return ArtworkImage.gen(
|
|
||||||
art_id=art_id, page=page, file_name=filename, file_extension=url.split(".")[-1], data=response.content
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_new_list(self, gids: int, type_id: int, page_size: int = 20):
|
async def get_new_list(self, gids: int, type_id: int, page_size: int = 20):
|
||||||
"""
|
"""
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from enum import Enum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
from pydantic import BaseModel, PrivateAttr
|
from pydantic import BaseModel, PrivateAttr
|
||||||
|
|
||||||
__all__ = ("ArtworkImage", "PostInfo", "LiveInfo", "LiveCode", "LiveCodeHoYo")
|
__all__ = (
|
||||||
|
"ArtworkImage",
|
||||||
|
"PostInfo",
|
||||||
|
"LiveInfo",
|
||||||
|
"LiveCode",
|
||||||
|
"LiveCodeHoYo",
|
||||||
|
"PostTypeEnum",
|
||||||
|
"PostRecommend",
|
||||||
|
"HoYoPostMultiLang",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArtworkImage(BaseModel):
|
class ArtworkImage(BaseModel):
|
||||||
@ -55,6 +65,7 @@ class ArtworkImage(BaseModel):
|
|||||||
|
|
||||||
class PostInfo(BaseModel):
|
class PostInfo(BaseModel):
|
||||||
_data: dict = PrivateAttr()
|
_data: dict = PrivateAttr()
|
||||||
|
hoyolab: bool
|
||||||
post_id: int
|
post_id: int
|
||||||
user_uid: int
|
user_uid: int
|
||||||
subject: str
|
subject: str
|
||||||
@ -67,20 +78,21 @@ class PostInfo(BaseModel):
|
|||||||
self._data = _data
|
self._data = _data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def paste_data(cls, data: dict) -> "PostInfo":
|
def paste_data(cls, data: dict, hoyolab: bool = False) -> "PostInfo":
|
||||||
_data_post = data["post"]
|
_data_post = data["post"]
|
||||||
post = _data_post["post"]
|
post = _data_post["post"]
|
||||||
post_id = post["post_id"]
|
post_id = post["post_id"]
|
||||||
subject = post["subject"]
|
subject = post["subject"]
|
||||||
image_list = _data_post["image_list"]
|
image_list = _data_post["image_list"]
|
||||||
image_urls = [image["url"] for image in image_list]
|
image_urls = [image["url"] for image in image_list]
|
||||||
vod_list = _data_post["vod_list"]
|
vod_list = _data_post.get("vod_list", [])
|
||||||
video_urls = [vod["resolutions"][-1]["url"] for vod in vod_list]
|
video_urls = [vod["resolutions"][-1]["url"] for vod in vod_list]
|
||||||
created_at = post["created_at"]
|
created_at = post["created_at"]
|
||||||
user = _data_post["user"] # 用户数据
|
user = _data_post["user"] # 用户数据
|
||||||
user_uid = user["uid"] # 用户ID
|
user_uid = user["uid"] # 用户ID
|
||||||
return PostInfo(
|
return PostInfo(
|
||||||
_data=data,
|
_data=data,
|
||||||
|
hoyolab=hoyolab,
|
||||||
post_id=post_id,
|
post_id=post_id,
|
||||||
user_uid=user_uid,
|
user_uid=user_uid,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
@ -92,6 +104,19 @@ class PostInfo(BaseModel):
|
|||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
return self._data[item]
|
return self._data[item]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type_enum(self) -> "PostTypeEnum":
|
||||||
|
return PostTypeEnum.CN if not self.hoyolab else PostTypeEnum.OS
|
||||||
|
|
||||||
|
def get_url(self, short_name: str) -> str:
|
||||||
|
if not self.hoyolab:
|
||||||
|
return f"https://www.miyoushe.com/{short_name}/article/{self.post_id}"
|
||||||
|
return f"https://www.hoyolab.com/article/{self.post_id}"
|
||||||
|
|
||||||
|
def get_fix_url(self, short_name: str) -> str:
|
||||||
|
url = self.get_url(short_name)
|
||||||
|
return url.replace(".com/", ".pp.ua/")
|
||||||
|
|
||||||
|
|
||||||
class LiveInfo(BaseModel):
|
class LiveInfo(BaseModel):
|
||||||
act_type: str
|
act_type: str
|
||||||
@ -125,3 +150,24 @@ class LiveCodeHoYo(BaseModel):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def guess_offline_at() -> datetime:
|
def guess_offline_at() -> datetime:
|
||||||
return datetime.now().replace(hour=12, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
return datetime.now().replace(hour=12, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
|
class PostTypeEnum(str, Enum):
|
||||||
|
"""社区类型枚举"""
|
||||||
|
|
||||||
|
NULL = "null"
|
||||||
|
CN = "cn"
|
||||||
|
OS = "os"
|
||||||
|
|
||||||
|
|
||||||
|
class HoYoPostMultiLang(BaseModel):
|
||||||
|
lang_subject: dict
|
||||||
|
|
||||||
|
|
||||||
|
class PostRecommend(BaseModel):
|
||||||
|
hoyolab: bool = False
|
||||||
|
post_id: int
|
||||||
|
subject: str
|
||||||
|
banner: Optional[str] = None
|
||||||
|
official_type: Optional[int] = None
|
||||||
|
multi_language_info: Optional[HoYoPostMultiLang] = None
|
||||||
|
22
pdm.lock
22
pdm.lock
@ -4,8 +4,8 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
groups = ["default", "genshin-artifact", "pyro", "test"]
|
groups = ["default", "genshin-artifact", "pyro", "test"]
|
||||||
strategy = ["cross_platform", "inherit_metadata"]
|
strategy = ["cross_platform", "inherit_metadata"]
|
||||||
lock_version = "4.4.1"
|
lock_version = "4.4.2"
|
||||||
content_hash = "sha256:feb6abdecc6245225feb204fe8001405c9f32ab5e33cd70f4067eb6265734d83"
|
content_hash = "sha256:d7909b325935f473694ce0d17f58f2c7cb59d89b99bcfa6be284426ca92d6254"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiocsv"
|
name = "aiocsv"
|
||||||
@ -1063,7 +1063,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "importlib-metadata"
|
name = "importlib-metadata"
|
||||||
version = "7.2.1"
|
version = "8.0.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Read metadata from Python packages"
|
summary = "Read metadata from Python packages"
|
||||||
groups = ["default"]
|
groups = ["default"]
|
||||||
@ -1072,8 +1072,8 @@ dependencies = [
|
|||||||
"zipp>=0.5",
|
"zipp>=0.5",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "importlib_metadata-7.2.1-py3-none-any.whl", hash = "sha256:ffef94b0b66046dd8ea2d619b701fe978d9264d38f3998bc4c27ec3b146a87c8"},
|
{file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"},
|
||||||
{file = "importlib_metadata-7.2.1.tar.gz", hash = "sha256:509ecb2ab77071db5137c655e24ceb3eee66e7bbc6574165d0d114d9fc4bbe68"},
|
{file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2137,7 +2137,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "5.0.6"
|
version = "5.0.7"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "Python client for Redis database and key-value store"
|
summary = "Python client for Redis database and key-value store"
|
||||||
groups = ["default"]
|
groups = ["default"]
|
||||||
@ -2145,8 +2145,8 @@ dependencies = [
|
|||||||
"async-timeout>=4.0.3; python_full_version < \"3.11.3\"",
|
"async-timeout>=4.0.3; python_full_version < \"3.11.3\"",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "redis-5.0.6-py3-none-any.whl", hash = "sha256:c0d6d990850c627bbf7be01c5c4cbaadf67b48593e913bb71c9819c30df37eee"},
|
{file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"},
|
||||||
{file = "redis-5.0.6.tar.gz", hash = "sha256:38473cd7c6389ad3e44a91f4c3eaf6bcb8a9f746007f29bf4fb20824ff0b2197"},
|
{file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2167,7 +2167,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry-sdk"
|
name = "sentry-sdk"
|
||||||
version = "2.6.0"
|
version = "2.7.0"
|
||||||
requires_python = ">=3.6"
|
requires_python = ">=3.6"
|
||||||
summary = "Python client for Sentry (https://sentry.io)"
|
summary = "Python client for Sentry (https://sentry.io)"
|
||||||
groups = ["default"]
|
groups = ["default"]
|
||||||
@ -2176,8 +2176,8 @@ dependencies = [
|
|||||||
"urllib3>=1.26.11",
|
"urllib3>=1.26.11",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "sentry_sdk-2.6.0-py2.py3-none-any.whl", hash = "sha256:422b91cb49378b97e7e8d0e8d5a1069df23689d45262b86f54988a7db264e874"},
|
{file = "sentry_sdk-2.7.0-py2.py3-none-any.whl", hash = "sha256:db9594c27a4d21c1ebad09908b1f0dc808ef65c2b89c1c8e7e455143262e37c1"},
|
||||||
{file = "sentry_sdk-2.6.0.tar.gz", hash = "sha256:65cc07e9c6995c5e316109f138570b32da3bd7ff8d0d0ee4aaf2628c3dd8127d"},
|
{file = "sentry_sdk-2.7.0.tar.gz", hash = "sha256:d846a211d4a0378b289ced3c434480945f110d0ede00450ba631fc2852e7a0d4"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
from asyncio import create_subprocess_shell, subprocess
|
from asyncio import create_subprocess_shell, subprocess
|
||||||
from typing import List, Optional, Tuple, TYPE_CHECKING, Union
|
from functools import partial
|
||||||
|
from typing import List, Optional, Tuple, TYPE_CHECKING, Union, Dict
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
from arkowrapper import ArkoWrapper
|
from arkowrapper import ArkoWrapper
|
||||||
@ -23,8 +24,11 @@ from telegram.helpers import escape_markdown
|
|||||||
|
|
||||||
from core.config import config
|
from core.config import config
|
||||||
from core.plugin import Plugin, conversation, handler
|
from core.plugin import Plugin, conversation, handler
|
||||||
from modules.apihelper.client.components.hyperion import Hyperion
|
from gram_core.basemodel import Settings
|
||||||
|
from modules.apihelper.client.components.hoyolab import Hoyolab
|
||||||
|
from modules.apihelper.client.components.hyperion import Hyperion, HyperionBase
|
||||||
from modules.apihelper.error import APIHelperException
|
from modules.apihelper.error import APIHelperException
|
||||||
|
from modules.apihelper.models.genshin.hyperion import PostTypeEnum
|
||||||
from utils.helpers import sha1
|
from utils.helpers import sha1
|
||||||
from utils.log import logger
|
from utils.log import logger
|
||||||
|
|
||||||
@ -44,8 +48,18 @@ class PostHandlerData:
|
|||||||
self.tags: Optional[List[str]] = []
|
self.tags: Optional[List[str]] = []
|
||||||
|
|
||||||
|
|
||||||
|
class PostConfig(Settings):
|
||||||
|
"""文章推送配置"""
|
||||||
|
|
||||||
|
chat_id: Optional[int] = 0
|
||||||
|
|
||||||
|
class Config(Settings.Config):
|
||||||
|
env_prefix = "post_"
|
||||||
|
|
||||||
|
|
||||||
CHECK_POST, SEND_POST, CHECK_COMMAND, GTE_DELETE_PHOTO = range(10900, 10904)
|
CHECK_POST, SEND_POST, CHECK_COMMAND, GTE_DELETE_PHOTO = range(10900, 10904)
|
||||||
GET_POST_CHANNEL, GET_TAGS, GET_TEXT = range(10904, 10907)
|
GET_POST_CHANNEL, GET_TAGS, GET_TEXT = range(10904, 10907)
|
||||||
|
post_config = PostConfig()
|
||||||
|
|
||||||
|
|
||||||
class Post(Plugin.Conversation):
|
class Post(Plugin.Conversation):
|
||||||
@ -56,13 +70,14 @@ class Post(Plugin.Conversation):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.gids = 2
|
self.gids = 2
|
||||||
self.short_name = "ys"
|
self.short_name = "ys"
|
||||||
self.last_post_id_list: List[int] = []
|
self.last_post_id_list: Dict[PostTypeEnum, List[int]] = {PostTypeEnum.CN: [], PostTypeEnum.OS: []}
|
||||||
self.ffmpeg_enable = False
|
self.ffmpeg_enable = False
|
||||||
self.cache_dir = os.path.join(os.getcwd(), "cache")
|
self.cache_dir = os.path.join(os.getcwd(), "cache")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_bbs_client() -> Hyperion:
|
def get_bbs_client(bbs_type: "PostTypeEnum") -> "HyperionBase":
|
||||||
return Hyperion(
|
class_type = Hyperion if bbs_type == PostTypeEnum.CN else Hoyolab
|
||||||
|
return class_type(
|
||||||
timeout=Timeout(
|
timeout=Timeout(
|
||||||
connect=config.connect_timeout,
|
connect=config.connect_timeout,
|
||||||
read=config.read_timeout,
|
read=config.read_timeout,
|
||||||
@ -74,7 +89,10 @@ class Post(Plugin.Conversation):
|
|||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
if config.channels and len(config.channels) > 0:
|
if config.channels and len(config.channels) > 0:
|
||||||
logger.success("文章定时推送处理已经开启")
|
logger.success("文章定时推送处理已经开启")
|
||||||
self.application.job_queue.run_repeating(self.task, 60)
|
cn_task = partial(self.task, post_type=PostTypeEnum.CN)
|
||||||
|
os_task = partial(self.task, post_type=PostTypeEnum.OS)
|
||||||
|
self.application.job_queue.run_repeating(cn_task, 30, name="post_cn_task")
|
||||||
|
self.application.job_queue.run_repeating(os_task, 30, name="post_os_task")
|
||||||
logger.success("文章定时推送处理已经开启")
|
logger.success("文章定时推送处理已经开启")
|
||||||
output, _ = await self.execute("ffmpeg -version")
|
output, _ = await self.execute("ffmpeg -version")
|
||||||
if "ffmpeg version" in output:
|
if "ffmpeg version" in output:
|
||||||
@ -84,8 +102,8 @@ class Post(Plugin.Conversation):
|
|||||||
else:
|
else:
|
||||||
logger.warning("ffmpeg 不可用 已经禁用编码转换")
|
logger.warning("ffmpeg 不可用 已经禁用编码转换")
|
||||||
|
|
||||||
async def task(self, context: "ContextTypes.DEFAULT_TYPE"):
|
async def task(self, context: "ContextTypes.DEFAULT_TYPE", post_type: "PostTypeEnum"):
|
||||||
bbs = self.get_bbs_client()
|
bbs = self.get_bbs_client(post_type)
|
||||||
temp_post_id_list: List[int] = []
|
temp_post_id_list: List[int] = []
|
||||||
|
|
||||||
# 请求推荐POST列表并处理
|
# 请求推荐POST列表并处理
|
||||||
@ -95,22 +113,25 @@ class Post(Plugin.Conversation):
|
|||||||
logger.error("获取首页推荐信息失败 %s", str(exc))
|
logger.error("获取首页推荐信息失败 %s", str(exc))
|
||||||
return
|
return
|
||||||
|
|
||||||
for data_list in official_recommended_posts["list"]:
|
for data_list in official_recommended_posts:
|
||||||
temp_post_id_list.append(data_list["post_id"])
|
temp_post_id_list.append(data_list.post_id)
|
||||||
|
|
||||||
|
last_post_id_list = self.last_post_id_list[post_type]
|
||||||
# 判断是否为空
|
# 判断是否为空
|
||||||
if len(self.last_post_id_list) == 0:
|
if len(last_post_id_list) == 0:
|
||||||
for temp_list in temp_post_id_list:
|
for temp_list in temp_post_id_list:
|
||||||
self.last_post_id_list.append(temp_list)
|
last_post_id_list.append(temp_list)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 筛选出新推送的文章
|
# 筛选出新推送的文章
|
||||||
new_post_id_list = set(temp_post_id_list).difference(set(self.last_post_id_list))
|
last_post_id_list = self.last_post_id_list[post_type]
|
||||||
|
new_post_id_list = set(temp_post_id_list).difference(set(last_post_id_list))
|
||||||
|
|
||||||
if not new_post_id_list:
|
if not new_post_id_list:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.last_post_id_list = temp_post_id_list
|
self.last_post_id_list[post_type] = temp_post_id_list
|
||||||
|
chat_id = post_config.chat_id or config.owner
|
||||||
|
|
||||||
for post_id in new_post_id_list:
|
for post_id in new_post_id_list:
|
||||||
try:
|
try:
|
||||||
@ -119,21 +140,26 @@ class Post(Plugin.Conversation):
|
|||||||
logger.error("获取文章信息失败 %s", str(exc))
|
logger.error("获取文章信息失败 %s", str(exc))
|
||||||
text = f"获取 post_id[{post_id}] 文章信息失败 {str(exc)}"
|
text = f"获取 post_id[{post_id}] 文章信息失败 {str(exc)}"
|
||||||
try:
|
try:
|
||||||
await context.bot.send_message(config.owner, text)
|
await context.bot.send_message(chat_id, text)
|
||||||
except BadRequest as _exc:
|
except BadRequest as _exc:
|
||||||
logger.error("发送消息失败 %s", _exc.message)
|
logger.error("发送消息失败 %s", _exc.message)
|
||||||
return
|
return
|
||||||
|
type_name = post_info.type_enum.value
|
||||||
buttons = [
|
buttons = [
|
||||||
[
|
[
|
||||||
InlineKeyboardButton("确认", callback_data=f"post_admin|confirm|{post_info.post_id}"),
|
InlineKeyboardButton("确认", callback_data=f"post_admin|confirm|{type_name}|{post_info.post_id}"),
|
||||||
InlineKeyboardButton("取消", callback_data=f"post_admin|cancel|{post_info.post_id}"),
|
InlineKeyboardButton("取消", callback_data=f"post_admin|cancel|{type_name}|{post_info.post_id}"),
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
url = f"https://www.miyoushe.pp.ua/{self.short_name}/article/{post_info.post_id}"
|
url = post_info.get_fix_url(self.short_name)
|
||||||
text = f"发现官网推荐文章 <a href='{url}'>{post_info.subject}</a>\n是否开始处理"
|
tag = f"#{self.short_name} #{post_type.value} #{self.short_name}_{post_type.value}"
|
||||||
|
text = f"发现官网推荐文章 <a href='{url}'>{post_info.subject}</a>\n是否开始处理 {tag}"
|
||||||
try:
|
try:
|
||||||
await context.bot.send_message(
|
await context.bot.send_message(
|
||||||
config.owner, text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(buttons)
|
chat_id,
|
||||||
|
text,
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
reply_markup=InlineKeyboardMarkup(buttons),
|
||||||
)
|
)
|
||||||
except BadRequest as exc:
|
except BadRequest as exc:
|
||||||
logger.error("发送消息失败 %s", exc.message)
|
logger.error("发送消息失败 %s", exc.message)
|
||||||
@ -262,29 +288,31 @@ class Post(Plugin.Conversation):
|
|||||||
message = callback_query.message
|
message = callback_query.message
|
||||||
logger.info("用户 %s[%s] POST命令请求", user.full_name, user.id)
|
logger.info("用户 %s[%s] POST命令请求", user.full_name, user.id)
|
||||||
|
|
||||||
async def get_post_admin_callback(callback_query_data: str) -> Tuple[str, int]:
|
async def get_post_admin_callback(callback_query_data: str) -> Tuple[str, PostTypeEnum, int]:
|
||||||
_data = callback_query_data.split("|")
|
_data = callback_query_data.split("|")
|
||||||
_result = _data[1]
|
_result = _data[1]
|
||||||
_post_id = int(_data[2])
|
_post_type = PostTypeEnum(_data[2])
|
||||||
logger.debug("callback_query_data函数返回 result[%s] post_id[%s]", _result, _post_id)
|
_post_id = int(_data[3])
|
||||||
return _result, _post_id
|
logger.debug(
|
||||||
|
"callback_query_data函数返回 result[%s] _post_type[%s] post_id[%s]", _result, _post_type, _post_id
|
||||||
|
)
|
||||||
|
return _result, _post_type, _post_id
|
||||||
|
|
||||||
result, post_id = await get_post_admin_callback(callback_query.data)
|
result, post_type, post_id = await get_post_admin_callback(callback_query.data)
|
||||||
|
|
||||||
if result == "cancel":
|
if result == "cancel":
|
||||||
await message.reply_text("操作已经取消")
|
await message.reply_text("操作已经取消")
|
||||||
await message.delete()
|
await message.delete()
|
||||||
elif result == "confirm":
|
elif result == "confirm":
|
||||||
reply_text = await message.reply_text("正在处理")
|
reply_text = await message.reply_text("正在处理")
|
||||||
status = await self.send_post_info(post_handler_data, message, post_id)
|
status = await self.send_post_info(post_handler_data, message, post_id, post_type)
|
||||||
await reply_text.delete()
|
await reply_text.delete()
|
||||||
return status
|
return status
|
||||||
|
|
||||||
await message.reply_text("非法参数")
|
|
||||||
return ConversationHandler.END
|
return ConversationHandler.END
|
||||||
|
|
||||||
@conversation.entry_point
|
@conversation.entry_point
|
||||||
@handler.command(command="post", filters=filters.ChatType.PRIVATE, block=False, admin=True)
|
@handler.command(command="post", block=False, admin=True)
|
||||||
async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int:
|
async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int:
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
message = update.effective_message
|
message = update.effective_message
|
||||||
@ -307,14 +335,16 @@ class Post(Plugin.Conversation):
|
|||||||
await message.reply_text("退出投稿", reply_markup=ReplyKeyboardRemove())
|
await message.reply_text("退出投稿", reply_markup=ReplyKeyboardRemove())
|
||||||
return ConversationHandler.END
|
return ConversationHandler.END
|
||||||
|
|
||||||
post_id = Hyperion.extract_post_id(update.message.text)
|
post_id, post_type = Hyperion.extract_post_id(update.message.text)
|
||||||
if post_id == -1:
|
if post_id == -1:
|
||||||
await message.reply_text("获取作品ID错误,请检查连接是否合法", reply_markup=ReplyKeyboardRemove())
|
await message.reply_text("获取作品ID错误,请检查连接是否合法", reply_markup=ReplyKeyboardRemove())
|
||||||
return ConversationHandler.END
|
return ConversationHandler.END
|
||||||
return await self.send_post_info(post_handler_data, message, post_id)
|
return await self.send_post_info(post_handler_data, message, post_id, post_type)
|
||||||
|
|
||||||
async def send_post_info(self, post_handler_data: PostHandlerData, message: "Message", post_id: int) -> int:
|
async def send_post_info(
|
||||||
bbs = self.get_bbs_client()
|
self, post_handler_data: PostHandlerData, message: "Message", post_id: int, post_type: "PostTypeEnum"
|
||||||
|
) -> int:
|
||||||
|
bbs = self.get_bbs_client(post_type)
|
||||||
post_info = await bbs.get_post_info(self.gids, post_id)
|
post_info = await bbs.get_post_info(self.gids, post_id)
|
||||||
post_images = await bbs.get_images_by_post_id(self.gids, post_id)
|
post_images = await bbs.get_images_by_post_id(self.gids, post_id)
|
||||||
await bbs.close()
|
await bbs.close()
|
||||||
@ -323,7 +353,8 @@ class Post(Plugin.Conversation):
|
|||||||
post_subject = post_data["subject"]
|
post_subject = post_data["subject"]
|
||||||
post_soup = BeautifulSoup(post_data["content"], features="html.parser")
|
post_soup = BeautifulSoup(post_data["content"], features="html.parser")
|
||||||
post_text, too_long = self.parse_post_text(post_soup, post_subject)
|
post_text, too_long = self.parse_post_text(post_soup, post_subject)
|
||||||
post_text += f"\n[source](https://www.miyoushe.com/{self.short_name}/article/{post_id})"
|
url = post_info.get_url(self.short_name)
|
||||||
|
post_text += f"\n[source]({url})"
|
||||||
if too_long or len(post_text) >= MessageLimit.CAPTION_LENGTH:
|
if too_long or len(post_text) >= MessageLimit.CAPTION_LENGTH:
|
||||||
post_text = post_text[: MessageLimit.CAPTION_LENGTH]
|
post_text = post_text[: MessageLimit.CAPTION_LENGTH]
|
||||||
await message.reply_text(f"警告!图片字符描述已经超过 {MessageLimit.CAPTION_LENGTH} 个字,已经切割")
|
await message.reply_text(f"警告!图片字符描述已经超过 {MessageLimit.CAPTION_LENGTH} 个字,已经切割")
|
||||||
|
@ -33,7 +33,7 @@ dependencies = [
|
|||||||
"arko-wrapper<1.0.0,>=0.2.8",
|
"arko-wrapper<1.0.0,>=0.2.8",
|
||||||
"fastapi<1.0.0,>=0.111.0",
|
"fastapi<1.0.0,>=0.111.0",
|
||||||
"uvicorn[standard]<1.0.0,>=0.30.1",
|
"uvicorn[standard]<1.0.0,>=0.30.1",
|
||||||
"sentry-sdk<3.0.0,>=2.6.0",
|
"sentry-sdk<3.0.0,>=2.7.0",
|
||||||
"GitPython<4.0.0,>=3.1.30",
|
"GitPython<4.0.0,>=3.1.30",
|
||||||
"openpyxl<4.0.0,>=3.1.1",
|
"openpyxl<4.0.0,>=3.1.1",
|
||||||
"async-lru<3.0.0,>=2.0.4",
|
"async-lru<3.0.0,>=2.0.4",
|
||||||
|
@ -6,7 +6,7 @@ aiofiles==24.1.0
|
|||||||
aiohttp==3.9.5
|
aiohttp==3.9.5
|
||||||
aiolimiter==1.1.0
|
aiolimiter==1.1.0
|
||||||
aiosignal==1.3.1
|
aiosignal==1.3.1
|
||||||
aiosqlite==0.20.0
|
aiosqlite[sqlite]==0.20.0
|
||||||
alembic==1.13.1
|
alembic==1.13.1
|
||||||
anyio==4.4.0
|
anyio==4.4.0
|
||||||
apscheduler==3.10.4
|
apscheduler==3.10.4
|
||||||
@ -45,9 +45,9 @@ httpcore==1.0.5
|
|||||||
httptools==0.6.1
|
httptools==0.6.1
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
idna==3.7
|
idna==3.7
|
||||||
importlib-metadata==7.2.1; python_version < "3.9"
|
importlib-metadata==8.0.0; python_version < "3.9"
|
||||||
importlib-resources==6.4.0; python_version < "3.9"
|
importlib-resources==6.4.0; python_version < "3.9"
|
||||||
influxdb-client==1.44.0
|
influxdb-client[async,ciso]==1.44.0
|
||||||
iniconfig==2.0.0
|
iniconfig==2.0.0
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
lxml==5.2.2
|
lxml==5.2.2
|
||||||
@ -79,14 +79,14 @@ python-dateutil==2.9.0.post0
|
|||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
python-genshin-artifact==1.0.7
|
python-genshin-artifact==1.0.7
|
||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
python-telegram-bot==21.3
|
python-telegram-bot[ext,rate-limiter]==21.3
|
||||||
pytz==2024.1
|
pytz==2024.1
|
||||||
pyyaml==6.0.1
|
pyyaml==6.0.1
|
||||||
rapidfuzz==3.9.3
|
rapidfuzz==3.9.3
|
||||||
reactivex==4.0.4
|
reactivex==4.0.4
|
||||||
redis==5.0.6
|
redis==5.0.7
|
||||||
rich==13.7.1
|
rich==13.7.1
|
||||||
sentry-sdk==2.6.0
|
sentry-sdk==2.7.0
|
||||||
setuptools==70.1.1
|
setuptools==70.1.1
|
||||||
shellingham==1.5.4
|
shellingham==1.5.4
|
||||||
simnet @ git+https://github.com/PaiGramTeam/SIMNet@277a33321a20909541b46bf4ecf794fd47e19fb1
|
simnet @ git+https://github.com/PaiGramTeam/SIMNet@277a33321a20909541b46bf4ecf794fd47e19fb1
|
||||||
@ -108,7 +108,7 @@ tzdata==2024.1; platform_system == "Windows"
|
|||||||
tzlocal==5.2
|
tzlocal==5.2
|
||||||
ujson==5.10.0
|
ujson==5.10.0
|
||||||
urllib3==2.2.2
|
urllib3==2.2.2
|
||||||
uvicorn==0.30.1
|
uvicorn[standard]==0.30.1
|
||||||
uvloop==0.19.0; (sys_platform != "cygwin" and sys_platform != "win32") and platform_python_implementation != "PyPy"
|
uvloop==0.19.0; (sys_platform != "cygwin" and sys_platform != "win32") and platform_python_implementation != "PyPy"
|
||||||
watchfiles==0.22.0
|
watchfiles==0.22.0
|
||||||
websockets==12.0
|
websockets==12.0
|
||||||
|
Loading…
Reference in New Issue
Block a user