feat: fcm push

This commit is contained in:
xtaodada 2023-08-03 17:10:17 +08:00
parent 2408824848
commit 4fc4951390
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
10 changed files with 297 additions and 195 deletions

View 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 ###

View File

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

View File

@ -5,6 +5,6 @@ from init import logs, bot
if __name__ == "__main__": if __name__ == "__main__":
logs.info("Bot 开始运行") logs.info("Bot 开始运行")
bot.start() bot.start()
logs.info("Bot 启动成功!") logs.info(f"Bot 启动成功!@{bot.me.username}")
idle() idle()
bot.stop() bot.stop()

View File

@ -1,8 +1,12 @@
import asyncio
import contextlib import contextlib
from asyncio import sleep, Lock from asyncio import sleep, Lock
from builtins import function
from concurrent.futures import ThreadPoolExecutor
from typing import Optional, Union from typing import Optional, Union
from aiohttp import ClientConnectorError from aiohttp import ClientConnectorError
from firebase_admin.exceptions import InvalidArgumentError
from mipa.exception import WebSocketNotConnected from mipa.exception import WebSocketNotConnected
from mipa.ext import commands from mipa.ext import commands
from mipa.router import Router from mipa.router import Router
@ -10,13 +14,24 @@ from mipac import (
Note, Note,
NotificationFollow, NotificationFollow,
NotificationFollowRequest, NotificationFollowRequest,
ChatMessage,
NotificationAchievement, NotificationAchievement,
NoteDeleted, NoteDeleted,
NotificationReaction,
NotificationNote,
) )
from mipac.client import Client as MisskeyClient 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.misskey import send_update
from defs.notice import ( from defs.notice import (
send_user_followed, send_user_followed,
@ -95,45 +110,70 @@ class MisskeyBot(commands.Bot):
await RevokeAction.process_delete_note(self.tg_user.user_id, note.note_id) 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} 完成") 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): 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 and self.tg_user.notice_topic != 0:
return await send_user_followed(
await send_user_followed( self.tg_user.chat_id, notice, self.tg_user.notice_topic
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): async def on_follow_request(self, notice: NotificationFollowRequest):
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0: if self.tg_user.chat_id != 0 and self.tg_user.notice_topic != 0:
return await send_follow_request(
await send_follow_request( self.tg_user.chat_id, notice, self.tg_user.notice_topic
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): async def on_follow_request_accept(self, notice: NotificationFollowRequest):
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0: if self.tg_user.chat_id != 0 and self.tg_user.notice_topic != 0:
return await send_follow_request_accept(
await send_follow_request_accept( self.tg_user.chat_id, notice, self.tg_user.notice_topic
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_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()
async def on_achievement_earned(self, notice: NotificationAchievement): async def on_achievement_earned(self, notice: NotificationAchievement):
if self.tg_user.chat_id == 0 or self.tg_user.notice_topic == 0: if self.tg_user.chat_id != 0 and self.tg_user.notice_topic != 0:
return await send_achievement_earned(
await send_achievement_earned( self.tg_user.chat_id, notice, self.tg_user.notice_topic
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] = {} misskey_bot_map: dict[int, MisskeyBot] = {}

View File

@ -20,3 +20,4 @@ class User(SQLModel, table=True):
notice_topic: int = Field(default=0) notice_topic: int = Field(default=0)
instance_user_id: str = Field(default="") instance_user_id: str = Field(default="")
push_chat_id: int = Field(default=0, sa_column=Column(sa.BigInteger)) push_chat_id: int = Field(default=0, sa_column=Column(sa.BigInteger))
fcm_token: str = Field(sa_column=Column(sa.String, nullable=True, default=""))

View File

@ -158,3 +158,14 @@ class UserAction:
user.push_chat_id = push_chat_id user.push_chat_id = push_chat_id
await UserAction.update_user(user) await UserAction.update_user(user)
return True 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
View 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 TokenAPP 请联系实例管理员索要。", quote=True)

View File

@ -10,3 +10,4 @@ aiofiles==23.1.0
pillow==10.0.0 pillow==10.0.0
cashews==6.2.0 cashews==6.2.0
alembic==1.11.1 alembic==1.11.1
firebase-admin==6.2.0