mirror of
https://github.com/Xtao-Labs/misskey2telegram.git
synced 2024-11-22 05:53:09 +00:00
feat: support self timeline push
This commit is contained in:
parent
44e091752b
commit
37e2294a48
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 实例的账号!"""
|
||||
|
Loading…
Reference in New Issue
Block a user