WismerBot / app.py
TonyD365's picture
Update app.py
05f3b66 verified
Raw
History Blame Contribute Delete
12.5 kB
# 导入自定义 DNS 库
from dns_resolver import dns_manager
# --- 注入 DNS 补丁 ---
# 必须在所有网络连接尝试之前执行
dns_manager.patch_socket()
import os
import discord
import time
from fastapi import FastAPI
import uvicorn
import asyncio
from threading import Thread
from discord import app_commands, ui
# 导入数据库基础类
from mongo import MongoManager
# 导入学科业务函数和学科列表
from material import (
CATEGORIES,
upload_material_logic,
approve_material_logic,
delete_material_logic,
list_materials_logic
)
# 初始化数据库管理器
db_manager = MongoManager(os.getenv("MONGO_URL"), "tonyd")
# 身份组权限列表
mod = [1357078628969746462, 1357079285113819146, 1436537252317626449, 1357079707539214417, 1357079889160835183]
# --- 1. FastAPI 保活 ---
app = FastAPI()
# 使用显式的方法定义,并使用 JSONResponse 确保兼容性
@app.api_route("/", methods=["GET", "HEAD"])
async def status():
# 使用 JSONResponse 包装,防止 Starlette 误判
return {"status": "hi"}
def run_flask():
# Hugging Face 默认使用 7860 端口
port = int(os.environ.get('PORT', 7860))
uvicorn.run(app, host='0.0.0.0', port=port)
# --- 2. 辅助函数 ---
def format_pings(role_input: str):
pass
# --- 3. Modal 表单定义 ---
class AnnounceModal(ui.Modal):
def __init__(self, channel: discord.TextChannel, ping_roles_str: str = None):
super().__init__(title="New Announcement")
self.channel = channel
self.ping_roles_str = ping_roles_str
announcement_title = ui.TextInput(label="Title", placeholder="Enter title...", style=discord.TextStyle.short)
announcement_body = ui.TextInput(label="Body", placeholder="Enter message...", style=discord.TextStyle.long)
async def on_submit(self, interaction: discord.Interaction):
content = self.announcement_body.value
pings = self.ping_roles_str
if pings: pass
embed = discord.Embed(
title=self.announcement_title.value,
description=content,
color=discord.Color.blue()
)
footer = f"\n\n||{pings}||\n\n---\n**Sent by:** ||{interaction.user.mention}||\n**Time:** <t:{int(time.time())}:f>"
embed.description += footer
await self.channel.send(embed=embed)
await interaction.response.send_message(f"✅ Sent to {self.channel.mention}!", ephemeral=False)
class EditAnnounceModal(ui.Modal):
def __init__(self, message: discord.Message, new_ping_roles_str: str = None):
super().__init__(title="Edit Announcement")
self.message = message
self.new_ping_roles_str = new_ping_roles_str
old_title = message.embeds[0].title if message.embeds else ""
old_body = message.embeds[0].description.split("\n\n---")[0] if message.embeds else ""
self.edit_title = ui.TextInput(label="Title", default=old_title, style=discord.TextStyle.short)
self.edit_body = ui.TextInput(label="Body", default=old_body, style=discord.TextStyle.long)
self.add_item(self.edit_title)
self.add_item(self.edit_body)
async def on_submit(self, interaction: discord.Interaction):
content = self.edit_body.value
pings = self.new_ping_roles_str
if pings: pass
new_embed = discord.Embed(
title=self.edit_title.value,
description=content,
color=discord.Color.blue()
)
footer = f"\n\n||{pings}||\n\n---\n**Edited by:** ||{interaction.user.mention}||\n**Time:** <t:{int(time.time())}:f>"
new_embed.description += footer
await self.message.edit(embed=new_embed)
await interaction.response.send_message(f"✅ Updated announcement {self.message.id} !", ephemeral=False)
# --- 4. 机器人主体 ---
class MyBot(discord.Client):
def __init__(self):
super().__init__(intents=discord.Intents.default())
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
await self.tree.sync()
print(f"✅ Slash commands synced")
client = MyBot()
client.db = db_manager
# --- 5. 指令定义 ---
@client.tree.command(name="announce", description="Post announcement")
async def announce(interaction: discord.Interaction, channel: discord.TextChannel, role_ids: str = None):
if not any(role.id in mod for role in interaction.user.roles):
await interaction.response.send_message("❌ **Access Denied**", ephemeral=True)
return
await interaction.response.send_modal(AnnounceModal(channel, role_ids))
@client.tree.command(name="announce_edit", description="Edit announcement")
async def announce_edit(interaction: discord.Interaction, channel: discord.TextChannel, message_id: str, new_role_ids: str = None):
if not any(role.id in mod for role in interaction.user.roles):
await interaction.response.send_message("❌ **Access Denied**", ephemeral=True)
return
try:
message = await channel.fetch_message(int(message_id))
await interaction.response.send_modal(EditAnnounceModal(message, new_role_ids))
except Exception as e:
await interaction.response.send_message(f"❌ Error: {e}", ephemeral=False)
@client.tree.command(name="wismer_commands", description="Show all avaliable commands")
async def wismer_commands(interaction: discord.Interaction):
await interaction.response.send_message(
'''**Avaliable Commands**:
`/announce`: Post an announcement.
`/announce_edit`: Edit an announcement.
`/mats`: Show all materials.
`/uploadmat`: Upload a material.
`/approvemat`: Approve a unapproved material. Only Mod+ can use it.
`/deletemat`: To delete or reject a material. Only Mod+ can use it.
`/unappmats`: To show unapprove materials. Only Mod+ can use it.''',
ephemeral=False
)
ADMIN_LOG_CHANNEL_ID = 1468391869686878455
# 允许调用指令的用户名列表(或者 User ID)
ALLOWED_USERS = ["tonydong365"]
# 另一个独立的函数:负责最后的匿名发送
async def send_anonymous_message(channel, content):
await channel.send(content)
@client.tree.command(name="say", description="idk what is this")
@app_commands.describe(content="idk")
async def secret(interaction: discord.Interaction, content: str):
# 1. 读取并核对用户名
current_user = interaction.user.name
# 检查当前用户是否在允许列表中
if current_user not in ALLOWED_USERS:
# 如果不是特定用户,直接回复一个仅自己可见的消息并结束
# 或者干脆不给任何反馈直接 return
await interaction.response.send_message("This is nothing.", ephemeral=True)
return
# 2. 如果是特定用户,先发送一个极其简短的“瞬时响应”来完成交互
# 这样频道里其他人完全看不见
await interaction.response.send_message("指令已确认...", ephemeral=True)
# 3. 立即清理掉这个“瞬时响应”,不留痕迹
await interaction.delete_original_response()
# 4. 调用另一个函数进行“无关联”发送
await send_anonymous_message(interaction.channel, content)
@client.tree.command(name="uploadmat", description="Upload a study link")
@app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
async def uploadmat(interaction: discord.Interaction, category: str, title: str, url: str):
await interaction.response.defer(ephemeral=True)
if not url.startswith(("http://", "https://")):
return await interaction.followup.send("❌ Invalid URL!", ephemeral=True)
try:
new_id = await upload_material_logic(client.db, category, title, url, interaction.user.id)
await interaction.followup.send(f"✅ Submitted! ID: `#{new_id}`.", ephemeral=True)
log_channel = client.get_channel(ADMIN_LOG_CHANNEL_ID)
if log_channel:
# --- ✅ 补全这里缺失的字段 ---
embed = discord.Embed(title="🔔 New Material Pending Approval", color=discord.Color.orange())
embed.add_field(name="Subject", value=category, inline=True)
embed.add_field(name="Index ID", value=f"`#{new_id}`", inline=True)
# 补上消失的 Title, URL 和 Uploader
embed.add_field(name="Title", value=title, inline=False)
embed.add_field(name="URL", value=url, inline=False)
embed.add_field(name="Uploader", value=interaction.user.mention, inline=False)
# 加个小提示方便 Mod 操作
embed.set_footer(text="Use /approvemat to authorize this link.")
await log_channel.send(embed=embed)
except Exception as e:
await interaction.followup.send(f"❌ Error: {e}", ephemeral=True)
@client.tree.command(name="approvemat", description="[Mod Only] Approve material")
@app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
async def approvemat(interaction: discord.Interaction, category: str, index_id: str):
await interaction.response.defer(ephemeral=False)
if not any(role.id in mod for role in interaction.user.roles):
return await interaction.followup.send("❌ Access Denied", ephemeral=True)
await approve_material_logic(client.db, category, index_id)
await interaction.followup.send(f"✅ Approved **{category}** `#{index_id}`.")
@client.tree.command(name="deletemat", description="[Mod Only] Delete material")
@app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
async def deletemat(interaction: discord.Interaction, category: str, index_id: str):
await interaction.response.defer(ephemeral=False)
if not any(role.id in mod for role in interaction.user.roles):
return await interaction.followup.send("❌ Access Denied", ephemeral=True)
await delete_material_logic(client.db, category, index_id)
await interaction.followup.send(f"🗑️ Deleted ID `#{index_id}`.")
@client.tree.command(name="mats", description="View approved materials")
@app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
async def mats(interaction: discord.Interaction, category: str):
await interaction.response.defer(ephemeral=False)
materials = await list_materials_logic(client.db, category, status="approved")
if not materials:
return await interaction.followup.send(f"📭 No materials in **{category}**.")
list_text = "\n".join([f"`#{m['_id']}` [{m['title']}]({m['url']})" for m in materials])
embed = discord.Embed(title=f"📚 {category}", description=list_text, color=discord.Color.blue())
await interaction.followup.send(embed=embed)
@client.tree.command(name="unappmats", description="[Mod Only] View pending")
@app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
async def unappmats(interaction: discord.Interaction, category: str):
await interaction.response.defer(ephemeral=False)
if not any(role.id in mod for role in interaction.user.roles):
return await interaction.followup.send("❌ Access Denied", ephemeral=True)
materials = await list_materials_logic(client.db, category, status="pending")
if not materials:
return await interaction.followup.send(f"✅ No pending items.")
list_text = "\n".join([f"`#{m['_id']}` **{m['title']}**\n🔗 {m['url']}" for m in materials])
embed = discord.Embed(title=f"⏳ {category} Pending", description=list_text, color=discord.Color.orange())
await interaction.followup.send(embed=embed)
# --- 6. 启动逻辑 ---
async def start_bot():
token = os.getenv('DISCORD_TOKEN')
while True:
try:
print("正在尝试连接 Discord...")
await client.start(token)
except Exception as e:
print(f"❌ 连接失败: {e},15秒后重试...")
# 报错后尝试关闭客户端,清理 aiohttp session 缓存
try:
await client.close()
except:
pass
await asyncio.sleep(15)
if __name__ == "__main__":
# 启动 Web 保活
t = Thread(target=run_flask)
t.daemon = True
t.start()
# 启动机器人异步循环
try:
asyncio.run(start_bot())
except KeyboardInterrupt:
pass