feat: revoke note

This commit is contained in:
xtaodada 2023-07-21 22:00:49 +08:00
parent 0964f9f3f4
commit 5d1d534d19
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
7 changed files with 150 additions and 54 deletions

View File

@ -1,11 +1,10 @@
import contextlib import contextlib
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional, List
import aiofiles as aiofiles import aiofiles as aiofiles
from mipac import Note from mipac import Note, File
from mipac.models.lite import LiteUser from mipac.models.lite import LiteUser
from mipac.types import IDriveFile
from pyrogram.enums import ParseMode from pyrogram.enums import ParseMode
from pyrogram.errors import MediaEmpty from pyrogram.errors import MediaEmpty
from pyrogram.types import ( from pyrogram.types import (
@ -15,6 +14,7 @@ from pyrogram.types import (
InputMediaVideo, InputMediaVideo,
InputMediaDocument, InputMediaDocument,
InputMediaAudio, InputMediaAudio,
Message,
) )
from defs.image import webp_to_png from defs.image import webp_to_png
@ -106,8 +106,8 @@ def get_content(host: str, note: Note) -> str:
async def send_text( async def send_text(
host: str, cid: int, note: Note, reply_to_message_id: int, show_second: bool host: str, cid: int, note: Note, reply_to_message_id: int, show_second: bool
): ) -> Message:
await bot.send_message( return await bot.send_message(
cid, cid,
get_content(host, note), get_content(host, note),
reply_to_message_id=reply_to_message_id, reply_to_message_id=reply_to_message_id,
@ -128,10 +128,10 @@ def deprecated_to_text(func):
return wrapper return wrapper
async def fetch_document(file: IDriveFile) -> Optional[str]: async def fetch_document(file: File) -> Optional[str]:
file_name = "downloads/" + file.get("name", "file") file_name = "downloads/" + file.name
file_url = file.get("url", None) file_url = file.url
if file.get("size", 0) > 10 * 1024 * 1024: if file.size > 10 * 1024 * 1024:
return file_url return file_url
if not file_url: if not file_url:
return file_url return file_url
@ -157,10 +157,10 @@ async def send_photo(
note: Note, note: Note,
reply_to_message_id: int, reply_to_message_id: int,
show_second: bool, show_second: bool,
): ) -> Message:
if not url: if not url:
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)
await bot.send_photo( return await bot.send_photo(
cid, cid,
url, url,
reply_to_message_id=reply_to_message_id, reply_to_message_id=reply_to_message_id,
@ -179,10 +179,10 @@ async def send_video(
note: Note, note: Note,
reply_to_message_id: int, reply_to_message_id: int,
show_second: bool, show_second: bool,
): ) -> Message:
if not url: if not url:
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)
await bot.send_video( return await bot.send_video(
cid, cid,
url, url,
reply_to_message_id=reply_to_message_id, reply_to_message_id=reply_to_message_id,
@ -201,10 +201,10 @@ async def send_audio(
note: Note, note: Note,
reply_to_message_id: int, reply_to_message_id: int,
show_second: bool, show_second: bool,
): ) -> Message:
if not url: if not url:
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)
await bot.send_audio( return await bot.send_audio(
cid, cid,
url, url,
reply_to_message_id=reply_to_message_id, reply_to_message_id=reply_to_message_id,
@ -223,10 +223,10 @@ async def send_document(
note: Note, note: Note,
reply_to_message_id: int, reply_to_message_id: int,
show_second: bool, show_second: bool,
): ) -> Message:
if not url: if not url:
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)
await bot.send_document( msg = await bot.send_document(
cid, cid,
url, url,
reply_to_message_id=reply_to_message_id, reply_to_message_id=reply_to_message_id,
@ -237,15 +237,16 @@ async def send_document(
) )
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
await delete_file(url) 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 = [] media_lists = []
for file_ in files: for file_ in files:
file_url = file_.get("url", None) file_url = file_.url
if not file_url: if not file_url:
continue continue
file_type = file_.get("type", "") file_type = file_.type
if file_type.startswith("image"): if file_type.startswith("image"):
media_lists.append( media_lists.append(
InputMediaPhoto( InputMediaPhoto(
@ -280,14 +281,14 @@ async def get_media_group(files: list[IDriveFile]) -> list:
async def send_group( async def send_group(
host: str, host: str,
cid: int, cid: int,
files: list[IDriveFile], files: list[File],
note: Note, note: Note,
reply_to_message_id: int, reply_to_message_id: int,
show_second: bool, show_second: bool,
): ) -> List[Message]:
groups = await get_media_group(files) groups = await get_media_group(files)
if len(groups) == 0: 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 photo, video, audio, document, msg = [], [], [], [], None
for i in groups: for i in groups:
if isinstance(i, InputMediaPhoto): if isinstance(i, InputMediaPhoto):
@ -341,33 +342,41 @@ async def send_group(
reply_to_message_id=reply_to_message_id, reply_to_message_id=reply_to_message_id,
) )
if msg and isinstance(msg, list): if msg and isinstance(msg, list):
msg = msg[0] msg_ids = msg
await send_text(host, cid, note, msg.id if msg else None, show_second) 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( async def send_update(
host: str, cid: int, note: Note, topic_id: Optional[int], show_second: bool host: str, cid: int, note: Note, topic_id: Optional[int], show_second: bool
): ) -> List[Message]:
files = list(note.files) files = list(note.files)
if note.reply: if note.reply:
files.extend(iter(note.reply.files)) files.extend(iter(note.reply.files))
if note.renote: if note.renote:
files.extend(iter(note.renote.files)) files.extend(iter(note.renote.files))
files = list({f.get("id"): f for f in files}.values())
match len(files): match len(files):
case 0: 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: case 1:
file = files[0] file = files[0]
file_type = file.get("type", "") file_type = file.type
url = await fetch_document(file) url = await fetch_document(file)
if file_type.startswith("image"): 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"): 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"): 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: 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 _: case _:
await send_group(host, cid, files, note, topic_id, show_second) return await send_group(host, cid, files, note, topic_id, show_second)

View File

@ -7,6 +7,7 @@ api_id: int = 0
api_hash: str = "" api_hash: str = ""
# [Basic] # [Basic]
ipv6: Union[bool, str] = "False" ipv6: Union[bool, str] = "False"
cache_uri: str = "mem://"
# [misskey] # [misskey]
web_domain: str = "" web_domain: str = ""
admin: int = 0 admin: int = 0
@ -16,6 +17,7 @@ config.read("config.ini")
api_id = config.getint("pyrogram", "api_id", fallback=api_id) api_id = config.getint("pyrogram", "api_id", fallback=api_id)
api_hash = config.get("pyrogram", "api_hash", fallback=api_hash) api_hash = config.get("pyrogram", "api_hash", fallback=api_hash)
ipv6 = config.get("basic", "ipv6", fallback=ipv6) 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) web_domain = config.get("misskey", "web_domain", fallback=web_domain)
admin = config.getint("misskey", "admin", fallback=admin) admin = config.getint("misskey", "admin", fallback=admin)
try: try:

12
init.py
View File

@ -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 httpx
import pyrogram import pyrogram
from cashews import cache
from models.fix_topic import fix_topic 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.services.scheduler import scheduler
from models.sqlite import Sqlite from models.sqlite import Sqlite
# Set Cache
cache.setup(cache_uri)
# Enable logging # Enable logging
logs = getLogger(__name__) logs = getLogger(__name__)
logging_handler = StreamHandler() logging_handler = StreamHandler()
dt_fmt = '%Y-%m-%d %H:%M:%S' dt_fmt = "%Y-%m-%d %H:%M:%S"
formatter = Formatter( formatter = Formatter(
'[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{' "[{asctime}] [{levelname:<8}] {name}: {message}", dt_fmt, style="{"
) )
logging_handler.setFormatter(formatter) logging_handler.setFormatter(formatter)
root_logger = getLogger() root_logger = getLogger()

View File

@ -1,5 +1,5 @@
import contextlib import contextlib
from asyncio import sleep from asyncio import sleep, Lock
from typing import Optional, Union from typing import Optional, Union
from aiohttp import ClientConnectorError from aiohttp import ClientConnectorError
@ -12,6 +12,7 @@ from mipac import (
NotificationFollowRequest, NotificationFollowRequest,
ChatMessage, ChatMessage,
NotificationAchievement, NotificationAchievement,
NoteDeleted,
) )
from mipac.client import Client as MisskeyClient from mipac.client import Client as MisskeyClient
@ -25,6 +26,7 @@ from defs.notice import (
) )
from models.models.user import User, TokenStatusEnum from models.models.user import User, TokenStatusEnum
from models.services.revoke import RevokeAction
from models.services.user import UserAction from models.services.user import UserAction
from init import bot, logs, sqlite from init import bot, logs, sqlite
@ -36,30 +38,47 @@ class MisskeyBot(commands.Bot):
self.user_id: int = user.user_id self.user_id: int = user.user_id
self.instance_user_id: str = user.instance_user_id self.instance_user_id: str = user.instance_user_id
self.tg_user: User = user 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): 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}") logs.info(f"成功启动 Misskey Bot WS 任务 {self.user_id}")
async def on_reconnect(self, ws): 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): async def on_note(self, note: Note):
logs.info(f"{self.tg_user.user_id} 收到新 note {note.id}") 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: async with self.lock:
await send_update( if self.tg_user.chat_id != 0 and self.tg_user.timeline_topic != 0:
self.tg_user.host, msgs = await send_update(
self.tg_user.chat_id, self.tg_user.host,
note, self.tg_user.chat_id,
self.tg_user.timeline_topic, note,
True, 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( await RevokeAction.push(self.tg_user.user_id, note.id, msgs)
self.tg_user.host, self.tg_user.push_chat_id, note, None, False 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} 完成") 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): async def on_user_followed(self, notice: NotificationFollow):
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0: if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0:
return return

58
models/services/revoke.py Normal file
View File

@ -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)

View File

@ -15,6 +15,9 @@ help_msg = f"""这里是 Bot 帮助
6. 你可以在 Notice Topic 中发送 @username@hostname 或者 @username 来查找用户对用户进行关注取消关注操作 6. 你可以在 Notice Topic 中发送 @username@hostname 或者 @username 来查找用户对用户进行关注取消关注操作
7. 请注意BOT 会持续监听帖子 2 小时如果 2 小时内删帖则会同步删除 Telegram 推送使用
更多功能正在开发中敬请期待""" 更多功能正在开发中敬请期待"""

View File

@ -7,4 +7,5 @@ sqlmodel==0.0.8
aiosqlite==0.19.0 aiosqlite==0.19.0
PyYAML==6.0.1 PyYAML==6.0.1
aiofiles==23.1.0 aiofiles==23.1.0
pillow pillow==10.0.0
cashews==6.2.0