diff --git a/defs/misskey.py b/defs/misskey.py index 9e44fbf..47f7e71 100644 --- a/defs/misskey.py +++ b/defs/misskey.py @@ -1,11 +1,10 @@ import contextlib from datetime import datetime, timedelta, timezone -from typing import Optional +from typing import Optional, List import aiofiles as aiofiles -from mipac import Note +from mipac import Note, File from mipac.models.lite import LiteUser -from mipac.types import IDriveFile from pyrogram.enums import ParseMode from pyrogram.errors import MediaEmpty from pyrogram.types import ( @@ -15,6 +14,7 @@ from pyrogram.types import ( InputMediaVideo, InputMediaDocument, InputMediaAudio, + Message, ) from defs.image import webp_to_png @@ -106,8 +106,8 @@ def get_content(host: str, note: Note) -> str: async def send_text( host: str, cid: int, note: Note, reply_to_message_id: int, show_second: bool -): - await bot.send_message( +) -> Message: + return await bot.send_message( cid, get_content(host, note), reply_to_message_id=reply_to_message_id, @@ -128,10 +128,10 @@ def deprecated_to_text(func): return wrapper -async def fetch_document(file: IDriveFile) -> Optional[str]: - file_name = "downloads/" + file.get("name", "file") - file_url = file.get("url", None) - if file.get("size", 0) > 10 * 1024 * 1024: +async def fetch_document(file: File) -> Optional[str]: + file_name = "downloads/" + file.name + file_url = file.url + if file.size > 10 * 1024 * 1024: return file_url if not file_url: return file_url @@ -157,10 +157,10 @@ async def send_photo( note: Note, reply_to_message_id: int, show_second: bool, -): +) -> Message: if not url: return await send_text(host, cid, note, reply_to_message_id, show_second) - await bot.send_photo( + return await bot.send_photo( cid, url, reply_to_message_id=reply_to_message_id, @@ -179,10 +179,10 @@ async def send_video( note: Note, reply_to_message_id: int, show_second: bool, -): +) -> Message: if not url: return await send_text(host, cid, note, reply_to_message_id, show_second) - await bot.send_video( + return await bot.send_video( cid, url, reply_to_message_id=reply_to_message_id, @@ -201,10 +201,10 @@ async def send_audio( note: Note, reply_to_message_id: int, show_second: bool, -): +) -> Message: if not url: return await send_text(host, cid, note, reply_to_message_id, show_second) - await bot.send_audio( + return await bot.send_audio( cid, url, reply_to_message_id=reply_to_message_id, @@ -223,10 +223,10 @@ async def send_document( note: Note, reply_to_message_id: int, show_second: bool, -): +) -> Message: if not url: return await send_text(host, cid, note, reply_to_message_id, show_second) - await bot.send_document( + msg = await bot.send_document( cid, url, reply_to_message_id=reply_to_message_id, @@ -237,15 +237,16 @@ async def send_document( ) with contextlib.suppress(Exception): await delete_file(url) + return msg -async def get_media_group(files: list[IDriveFile]) -> list: +async def get_media_group(files: list[File]) -> list: media_lists = [] for file_ in files: - file_url = file_.get("url", None) + file_url = file_.url if not file_url: continue - file_type = file_.get("type", "") + file_type = file_.type if file_type.startswith("image"): media_lists.append( InputMediaPhoto( @@ -280,14 +281,14 @@ async def get_media_group(files: list[IDriveFile]) -> list: async def send_group( host: str, cid: int, - files: list[IDriveFile], + files: list[File], note: Note, reply_to_message_id: int, show_second: bool, -): +) -> List[Message]: groups = await get_media_group(files) if len(groups) == 0: - return await send_text(host, cid, note, reply_to_message_id, show_second) + return [await send_text(host, cid, note, reply_to_message_id, show_second)] photo, video, audio, document, msg = [], [], [], [], None for i in groups: if isinstance(i, InputMediaPhoto): @@ -341,33 +342,41 @@ async def send_group( reply_to_message_id=reply_to_message_id, ) if msg and isinstance(msg, list): - msg = msg[0] - await send_text(host, cid, note, msg.id if msg else None, show_second) + msg_ids = msg + elif msg: + msg_ids = [msg] + else: + msg_ids = [] + tmsg = await send_text( + host, cid, note, msg_ids[0].id if msg_ids else None, show_second + ) + if tmsg: + msg_ids.append(tmsg) + return msg_ids async def send_update( host: str, cid: int, note: Note, topic_id: Optional[int], show_second: bool -): +) -> List[Message]: files = list(note.files) if note.reply: files.extend(iter(note.reply.files)) if note.renote: files.extend(iter(note.renote.files)) - files = list({f.get("id"): f for f in files}.values()) match len(files): case 0: - await send_text(host, cid, note, topic_id, show_second) + return [await send_text(host, cid, note, topic_id, show_second)] case 1: file = files[0] - file_type = file.get("type", "") + file_type = file.type url = await fetch_document(file) if file_type.startswith("image"): - await send_photo(host, cid, url, note, topic_id, show_second) + return await send_photo(host, cid, url, note, topic_id, show_second) elif file_type.startswith("video"): - await send_video(host, cid, url, note, topic_id, show_second) + return await send_video(host, cid, url, note, topic_id, show_second) elif file_type.startswith("audio"): - await send_audio(host, cid, url, note, topic_id, show_second) + return await send_audio(host, cid, url, note, topic_id, show_second) else: - await send_document(host, cid, url, note, topic_id, show_second) + return await send_document(host, cid, url, note, topic_id, show_second) case _: - await send_group(host, cid, files, note, topic_id, show_second) + return await send_group(host, cid, files, note, topic_id, show_second) diff --git a/glover.py b/glover.py index d096991..e7bf35d 100644 --- a/glover.py +++ b/glover.py @@ -7,6 +7,7 @@ api_id: int = 0 api_hash: str = "" # [Basic] ipv6: Union[bool, str] = "False" +cache_uri: str = "mem://" # [misskey] web_domain: str = "" admin: int = 0 @@ -16,6 +17,7 @@ config.read("config.ini") api_id = config.getint("pyrogram", "api_id", fallback=api_id) api_hash = config.get("pyrogram", "api_hash", fallback=api_hash) ipv6 = config.get("basic", "ipv6", fallback=ipv6) +cache_uri = config.get("basic", "cache_uri", fallback=cache_uri) web_domain = config.get("misskey", "web_domain", fallback=web_domain) admin = config.getint("misskey", "admin", fallback=admin) try: diff --git a/init.py b/init.py index 4a17807..30688c0 100644 --- a/init.py +++ b/init.py @@ -1,19 +1,23 @@ -from logging import getLogger, INFO, ERROR, StreamHandler, basicConfig, CRITICAL, Formatter +from logging import getLogger, INFO, StreamHandler, basicConfig, CRITICAL, Formatter import httpx import pyrogram +from cashews import cache + from models.fix_topic import fix_topic -from glover import api_id, api_hash, ipv6 +from glover import api_id, api_hash, ipv6, cache_uri from models.services.scheduler import scheduler from models.sqlite import Sqlite +# Set Cache +cache.setup(cache_uri) # Enable logging logs = getLogger(__name__) logging_handler = StreamHandler() -dt_fmt = '%Y-%m-%d %H:%M:%S' +dt_fmt = "%Y-%m-%d %H:%M:%S" formatter = Formatter( - '[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{' + "[{asctime}] [{levelname:<8}] {name}: {message}", dt_fmt, style="{" ) logging_handler.setFormatter(formatter) root_logger = getLogger() diff --git a/misskey_init.py b/misskey_init.py index 7dda5fe..02a097d 100644 --- a/misskey_init.py +++ b/misskey_init.py @@ -1,5 +1,5 @@ import contextlib -from asyncio import sleep +from asyncio import sleep, Lock from typing import Optional, Union from aiohttp import ClientConnectorError @@ -12,6 +12,7 @@ from mipac import ( NotificationFollowRequest, ChatMessage, NotificationAchievement, + NoteDeleted, ) from mipac.client import Client as MisskeyClient @@ -25,6 +26,7 @@ from defs.notice import ( ) from models.models.user import User, TokenStatusEnum +from models.services.revoke import RevokeAction from models.services.user import UserAction from init import bot, logs, sqlite @@ -36,30 +38,47 @@ class MisskeyBot(commands.Bot): self.user_id: int = user.user_id self.instance_user_id: str = user.instance_user_id self.tg_user: User = user + self.lock = Lock() + + async def when_start(self, ws): + await Router(ws).connect_channel(["main", "home"]) + subs = await RevokeAction.get_all_subs(self.tg_user.user_id) + for sub in subs: + await Router(ws).capture_message(sub) async def on_ready(self, ws): - await Router(ws).connect_channel(["main", "home"]) + await self.when_start(ws) logs.info(f"成功启动 Misskey Bot WS 任务 {self.user_id}") async def on_reconnect(self, ws): - await Router(ws).connect_channel(["main", "home"]) + await self.when_start(ws) + logs.info(f"成功重连 Misskey Bot WS 任务 {self.user_id}") async def on_note(self, note: Note): logs.info(f"{self.tg_user.user_id} 收到新 note {note.id}") - if self.tg_user.chat_id != 0 and self.tg_user.timeline_topic != 0: - await send_update( - self.tg_user.host, - self.tg_user.chat_id, - note, - self.tg_user.timeline_topic, - True, - ) - if note.user_id == self.instance_user_id and self.tg_user.push_chat_id != 0: - await send_update( - self.tg_user.host, self.tg_user.push_chat_id, note, None, False - ) + async with self.lock: + if self.tg_user.chat_id != 0 and self.tg_user.timeline_topic != 0: + msgs = await send_update( + self.tg_user.host, + self.tg_user.chat_id, + note, + self.tg_user.timeline_topic, + True, + ) + await RevokeAction.push(self.tg_user.user_id, note.id, msgs) + if note.user_id == self.instance_user_id and self.tg_user.push_chat_id != 0: + msgs = await send_update( + self.tg_user.host, self.tg_user.push_chat_id, note, None, False + ) + await RevokeAction.push(self.tg_user.user_id, note.id, msgs) logs.info(f"{self.tg_user.user_id} 处理 note {note.id} 完成") + async def on_note_deleted(self, note: NoteDeleted): + logs.info(f"{self.tg_user.user_id} 收到 note 删除 {note.note_id}") + async with self.lock: + await RevokeAction.process_delete_note(self.tg_user.user_id, note.note_id) + logs.info(f"{self.tg_user.user_id} 处理 note 删除 {note.note_id} 完成") + async def on_user_followed(self, notice: NotificationFollow): if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0: return diff --git a/models/services/revoke.py b/models/services/revoke.py new file mode 100644 index 0000000..5bbc282 --- /dev/null +++ b/models/services/revoke.py @@ -0,0 +1,58 @@ +import base64 +from cashews import cache +from pyrogram.types import Message + +from init import bot + + +class RevokeAction: + HOURS: int = 2 + + @staticmethod + def encode_messages(messages: list[Message]) -> str: + ids = [str(message.id) for message in messages] + cid = messages[0].chat.id + text = f"{cid}:{','.join(ids)}" + return base64.b64encode(text.encode()).decode() + + @staticmethod + def decode_messages(text: str) -> tuple[int, list[int]]: + text = base64.b64decode(text.encode()).decode() + cid, ids = text.split(":") + return int(cid), [int(mid) for mid in ids.split(",")] + + @staticmethod + async def push(uid: int, note_id: str, messages: list[Message]): + await cache.set( + f"sub:{uid}:{note_id}", + RevokeAction.encode_messages(messages), + expire=60 * 60 * RevokeAction.HOURS, + ) + + @staticmethod + async def get(uid: int, note_id: str) -> tuple[int, list[int]]: + text = await cache.get(f"sub:{uid}:{note_id}") + if text is None: + raise ValueError("No such sub note: {}".format(note_id)) + return RevokeAction.decode_messages(text) + + @staticmethod + async def get_all_subs(uid: int) -> list[str]: + keys = [] + async for key in cache.scan(f"sub:{uid}:*"): + key: str + keys.append(key[4:]) + return keys + + @staticmethod + async def process_delete_note(uid: int, note_id: str): + try: + cid, msgs = await RevokeAction.get(uid, note_id) + except ValueError: + return + await RevokeAction._delete_message(cid, msgs) + await cache.delete(f"sub:{uid}:{note_id}") + + @staticmethod + async def _delete_message(cid: int, msgs: list[int]): + await bot.delete_messages(cid, msgs) diff --git a/modules/help.py b/modules/help.py index 8eabca5..45b6d1e 100644 --- a/modules/help.py +++ b/modules/help.py @@ -15,6 +15,9 @@ help_msg = f"""这里是 Bot 帮助 6. 你可以在 Notice Topic 中发送 @username@hostname 或者 @username 来查找用户,对用户进行关注、取消关注操作 +7. 请注意:BOT 会持续监听帖子 2 小时,如果 2 小时内删帖则会同步删除 Telegram 推送,使用 + + 更多功能正在开发中,敬请期待!""" diff --git a/requirements.txt b/requirements.txt index 185733f..5cf7ab9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ sqlmodel==0.0.8 aiosqlite==0.19.0 PyYAML==6.0.1 aiofiles==23.1.0 -pillow +pillow==10.0.0 +cashews==6.2.0