mirror of
https://github.com/Xtao-Labs/misskey2telegram.git
synced 2024-11-22 05:53:09 +00:00
feat: support mult misskey instance
This commit is contained in:
parent
871d3f4a01
commit
6dd58cb15d
79
defs/chat.py
79
defs/chat.py
@ -5,28 +5,27 @@ from mipac.models.lite import LiteUser
|
||||
from pyrogram.errors import MediaEmpty
|
||||
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from glover import misskey_host
|
||||
from init import bot, request
|
||||
from models.services.scheduler import add_delete_file_job, delete_file
|
||||
|
||||
|
||||
def get_user_link(user: LiteUser) -> str:
|
||||
def get_user_link(host: str, user: LiteUser) -> str:
|
||||
if user.host:
|
||||
return f"https://{user.host}/@{user.username}"
|
||||
return f"{misskey_host}/@{user.username}"
|
||||
return f"https://{host}/@{user.username}@{user.host}"
|
||||
return f"https://{host}/@{user.username}"
|
||||
|
||||
|
||||
def get_source_link(message: ChatMessage) -> str:
|
||||
def get_source_link(host: str, message: ChatMessage) -> str:
|
||||
return (
|
||||
f"{misskey_host}/my/messaging/{message.user.username}?cid={message.user.id}"
|
||||
f"https://{host}/my/messaging/{message.user.username}?cid={message.user.id}"
|
||||
if not message.group and message.user
|
||||
else f"{misskey_host}/my/messaging/group/{message.group.id}"
|
||||
else f"https://{host}/my/messaging/group/{message.group.id}"
|
||||
)
|
||||
|
||||
|
||||
def gen_button(message: ChatMessage):
|
||||
author = get_user_link(message.user)
|
||||
source = get_source_link(message)
|
||||
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),
|
||||
@ -34,24 +33,26 @@ def gen_button(message: ChatMessage):
|
||||
return InlineKeyboardMarkup([first_line])
|
||||
|
||||
|
||||
def get_content(message: ChatMessage) -> str:
|
||||
def get_content(host: str, message: ChatMessage) -> str:
|
||||
content = message.text or ""
|
||||
content = content[:768]
|
||||
user = f'<a href="{get_user_link(message.user)}">{message.user.nickname}</a>'
|
||||
user = f'<a href="{get_user_link(host, message.user)}">{message.user.nickname}</a>'
|
||||
if message.group:
|
||||
group = f'<a href="{get_source_link(message)}">{message.group.name}</a>'
|
||||
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(cid: int, message: ChatMessage, reply_to_message_id: int):
|
||||
async def send_text(
|
||||
host: str, cid: int, message: ChatMessage, reply_to_message_id: int
|
||||
):
|
||||
await bot.send_message(
|
||||
cid,
|
||||
get_content(message),
|
||||
get_content(host, message),
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
reply_markup=gen_button(message),
|
||||
reply_markup=gen_button(host, message),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
@ -61,53 +62,53 @@ def deprecated_to_text(func):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except MediaEmpty:
|
||||
return await send_text(args[0], args[2], args[3])
|
||||
return await send_text(args[0], args[1], args[3], args[4])
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_photo(
|
||||
cid: int, url: str, message: ChatMessage, reply_to_message_id: int
|
||||
host: str, cid: int, url: str, message: ChatMessage, reply_to_message_id: int
|
||||
):
|
||||
if not url:
|
||||
return await send_text(cid, message, reply_to_message_id)
|
||||
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(message),
|
||||
reply_markup=gen_button(message),
|
||||
caption=get_content(host, message),
|
||||
reply_markup=gen_button(host, message),
|
||||
)
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_video(
|
||||
cid: int, url: str, message: ChatMessage, reply_to_message_id: int
|
||||
host: str, cid: int, url: str, message: ChatMessage, reply_to_message_id: int
|
||||
):
|
||||
if not url:
|
||||
return await send_text(cid, message, reply_to_message_id)
|
||||
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(message),
|
||||
reply_markup=gen_button(message),
|
||||
caption=get_content(host, message),
|
||||
reply_markup=gen_button(host, message),
|
||||
)
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_audio(
|
||||
cid: int, url: str, message: ChatMessage, reply_to_message_id: int
|
||||
host: str, cid: int, url: str, message: ChatMessage, reply_to_message_id: int
|
||||
):
|
||||
if not url:
|
||||
return await send_text(cid, message, reply_to_message_id)
|
||||
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(message),
|
||||
reply_markup=gen_button(message),
|
||||
caption=get_content(host, message),
|
||||
reply_markup=gen_button(host, message),
|
||||
)
|
||||
|
||||
|
||||
@ -129,31 +130,31 @@ async def fetch_document(file: File) -> Optional[str]:
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_document(
|
||||
cid: int, file: File, message: ChatMessage, reply_to_message_id: int
|
||||
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(cid, message, reply_to_message_id)
|
||||
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(message),
|
||||
reply_markup=gen_button(message),
|
||||
caption=get_content(host, message),
|
||||
reply_markup=gen_button(host, message),
|
||||
)
|
||||
await delete_file(file)
|
||||
|
||||
|
||||
async def send_chat_message(cid: int, message: ChatMessage, topic_id: int):
|
||||
async def send_chat_message(host: str, cid: int, message: ChatMessage, topic_id: int):
|
||||
if not message.file:
|
||||
return await send_text(cid, message, topic_id)
|
||||
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(cid, file_url, message, topic_id)
|
||||
await send_photo(host, cid, file_url, message, topic_id)
|
||||
elif file_type.startswith("video"):
|
||||
await send_video(cid, file_url, message, topic_id)
|
||||
await send_video(host, cid, file_url, message, topic_id)
|
||||
elif file_type.startswith("audio"):
|
||||
await send_audio(cid, file_url, message, topic_id)
|
||||
await send_audio(host, cid, file_url, message, topic_id)
|
||||
else:
|
||||
await send_document(cid, message.file, message, topic_id)
|
||||
await send_document(host, cid, message.file, message, topic_id)
|
||||
|
30
defs/check_node.py
Normal file
30
defs/check_node.py
Normal file
@ -0,0 +1,30 @@
|
||||
from httpx import URL, InvalidURL
|
||||
|
||||
from init import request
|
||||
|
||||
|
||||
def get_host(url: str) -> str:
|
||||
try:
|
||||
url = URL(url)
|
||||
except InvalidURL:
|
||||
return ""
|
||||
return url.host
|
||||
|
||||
|
||||
async def check_host(host: str) -> bool:
|
||||
if not host:
|
||||
return False
|
||||
try:
|
||||
req = await request.get(f"https://{host}/.well-known/nodeinfo")
|
||||
req.raise_for_status()
|
||||
node_url = req.json()["links"][0]["href"]
|
||||
req = await request.get(node_url)
|
||||
req.raise_for_status()
|
||||
data = req.json()
|
||||
if data["software"]["name"] != "misskey":
|
||||
raise ValueError
|
||||
if not data["software"]["version"].startswith("13."):
|
||||
raise ValueError
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
@ -6,7 +6,6 @@ from mipac.models.lite import LiteUser
|
||||
from mipac.types import IDriveFile
|
||||
from pyrogram.enums import ParseMode
|
||||
from pyrogram.errors import MediaEmpty
|
||||
|
||||
from pyrogram.types import (
|
||||
InlineKeyboardMarkup,
|
||||
InlineKeyboardButton,
|
||||
@ -16,18 +15,17 @@ from pyrogram.types import (
|
||||
InputMediaAudio,
|
||||
)
|
||||
|
||||
from glover import misskey_host
|
||||
from init import bot, request
|
||||
from models.services.scheduler import add_delete_file_job, delete_file
|
||||
|
||||
|
||||
def get_note_url(note: Note) -> str:
|
||||
return f"{misskey_host}/notes/{note.id}"
|
||||
def get_note_url(host: str, note: Note) -> str:
|
||||
return f"https://{host}/notes/{note.id}"
|
||||
|
||||
|
||||
def gen_button(note: Note, author: str):
|
||||
source = get_note_url(note)
|
||||
reply_source = get_note_url(note.reply) if note.reply else None
|
||||
def gen_button(host: str, note: Note, author: str):
|
||||
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
|
||||
if reply_source:
|
||||
first_line = [
|
||||
@ -48,14 +46,16 @@ def gen_button(note: Note, author: str):
|
||||
return InlineKeyboardMarkup([first_line, second_line])
|
||||
|
||||
|
||||
def get_user_link(user: LiteUser) -> str:
|
||||
def get_user_link(host: str, user: LiteUser) -> str:
|
||||
if user.host:
|
||||
return f"https://{user.host}/@{user.username}"
|
||||
return f"{misskey_host}/@{user.username}"
|
||||
return f"https://{host}/@{user.username}@{user.host}"
|
||||
return f"https://{host}/@{user.username}"
|
||||
|
||||
|
||||
def get_user_alink(user: LiteUser) -> str:
|
||||
return "<a href=\"{}\">{}</a>".format(get_user_link(user), user.nickname or f"@{user.username}")
|
||||
def get_user_alink(host: str, user: LiteUser) -> str:
|
||||
return '<a href="{}">{}</a>'.format(
|
||||
get_user_link(host, user), user.nickname or f"@{user.username}"
|
||||
)
|
||||
|
||||
|
||||
def get_post_time(date: datetime) -> str:
|
||||
@ -66,7 +66,7 @@ def get_post_time(date: datetime) -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def get_content(note: Note) -> str:
|
||||
def get_content(host: str, note: Note) -> str:
|
||||
content = note.content or ""
|
||||
action = "发表"
|
||||
origin = ""
|
||||
@ -76,7 +76,7 @@ def get_content(note: Note) -> str:
|
||||
action = "转推"
|
||||
content = note.renote.content or content
|
||||
origin = (
|
||||
f'\n{get_user_alink(note.renote.author)} '
|
||||
f"\n{get_user_alink(host, note.renote.author)} "
|
||||
f"发表于 {get_post_time(note.renote.created_at)}"
|
||||
)
|
||||
content = content[:768]
|
||||
@ -84,16 +84,16 @@ def get_content(note: Note) -> str:
|
||||
|
||||
<code>{content}</code>
|
||||
|
||||
{get_user_alink(note.author)} {action}于 {get_post_time(note.created_at)}{origin}
|
||||
{get_user_alink(host, note.author)} {action}于 {get_post_time(note.created_at)}{origin}
|
||||
点赞: {sum(show_note.reactions.values())} | 回复: {show_note.replies_count} | 转发: {show_note.renote_count}"""
|
||||
|
||||
|
||||
async def send_text(cid: int, note: Note, reply_to_message_id: int):
|
||||
async def send_text(host: str, cid: int, note: Note, reply_to_message_id: int):
|
||||
await bot.send_message(
|
||||
cid,
|
||||
get_content(note),
|
||||
get_content(host, note),
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
reply_markup=gen_button(note, get_user_link(note.author)),
|
||||
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
@ -103,47 +103,53 @@ def deprecated_to_text(func):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except MediaEmpty:
|
||||
return await send_text(args[0], args[2], args[3])
|
||||
return await send_text(args[0], args[1], args[3], args[4])
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_photo(cid: int, url: str, note: Note, reply_to_message_id: int):
|
||||
async def send_photo(
|
||||
host: str, cid: int, url: str, note: Note, reply_to_message_id: int
|
||||
):
|
||||
if not url:
|
||||
return await send_text(cid, note, reply_to_message_id)
|
||||
return await send_text(host, cid, note, reply_to_message_id)
|
||||
await bot.send_photo(
|
||||
cid,
|
||||
url,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
caption=get_content(note),
|
||||
reply_markup=gen_button(note, get_user_link(note.author)),
|
||||
caption=get_content(host, note),
|
||||
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
|
||||
)
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_video(cid: int, url: str, note: Note, reply_to_message_id: int):
|
||||
async def send_video(
|
||||
host: str, cid: int, url: str, note: Note, reply_to_message_id: int
|
||||
):
|
||||
if not url:
|
||||
return await send_text(cid, note, reply_to_message_id)
|
||||
return await send_text(host, cid, note, reply_to_message_id)
|
||||
await bot.send_video(
|
||||
cid,
|
||||
url,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
caption=get_content(note),
|
||||
reply_markup=gen_button(note, get_user_link(note.author)),
|
||||
caption=get_content(host, note),
|
||||
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
|
||||
)
|
||||
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_audio(cid: int, url: str, note: Note, reply_to_message_id: int):
|
||||
async def send_audio(
|
||||
host: str, cid: int, url: str, note: Note, reply_to_message_id: int
|
||||
):
|
||||
if not url:
|
||||
return await send_text(cid, note, reply_to_message_id)
|
||||
return await send_text(host, cid, note, reply_to_message_id)
|
||||
await bot.send_audio(
|
||||
cid,
|
||||
url,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
caption=get_content(note),
|
||||
reply_markup=gen_button(note, get_user_link(note.author)),
|
||||
caption=get_content(host, note),
|
||||
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
|
||||
)
|
||||
|
||||
|
||||
@ -165,17 +171,17 @@ async def fetch_document(file: IDriveFile) -> Optional[str]:
|
||||
|
||||
@deprecated_to_text
|
||||
async def send_document(
|
||||
cid: int, file: IDriveFile, note: Note, reply_to_message_id: int
|
||||
host: str, cid: int, file: IDriveFile, note: Note, reply_to_message_id: int
|
||||
):
|
||||
file = await fetch_document(file)
|
||||
if not file:
|
||||
return await send_text(cid, note, reply_to_message_id)
|
||||
return await send_text(host, cid, note, reply_to_message_id)
|
||||
await bot.send_document(
|
||||
cid,
|
||||
file,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
caption=get_content(note),
|
||||
reply_markup=gen_button(note, get_user_link(note.author)),
|
||||
caption=get_content(host, note),
|
||||
reply_markup=gen_button(host, note, get_user_link(host, note.author)),
|
||||
)
|
||||
await delete_file(file)
|
||||
|
||||
@ -219,11 +225,11 @@ async def get_media_group(files: list[IDriveFile]) -> list:
|
||||
|
||||
|
||||
async def send_group(
|
||||
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
|
||||
):
|
||||
groups = await get_media_group(files)
|
||||
if len(groups) == 0:
|
||||
return await send_text(cid, note, reply_to_message_id)
|
||||
return await send_text(host, cid, note, reply_to_message_id)
|
||||
photo, video, audio, document, msg = [], [], [], [], None
|
||||
for i in groups:
|
||||
if isinstance(i, InputMediaPhoto):
|
||||
@ -278,10 +284,10 @@ async def send_group(
|
||||
)
|
||||
if msg and isinstance(msg, list):
|
||||
msg = msg[0]
|
||||
await send_text(cid, note, msg.id if msg else None)
|
||||
await send_text(host, cid, note, msg.id if msg else None)
|
||||
|
||||
|
||||
async def send_update(cid: int, note: Note, topic_id: int):
|
||||
async def send_update(host: str, cid: int, note: Note, topic_id: int):
|
||||
files = list(note.files)
|
||||
if note.reply:
|
||||
files.extend(iter(note.reply.files))
|
||||
@ -290,18 +296,18 @@ async def send_update(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(cid, note, topic_id)
|
||||
await send_text(host, cid, note, topic_id)
|
||||
case 1:
|
||||
file = files[0]
|
||||
file_url = file.get("url", None)
|
||||
file_type = file.get("type", "")
|
||||
if file_type.startswith("image"):
|
||||
await send_photo(cid, file_url, note, topic_id)
|
||||
await send_photo(host, cid, file_url, note, topic_id)
|
||||
elif file_type.startswith("video"):
|
||||
await send_video(cid, file_url, note, topic_id)
|
||||
await send_video(host, cid, file_url, note, topic_id)
|
||||
elif file_type.startswith("audio"):
|
||||
await send_audio(cid, file_url, note, topic_id)
|
||||
await send_audio(host, cid, file_url, note, topic_id)
|
||||
else:
|
||||
await send_document(cid, file, note, topic_id)
|
||||
await send_document(host, cid, file, note, topic_id)
|
||||
case _:
|
||||
await send_group(cid, files, note, topic_id)
|
||||
await send_group(host, cid, files, note, topic_id)
|
||||
|
@ -1,10 +1,11 @@
|
||||
from json import load
|
||||
|
||||
from mipac.models.lite.user import LiteUser
|
||||
from mipac.models.notification import (
|
||||
NotificationFollow,
|
||||
NotificationFollowRequest,
|
||||
NotificationAchievement,
|
||||
)
|
||||
from mipac.models.lite.user import LiteUser
|
||||
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from init import bot
|
||||
|
@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from mipac import UserDetailed
|
||||
from mipac.errors import FailedToResolveRemoteUserError
|
||||
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
@ -2,157 +2,157 @@
|
||||
"notes1": [
|
||||
"初来乍到",
|
||||
"第一次发帖",
|
||||
"祝您在Misskey玩的愉快~"
|
||||
"祝您在 Misskey 玩的愉快~"
|
||||
],
|
||||
"notes10": [
|
||||
"一些帖子",
|
||||
"发布了10篇帖子",
|
||||
"发布了 10 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes100": [
|
||||
"很多帖子",
|
||||
"发布了100篇帖子",
|
||||
"发布了 100 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes500": [
|
||||
"满是帖子",
|
||||
"发布了500篇帖子",
|
||||
"发布了 500 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes1000": [
|
||||
"积帖成山",
|
||||
"发布了1,000篇帖子",
|
||||
"发布了 1,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes5000": [
|
||||
"帖如泉涌",
|
||||
"发布了5,000篇帖子",
|
||||
"发布了 5,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes10000": [
|
||||
"超级帖",
|
||||
"发布了10,000篇帖子",
|
||||
"发布了 10,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes20000": [
|
||||
"还想要更多帖子",
|
||||
"发布了20,000篇帖子",
|
||||
"发布了 20,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes30000": [
|
||||
"帖子帖子帖子",
|
||||
"发布了30,000篇帖子",
|
||||
"发布了 30,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes40000": [
|
||||
"帖子工厂",
|
||||
"发布了40,000篇帖子",
|
||||
"发布了 40,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes50000": [
|
||||
"帖子星球",
|
||||
"发布了50,000篇帖子",
|
||||
"发布了 50,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes60000": [
|
||||
"帖子类星体",
|
||||
"发布了60,000篇帖子",
|
||||
"发布了 60,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes70000": [
|
||||
"帖子黑洞",
|
||||
"发布了70,000篇帖子",
|
||||
"发布了 70,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes80000": [
|
||||
"帖子星系",
|
||||
"发布了80,000篇帖子",
|
||||
"发布了 80,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes90000": [
|
||||
"帖子起源",
|
||||
"发布了90,000篇帖子",
|
||||
"发布了 90,000 篇帖子",
|
||||
""
|
||||
],
|
||||
"notes100000": [
|
||||
"ALL YOUR NOTE ARE BELONG TO US",
|
||||
"发布了100,000篇帖子",
|
||||
"发布了 100,000 篇帖子",
|
||||
"真的有那么多可以写的东西吗?"
|
||||
],
|
||||
"login3": [
|
||||
"初学者 I",
|
||||
"连续登录3天",
|
||||
"今天开始我就是Misskist!"
|
||||
"累计登录 3 天",
|
||||
"今天开始我就是 Misskist!"
|
||||
],
|
||||
"login7": [
|
||||
"初学者 II",
|
||||
"连续登录7天",
|
||||
"累计登录 7 天",
|
||||
"您开始习惯了吗?"
|
||||
],
|
||||
"login15": [
|
||||
"初学者 III",
|
||||
"连续登录15天",
|
||||
"累计登录 15 天",
|
||||
""
|
||||
],
|
||||
"login30": [
|
||||
"Misskist Ⅰ",
|
||||
"连续登录30天",
|
||||
"累计登录 30 天",
|
||||
""
|
||||
],
|
||||
"login60": [
|
||||
"Misskist Ⅱ",
|
||||
"连续登录60天",
|
||||
"累计登录 60 天",
|
||||
""
|
||||
],
|
||||
"login100": [
|
||||
"Misskist Ⅲ",
|
||||
"总登入100天",
|
||||
"那个用户,是Misskist喔"
|
||||
"累计登入 100 天",
|
||||
"那个用户,是 Misskist 喔"
|
||||
],
|
||||
"login200": [
|
||||
"定期联系Ⅰ",
|
||||
"总登录天数200天",
|
||||
"累计登录 200 天",
|
||||
""
|
||||
],
|
||||
"login300": [
|
||||
"定期联系Ⅱ",
|
||||
"总登录天数300天",
|
||||
"累计登录 300 天",
|
||||
""
|
||||
],
|
||||
"login400": [
|
||||
"定期联系Ⅲ",
|
||||
"总登录天数400天",
|
||||
"累计登录 400 天",
|
||||
""
|
||||
],
|
||||
"login500": [
|
||||
"老熟人Ⅰ",
|
||||
"总登录天数500天",
|
||||
"累计登录 500 天",
|
||||
"诸君,我喜欢贴文"
|
||||
],
|
||||
"login600": [
|
||||
"老熟人Ⅱ",
|
||||
"总登录天数600天",
|
||||
"累计登录 600 天",
|
||||
""
|
||||
],
|
||||
"login700": [
|
||||
"老熟人Ⅲ",
|
||||
"总登录天数700天",
|
||||
"累计登录 700 天",
|
||||
""
|
||||
],
|
||||
"login800": [
|
||||
"帖子大师Ⅰ",
|
||||
"总登录天数800天",
|
||||
"帖子大师 Ⅰ",
|
||||
"累计登录 800 天",
|
||||
""
|
||||
],
|
||||
"login900": [
|
||||
"帖子大师Ⅱ",
|
||||
"总登录天数900天",
|
||||
"帖子大师 Ⅱ",
|
||||
"累计登录 900 天",
|
||||
""
|
||||
],
|
||||
"login1000": [
|
||||
"帖子大师Ⅲ",
|
||||
"总登录天数1000天",
|
||||
"感谢您使用Misskey!"
|
||||
"帖子大师 Ⅲ",
|
||||
"累计登录 1000 天",
|
||||
"感谢您使用 Misskey!"
|
||||
],
|
||||
"noteClipped1": [
|
||||
"忍不住要收藏到便签",
|
||||
@ -186,22 +186,22 @@
|
||||
],
|
||||
"following10": [
|
||||
"关注,跟随",
|
||||
"关注超过10人",
|
||||
"关注超过 10 人",
|
||||
""
|
||||
],
|
||||
"following50": [
|
||||
"我的朋友很多",
|
||||
"关注超过50人",
|
||||
"关注超过 50 人",
|
||||
""
|
||||
],
|
||||
"following100": [
|
||||
"我的朋友很多",
|
||||
"关注超过100人",
|
||||
"胜友如云",
|
||||
"关注超过 100 人",
|
||||
""
|
||||
],
|
||||
"following300": [
|
||||
"朋友成群",
|
||||
"关注数超过300",
|
||||
"关注数超过 300",
|
||||
""
|
||||
],
|
||||
"followers1": [
|
||||
@ -211,37 +211,37 @@
|
||||
],
|
||||
"followers10": [
|
||||
"关注我吧!",
|
||||
"拥有超过10名关注者",
|
||||
"拥有超过 10 名关注者",
|
||||
""
|
||||
],
|
||||
"followers50": [
|
||||
"三五成群",
|
||||
"拥有超过50名关注者",
|
||||
"拥有超过 50 名关注者",
|
||||
""
|
||||
],
|
||||
"followers100": [
|
||||
"胜友如云",
|
||||
"拥有超过100名关注者",
|
||||
"拥有超过 100 名关注者",
|
||||
""
|
||||
],
|
||||
"followers300": [
|
||||
"排列成行",
|
||||
"拥有超过300名关注者",
|
||||
"拥有超过 300 名关注者",
|
||||
""
|
||||
],
|
||||
"followers500": [
|
||||
"信号塔",
|
||||
"拥有超过500名关注者",
|
||||
"拥有超过 500 名关注者",
|
||||
""
|
||||
],
|
||||
"followers1000": [
|
||||
"大影响家",
|
||||
"拥有超过1000名关注者",
|
||||
"拥有超过 1000 名关注者",
|
||||
""
|
||||
],
|
||||
"collectAchievements30": [
|
||||
"成就收藏家",
|
||||
"获得超过30个成就",
|
||||
"获得超过 30 个成就",
|
||||
""
|
||||
],
|
||||
"viewAchievements3min": [
|
||||
@ -251,7 +251,7 @@
|
||||
],
|
||||
"iLoveMisskey": [
|
||||
"I Love Misskey",
|
||||
"发布\"I ❤ #Misskey\"帖子",
|
||||
"发布 \"I ❤ #Misskey\" 帖子",
|
||||
"感谢您使用 Misskey ! by 开发团队"
|
||||
],
|
||||
"foundTreasure": [
|
||||
@ -261,12 +261,12 @@
|
||||
],
|
||||
"client30min": [
|
||||
"休息一下!",
|
||||
"启动客户端超过30分钟",
|
||||
"启动客户端超过 30 分钟",
|
||||
""
|
||||
],
|
||||
"client60min": [
|
||||
"Misskey重度依赖",
|
||||
"启动客户端超过60分钟",
|
||||
"Misskey 重度依赖",
|
||||
"启动客户端超过 60 分钟",
|
||||
""
|
||||
],
|
||||
"noteDeletedWithin1min": [
|
||||
@ -281,7 +281,7 @@
|
||||
],
|
||||
"postedAt0min0sec": [
|
||||
"报时",
|
||||
"在0点发布一篇帖子",
|
||||
"在 0 点发布一篇帖子",
|
||||
"嘣 嘣 嘣 Biu——!"
|
||||
],
|
||||
"selfQuote": [
|
||||
@ -291,7 +291,7 @@
|
||||
],
|
||||
"htl20npm": [
|
||||
"流动的时间线",
|
||||
"在首页时间线的流速超过20npm",
|
||||
"在首页时间线的流速超过 20npm",
|
||||
""
|
||||
],
|
||||
"viewInstanceChart": [
|
||||
@ -301,7 +301,7 @@
|
||||
],
|
||||
"outputHelloWorldOnScratchpad": [
|
||||
"Hello, world!",
|
||||
"在AiScript控制台中输出 hello world",
|
||||
"在 AiScript 控制台中输出 hello world",
|
||||
""
|
||||
],
|
||||
"open3windows": [
|
||||
@ -316,7 +316,7 @@
|
||||
],
|
||||
"reactWithoutRead": [
|
||||
"有好好读过吗?",
|
||||
"在含有100字以上的帖子被发出三秒内做出回应",
|
||||
"在含有 100 字以上的帖子被发出三秒内做出回应",
|
||||
""
|
||||
],
|
||||
"clickedClickHere": [
|
||||
@ -326,27 +326,27 @@
|
||||
],
|
||||
"justPlainLucky": [
|
||||
"超高校级的幸运",
|
||||
"每10秒有0.01的概率自动获得",
|
||||
"每 10 秒有 0.01 的概率自动获得",
|
||||
""
|
||||
],
|
||||
"setNameToSyuilo": [
|
||||
"像神一样呐",
|
||||
"将名称设定为syuilo",
|
||||
"将名称设定为 syuilo",
|
||||
""
|
||||
],
|
||||
"passedSinceAccountCreated1": [
|
||||
"一周年",
|
||||
"账户创建时间超过1年",
|
||||
"账户创建时间超过 1 年",
|
||||
""
|
||||
],
|
||||
"passedSinceAccountCreated2": [
|
||||
"二周年",
|
||||
"账户创建时间超过2年",
|
||||
"账户创建时间超过 2 年",
|
||||
""
|
||||
],
|
||||
"passedSinceAccountCreated3": [
|
||||
"三周年",
|
||||
"账户创建时间超过3年",
|
||||
"账户创建时间超过 3 年",
|
||||
""
|
||||
],
|
||||
"loggedInOnBirthday": [
|
||||
@ -366,7 +366,7 @@
|
||||
],
|
||||
"brainDiver": [
|
||||
"Brain Diver",
|
||||
"发布了包含Brain Diver链接的帖子",
|
||||
"发布了包含 Brain Diver 链接的帖子",
|
||||
"Misskey-Misskey La-Tu-Ma"
|
||||
]
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from httpx import get
|
||||
from json import dump
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from httpx import get
|
||||
|
||||
json_path = Path(__file__).parent / "achievement.json"
|
||||
|
||||
|
17
glover.py
17
glover.py
@ -1,4 +1,3 @@
|
||||
import re
|
||||
from configparser import RawConfigParser
|
||||
from typing import Union
|
||||
from distutils.util import strtobool
|
||||
@ -9,9 +8,6 @@ api_hash: str = ""
|
||||
# [Basic]
|
||||
ipv6: Union[bool, str] = "False"
|
||||
# [misskey]
|
||||
misskey_url: str = ""
|
||||
misskey_host: str = ""
|
||||
misskey_domain: str = ""
|
||||
web_domain: str = ""
|
||||
admin: int = 0
|
||||
|
||||
@ -20,20 +16,9 @@ config.read("config.ini")
|
||||
api_id = config.getint("pyrogram", "api_id", fallback=api_id)
|
||||
api_hash = config.get("pyrogram", "api_hash", fallback=api_hash)
|
||||
ipv6 = config.get("basic", "ipv6", fallback=ipv6)
|
||||
misskey_url = config.get("misskey", "url", fallback=misskey_url)
|
||||
if origin_url := re.search(r"wss?://(.*)/streaming", misskey_url):
|
||||
misskey_host = (
|
||||
origin_url[0]
|
||||
.replace("wss", "https")
|
||||
.replace("ws", "http")
|
||||
.replace("/streaming", "")
|
||||
)
|
||||
else:
|
||||
misskey_host = misskey_url
|
||||
misskey_domain = re.search(r"https?://(.*)", misskey_host)[1]
|
||||
web_domain = config.get("misskey", "web_domain", fallback=web_domain)
|
||||
admin = config.getint("misskey", "admin", fallback=admin)
|
||||
try:
|
||||
ipv6 = strtobool(ipv6)
|
||||
ipv6 = bool(strtobool(ipv6))
|
||||
except ValueError:
|
||||
ipv6 = False
|
||||
|
@ -23,7 +23,6 @@ from defs.notice import (
|
||||
send_follow_request_accept,
|
||||
send_achievement_earned,
|
||||
)
|
||||
from glover import misskey_url, misskey_host
|
||||
|
||||
from models.models.user import User, TokenStatusEnum
|
||||
from models.services.user import UserAction
|
||||
@ -45,41 +44,37 @@ class MisskeyBot(commands.Bot):
|
||||
await Router(ws).connect_channel(["main", "home"])
|
||||
|
||||
async def on_note(self, note: Note):
|
||||
if self.tg_user:
|
||||
await send_update(self.tg_user.chat_id, note, self.tg_user.timeline_topic)
|
||||
await send_update(
|
||||
self.tg_user.host, self.tg_user.chat_id, note, self.tg_user.timeline_topic
|
||||
)
|
||||
|
||||
async def on_user_followed(self, notice: NotificationFollow):
|
||||
if self.tg_user:
|
||||
await send_user_followed(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
await send_user_followed(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
|
||||
async def on_follow_request(self, notice: NotificationFollowRequest):
|
||||
if self.tg_user:
|
||||
await send_follow_request(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
await send_follow_request(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
|
||||
async def on_follow_request_accept(self, notice: NotificationFollowRequest):
|
||||
if self.tg_user:
|
||||
await send_follow_request_accept(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
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:
|
||||
await send_chat_message(
|
||||
self.tg_user.chat_id, message, self.tg_user.notice_topic
|
||||
)
|
||||
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):
|
||||
await message.api.read()
|
||||
|
||||
async def on_achievement_earned(self, notice: NotificationAchievement):
|
||||
if self.tg_user:
|
||||
await send_achievement_earned(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
await send_achievement_earned(
|
||||
self.tg_user.chat_id, notice, self.tg_user.notice_topic
|
||||
)
|
||||
|
||||
|
||||
misskey_bot_map: dict[int, MisskeyBot] = {}
|
||||
@ -99,16 +94,16 @@ async def run(user: User):
|
||||
misskey = await create_or_get_misskey_bot(user)
|
||||
try:
|
||||
logs.info(f"尝试启动 Misskey Bot WS 任务 {user.user_id}")
|
||||
await misskey.start(misskey_url, user.token)
|
||||
await misskey.start(f"wss://{user.host}", user.token)
|
||||
except ClientConnectorError:
|
||||
await sleep(3)
|
||||
await run(user)
|
||||
|
||||
|
||||
async def test_token(token: str) -> bool:
|
||||
async def test_token(host: str, token: str) -> bool:
|
||||
try:
|
||||
logs.info(f"验证 Token {token}")
|
||||
client = MisskeyClient(misskey_host, token)
|
||||
logs.info(f"验证 Token {host} {token}")
|
||||
client = MisskeyClient(f"https://{host}", token)
|
||||
await client.http.login()
|
||||
await client.http.close_session()
|
||||
return True
|
||||
@ -124,7 +119,7 @@ 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.token):
|
||||
if not await test_token(user.host, user.token):
|
||||
await UserAction.set_user_status(user_id, TokenStatusEnum.INVALID_TOKEN)
|
||||
return False
|
||||
bot.loop.create_task(run(user))
|
||||
@ -135,7 +130,7 @@ async def init_misskey_bot():
|
||||
await sqlite.create_db_and_tables()
|
||||
count = 0
|
||||
for user in await UserAction.get_all_token_ok_users():
|
||||
if not await test_token(user.token):
|
||||
if not await test_token(user.host, user.token):
|
||||
user.status = TokenStatusEnum.INVALID_TOKEN
|
||||
await UserAction.update_user(user)
|
||||
continue
|
||||
|
@ -12,8 +12,9 @@ class User(SQLModel, table=True):
|
||||
__table_args__ = dict(mysql_charset="utf8mb4", mysql_collate="utf8mb4_general_ci")
|
||||
|
||||
user_id: int = Field(primary_key=True)
|
||||
host: str = Field(default="")
|
||||
token: str = Field(default="")
|
||||
status: TokenStatusEnum = Field(sa_column=Column(Enum(TokenStatusEnum)))
|
||||
chat_id: int = Field(default=0)
|
||||
chat_id: int = Field(default=0, primary_key=True)
|
||||
timeline_topic: int = Field(default=0)
|
||||
notice_topic: int = Field(default=0)
|
||||
|
@ -35,6 +35,7 @@ class UserAction:
|
||||
.where(User.timeline_topic != 0)
|
||||
.where(User.notice_topic != 0)
|
||||
.where(User.token != "")
|
||||
.where(User.host != "")
|
||||
)
|
||||
results = await session.exec(statement)
|
||||
return user[0] if (user := results.first()) else None
|
||||
@ -80,12 +81,22 @@ class UserAction:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def change_user_token(user_id: int, token: str) -> bool:
|
||||
async def change_user_host(user_id: int, host: str) -> bool:
|
||||
user = await UserAction.get_user_by_id(user_id)
|
||||
if not user:
|
||||
user = User(
|
||||
user_id=user_id, token=token, status=TokenStatusEnum.STATUS_SUCCESS
|
||||
user_id=user_id, host=host, status=TokenStatusEnum.INVALID_TOKEN
|
||||
)
|
||||
user.host = host
|
||||
user.status = TokenStatusEnum.INVALID_TOKEN
|
||||
await UserAction.update_user(user)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def change_user_token(user_id: int, token: str) -> bool:
|
||||
user = await UserAction.get_user_by_id(user_id)
|
||||
if not user:
|
||||
return False
|
||||
user.token = token
|
||||
user.status = TokenStatusEnum.STATUS_SUCCESS
|
||||
await UserAction.update_user(user)
|
||||
|
@ -1,7 +1,8 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from models.models.user import User
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ["User", "Sqlite"]
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
from models.services.scheduler import scheduler
|
||||
from defs.announcement import get_unread_announcements
|
||||
|
||||
from misskey_init import misskey_bot_map
|
||||
from models.services.scheduler import scheduler
|
||||
|
||||
|
||||
@scheduler.scheduled_job("interval", minutes=15, id="check_announcement")
|
||||
|
@ -1,9 +1,8 @@
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import Message
|
||||
|
||||
from models.services.user import UserAction
|
||||
|
||||
from misskey_init import rerun_misskey_bot
|
||||
from models.services.user import UserAction
|
||||
|
||||
|
||||
async def pre_check(message: Message):
|
||||
|
@ -1,4 +1,5 @@
|
||||
import contextlib
|
||||
|
||||
from mipac.errors import (
|
||||
InternalErrorError,
|
||||
AlreadyFollowingError,
|
||||
|
@ -1,22 +1,25 @@
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from glover import misskey_host, web_domain, misskey_domain
|
||||
from defs.check_node import get_host, check_host
|
||||
from glover import web_domain
|
||||
from init import bot
|
||||
from misskey_init import test_token, rerun_misskey_bot
|
||||
from models.services.user import UserAction
|
||||
|
||||
des = f"""欢迎使用 {bot.me.first_name},这是一个用于在 Telegram 上使用 Misskey 的机器人。按下方教程开始使用:
|
||||
|
||||
1. 点击下方按钮绑定 Misskey 账号
|
||||
1. 使用 `/start https://[misskey_domain]` 设置账号所在 Misskey 实例地址(仅支持 https 链接)
|
||||
|
||||
2. 在论坛群组中使用 /bind_timeline 绑定 Timeline 话题,接收时间线更新
|
||||
2. 点击 start 之后回复你的按钮绑定所在 Misskey 实例的账号
|
||||
|
||||
3. 在论坛群组中使用 /bind_notice 绑定 Notice 话题,接收通知
|
||||
3. 在论坛群组中使用 /bind_timeline 绑定 Timeline 话题,接收时间线更新
|
||||
|
||||
4. 在论坛群组中使用 /bind_notice 绑定 Notice 话题,接收通知
|
||||
|
||||
至此,你便可以在 Telegram 接收 Misskey 消息,同时你可以私聊我使用 /status 查看 Bot 运行状态
|
||||
|
||||
此 Bot 仅支持绑定 {misskey_host} 的 Misskey 账号!"""
|
||||
此 Bot 仅支持 Misskey V13 实例的账号!"""
|
||||
|
||||
|
||||
async def finish_check(message: Message):
|
||||
@ -26,8 +29,40 @@ async def finish_check(message: Message):
|
||||
await message.reply("Token 设置完成,请绑定群组。", quote=True)
|
||||
|
||||
|
||||
def gen_url():
|
||||
return f"https://{web_domain}/gen?host={misskey_domain}&back_host={web_domain}&username={bot.me.username}"
|
||||
def gen_url(domain: str):
|
||||
return f"https://{web_domain}/gen?host={domain}&back_host={web_domain}&username={bot.me.username}"
|
||||
|
||||
|
||||
async def change_host(message: Message, token_or_host: str):
|
||||
host = get_host(token_or_host)
|
||||
if await check_host(host):
|
||||
await UserAction.change_user_host(message.from_user.id, host)
|
||||
await message.reply(
|
||||
"Host 验证成功,请点击下方按钮绑定账号。",
|
||||
quote=True,
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
[
|
||||
[
|
||||
InlineKeyboardButton(text="绑定 Misskey 账号", url=gen_url(host)),
|
||||
]
|
||||
]
|
||||
),
|
||||
)
|
||||
else:
|
||||
await message.reply("Host 验证失败,请检查 Host 是否正在运行 Misskey V13", quote=True)
|
||||
|
||||
|
||||
async def change_token(message: Message, token_or_host: str):
|
||||
if user := await UserAction.get_user_by_id(message.from_user.id):
|
||||
if user.host:
|
||||
if await test_token(user.host, token_or_host):
|
||||
await UserAction.change_user_token(message.from_user.id, token_or_host)
|
||||
await message.reply(
|
||||
"Token 验证成功,绑定账号完成。\n当你撤销此登录时,你可以重新点击按钮授权。", quote=True
|
||||
)
|
||||
await finish_check(message)
|
||||
else:
|
||||
await message.reply("Token 验证失败,请检查 Token 是否正确", quote=True)
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private & filters.command(["start"]))
|
||||
@ -36,27 +71,13 @@ async def start_command(_: Client, message: Message):
|
||||
回应 start
|
||||
"""
|
||||
if len(message.command) == 2:
|
||||
token = message.command[1]
|
||||
if not token:
|
||||
token_or_host = message.command[1]
|
||||
if not token_or_host:
|
||||
await message.reply(des, quote=True)
|
||||
return
|
||||
if await test_token(token):
|
||||
await UserAction.change_user_token(message.from_user.id, token)
|
||||
await message.reply(
|
||||
"Token 验证成功,绑定账号完成。\n" "当你撤销此登录时,你可以重新点击按钮授权。", quote=True
|
||||
)
|
||||
await finish_check(message)
|
||||
else:
|
||||
await message.reply("Token 验证失败,请检查 Token 是否正确", quote=True)
|
||||
if token_or_host.startswith("https://"):
|
||||
await change_host(message, token_or_host)
|
||||
return
|
||||
await change_token(message, token_or_host)
|
||||
return
|
||||
await message.reply(
|
||||
des,
|
||||
quote=True,
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
[
|
||||
[
|
||||
InlineKeyboardButton(text="绑定 Misskey 账号", url=gen_url()),
|
||||
]
|
||||
]
|
||||
),
|
||||
)
|
||||
await message.reply(des, quote=True)
|
||||
|
@ -2,7 +2,6 @@ from pyrogram import filters, Client
|
||||
from pyrogram.types import Message
|
||||
|
||||
from glover import admin
|
||||
|
||||
from misskey_init import get_misskey_bot, rerun_misskey_bot, misskey_bot_map
|
||||
from models.models.user import TokenStatusEnum
|
||||
from models.services.user import UserAction
|
||||
|
@ -5,4 +5,4 @@ apscheduler==3.10.1
|
||||
sqlalchemy==1.4.41
|
||||
sqlmodel==0.0.8
|
||||
aiosqlite==0.19.0
|
||||
PyYAML==6.0
|
||||
PyYAML==6.0.1
|
||||
|
Loading…
Reference in New Issue
Block a user