From b38e03077e434e356c90a434627bcd0be02de86a Mon Sep 17 00:00:00 2001 From: lumina37 Date: Wed, 26 Feb 2025 12:50:13 +0800 Subject: [PATCH] feat: last replyer #233 #250 --- aiotieba/api/_protobuf/ThreadInfo.proto | 1 + aiotieba/api/_protobuf/ThreadInfo_pb2.py | 16 +- aiotieba/api/get_last_replyers/__init__.py | 2 + aiotieba/api/get_last_replyers/_api.py | 55 ++++ aiotieba/api/get_last_replyers/_classdef.py | 268 ++++++++++++++++++ .../protobuf/FrsPageReqIdl4lp.proto | 18 ++ .../protobuf/FrsPageReqIdl4lp_pb2.py | 25 ++ .../protobuf/FrsPageResIdl4lp.proto | 20 ++ .../protobuf/FrsPageResIdl4lp_pb2.py | 29 ++ aiotieba/client.py | 37 +++ docs/ref/classdef/last_replyers.md | 1 + mkdocs.yml | 1 + 12 files changed, 465 insertions(+), 8 deletions(-) create mode 100644 aiotieba/api/get_last_replyers/__init__.py create mode 100644 aiotieba/api/get_last_replyers/_api.py create mode 100644 aiotieba/api/get_last_replyers/_classdef.py create mode 100644 aiotieba/api/get_last_replyers/protobuf/FrsPageReqIdl4lp.proto create mode 100644 aiotieba/api/get_last_replyers/protobuf/FrsPageReqIdl4lp_pb2.py create mode 100644 aiotieba/api/get_last_replyers/protobuf/FrsPageResIdl4lp.proto create mode 100644 aiotieba/api/get_last_replyers/protobuf/FrsPageResIdl4lp_pb2.py create mode 100644 docs/ref/classdef/last_replyers.md diff --git a/aiotieba/api/_protobuf/ThreadInfo.proto b/aiotieba/api/_protobuf/ThreadInfo.proto index 2cb27649..7f7fe5bb 100644 --- a/aiotieba/api/_protobuf/ThreadInfo.proto +++ b/aiotieba/api/_protobuf/ThreadInfo.proto @@ -19,6 +19,7 @@ message ThreadInfo { int32 is_good = 10; int32 is_voice_thread = 15; User author = 18; + User last_replyer = 19; repeated Voice voice_info = 23; int32 thread_type = 26; int64 fid = 27; diff --git a/aiotieba/api/_protobuf/ThreadInfo_pb2.py b/aiotieba/api/_protobuf/ThreadInfo_pb2.py index c0d99510..94986201 100644 --- a/aiotieba/api/_protobuf/ThreadInfo_pb2.py +++ b/aiotieba/api/_protobuf/ThreadInfo_pb2.py @@ -17,7 +17,7 @@ from . import Voice_pb2 as Voice__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b"\n\x10ThreadInfo.proto\x1a\nUser.proto\x1a\x0bVoice.proto\x1a\x0ePollInfo.proto\x1a\x0fVideoInfo.proto\x1a\x0fPbContent.proto\x1a\x0b\x41gree.proto\x1a\x0bMedia.proto\"\xcc\x08\n\nThreadInfo\x12\n\n\x02id\x18\x01 \x01(\x03\x12\r\n\x05title\x18\x03 \x01(\t\x12\x11\n\treply_num\x18\x04 \x01(\x05\x12\x10\n\x08view_num\x18\x05 \x01(\x05\x12\x15\n\rlast_time_int\x18\x07 \x01(\x05\x12\x0e\n\x06is_top\x18\t \x01(\x05\x12\x0f\n\x07is_good\x18\n \x01(\x05\x12\x17\n\x0fis_voice_thread\x18\x0f \x01(\x05\x12\x15\n\x06\x61uthor\x18\x12 \x01(\x0b\x32\x05.User\x12\x1a\n\nvoice_info\x18\x17 \x03(\x0b\x32\x06.Voice\x12\x13\n\x0bthread_type\x18\x1a \x01(\x05\x12\x0b\n\x03\x66id\x18\x1b \x01(\x03\x12\r\n\x05\x66name\x18\x1c \x01(\t\x12\x13\n\x0bis_livepost\x18\x1e \x01(\x05\x12\x15\n\rfirst_post_id\x18( \x01(\x03\x12\x13\n\x0b\x63reate_time\x18- \x01(\x05\x12\x0f\n\x07post_id\x18\x34 \x01(\x03\x12\x11\n\tauthor_id\x18\x38 \x01(\x03\x12\r\n\x05is_ad\x18; \x01(\r\x12\x1c\n\tpoll_info\x18J \x01(\x0b\x32\t.PollInfo\x12\x1e\n\nvideo_info\x18O \x01(\x0b\x32\n.VideoInfo\x12\x1e\n\x16is_godthread_recommend\x18U \x01(\x05\x12\x15\n\x05\x61gree\x18~ \x01(\x0b\x32\x06.Agree\x12\x12\n\tshare_num\x18\x87\x01 \x01(\x05\x12\x39\n\x12origin_thread_info\x18\x8d\x01 \x01(\x0b\x32\x1c.ThreadInfo.OriginThreadInfo\x12'\n\x12\x66irst_post_content\x18\x8e\x01 \x03(\x0b\x32\n.PbContent\x12\x18\n\x0fis_share_thread\x18\x8f\x01 \x01(\x05\x12\x0f\n\x06tab_id\x18\xaf\x01 \x01(\x05\x12\x13\n\nis_deleted\x18\xb5\x01 \x01(\x05\x12\x14\n\x0bis_frs_mask\x18\xc6\x01 \x01(\x05\x12\x30\n\rcustom_figure\x18\xd3\x01 \x01(\x0b\x32\x18.ThreadInfo.CustomFigure\x12.\n\x0c\x63ustom_state\x18\xd4\x01 \x01(\x0b\x32\x17.ThreadInfo.CustomState\x1a\xe5\x01\n\x10OriginThreadInfo\x12\r\n\x05title\x18\x01 \x01(\t\x12\x15\n\x05media\x18\x02 \x03(\x0b\x32\x06.Media\x12\r\n\x05\x66name\x18\x04 \x01(\t\x12\x0b\n\x03tid\x18\x05 \x01(\t\x12\x0b\n\x03\x66id\x18\x07 \x01(\x03\x12\x1a\n\nvoice_info\x18\x0c \x03(\x0b\x32\x06.Voice\x12\x1e\n\nvideo_info\x18\r \x01(\x0b\x32\n.VideoInfo\x12\x1b\n\x07\x63ontent\x18\x0e \x03(\x0b\x32\n.PbContent\x12\x1c\n\tpoll_info\x18\x15 \x01(\x0b\x32\t.PollInfo\x12\x0b\n\x03pid\x18\x19 \x01(\x03\x1a(\n\x0c\x43ustomFigure\x12\x18\n\x10\x62\x61\x63kground_value\x18\x03 \x01(\t\x1a\x1e\n\x0b\x43ustomState\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\tb\x06proto3" + b"\n\x10ThreadInfo.proto\x1a\nUser.proto\x1a\x0bVoice.proto\x1a\x0ePollInfo.proto\x1a\x0fVideoInfo.proto\x1a\x0fPbContent.proto\x1a\x0b\x41gree.proto\x1a\x0bMedia.proto\"\xe9\x08\n\nThreadInfo\x12\n\n\x02id\x18\x01 \x01(\x03\x12\r\n\x05title\x18\x03 \x01(\t\x12\x11\n\treply_num\x18\x04 \x01(\x05\x12\x10\n\x08view_num\x18\x05 \x01(\x05\x12\x15\n\rlast_time_int\x18\x07 \x01(\x05\x12\x0e\n\x06is_top\x18\t \x01(\x05\x12\x0f\n\x07is_good\x18\n \x01(\x05\x12\x17\n\x0fis_voice_thread\x18\x0f \x01(\x05\x12\x15\n\x06\x61uthor\x18\x12 \x01(\x0b\x32\x05.User\x12\x1b\n\x0clast_replyer\x18\x13 \x01(\x0b\x32\x05.User\x12\x1a\n\nvoice_info\x18\x17 \x03(\x0b\x32\x06.Voice\x12\x13\n\x0bthread_type\x18\x1a \x01(\x05\x12\x0b\n\x03\x66id\x18\x1b \x01(\x03\x12\r\n\x05\x66name\x18\x1c \x01(\t\x12\x13\n\x0bis_livepost\x18\x1e \x01(\x05\x12\x15\n\rfirst_post_id\x18( \x01(\x03\x12\x13\n\x0b\x63reate_time\x18- \x01(\x05\x12\x0f\n\x07post_id\x18\x34 \x01(\x03\x12\x11\n\tauthor_id\x18\x38 \x01(\x03\x12\r\n\x05is_ad\x18; \x01(\r\x12\x1c\n\tpoll_info\x18J \x01(\x0b\x32\t.PollInfo\x12\x1e\n\nvideo_info\x18O \x01(\x0b\x32\n.VideoInfo\x12\x1e\n\x16is_godthread_recommend\x18U \x01(\x05\x12\x15\n\x05\x61gree\x18~ \x01(\x0b\x32\x06.Agree\x12\x12\n\tshare_num\x18\x87\x01 \x01(\x05\x12\x39\n\x12origin_thread_info\x18\x8d\x01 \x01(\x0b\x32\x1c.ThreadInfo.OriginThreadInfo\x12'\n\x12\x66irst_post_content\x18\x8e\x01 \x03(\x0b\x32\n.PbContent\x12\x18\n\x0fis_share_thread\x18\x8f\x01 \x01(\x05\x12\x0f\n\x06tab_id\x18\xaf\x01 \x01(\x05\x12\x13\n\nis_deleted\x18\xb5\x01 \x01(\x05\x12\x14\n\x0bis_frs_mask\x18\xc6\x01 \x01(\x05\x12\x30\n\rcustom_figure\x18\xd3\x01 \x01(\x0b\x32\x18.ThreadInfo.CustomFigure\x12.\n\x0c\x63ustom_state\x18\xd4\x01 \x01(\x0b\x32\x17.ThreadInfo.CustomState\x1a\xe5\x01\n\x10OriginThreadInfo\x12\r\n\x05title\x18\x01 \x01(\t\x12\x15\n\x05media\x18\x02 \x03(\x0b\x32\x06.Media\x12\r\n\x05\x66name\x18\x04 \x01(\t\x12\x0b\n\x03tid\x18\x05 \x01(\t\x12\x0b\n\x03\x66id\x18\x07 \x01(\x03\x12\x1a\n\nvoice_info\x18\x0c \x03(\x0b\x32\x06.Voice\x12\x1e\n\nvideo_info\x18\r \x01(\x0b\x32\n.VideoInfo\x12\x1b\n\x07\x63ontent\x18\x0e \x03(\x0b\x32\n.PbContent\x12\x1c\n\tpoll_info\x18\x15 \x01(\x0b\x32\t.PollInfo\x12\x0b\n\x03pid\x18\x19 \x01(\x03\x1a(\n\x0c\x43ustomFigure\x12\x18\n\x10\x62\x61\x63kground_value\x18\x03 \x01(\t\x1a\x1e\n\x0b\x43ustomState\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\tb\x06proto3" ) _globals = globals() @@ -26,10 +26,10 @@ if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None _globals["_THREADINFO"]._serialized_start = 122 - _globals["_THREADINFO"]._serialized_end = 1222 - _globals["_THREADINFO_ORIGINTHREADINFO"]._serialized_start = 919 - _globals["_THREADINFO_ORIGINTHREADINFO"]._serialized_end = 1148 - _globals["_THREADINFO_CUSTOMFIGURE"]._serialized_start = 1150 - _globals["_THREADINFO_CUSTOMFIGURE"]._serialized_end = 1190 - _globals["_THREADINFO_CUSTOMSTATE"]._serialized_start = 1192 - _globals["_THREADINFO_CUSTOMSTATE"]._serialized_end = 1222 + _globals["_THREADINFO"]._serialized_end = 1251 + _globals["_THREADINFO_ORIGINTHREADINFO"]._serialized_start = 948 + _globals["_THREADINFO_ORIGINTHREADINFO"]._serialized_end = 1177 + _globals["_THREADINFO_CUSTOMFIGURE"]._serialized_start = 1179 + _globals["_THREADINFO_CUSTOMFIGURE"]._serialized_end = 1219 + _globals["_THREADINFO_CUSTOMSTATE"]._serialized_start = 1221 + _globals["_THREADINFO_CUSTOMSTATE"]._serialized_end = 1251 diff --git a/aiotieba/api/get_last_replyers/__init__.py b/aiotieba/api/get_last_replyers/__init__.py new file mode 100644 index 00000000..61d123f7 --- /dev/null +++ b/aiotieba/api/get_last_replyers/__init__.py @@ -0,0 +1,2 @@ +from ._api import CMD, pack_proto, parse_body, request_http, request_ws +from ._classdef import Thread_lp, Threads_lp, UserInfo_lp diff --git a/aiotieba/api/get_last_replyers/_api.py b/aiotieba/api/get_last_replyers/_api.py new file mode 100644 index 00000000..bc4120f8 --- /dev/null +++ b/aiotieba/api/get_last_replyers/_api.py @@ -0,0 +1,55 @@ +import yarl + +from ...const import APP_BASE_HOST +from ...core import HttpCore, WsCore +from ...exception import TiebaServerError +from ._classdef import Threads_lp +from .protobuf import FrsPageReqIdl4lp_pb2, FrsPageResIdl4lp_pb2 + +CMD = 301001 + + +def pack_proto(fname: str, pn: int, rn: int, sort: int, is_good: bool) -> bytes: + req_proto = FrsPageReqIdl4lp_pb2.FrsPageReqIdl4lp() + req_proto.data.common._client_type = 2 + req_proto.data.common._client_version = "6.0.1" + req_proto.data.kw = fname + req_proto.data.pn = 0 if pn == 1 else pn + req_proto.data.rn = rn + req_proto.data.rn_need = rn + 5 + req_proto.data.is_good = is_good + req_proto.data.sort_type = sort + + return req_proto.SerializeToString() + + +def parse_body(body: bytes) -> Threads_lp: + res_proto = FrsPageResIdl4lp_pb2.FrsPageResIdl4lp() + res_proto.ParseFromString(body) + + if code := res_proto.error.errorno: + raise TiebaServerError(code, res_proto.error.errmsg) + + data_proto = res_proto.data + threads = Threads_lp.from_tbdata(data_proto) + + return threads + + +async def request_http(http_core: HttpCore, fname: str, pn: int, rn: int, sort: int, is_good: bool) -> Threads_lp: + data = pack_proto(fname, pn, rn, sort, is_good) + + request = http_core.pack_proto_request( + yarl.URL.build(scheme="http", host=APP_BASE_HOST, path="/c/f/frs/page", query_string=f"cmd={CMD}"), + data, + ) + + body = await http_core.net_core.send_request(request, read_bufsize=256 * 1024) + return parse_body(body) + + +async def request_ws(ws_core: WsCore, fname: str, pn: int, rn: int, sort: int, is_good: bool) -> Threads_lp: + data = pack_proto(fname, pn, rn, sort, is_good) + + response = await ws_core.send(data, CMD) + return parse_body(await response.read()) diff --git a/aiotieba/api/get_last_replyers/_classdef.py b/aiotieba/api/get_last_replyers/_classdef.py new file mode 100644 index 00000000..0bf253e7 --- /dev/null +++ b/aiotieba/api/get_last_replyers/_classdef.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +import dataclasses as dcs +from functools import cached_property + +from ...exception import TbErrorExt +from .._classdef import Containers, TypeMessage + + +@dcs.dataclass +class Page_lp: + """ + 页信息 + + Attributes: + page_size (int): 页大小 + current_page (int): 当前页码 + total_page (int): 总页码 + total_count (int): 总计数 + + has_more (bool): 是否有后继页 + has_prev (bool): 是否有前驱页 + """ + + page_size: int = 0 + current_page: int = 0 + total_page: int = 0 + total_count: int = 0 + + has_more: bool = False + has_prev: bool = False + + @staticmethod + def from_tbdata(data_proto: TypeMessage) -> Page_lp: + page_size = data_proto.page_size + current_page = data_proto.current_page + total_page = data_proto.total_page + total_count = data_proto.total_count + has_more = bool(data_proto.has_more) + has_prev = bool(data_proto.has_prev) + return Page_lp(page_size, current_page, total_page, total_count, has_more, has_prev) + + +@dcs.dataclass +class UserInfo_lp: + """ + 用户信息 + + Attributes: + user_id (int): user_id + portrait (str): portrait + user_name (str): 用户名 + nick_name_old (str): 旧版昵称 + + nick_name (str): 用户昵称 + show_name (str): 显示名称 + log_name (str): 用于在日志中记录用户信息 + """ + + user_id: int = 0 + portrait: str = "" + user_name: str = "" + nick_name_old: str = "" + + @staticmethod + def from_tbdata(data_proto: TypeMessage) -> UserInfo_lp: + user_id = data_proto.id + portrait = data_proto.portrait + if "?" in portrait: + portrait = portrait[:-13] + user_name = data_proto.name + nick_name_old = data_proto.name_show + return UserInfo_lp(user_id, portrait, user_name, nick_name_old) + + def __str__(self) -> str: + return self.user_name or self.portrait or str(self.user_id) + + def __eq__(self, obj: UserInfo_lp) -> bool: + return self.user_id == obj.user_id + + def __hash__(self) -> int: + return self.user_id + + def __bool__(self) -> bool: + return bool(self.user_id) + + @property + def nick_name(self) -> str: + return self.nick_name_old + + @property + def show_name(self) -> str: + return self.nick_name_old or self.user_name + + @cached_property + def log_name(self) -> str: + if self.user_name: + return self.user_name + elif self.portrait: + return f"{self.nick_name_old}/{self.portrait}" + else: + return str(self.user_id) + + +@dcs.dataclass +class LastReplyer: + """ + 最后回复者的用户信息 + + Attributes: + user_id (int): user_id + user_name (str): 用户名 + nick_name_old (str): 旧版昵称 + + nick_name (str): 用户昵称 + show_name (str): 显示名称 + log_name (str): 用于在日志中记录用户信息 + """ + + user_id: int = 0 + user_name: str = "" + nick_name_old: str = "" + + @staticmethod + def from_tbdata(data_proto: TypeMessage) -> LastReplyer: + user_id = data_proto.id + user_name = data_proto.name + nick_name_old = data_proto.name_show + return LastReplyer(user_id, user_name, nick_name_old) + + def __str__(self) -> str: + return self.user_name or str(self.user_id) + + def __eq__(self, obj: LastReplyer) -> bool: + return self.user_id == obj.user_id + + def __hash__(self) -> int: + return self.user_id + + def __bool__(self) -> bool: + return bool(self.user_id) + + @property + def nick_name(self) -> str: + return self.nick_name_old + + @property + def show_name(self) -> str: + return self.nick_name_old or self.user_name + + @cached_property + def log_name(self) -> str: + return self.user_name or str(self.user_id) + + +@dcs.dataclass +class Thread_lp: + """ + 主题帖信息 + + Attributes: + text (str): 文本内容 + title (str): 标题内容 + + fid (int): 所在吧id + fname (str): 所在贴吧名 + tid (int): 主题帖tid + pid (int): 首楼回复pid + user (UserInfo_lp): 发布者的用户信息 + author_id (int): 发布者的user_id + last_replyer (LastReplyer): 最后回复者的用户信息 + + create_time (int): 创建时间 10位时间戳 以秒为单位 + last_time (int): 最后回复时间 10位时间戳 以秒为单位 + """ + + title: str = "" + + fid: int = 0 + fname: str = "" + tid: int = 0 + pid: int = 0 + user: UserInfo_lp = dcs.field(default_factory=UserInfo_lp) + last_replyer: LastReplyer = dcs.field(default_factory=LastReplyer) + + create_time: int = 0 + last_time: int = 0 + + @staticmethod + def from_tbdata(data_proto: TypeMessage) -> None: + title = data_proto.title + tid = data_proto.id + pid = data_proto.first_post_id + user = UserInfo_lp.from_tbdata(data_proto.author) + last_replyer = LastReplyer.from_tbdata(data_proto.last_replyer) + create_time = data_proto.create_time + last_time = data_proto.last_time_int + return Thread_lp(title, 0, "", tid, pid, user, last_replyer, create_time, last_time) + + def __eq__(self, obj: Thread_lp) -> bool: + return self.pid == obj.pid + + def __hash__(self) -> int: + return self.pid + + @property + def text(self) -> str: + return self.title + + @property + def author_id(self) -> int: + return self.user.user_id + + +@dcs.dataclass +class Forum_lp: + """ + 吧信息 + + Attributes: + fid (int): 贴吧id + fname (str): 贴吧名 + """ + + fid: int = 0 + fname: str = "" + + @staticmethod + def from_tbdata(data_proto: TypeMessage) -> Forum_lp: + forum_proto = data_proto.forum + fid = forum_proto.id + fname = forum_proto.name + return Forum_lp(fid, fname) + + +@dcs.dataclass +class Threads_lp(TbErrorExt, Containers[Thread_lp]): + """ + 主题帖列表 + + Attributes: + objs (list[Thread_lp]): 主题帖列表 + err (Exception | None): 捕获的异常 + + page (Page_lp): 页信息 + has_more (bool): 是否还有下一页 + + forum (Forum_lp): 所在吧信息 + """ + + page: Page_lp = dcs.field(default_factory=Page_lp) + forum: Forum_lp = dcs.field(default_factory=Forum_lp) + + @staticmethod + def from_tbdata(data_proto: TypeMessage) -> Threads_lp: + page = Page_lp.from_tbdata(data_proto.page) + forum = Forum_lp.from_tbdata(data_proto) + + objs = [Thread_lp.from_tbdata(p) for p in data_proto.thread_list] + for thread in objs: + thread.fname = forum.fname + thread.fid = forum.fid + + return Threads_lp(objs, page, forum) + + @property + def has_more(self) -> bool: + return self.page.has_more diff --git a/aiotieba/api/get_last_replyers/protobuf/FrsPageReqIdl4lp.proto b/aiotieba/api/get_last_replyers/protobuf/FrsPageReqIdl4lp.proto new file mode 100644 index 00000000..d9bf530b --- /dev/null +++ b/aiotieba/api/get_last_replyers/protobuf/FrsPageReqIdl4lp.proto @@ -0,0 +1,18 @@ +// tbclient.FrsPage.FrsPageReqIdl +syntax = "proto3"; + +import "CommonReq.proto"; + +message FrsPageReqIdl4lp { + message DataReq { + CommonReq common = 39; + string kw = 1; + int32 rn = 2; + int32 rn_need = 3; + int32 is_good = 4; + int32 cid = 5; + int32 pn = 15; + int32 sort_type = 47; + } + DataReq data = 1; +} diff --git a/aiotieba/api/get_last_replyers/protobuf/FrsPageReqIdl4lp_pb2.py b/aiotieba/api/get_last_replyers/protobuf/FrsPageReqIdl4lp_pb2.py new file mode 100644 index 00000000..31c627b6 --- /dev/null +++ b/aiotieba/api/get_last_replyers/protobuf/FrsPageReqIdl4lp_pb2.py @@ -0,0 +1,25 @@ +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_sym_db = _symbol_database.Default() + + +from ..._protobuf import CommonReq_pb2 as CommonReq__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b"\n\x16\x46rsPageReqIdl4lp.proto\x1a\x0f\x43ommonReq.proto\"\xc9\x01\n\x10\x46rsPageReqIdl4lp\x12'\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x19.FrsPageReqIdl4lp.DataReq\x1a\x8b\x01\n\x07\x44\x61taReq\x12\x1a\n\x06\x63ommon\x18' \x01(\x0b\x32\n.CommonReq\x12\n\n\x02kw\x18\x01 \x01(\t\x12\n\n\x02rn\x18\x02 \x01(\x05\x12\x0f\n\x07rn_need\x18\x03 \x01(\x05\x12\x0f\n\x07is_good\x18\x04 \x01(\x05\x12\x0b\n\x03\x63id\x18\x05 \x01(\x05\x12\n\n\x02pn\x18\x0f \x01(\x05\x12\x11\n\tsort_type\x18/ \x01(\x05\x62\x06proto3" +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "FrsPageReqIdl4lp_pb2", _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals["_FRSPAGEREQIDL4LP"]._serialized_start = 44 + _globals["_FRSPAGEREQIDL4LP"]._serialized_end = 245 + _globals["_FRSPAGEREQIDL4LP_DATAREQ"]._serialized_start = 106 + _globals["_FRSPAGEREQIDL4LP_DATAREQ"]._serialized_end = 245 diff --git a/aiotieba/api/get_last_replyers/protobuf/FrsPageResIdl4lp.proto b/aiotieba/api/get_last_replyers/protobuf/FrsPageResIdl4lp.proto new file mode 100644 index 00000000..3b29e8ea --- /dev/null +++ b/aiotieba/api/get_last_replyers/protobuf/FrsPageResIdl4lp.proto @@ -0,0 +1,20 @@ +// tbclient.FrsPage.FrsPageResIdl +syntax = "proto3"; + +import "Error.proto"; +import "Page.proto"; +import "ThreadInfo.proto"; + +message FrsPageResIdl4lp { + Error error = 1; + message DataRes { + message ForumInfo { + int64 id = 1; + string name = 2; + } + ForumInfo forum = 2; + Page page = 4; + repeated ThreadInfo thread_list = 7; + } + DataRes data = 2; +} diff --git a/aiotieba/api/get_last_replyers/protobuf/FrsPageResIdl4lp_pb2.py b/aiotieba/api/get_last_replyers/protobuf/FrsPageResIdl4lp_pb2.py new file mode 100644 index 00000000..2895a9ab --- /dev/null +++ b/aiotieba/api/get_last_replyers/protobuf/FrsPageResIdl4lp_pb2.py @@ -0,0 +1,29 @@ +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_sym_db = _symbol_database.Default() + + +from ..._protobuf import Error_pb2 as Error__pb2 +from ..._protobuf import Page_pb2 as Page__pb2 +from ..._protobuf import ThreadInfo_pb2 as ThreadInfo__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b"\n\x16\x46rsPageResIdl4lp.proto\x1a\x0b\x45rror.proto\x1a\nPage.proto\x1a\x10ThreadInfo.proto\"\xf0\x01\n\x10\x46rsPageResIdl4lp\x12\x15\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x06.Error\x12'\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x19.FrsPageResIdl4lp.DataRes\x1a\x9b\x01\n\x07\x44\x61taRes\x12\x32\n\x05\x66orum\x18\x02 \x01(\x0b\x32#.FrsPageResIdl4lp.DataRes.ForumInfo\x12\x13\n\x04page\x18\x04 \x01(\x0b\x32\x05.Page\x12 \n\x0bthread_list\x18\x07 \x03(\x0b\x32\x0b.ThreadInfo\x1a%\n\tForumInfo\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x0c\n\x04name\x18\x02 \x01(\tb\x06proto3" +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "FrsPageResIdl4lp_pb2", _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals["_FRSPAGERESIDL4LP"]._serialized_start = 70 + _globals["_FRSPAGERESIDL4LP"]._serialized_end = 310 + _globals["_FRSPAGERESIDL4LP_DATARES"]._serialized_start = 155 + _globals["_FRSPAGERESIDL4LP_DATARES"]._serialized_end = 310 + _globals["_FRSPAGERESIDL4LP_DATARES_FORUMINFO"]._serialized_start = 273 + _globals["_FRSPAGERESIDL4LP_DATARES_FORUMINFO"]._serialized_end = 310 diff --git a/aiotieba/client.py b/aiotieba/client.py index e9091a81..ff27b33b 100644 --- a/aiotieba/client.py +++ b/aiotieba/client.py @@ -44,6 +44,7 @@ get_forum_detail, get_group_msg, get_images, + get_last_replyers, get_member_users, get_posts, get_rank_forums, @@ -504,6 +505,42 @@ async def get_comments(self, tid: int, pid: int, pn: int = 1, *, is_comment: boo return await get_comments.request_http(self._http_core, tid, pid, pn, is_comment) + @handle_exception(get_last_replyers.Threads_lp) + @_try_websocket + async def get_last_replyers( + self, + fname_or_fid: str | int, + /, + pn: int = 1, + *, + rn: int = 30, + sort: ThreadSortType = ThreadSortType.REPLY, + is_good: bool = False, + ) -> get_last_replyers.Threads_lp: + """ + 通过旧版接口获取带最后回复人的首页帖子 + + Args: + fname_or_fid (str | int): 贴吧名或fid 优先贴吧名 + pn (int, optional): 页码. Defaults to 1. + rn (int, optional): 请求的条目数. Defaults to 30. Max to 100. + sort (ThreadSortType, optional): HOT热门排序 REPLY按回复时间 CREATE按发布时间 FOLLOW关注的人. Defaults to ThreadSortType.REPLY. + is_good (bool, optional): True则获取精品区帖子 False则获取普通区帖子. Defaults to False. + + Returns: + Threads_lp: 带最后回复人的帖子列表 + + Note: + 该接口主要用于反挖坟 目前未封装完整的返回信息 + """ + + fname = fname_or_fid if isinstance(fname_or_fid, str) else await self.__get_fname(fname_or_fid) + + if self._ws_core.status == WsStatus.OPEN: + return await get_last_replyers.request_ws(self._ws_core, fname, pn, rn, sort, is_good) + + return await get_last_replyers.request_http(self._http_core, fname, pn, rn, sort, is_good) + @handle_exception(search_exact.ExactSearches) async def search_exact( self, diff --git a/docs/ref/classdef/last_replyers.md b/docs/ref/classdef/last_replyers.md new file mode 100644 index 00000000..5f574cdd --- /dev/null +++ b/docs/ref/classdef/last_replyers.md @@ -0,0 +1 @@ +::: aiotieba.api.get_last_replyers._classdef diff --git a/mkdocs.yml b/mkdocs.yml index aaa36a6f..f97cbbce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,7 @@ nav: - threads: ref/classdef/threads.md - posts: ref/classdef/posts.md - comments: ref/classdef/comments.md + - last_replyers: ref/classdef/last_replyers.md - searches: ref/classdef/searches.md - user_info: ref/classdef/user_info.md - profile: ref/classdef/profile.md