mirror of
https://github.com/Xtao-Labs/misskey2telegram.git
synced 2024-11-22 05:53:09 +00:00
feat: fcm push
This commit is contained in:
parent
2408824848
commit
4fc4951390
30
alembic/versions/fcdaa7ac5975_fcm.py
Normal file
30
alembic/versions/fcdaa7ac5975_fcm.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""fcm
|
||||
|
||||
Revision ID: fcdaa7ac5975
|
||||
Revises: a89b6e618441
|
||||
Create Date: 2023-08-03 16:45:44.084709
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "fcdaa7ac5975"
|
||||
down_revision = "a89b6e618441"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"user", sa.Column("fcm_token", sa.String(), nullable=True, default="")
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("user", "fcm_token")
|
||||
# ### end Alembic commands ###
|
160
defs/chat.py
160
defs/chat.py
@ -1,160 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from mipac import ChatMessage, File
|
||||
from mipac.models.lite import LiteUser
|
||||
from pyrogram.errors import MediaEmpty
|
||||
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from init import bot, request
|
||||
from models.services.scheduler import add_delete_file_job, delete_file
|
||||
|
||||
|
||||
def get_user_link(host: str, user: LiteUser) -> str:
|
||||
if user.host:
|
||||
return f"https://{host}/@{user.username}@{user.host}"
|
||||
return f"https://{host}/@{user.username}"
|
||||
|
||||
|
||||
def get_source_link(host: str, message: ChatMessage) -> str:
|
||||
return (
|
||||
f"https://{host}/my/messaging/{message.user.username}?cid={message.user.id}"
|
||||
if not message.group and message.user
|
||||
else f"https://{host}/my/messaging/group/{message.group.id}"
|
||||
)
|
||||
|
||||
|
||||
def gen_button(host: str, message: ChatMessage):
|
||||
author = get_user_link(host, message.user)
|
||||
source = get_source_link(host, message)
|
||||
first_line = [
|
||||
InlineKeyboardButton(text="Chat", url=source),
|
||||
InlineKeyboardButton(text="Author", url=author),
|
||||
]
|
||||
return InlineKeyboardMarkup([first_line])
|
||||
|
||||
|
||||
def get_content(host: str, message: ChatMessage) -> str:
|
||||
content = message.text or ""
|
||||
content = content[:768]
|
||||
user = f'<a href="{get_user_link(host, message.user)}">{message.user.nickname}</a>'
|
||||
if message.group:
|
||||
group = f'<a href="{get_source_link(host, message)}">{message.group.name}</a>'
|
||||
user += f" ( {group} )"
|
||||
return f"""<b>Misskey Message</b>
|
||||
|
||||
{user}: <code>{content}</code>"""
|
||||
|
||||
|
||||
async def send_text(
|
||||
host: str, cid: int, message: ChatMessage, reply_to_message_id: int
|
||||
):
|
||||
await bot.send_message(
|
||||
cid,
|
||||
get_content(host, message),
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
reply_markup=gen_button(host, message),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
def deprecated_to_text(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except MediaEmpty:
|
||||
return await send_text(args[0], args[1], args[3], args[4])
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_photo(
|
||||
host: str, cid: int, url: str, message: ChatMessage, reply_to_message_id: int
|
||||
):
|
||||
if not url:
|
||||
return await send_text(host, cid, message, reply_to_message_id)
|
||||
await bot.send_photo(
|
||||
cid,
|
||||
url,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
caption=get_content(host, message),
|
||||
reply_markup=gen_button(host, message),
|
||||
)
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_video(
|
||||
host: str, cid: int, url: str, message: ChatMessage, reply_to_message_id: int
|
||||
):
|
||||
if not url:
|
||||
return await send_text(host, cid, message, reply_to_message_id)
|
||||
await bot.send_video(
|
||||
cid,
|
||||
url,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
caption=get_content(host, message),
|
||||
reply_markup=gen_button(host, message),
|
||||
)
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_audio(
|
||||
host: str, cid: int, url: str, message: ChatMessage, reply_to_message_id: int
|
||||
):
|
||||
if not url:
|
||||
return await send_text(host, cid, message, reply_to_message_id)
|
||||
await bot.send_audio(
|
||||
cid,
|
||||
url,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
caption=get_content(host, message),
|
||||
reply_markup=gen_button(host, message),
|
||||
)
|
||||
|
||||
|
||||
async def fetch_document(file: File) -> Optional[str]:
|
||||
file_name = f"downloads/{file.name}"
|
||||
file_url = file.url
|
||||
if file.size > 10 * 1024 * 1024:
|
||||
return file_url
|
||||
if not file_url:
|
||||
return file_url
|
||||
req = await request.get(file_url)
|
||||
if req.status_code != 200:
|
||||
return file_url
|
||||
with open(file_name, "wb") as f:
|
||||
f.write(req.content)
|
||||
add_delete_file_job(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_document(
|
||||
host: str, cid: int, file: File, message: ChatMessage, reply_to_message_id: int
|
||||
):
|
||||
file = await fetch_document(file)
|
||||
if not file:
|
||||
return await send_text(host, cid, message, reply_to_message_id)
|
||||
await bot.send_document(
|
||||
cid,
|
||||
file,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
caption=get_content(host, message),
|
||||
reply_markup=gen_button(host, message),
|
||||
)
|
||||
await delete_file(file)
|
||||
|
||||
|
||||
async def send_chat_message(host: str, cid: int, message: ChatMessage, topic_id: int):
|
||||
if not message.file:
|
||||
return await send_text(host, cid, message, topic_id)
|
||||
file_url = message.file.url
|
||||
file_type = message.file.type
|
||||
if file_type.startswith("image"):
|
||||
await send_photo(host, cid, file_url, message, topic_id)
|
||||
elif file_type.startswith("video"):
|
||||
await send_video(host, cid, file_url, message, topic_id)
|
||||
elif file_type.startswith("audio"):
|
||||
await send_audio(host, cid, file_url, message, topic_id)
|
||||
else:
|
||||
await send_document(host, cid, message.file, message, topic_id)
|
139
defs/fcm_notice.py
Normal file
139
defs/fcm_notice.py
Normal file
@ -0,0 +1,139 @@
|
||||
from firebase_admin import messaging
|
||||
from mipac import (
|
||||
NotificationAchievement,
|
||||
NotificationFollowRequest,
|
||||
NotificationFollow,
|
||||
NotificationReaction,
|
||||
Note,
|
||||
NotificationNote,
|
||||
)
|
||||
|
||||
from defs.notice import achievement_map
|
||||
from glover import web_domain
|
||||
|
||||
from fcm_init import google_app
|
||||
|
||||
|
||||
def check_fcm_token(token: str) -> bool:
|
||||
message = messaging.Message(
|
||||
notification=messaging.Notification(
|
||||
title="Misskey Telegram Bridge",
|
||||
body="FCM Test",
|
||||
),
|
||||
token=token,
|
||||
)
|
||||
try:
|
||||
messaging.send(message, app=google_app)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def send_fcm_message(token: str, title: str, body: str, img: str = None):
|
||||
message = messaging.Message(
|
||||
notification=messaging.Notification(
|
||||
title=title,
|
||||
body=body,
|
||||
image=img,
|
||||
),
|
||||
token=token,
|
||||
)
|
||||
messaging.send(message, app=google_app)
|
||||
|
||||
|
||||
def gen_image_url(url: str) -> str:
|
||||
return f"https://{web_domain}/1.jpg?url={url}"
|
||||
|
||||
|
||||
def send_fcm_user_followed(token: str, notice: NotificationFollow):
|
||||
title = notice.user.nickname
|
||||
body = "关注了你"
|
||||
image = gen_image_url(notice.user.avatar_url)
|
||||
send_fcm_message(token, title, body, image)
|
||||
|
||||
|
||||
def send_fcm_follow_request(token: str, notice: NotificationFollowRequest):
|
||||
title = notice.user.nickname
|
||||
body = "请求关注你"
|
||||
image = gen_image_url(notice.user.avatar_url)
|
||||
send_fcm_message(token, title, body, image)
|
||||
|
||||
|
||||
def send_fcm_follow_request_accept(token: str, notice: NotificationFollowRequest):
|
||||
title = notice.user.nickname
|
||||
body = "接受了你的关注请求"
|
||||
image = gen_image_url(notice.user.avatar_url)
|
||||
send_fcm_message(token, title, body, image)
|
||||
|
||||
|
||||
def send_fcm_achievement_earned(
|
||||
token: str,
|
||||
notice: NotificationAchievement,
|
||||
):
|
||||
name, desc, note = achievement_map.get(notice.achievement, ("", "", ""))
|
||||
title = "你获得了新成就!"
|
||||
body = f"{name}:{desc} {f'- {note}' if note else ''}"
|
||||
send_fcm_message(token, title, body)
|
||||
|
||||
|
||||
def format_notice_note(note: Note):
|
||||
text = ""
|
||||
if note.content:
|
||||
text = note.content
|
||||
if note.reply:
|
||||
text += f" RE: {note.reply.content}"
|
||||
if note.renote:
|
||||
text += f" QT: {note.renote.content}"
|
||||
if len(text) >= 100:
|
||||
text = text[:100] + "..."
|
||||
return text.strip()
|
||||
|
||||
|
||||
def send_fcm_reaction(
|
||||
token: str,
|
||||
notice: NotificationReaction,
|
||||
):
|
||||
title = notice.user.nickname
|
||||
body = f"{notice.reaction} 了你的推文\n{format_notice_note(notice.note)}"
|
||||
image = gen_image_url(notice.user.avatar_url)
|
||||
send_fcm_message(token, title, body, image)
|
||||
|
||||
|
||||
def send_fcm_mention(
|
||||
token: str,
|
||||
notice: NotificationNote,
|
||||
):
|
||||
title = notice.user.nickname
|
||||
body = f"提到了你\n{format_notice_note(notice.note)}"
|
||||
image = gen_image_url(notice.user.avatar_url)
|
||||
send_fcm_message(token, title, body, image)
|
||||
|
||||
|
||||
def send_fcm_reply(
|
||||
token: str,
|
||||
notice: NotificationNote,
|
||||
):
|
||||
title = notice.user.nickname
|
||||
body = f"回复了你\n{format_notice_note(notice.note)}"
|
||||
image = gen_image_url(notice.user.avatar_url)
|
||||
send_fcm_message(token, title, body, image)
|
||||
|
||||
|
||||
def send_fcm_renote(
|
||||
token: str,
|
||||
notice: NotificationNote,
|
||||
):
|
||||
title = notice.user.nickname
|
||||
body = f"转发了你的推文\n{format_notice_note(notice.note)}"
|
||||
image = gen_image_url(notice.user.avatar_url)
|
||||
send_fcm_message(token, title, body, image)
|
||||
|
||||
|
||||
def send_fcm_quote(
|
||||
token: str,
|
||||
notice: NotificationNote,
|
||||
):
|
||||
title = notice.user.nickname
|
||||
body = f"引用了你的推文\n{format_notice_note(notice.note)}"
|
||||
image = gen_image_url(notice.user.avatar_url)
|
||||
send_fcm_message(token, title, body, image)
|
5
fcm_init.py
Normal file
5
fcm_init.py
Normal file
@ -0,0 +1,5 @@
|
||||
import firebase_admin
|
||||
from firebase_admin import credentials
|
||||
|
||||
cred = credentials.Certificate("data/google.json")
|
||||
google_app = firebase_admin.initialize_app(cred)
|
2
main.py
2
main.py
@ -5,6 +5,6 @@ from init import logs, bot
|
||||
if __name__ == "__main__":
|
||||
logs.info("Bot 开始运行")
|
||||
bot.start()
|
||||
logs.info("Bot 启动成功!")
|
||||
logs.info(f"Bot 启动成功!@{bot.me.username}")
|
||||
idle()
|
||||
bot.stop()
|
||||
|
108
misskey_init.py
108
misskey_init.py
@ -1,8 +1,12 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
from asyncio import sleep, Lock
|
||||
from builtins import function
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Optional, Union
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
from firebase_admin.exceptions import InvalidArgumentError
|
||||
from mipa.exception import WebSocketNotConnected
|
||||
from mipa.ext import commands
|
||||
from mipa.router import Router
|
||||
@ -10,13 +14,24 @@ from mipac import (
|
||||
Note,
|
||||
NotificationFollow,
|
||||
NotificationFollowRequest,
|
||||
ChatMessage,
|
||||
NotificationAchievement,
|
||||
NoteDeleted,
|
||||
NotificationReaction,
|
||||
NotificationNote,
|
||||
)
|
||||
from mipac.client import Client as MisskeyClient
|
||||
|
||||
from defs.chat import send_chat_message
|
||||
from defs.fcm_notice import (
|
||||
send_fcm_user_followed,
|
||||
send_fcm_follow_request,
|
||||
send_fcm_follow_request_accept,
|
||||
send_fcm_achievement_earned,
|
||||
send_fcm_reaction,
|
||||
send_fcm_mention,
|
||||
send_fcm_reply,
|
||||
send_fcm_renote,
|
||||
send_fcm_quote,
|
||||
)
|
||||
from defs.misskey import send_update
|
||||
from defs.notice import (
|
||||
send_user_followed,
|
||||
@ -95,45 +110,70 @@ class MisskeyBot(commands.Bot):
|
||||
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 send_fcm(self, func: function, notice):
|
||||
logs.info(f"{self.tg_user.user_id} 发送 FCM 通知 {func.__name__}")
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
with ThreadPoolExecutor() as executor:
|
||||
await loop.run_in_executor(
|
||||
executor, func, self.tg_user.fcm_token, notice
|
||||
)
|
||||
except InvalidArgumentError:
|
||||
logs.error(f"{self.tg_user.user_id} 无效的 FCM Token")
|
||||
except Exception as e:
|
||||
logs.error(e)
|
||||
|
||||
async def on_user_followed(self, notice: NotificationFollow):
|
||||
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0:
|
||||
return
|
||||
await send_user_followed(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
if self.tg_user.chat_id != 0 and self.tg_user.notice_topic != 0:
|
||||
await send_user_followed(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
if self.tg_user.fcm_token:
|
||||
await self.send_fcm(send_fcm_user_followed, notice)
|
||||
|
||||
async def on_follow_request(self, notice: NotificationFollowRequest):
|
||||
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0:
|
||||
return
|
||||
await send_follow_request(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
if self.tg_user.chat_id != 0 and self.tg_user.notice_topic != 0:
|
||||
await send_follow_request(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
if self.tg_user.fcm_token:
|
||||
await self.send_fcm(send_fcm_follow_request, notice)
|
||||
|
||||
async def on_follow_request_accept(self, notice: NotificationFollowRequest):
|
||||
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0:
|
||||
return
|
||||
await send_follow_request_accept(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
|
||||
async def on_chat(self, message: ChatMessage):
|
||||
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0:
|
||||
return
|
||||
await send_chat_message(
|
||||
self.tg_user.host, self.tg_user.chat_id, message, self.tg_user.notice_topic
|
||||
)
|
||||
|
||||
async def on_chat_unread_message(self, message: ChatMessage):
|
||||
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0:
|
||||
return
|
||||
await message.api.read()
|
||||
if self.tg_user.chat_id != 0 and self.tg_user.notice_topic != 0:
|
||||
await send_follow_request_accept(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
if self.tg_user.fcm_token:
|
||||
await self.send_fcm(send_fcm_follow_request_accept, notice)
|
||||
|
||||
async def on_achievement_earned(self, notice: NotificationAchievement):
|
||||
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0:
|
||||
return
|
||||
await send_achievement_earned(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
if self.tg_user.chat_id != 0 and self.tg_user.notice_topic != 0:
|
||||
await send_achievement_earned(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
if self.tg_user.fcm_token:
|
||||
await self.send_fcm(send_fcm_achievement_earned, notice)
|
||||
|
||||
async def on_reaction(self, notice: NotificationReaction):
|
||||
if self.tg_user.fcm_token:
|
||||
await self.send_fcm(send_fcm_reaction, notice)
|
||||
|
||||
async def on_mention(self, notice: NotificationNote):
|
||||
if self.tg_user.fcm_token:
|
||||
await self.send_fcm(send_fcm_mention, notice)
|
||||
|
||||
async def on_reply(self, notice: NotificationNote):
|
||||
if self.tg_user.fcm_token:
|
||||
await self.send_fcm(send_fcm_reply, notice)
|
||||
|
||||
async def on_renote(self, notice: NotificationNote):
|
||||
if self.tg_user.fcm_token:
|
||||
await self.send_fcm(send_fcm_renote, notice)
|
||||
|
||||
async def on_quote(self, notice: NotificationNote):
|
||||
if self.tg_user.fcm_token:
|
||||
await self.send_fcm(send_fcm_quote, notice)
|
||||
|
||||
|
||||
misskey_bot_map: dict[int, MisskeyBot] = {}
|
||||
|
@ -20,3 +20,4 @@ class User(SQLModel, table=True):
|
||||
notice_topic: int = Field(default=0)
|
||||
instance_user_id: str = Field(default="")
|
||||
push_chat_id: int = Field(default=0, sa_column=Column(sa.BigInteger))
|
||||
fcm_token: str = Field(sa_column=Column(sa.String, nullable=True, default=""))
|
||||
|
@ -158,3 +158,14 @@ class UserAction:
|
||||
user.push_chat_id = push_chat_id
|
||||
await UserAction.update_user(user)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def change_user_fcm_token(user_id: int, fcm_token: str) -> bool:
|
||||
user = await UserAction.get_user_by_id(user_id)
|
||||
if not user:
|
||||
return False
|
||||
if user.fcm_token == fcm_token:
|
||||
return False
|
||||
user.fcm_token = fcm_token
|
||||
await UserAction.update_user(user)
|
||||
return True
|
||||
|
35
modules/fcm.py
Normal file
35
modules/fcm.py
Normal file
@ -0,0 +1,35 @@
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import Message
|
||||
|
||||
from defs.fcm_notice import check_fcm_token
|
||||
from misskey_init import rerun_misskey_bot
|
||||
from models.services.user import UserAction
|
||||
|
||||
|
||||
async def finish_check(message: Message):
|
||||
if await rerun_misskey_bot(message.from_user.id):
|
||||
await message.reply("设置完成,开始链接。", quote=True)
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private & filters.command(["fcm"]))
|
||||
async def bind_fcm_token_command(_: Client, message: Message):
|
||||
user = await UserAction.get_user_if_ok(message.from_user.id)
|
||||
if not user:
|
||||
await message.reply("请先私聊我绑定 Misskey 账号。", quote=True)
|
||||
return
|
||||
if len(message.command) == 2:
|
||||
fcm_token = message.command[1]
|
||||
loop = asyncio.get_event_loop()
|
||||
with ThreadPoolExecutor() as executor:
|
||||
success = await loop.run_in_executor(executor, check_fcm_token, fcm_token)
|
||||
if success:
|
||||
await UserAction.change_user_fcm_token(message.from_user.id, fcm_token)
|
||||
await message.reply("FCM Token 绑定成功。", quote=True)
|
||||
await finish_check(message)
|
||||
else:
|
||||
await message.reply("FCM Token 无效,请尝试重新获取。", quote=True)
|
||||
else:
|
||||
await message.reply("请提供 FCM Token,APP 请联系实例管理员索要。", quote=True)
|
@ -10,3 +10,4 @@ aiofiles==23.1.0
|
||||
pillow==10.0.0
|
||||
cashews==6.2.0
|
||||
alembic==1.11.1
|
||||
firebase-admin==6.2.0
|
||||
|
Loading…
Reference in New Issue
Block a user