Support hoyolab post plugin

This commit is contained in:
omg-xtao 2024-06-27 00:01:49 +08:00 committed by GitHub
parent 5f81e8c810
commit 29f71fb0ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 306 additions and 120 deletions

View File

@ -40,8 +40,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

View 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()

View File

@ -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: class 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)
@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):
""" """

View File

@ -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=23, minute=59, second=59, microsecond=999999) + timedelta(days=1) return datetime.now().replace(hour=23, minute=59, second=59, microsecond=999999) + 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

View File

@ -4,8 +4,8 @@
[metadata] [metadata]
groups = ["default", "pyro", "test"] groups = ["default", "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:7c20ef5aa9a946f2b922539de88fe7d316e2937a79c4e6f7c2a383316dca30c7" content_hash = "sha256:e65015a34b2d9388ab1032dfe348ae78abd4948a975192d3376819bb2979d8bd"
[[package]] [[package]]
name = "aiocsv" name = "aiocsv"
@ -1038,7 +1038,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"]
@ -1047,8 +1047,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]]
@ -2127,7 +2127,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"]
@ -2135,8 +2135,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]]
@ -2157,7 +2157,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"]
@ -2166,19 +2166,19 @@ 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]]
name = "setuptools" name = "setuptools"
version = "70.1.0" version = "70.1.1"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Easily download, build, install, upgrade, and uninstall Python packages" summary = "Easily download, build, install, upgrade, and uninstall Python packages"
groups = ["default"] groups = ["default"]
files = [ files = [
{file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"},
{file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"},
] ]
[[package]] [[package]]

View File

@ -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,9 +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 ArtworkImage from modules.apihelper.models.genshin.hyperion import ArtworkImage, PostTypeEnum
from utils.helpers import sha1 from utils.helpers import sha1
from utils.log import logger from utils.log import logger
@ -44,8 +47,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, GET_VIDEO = range(10904, 10908) GET_POST_CHANNEL, GET_TAGS, GET_TEXT, GET_VIDEO = range(10904, 10908)
post_config = PostConfig()
class Post(Plugin.Conversation): class Post(Plugin.Conversation):
@ -58,13 +71,14 @@ class Post(Plugin.Conversation):
def __init__(self): def __init__(self):
self.gids = 6 self.gids = 6
self.short_name = "sr" self.short_name = "sr"
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,
@ -76,7 +90,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:
@ -86,8 +103,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列表并处理
@ -97,22 +114,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:
@ -121,21 +141,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)
@ -268,29 +293,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
@ -313,14 +340,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()
@ -329,7 +358,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} 个字,已经切割")

View File

@ -34,7 +34,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",

View File

@ -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
@ -43,9 +43,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
@ -77,15 +77,15 @@ pytest-asyncio==0.23.7
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-dotenv==1.0.1 python-dotenv==1.0.1
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.0 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
six==1.16.0 six==1.16.0
@ -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