feat: support self timeline push

This commit is contained in:
omg-xtao 2023-07-20 23:35:10 +08:00 committed by GitHub
parent 44e091752b
commit 37e2294a48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 169 additions and 32 deletions

View File

@ -23,7 +23,7 @@ def get_note_url(host: str, note: Note) -> str:
return f"https://{host}/notes/{note.id}"
def gen_button(host: str, note: Note, author: str):
def gen_button(host: str, note: Note, author: str, show_second: bool):
source = get_note_url(host, note)
reply_source = get_note_url(host, note.reply) if note.reply else None
renote_id = note.renote_id if note.reply else note.id
@ -43,7 +43,11 @@ def gen_button(host: str, note: Note, author: str):
InlineKeyboardButton(text="❤️", callback_data=f"react:{renote_id}:love"),
InlineKeyboardButton(text="🌐", callback_data=f"translate:{renote_id}"),
]
return InlineKeyboardMarkup([first_line, second_line])
return (
InlineKeyboardMarkup([first_line, second_line])
if show_second
else InlineKeyboardMarkup([first_line])
)
def get_user_link(host: str, user: LiteUser) -> str:
@ -88,12 +92,16 @@ def get_content(host: str, note: Note) -> str:
点赞: {sum(show_note.reactions.values())} | 回复: {show_note.replies_count} | 转发: {show_note.renote_count}"""
async def send_text(host: str, cid: int, note: Note, reply_to_message_id: int):
async def send_text(
host: str, cid: int, note: Note, reply_to_message_id: int, show_second: bool
):
await bot.send_message(
cid,
get_content(host, note),
reply_to_message_id=reply_to_message_id,
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
reply_markup=gen_button(
host, note, get_user_link(host, note.author), show_second
),
disable_web_page_preview=True,
)
@ -103,53 +111,74 @@ def deprecated_to_text(func):
try:
return await func(*args, **kwargs)
except MediaEmpty:
return await send_text(args[0], args[1], args[3], args[4])
return await send_text(args[0], args[1], args[3], args[4], args[5])
return wrapper
@deprecated_to_text
async def send_photo(
host: str, cid: int, url: str, note: Note, reply_to_message_id: int
host: str,
cid: int,
url: str,
note: Note,
reply_to_message_id: int,
show_second: bool,
):
if not url:
return await send_text(host, cid, note, reply_to_message_id)
return await send_text(host, cid, note, reply_to_message_id, show_second)
await bot.send_photo(
cid,
url,
reply_to_message_id=reply_to_message_id,
caption=get_content(host, note),
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
reply_markup=gen_button(
host, note, get_user_link(host, note.author), show_second
),
)
@deprecated_to_text
async def send_video(
host: str, cid: int, url: str, note: Note, reply_to_message_id: int
host: str,
cid: int,
url: str,
note: Note,
reply_to_message_id: int,
show_second: bool,
):
if not url:
return await send_text(host, cid, note, reply_to_message_id)
return await send_text(host, cid, note, reply_to_message_id, show_second)
await bot.send_video(
cid,
url,
reply_to_message_id=reply_to_message_id,
caption=get_content(host, note),
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
reply_markup=gen_button(
host, note, get_user_link(host, note.author), show_second
),
)
@deprecated_to_text
async def send_audio(
host: str, cid: int, url: str, note: Note, reply_to_message_id: int
host: str,
cid: int,
url: str,
note: Note,
reply_to_message_id: int,
show_second: bool,
):
if not url:
return await send_text(host, cid, note, reply_to_message_id)
return await send_text(host, cid, note, reply_to_message_id, show_second)
await bot.send_audio(
cid,
url,
reply_to_message_id=reply_to_message_id,
caption=get_content(host, note),
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
reply_markup=gen_button(
host, note, get_user_link(host, note.author), show_second
),
)
@ -171,17 +200,24 @@ async def fetch_document(file: IDriveFile) -> Optional[str]:
@deprecated_to_text
async def send_document(
host: str, cid: int, file: IDriveFile, note: Note, reply_to_message_id: int
host: str,
cid: int,
file: IDriveFile,
note: Note,
reply_to_message_id: int,
show_second: bool,
):
file = await fetch_document(file)
if not file:
return await send_text(host, cid, note, reply_to_message_id)
return await send_text(host, cid, note, reply_to_message_id, show_second)
await bot.send_document(
cid,
file,
reply_to_message_id=reply_to_message_id,
caption=get_content(host, note),
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
reply_markup=gen_button(
host, note, get_user_link(host, note.author), show_second
),
)
await delete_file(file)
@ -225,11 +261,16 @@ async def get_media_group(files: list[IDriveFile]) -> list:
async def send_group(
host: str, cid: int, files: list[IDriveFile], note: Note, reply_to_message_id: int
host: str,
cid: int,
files: list[IDriveFile],
note: Note,
reply_to_message_id: int,
show_second: bool,
):
groups = await get_media_group(files)
if len(groups) == 0:
return await send_text(host, cid, note, reply_to_message_id)
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):
@ -284,10 +325,12 @@ async def send_group(
)
if msg and isinstance(msg, list):
msg = msg[0]
await send_text(host, cid, note, msg.id if msg else None)
await send_text(host, cid, note, msg.id if msg else None, show_second)
async def send_update(host: str, cid: int, note: Note, topic_id: int):
async def send_update(
host: str, cid: int, note: Note, topic_id: Optional[int], show_second: bool
):
files = list(note.files)
if note.reply:
files.extend(iter(note.reply.files))
@ -296,18 +339,18 @@ async def send_update(host: str, cid: int, note: Note, topic_id: int):
files = list({f.get("id"): f for f in files}.values())
match len(files):
case 0:
await send_text(host, cid, note, topic_id)
await send_text(host, cid, note, topic_id, show_second)
case 1:
file = files[0]
file_url = file.get("url", None)
file_type = file.get("type", "")
if file_type.startswith("image"):
await send_photo(host, cid, file_url, note, topic_id)
await send_photo(host, cid, file_url, note, topic_id, show_second)
elif file_type.startswith("video"):
await send_video(host, cid, file_url, note, topic_id)
await send_video(host, cid, file_url, note, topic_id, show_second)
elif file_type.startswith("audio"):
await send_audio(host, cid, file_url, note, topic_id)
await send_audio(host, cid, file_url, note, topic_id, show_second)
else:
await send_document(host, cid, file, note, topic_id)
await send_document(host, cid, file, note, topic_id, show_second)
case _:
await send_group(host, cid, files, note, topic_id)
await send_group(host, cid, files, note, topic_id, show_second)

View File

@ -1,6 +1,6 @@
import contextlib
from asyncio import sleep
from typing import Optional
from typing import Optional, Union
from aiohttp import ClientConnectorError
from mipa.exception import WebSocketNotConnected
@ -34,6 +34,7 @@ class MisskeyBot(commands.Bot):
def __init__(self, user: User):
super().__init__()
self.user_id: int = user.user_id
self.instance_user_id: str = user.instance_user_id
self.tg_user: User = user
async def on_ready(self, ws):
@ -45,8 +46,16 @@ class MisskeyBot(commands.Bot):
async def on_note(self, note: Note):
await send_update(
self.tg_user.host, self.tg_user.chat_id, note, self.tg_user.timeline_topic
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 def on_user_followed(self, notice: NotificationFollow):
await send_user_followed(
@ -100,13 +109,14 @@ async def run(user: User):
await run(user)
async def test_token(host: str, token: str) -> bool:
async def test_token(host: str, token: str) -> Union[str, bool]:
try:
logs.info(f"验证 Token {host} {token}")
client = MisskeyClient(f"https://{host}", token)
await client.http.login()
me = await client.api.user.action.get_me()
await client.http.close_session()
return True
return me.id
except Exception:
return False
@ -119,9 +129,12 @@ async def rerun_misskey_bot(user_id: int) -> bool:
user = await UserAction.get_user_if_ok(user_id)
if not user:
return False
if not await test_token(user.host, user.token):
mid = await test_token(user.host, user.token)
if not mid:
await UserAction.set_user_status(user_id, TokenStatusEnum.INVALID_TOKEN)
return False
user.instance_user_id = mid
await UserAction.change_instance_user_id(user_id, mid)
bot.loop.create_task(run(user))
return True

View File

@ -18,3 +18,5 @@ class User(SQLModel, table=True):
chat_id: int = Field(default=0, primary_key=True)
timeline_topic: int = Field(default=0)
notice_topic: int = Field(default=0)
instance_user_id: str = Field(default="")
push_chat_id: int = Field(default=0)

View File

@ -132,3 +132,25 @@ class UserAction:
user.notice_topic = notice
await UserAction.update_user(user)
return True
@staticmethod
async def change_instance_user_id(user_id: int, instance_user_id: str) -> bool:
user = await UserAction.get_user_by_id(user_id)
if not user:
return False
if user.instance_user_id == instance_user_id:
return False
user.instance_user_id = instance_user_id
await UserAction.update_user(user)
return True
@staticmethod
async def change_user_push(user_id: int, push_chat_id: int) -> bool:
user = await UserAction.get_user_by_id(user_id)
if not user:
return False
if user.push_chat_id == push_chat_id:
return False
user.push_chat_id = push_chat_id
await UserAction.update_user(user)
return True

View File

@ -1,4 +1,5 @@
from pyrogram import Client, filters
from pyrogram.enums import ChatType, ChatMemberStatus
from pyrogram.types import Message
from misskey_init import rerun_misskey_bot
@ -55,3 +56,57 @@ async def bind_notice_command(_: Client, message: Message):
else:
await message.reply("Notice 话题绑定失败,不能和 Timeline 话题相同。", quote=True)
await finish_check(message)
@Client.on_message(filters.incoming & filters.private & filters.command(["bind_push"]))
async def bind_push_command(client: Client, message: Message):
if len(message.command) != 2:
await message.reply(
"请使用 /bind_push <对话 ID> 的格式,绑定 Self Timeline Push。", quote=True
)
return
try:
push_chat_id = int(message.command[1])
except ValueError:
await message.reply("对话 ID 必须是数字。", quote=True)
return
try:
chat = await client.get_chat(push_chat_id)
if chat.type in [ChatType.SUPERGROUP, ChatType.CHANNEL, ChatType.GROUP]:
me = await client.get_chat_member(push_chat_id, "me")
if me.status not in [
ChatMemberStatus.OWNER,
ChatMemberStatus.ADMINISTRATOR,
]:
raise FileExistsError
you = await client.get_chat_member(push_chat_id, message.from_user.id)
if you.status not in [
ChatMemberStatus.OWNER,
ChatMemberStatus.ADMINISTRATOR,
]:
raise FileNotFoundError
except FileExistsError:
await message.reply("对话 ID 无效,我不是该对话的管理员。", quote=True)
return
except FileNotFoundError:
await message.reply("对话 ID 无效,你不是该对话的管理员。", quote=True)
return
except Exception:
await message.reply("对话 ID 无效。", quote=True)
return
if await UserAction.change_user_push(message.from_user.id, push_chat_id):
await message.reply("Self Timeline Push 对话绑定成功。", quote=True)
else:
await message.reply("Self Timeline Push 对话绑定失败,可能已经绑定过了。", quote=True)
await finish_check(message)
@Client.on_message(
filters.incoming & filters.private & filters.command(["unbind_push"])
)
async def unbind_push_command(_: Client, message: Message):
if await UserAction.get_user_by_id(message.from_user.id):
if await UserAction.change_user_push(message.from_user.id, 0):
await message.reply("Self Timeline Push 对话解绑成功。", quote=True)
else:
await message.reply("Self Timeline Push 对话解绑失败,可能没有绑定。", quote=True)

View File

@ -17,6 +17,8 @@ des = f"""欢迎使用 {bot.me.first_name},这是一个用于在 Telegram 上
4. 在论坛群组中使用 /bind_notice 绑定 Notice 话题接收通知
5. [可选] 在私聊中使用 `/bind_push [对话id]` 绑定本人时间线推送 /unbind_push 解除绑定
至此你便可以在 Telegram 接收 Misskey 消息同时你可以私聊我使用 /status 查看 Bot 运行状态
Bot 仅支持 Misskey V13 实例的账号"""