tonydong365 commited on
Commit
3894d14
·
1 Parent(s): 403f79c

Develop first time

Browse files
Files changed (7) hide show
  1. .DS_Store +0 -0
  2. .gitignore +19 -0
  3. Dockerfile +7 -0
  4. app.py +275 -0
  5. material.py +37 -0
  6. mongo.py +75 -0
  7. requirements.txt +5 -0
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.gitignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 字节码文件
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # 环境配置文件 (非常重要!不要上传 Key)
7
+ .env
8
+ .venv
9
+ venv/
10
+ env/
11
+ bin/
12
+ lib/
13
+
14
+ # IDE 配置
15
+ .vscode/
16
+ .idea/
17
+
18
+ # 日志文件
19
+ *.log
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ EXPOSE 7860
7
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import discord
3
+ import time
4
+ from fastapi import FastAPI
5
+ import uvicorn
6
+ from threading import Thread
7
+ from discord import app_commands, ui
8
+ # 导入数据库基础类
9
+ from mongo import MongoManager
10
+ # 导入学科业务函数和学科列表
11
+ from material import (
12
+ CATEGORIES,
13
+ upload_material_logic,
14
+ approve_material_logic,
15
+ delete_material_logic,
16
+ list_materials_logic
17
+ )
18
+ # 初始化数据库管理器
19
+ # 假设你已经设置了环境变量 MONGO_URL
20
+ db_manager = MongoManager(os.getenv("MONGO_URL"), "tonyd")
21
+
22
+ # 身份组权限列表
23
+ mod = [1357078628969746462, 1357079285113819146, 1436537252317626449, 1357079707539214417, 1357079889160835183]
24
+
25
+ # --- 1. Flask 保活 ---
26
+ app = FastAPI()
27
+ @app.get('/')
28
+ def home(): return {"Stauts": "Alive"}
29
+
30
+ def run_flask():
31
+ port = int(os.environ.get('PORT', 7860))
32
+ uvicorn.run(app, host='0.0.0.0', port=port)
33
+
34
+ # --- 2. 辅助函数:处理多个 Role Ping ---
35
+ def format_pings(role_input: str):
36
+ pass
37
+
38
+ # --- 3. Modal 表单定义 ---
39
+
40
+ class AnnounceModal(ui.Modal):
41
+ def __init__(self, channel: discord.TextChannel, ping_roles_str: str = None):
42
+ super().__init__(title="New Announcement")
43
+ self.channel = channel
44
+ self.ping_roles_str = ping_roles_str
45
+
46
+ announcement_title = ui.TextInput(label="Title", placeholder="Enter title...", style=discord.TextStyle.short)
47
+ announcement_body = ui.TextInput(label="Body", placeholder="Enter message...", style=discord.TextStyle.long)
48
+
49
+ async def on_submit(self, interaction: discord.Interaction):
50
+ # 准备正文
51
+ content = self.announcement_body.value
52
+ pings = self.ping_roles_str
53
+
54
+ # 按照你的要求:Ping 放在文本最下面,但是在 --- 上面
55
+ if pings:
56
+ pass
57
+
58
+ embed = discord.Embed(
59
+ title=self.announcement_title.value,
60
+ description=content,
61
+ color=discord.Color.blue()
62
+ )
63
+
64
+ # Footer 只留发送人和时间
65
+ footer = f"\n\n||{pings}||\n\n---\n**Sent by:** ||{interaction.user.mention}||\n**Time:** <t:{int(time.time())}:f>"
66
+ embed.description += footer
67
+
68
+ await self.channel.send(embed=embed)
69
+ await interaction.response.send_message(f"✅ Sent to {self.channel.mention}!", ephemeral=False)
70
+
71
+ class EditAnnounceModal(ui.Modal):
72
+ def __init__(self, message: discord.Message, new_ping_roles_str: str = None):
73
+ super().__init__(title="Edit Announcement")
74
+ self.message = message
75
+ self.new_ping_roles_str = new_ping_roles_str
76
+
77
+ old_title = message.embeds[0].title if message.embeds else ""
78
+ # 裁掉旧的 Footer
79
+ old_body = message.embeds[0].description.split("\n\n---")[0] if message.embeds else ""
80
+
81
+ self.edit_title = ui.TextInput(label="Title", default=old_title, style=discord.TextStyle.short)
82
+ self.edit_body = ui.TextInput(label="Body", default=old_body, style=discord.TextStyle.long)
83
+
84
+ self.add_item(self.edit_title)
85
+ self.add_item(self.edit_body)
86
+
87
+ async def on_submit(self, interaction: discord.Interaction):
88
+ content = self.edit_body.value
89
+ pings = self.new_ping_roles_str
90
+
91
+ if pings:
92
+ pass
93
+
94
+ new_embed = discord.Embed(
95
+ title=self.edit_title.value,
96
+ description=content,
97
+ color=discord.Color.blue()
98
+ )
99
+
100
+ footer = f"\n\n||{pings}||\n\n---\n**Edited by:** ||{interaction.user.mention}||\n**Time:** <t:{int(time.time())}:f>"
101
+ new_embed.description += footer
102
+
103
+ await self.message.edit(embed=new_embed)
104
+ await interaction.response.send_message(f"✅ Updated announcement {message.id} !", ephemeral=False)
105
+
106
+ # --- 4. 机器人主体 ---
107
+ class MyBot(discord.Client):
108
+ def __init__(self):
109
+ super().__init__(intents=discord.Intents.default())
110
+ self.tree = app_commands.CommandTree(self)
111
+
112
+ async def setup_hook(self):
113
+ await self.tree.sync()
114
+ print(f"✅ Slash commands synced")
115
+
116
+ client = MyBot()
117
+ # 如果你使用的是 client.tree,请确保将 db_manager 挂载到 client 上
118
+ # 这样在指令内部可以通过 interaction.client.db 访问
119
+ client.db = db_manager
120
+ # --- 5. 指令定义 ---
121
+
122
+ @client.tree.command(name="announce", description="Post announcement")
123
+ @app_commands.describe(
124
+ channel="Target channel",
125
+ role_ids="Input Role Pings"
126
+ )
127
+ async def announce(interaction: discord.Interaction, channel: discord.TextChannel, role_ids: str = None):
128
+ if not any(role.id in mod for role in interaction.user.roles):
129
+ await interaction.response.send_message("❌ **Access Denied**", ephemeral=True)
130
+ return
131
+ await interaction.response.send_modal(AnnounceModal(channel, role_ids))
132
+
133
+ @client.tree.command(name="announce_edit", description="Edit announcement")
134
+ @app_commands.describe(
135
+ channel="The channel",
136
+ message_id="The ID of the message",
137
+ new_role_ids="New Role Pings to add to the bottom"
138
+ )
139
+ async def announce_edit(interaction: discord.Interaction, channel: discord.TextChannel, message_id: str, new_role_ids: str = None):
140
+ if not any(role.id in mod for role in interaction.user.roles):
141
+ await interaction.response.send_message("❌ **Access Denied**", ephemeral=True)
142
+ return
143
+
144
+ try:
145
+ message = await channel.fetch_message(int(message_id))
146
+ await interaction.response.send_modal(EditAnnounceModal(message, new_role_ids))
147
+ except Exception as e:
148
+ await interaction.response.send_message(f"❌ Error: {e}", ephemeral=False)
149
+
150
+ @client.tree.command(name="wismer_commands", description="Show all avaliable commands for Wismer Bot")
151
+ async def wismer_commands(interaction: discord.Interaction):
152
+ await interaction.response.send_message(
153
+ '''**Avaliable Commands**:
154
+ `/announce`: Post an announcement in selected channel. Only Moderator+ can use it.
155
+ `/announce_edit`: Edit an announcement that sent by Wismer Bot. Only Moderator+ can use it.
156
+ `/mats`: Show all materials.
157
+ `/uploadmat`: Upload a material.''',
158
+ ephemeral=False
159
+ )
160
+
161
+
162
+
163
+ ADMIN_LOG_CHANNEL_ID = 1468391869686878455 # Replace with your Admin/Staff channel ID
164
+
165
+ # --- 1. Upload Material ---
166
+ # --- 1. Upload Material ---
167
+ @client.tree.command(name="uploadmat", description="Upload a study link (Requires Approval)")
168
+ @app_commands.describe(category="Subject", title="Resource Title", url="Resource Link")
169
+ @app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
170
+ async def uploadmat(interaction: discord.Interaction, category: str, title: str, url: str):
171
+ # 改为 ephemeral=True,因为你的反馈消息是发给个人看的
172
+ await interaction.response.defer(ephemeral=True)
173
+
174
+ if not url.startswith(("http://", "https://")):
175
+ return await interaction.followup.send("❌ Invalid URL! Must start with http or https.", ephemeral=True)
176
+
177
+ try:
178
+ # 执行逻辑
179
+ new_id = await upload_material_logic(client.db, category, title, url, interaction.user.id)
180
+
181
+ # 1. Feedback to User (保持和 defer 一致的 ephemeral)
182
+ await interaction.followup.send(
183
+ f"✅ Submitted! **{category}** ID: `#{new_id}`. Please wait for Moderator approval.",
184
+ ephemeral=True
185
+ )
186
+
187
+ # 2. Alert to Staff Channel (这是发送到频道,不涉及交互反馈)
188
+ log_channel = client.get_channel(ADMIN_LOG_CHANNEL_ID)
189
+ if log_channel:
190
+ embed = discord.Embed(title="🔔 New Material Pending Approval", color=discord.Color.orange())
191
+ embed.add_field(name="Subject", value=category, inline=True)
192
+ embed.add_field(name="Index ID", value=f"`#{new_id}`", inline=True)
193
+ embed.add_field(name="Title", value=title, inline=False)
194
+ embed.add_field(name="URL", value=url, inline=False)
195
+ embed.add_field(name="Uploader", value=interaction.user.mention, inline=False)
196
+ embed.set_footer(text="Use /approvemat to authorize this link.")
197
+ await log_channel.send(embed=embed)
198
+
199
+ except Exception as e:
200
+ print(f"Error in uploadmat: {e}")
201
+ # 如果报错了也给用户一个反馈,防止他们因为没反应而狂点
202
+ try:
203
+ await interaction.followup.send(f"❌ An error occurred: {e}", ephemeral=True)
204
+ except:
205
+ pass
206
+
207
+ # --- 2. Approve Material ---
208
+ @client.tree.command(name="approvemat", description="[Mod Only] Approve a pending material")
209
+ @app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
210
+ async def approvemat(interaction: discord.Interaction, category: str, index_id: str):
211
+ await interaction.response.defer(ephemeral=False)
212
+ # Changed MOD_ROLES to mod
213
+ if not any(role.id in mod for role in interaction.user.roles):
214
+ return await interaction.followup.send("❌ Access Denied: Staff Only", ephemeral=True)
215
+
216
+ await approve_material_logic(client.db, category, index_id)
217
+ await interaction.followup.send(
218
+ f"✅ Approved **{category}** `#{index_id}`. It is now visible to everyone.",
219
+ ephemeral=False
220
+ )
221
+
222
+ # --- 3. Delete Material ---
223
+ @client.tree.command(name="deletemat", description="[Mod Only] Delete a material")
224
+ @app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
225
+ async def deletemat(interaction: discord.Interaction, category: str, index_id: str):
226
+ # Changed MOD_ROLES to mod
227
+ await interaction.response.defer(ephemeral=False)
228
+ if not any(role.id in mod for role in interaction.user.roles):
229
+ return await interaction.followup.send("❌ Access Denied: Staff Only", ephemeral=True)
230
+
231
+ await delete_material_logic(client.db, category, index_id)
232
+ await interaction.followup.send(
233
+ f"🗑️ Deleted ID `#{index_id}` from **{category}**.",
234
+ ephemeral=False
235
+ )
236
+
237
+ # --- 4. List Approved Materials ---
238
+ @client.tree.command(name="mats", description="View approved study materials")
239
+ @app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
240
+ async def mats(interaction: discord.Interaction, category: str):
241
+ await interaction.response.defer(ephemeral=False)
242
+ materials = await list_materials_logic(client.db, category, status="approved")
243
+ if not materials:
244
+ return await interaction.followup.send(f"📭 No approved materials found in **{category}**.", ephemeral=True)
245
+
246
+ list_text = "\n".join([f"`#{m['_id']}` [{m['title']}]({m['url']})" for m in materials])
247
+ embed = discord.Embed(title=f"📚 {category} Resource Directory", description=list_text, color=discord.Color.blue())
248
+ await interaction.followup.send(embed=embed)
249
+
250
+ # --- 5. List Pending Materials ---
251
+ @client.tree.command(name="unappmats", description="[Mod Only] View pending materials")
252
+ @app_commands.choices(category=[app_commands.Choice(name=c, value=c) for c in CATEGORIES])
253
+ async def unappmats(interaction: discord.Interaction, category: str):
254
+ # Changed MOD_ROLES to mod
255
+ await interaction.response.defer(ephemeral=False)
256
+ if not any(role.id in mod for role in interaction.user.roles):
257
+ return await interaction.followup.send("❌ Access Denied: Staff Only", ephemeral=True)
258
+
259
+ materials = await list_materials_logic(client.db, category, status="pending")
260
+ if not materials:
261
+ return await interaction.followup.send(f"✅ No pending items in **{category}**.", ephemeral=True)
262
+
263
+ list_text = "\n".join([f"`#{m['_id']}` **{m['title']}**\n🔗 {m['url']}" for m in materials])
264
+ embed = discord.Embed(title=f"⏳ {category} Pending List", description=list_text, color=discord.Color.orange())
265
+ await interaction.followup.send(embed=embed)
266
+
267
+
268
+
269
+ # --- 启动 ---
270
+ if __name__ == "__main__":
271
+ t = Thread(target=run_flask)
272
+ t.daemon = True
273
+ t.start()
274
+ token = os.getenv('DISCORD_TOKEN')
275
+ client.run(token)
material.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+
3
+ CATEGORIES = ["Language", "Math", "Science", "Music", "Geography", "History", "Transition", "Health"]
4
+
5
+ # --- 内部辅助:获取自增 ID ---
6
+ async def _get_next_id(db, category):
7
+ await db.inc("counters", category, "current_value", 1)
8
+ doc = await db.get("counters", category)
9
+ return str(doc["current_value"])
10
+
11
+ # --- 上传资料 (默认 pending 状态) ---
12
+ async def upload_material_logic(db, category, title, url, user_id):
13
+ if category not in CATEGORIES: return None
14
+ new_id = await _get_next_id(db, category)
15
+ data = {
16
+ "title": title,
17
+ "url": url,
18
+ "author": user_id,
19
+ "status": "pending", # 初始状态为待审核
20
+ "time": int(time.time())
21
+ }
22
+ await db.set(category, new_id, data)
23
+ return new_id
24
+
25
+ # --- 审核资料 ---
26
+ async def approve_material_logic(db, category, index_id):
27
+ return await db.update(category, index_id, {"status": "approved"})
28
+
29
+ # --- 删除资料 ---
30
+ async def delete_material_logic(db, category, index_id):
31
+ return await db.delete(category, index_id)
32
+
33
+ # --- 获取列表 (根据状态过滤) ---
34
+ async def list_materials_logic(db, category, status="approved"):
35
+ # 如果传入了 category,只查该学科;否则可能需要遍历(为了简洁,这里建议指定 category)
36
+ query = {"status": status}
37
+ return await db.find_many(category, query=query, limit=20)
mongo.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import motor.motor_asyncio
2
+ import logging
3
+
4
+ # 设置简单的日志,方便你在 Render 控制台看到数据库报错
5
+ logging.basicConfig(level=logging.INFO)
6
+ logger = logging.getLogger("MongoManager")
7
+
8
+ class MongoManager:
9
+ def __init__(self, uri: str, db_name: str):
10
+ """
11
+ 初始化 MongoDB 连接
12
+ :param uri: 你的 mongodb+srv:// 字符串
13
+ :param db_name: 数据库名称
14
+ """
15
+ self.client = motor.motor_asyncio.AsyncIOMotorClient(uri)
16
+ self.db = self.client[db_name]
17
+ logger.info(f"Connected to MongoDB: {db_name}")
18
+
19
+ # --- 核心简洁接口 ---
20
+
21
+ async def set(self, collection: str, doc_id: any, data: dict):
22
+ """设置/覆盖数据 (id可以是数字、字符串或消息ID)"""
23
+ try:
24
+ return await self.db[collection].update_one(
25
+ {"_id": doc_id},
26
+ {"$set": data},
27
+ upsert=True
28
+ )
29
+ except Exception as e:
30
+ logger.error(f"Set Error: {e}")
31
+
32
+ async def update(self, collection: str, doc_id: any, update_data: dict):
33
+ """局部更新数据 (只修改传入的字段)"""
34
+ try:
35
+ return await self.db[collection].update_one(
36
+ {"_id": doc_id},
37
+ {"$set": update_data}
38
+ )
39
+ except Exception as e:
40
+ logger.error(f"Update Error: {e}")
41
+
42
+ async def get(self, collection: str, doc_id: any):
43
+ """获取单个数据"""
44
+ try:
45
+ return await self.db[collection].find_one({"_id": doc_id})
46
+ except Exception as e:
47
+ logger.error(f"Get Error: {e}")
48
+ return None
49
+
50
+ async def delete(self, collection: str, doc_id: any):
51
+ """删除单个数据"""
52
+ try:
53
+ return await self.db[collection].delete_one({"_id": doc_id})
54
+ except Exception as e:
55
+ logger.error(f"Delete Error: {e}")
56
+
57
+ async def find_many(self, collection: str, query: dict = {}, limit: int = 10, sort_field: str = "_id"):
58
+ """获取多条数据 (默认按ID倒序,即拿最新的)"""
59
+ try:
60
+ cursor = self.db[collection].find(query).sort(sort_field, -1).limit(limit)
61
+ return await cursor.to_list(length=limit)
62
+ except Exception as e:
63
+ logger.error(f"FindMany Error: {e}")
64
+ return []
65
+
66
+ async def inc(self, collection: str, doc_id: any, field: str, amount: int = 1):
67
+ """数字自增 (比如统计公告修改次数)"""
68
+ try:
69
+ return await self.db[collection].update_one(
70
+ {"_id": doc_id},
71
+ {"$inc": {field: amount}},
72
+ upsert=True
73
+ )
74
+ except Exception as e:
75
+ logger.error(f"Increment Error: {e}")
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ discord.py
2
+ motor
3
+ dnspython
4
+ fastapi
5
+ uvicorn