from functools import partial import html import re from typing import Any, ClassVar from bs4 import BeautifulSoup as bs from httpx import AsyncClient from nonebot.compat import type_validate_python from pydantic import BaseModel, Field from yarl import URL from nonebot_bison.post import Post from nonebot_bison.post.protocol import HTMLContentSupport from nonebot_bison.types import Category, RawPost, Target from nonebot_bison.utils import Site from .platform import NewMessage, StatusChange class ArkResponseBase(BaseModel): code: int msg: str class BulletinListItem(BaseModel): cid: str title: str category: int display_time: str = Field(alias="displayTime") updated_at: int = Field(alias="updatedAt") sticky: bool class BulletinList(BaseModel): list: list[BulletinListItem] class BulletinData(BaseModel): cid: str display_type: int = Field(alias="displayType") title: str category: int header: str content: str jump_link: str = Field(alias="jumpLink") banner_image_url: str = Field(alias="bannerImageUrl") display_time: str = Field(alias="displayTime") updated_at: int = Field(alias="updatedAt") class ArkBulletinListResponse(ArkResponseBase): data: BulletinList class ArkBulletinResponse(ArkResponseBase): data: BulletinData class ArknightsSite(Site): name = "arknights" schedule_type = "interval" schedule_setting: ClassVar[dict] = {"seconds": 30} class ArknightsPost(Post, HTMLContentSupport): def _cleantext(self, text: str, old_split="\n", new_split="\n") -> str: """清理文本:去掉所有多余的空格和换行""" lines = text.strip().split(old_split) cleaned_lines = [line.strip() for line in lines if line != ""] return new_split.join(cleaned_lines) async def get_html_content(self) -> str: return self.content async def get_plain_content(self) -> str: content = html.unescape(self.content) # 转义HTML特殊字符 content = re.sub( r'\

(.*?)\(.*?)\(.*?)\<\/span\>(.*?)\<\/strong\>(.*?)<\/p\>', # noqa: E501 r"==\4==\n", content, flags=re.DOTALL, ) # 去“标题型”p content = re.sub( r'\

(.*?)\<\/p\>', r"\2\n", content, flags=re.DOTALL, ) # 去左右对齐的p content = re.sub(r"\(.*?)\", r"\1\n", content, flags=re.DOTALL) # 去普通p content = re.sub(r'\(.*?)\<\/a\>', r"\1", content, flags=re.DOTALL) # 去a content = re.sub(r"
", "\n", content) # 去br content = re.sub(r"\(.*?)\", r"\1", content) # 去strong content = re.sub(r'(.*?)', r"\2", content) # 去color content = re.sub(r'

(.*?)
', "", content) # 去img return self._cleantext(content) class Arknights(NewMessage): categories: ClassVar[dict[Category, str]] = {1: "游戏公告"} platform_name = "arknights" name = "明日方舟游戏信息" enable_tag = False enabled = True is_common = False site = ArknightsSite has_target = False default_theme = "arknights" @classmethod async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None: return "明日方舟游戏信息" async def get_sub_list(self, _) -> list[BulletinListItem]: client = await self.ctx.get_client() raw_data = await client.get("https://ak-webview.hypergryph.com/api/game/bulletinList?target=IOS") return type_validate_python(ArkBulletinListResponse, raw_data.json()).data.list def get_id(self, post: BulletinListItem) -> Any: return post.cid def get_date(self, post: BulletinListItem) -> Any: # 为什么不使用post.updated_at? # update_at的时间是上传鹰角服务器的时间,而不是公告发布的时间 # 也就是说鹰角可能会在中午就把晚上的公告上传到服务器,但晚上公告才会显示,但是update_at就是中午的时间不会改变 # 如果指定了get_date,那么get_date会被优先使用, 并在获取到的值超过2小时时忽略这条post,导致其不会被发送 return None def get_category(self, _) -> Category: return Category(1) async def parse(self, raw_post: BulletinListItem) -> Post: client = await self.ctx.get_client() raw_data = await client.get(f"https://ak-webview.hypergryph.com/api/game/bulletin/{self.get_id(post=raw_post)}") data = type_validate_python(ArkBulletinResponse, raw_data.json()).data def title_escape(text: str) -> str: return text.replace("\\n", " - ") # gen title, content if data.header: # header是title的更详细版本 # header会和content一起出现 title = data.header else: # 只有一张图片 title = title_escape(data.title) return ArknightsPost( self, content=data.content, title=title, nickname="明日方舟游戏内公告", images=[data.banner_image_url] if data.banner_image_url else None, url=(url.human_repr() if (url := URL(data.jump_link)).scheme.startswith("http") else None), timestamp=data.updated_at, compress=True, ) class AkVersion(StatusChange): categories: ClassVar[dict[Category, str]] = {2: "更新信息"} platform_name = "arknights" name = "明日方舟游戏信息" enable_tag = False enabled = True is_common = False site = ArknightsSite has_target = False default_theme = "brief" @classmethod async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None: return "明日方舟游戏信息" async def get_status(self, _): client = await self.ctx.get_client() res_ver = await client.get("https://ak-conf.hypergryph.com/config/prod/official/IOS/version") res_preanounce = await client.get( "https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/preannouncement.meta.json" ) res = res_ver.json() res.update(res_preanounce.json()) return res def compare_status(self, _, old_status, new_status): res = [] ArkUpdatePost = partial(Post, self, "", nickname="明日方舟更新信息") if old_status.get("preAnnounceType") == 2 and new_status.get("preAnnounceType") == 0: res.append(ArkUpdatePost(title="登录界面维护公告上线(大概是开始维护了)")) elif old_status.get("preAnnounceType") == 0 and new_status.get("preAnnounceType") == 2: res.append(ArkUpdatePost(title="登录界面维护公告下线(大概是开服了,冲!)")) if old_status.get("clientVersion") != new_status.get("clientVersion"): res.append(ArkUpdatePost(title="游戏本体更新(大更新)")) if old_status.get("resVersion") != new_status.get("resVersion"): res.append(ArkUpdatePost(title="游戏资源更新(小更新)")) return res def get_category(self, _): return Category(2) async def parse(self, raw_post): return raw_post class MonsterSiren(NewMessage): categories: ClassVar[dict[Category, str]] = {3: "塞壬唱片新闻"} platform_name = "arknights" name = "明日方舟游戏信息" enable_tag = False enabled = True is_common = False site = ArknightsSite has_target = False @classmethod async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None: return "明日方舟游戏信息" async def get_sub_list(self, _) -> list[RawPost]: client = await self.ctx.get_client() raw_data = await client.get("https://monster-siren.hypergryph.com/api/news") return raw_data.json()["data"]["list"] def get_id(self, post: RawPost) -> Any: return post["cid"] def get_date(self, _) -> None: return None def get_category(self, _) -> Category: return Category(3) async def parse(self, raw_post: RawPost) -> Post: client = await self.ctx.get_client() url = f'https://monster-siren.hypergryph.com/info/{raw_post["cid"]}' res = await client.get(f'https://monster-siren.hypergryph.com/api/news/{raw_post["cid"]}') raw_data = res.json() content = raw_data["data"]["content"] content = content.replace("

", "

\n") soup = bs(content, "html.parser") imgs = [x["src"] for x in soup("img")] text = f'{raw_post["title"]}\n{soup.text.strip()}' return Post( self, content=text, images=imgs, url=url, nickname="塞壬唱片新闻", compress=True, ) class TerraHistoricusComic(NewMessage): categories: ClassVar[dict[Category, str]] = {4: "泰拉记事社漫画"} platform_name = "arknights" name = "明日方舟游戏信息" enable_tag = False enabled = True is_common = False site = ArknightsSite has_target = False default_theme = "brief" @classmethod async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None: return "明日方舟游戏信息" async def get_sub_list(self, _) -> list[RawPost]: client = await self.ctx.get_client() raw_data = await client.get("https://terra-historicus.hypergryph.com/api/recentUpdate") return raw_data.json()["data"] def get_id(self, post: RawPost) -> Any: return f'{post["comicCid"]}/{post["episodeCid"]}' def get_date(self, _) -> None: return None def get_category(self, _) -> Category: return Category(4) async def parse(self, raw_post: RawPost) -> Post: url = f'https://terra-historicus.hypergryph.com/comic/{raw_post["comicCid"]}/episode/{raw_post["episodeCid"]}' return Post( self, content=raw_post["subtitle"], title=f'{raw_post["title"]} - {raw_post["episodeShortTitle"]}', images=[raw_post["coverUrl"]], url=url, nickname="泰拉记事社漫画", compress=True, )