lockbot 2.7.0__tar.gz → 2.8.0__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.
Files changed (70) hide show
  1. {lockbot-2.7.0/python/lockbot.egg-info → lockbot-2.8.0}/PKG-INFO +1 -1
  2. {lockbot-2.7.0 → lockbot-2.8.0}/pyproject.toml +1 -1
  3. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/backup/scheduler.py +33 -9
  4. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/bots/schemas.py +4 -0
  5. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/base_bot.py +7 -0
  6. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/device_bot.py +6 -2
  7. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/i18n/en.py +5 -4
  8. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/i18n/zh.py +5 -3
  9. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/io.py +4 -2
  10. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/node_bot.py +6 -2
  11. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/queue_bot.py +9 -3
  12. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/utils.py +5 -1
  13. {lockbot-2.7.0 → lockbot-2.8.0/python/lockbot.egg-info}/PKG-INFO +1 -1
  14. {lockbot-2.7.0 → lockbot-2.8.0}/LICENSE +0 -0
  15. {lockbot-2.7.0 → lockbot-2.8.0}/MANIFEST.in +0 -0
  16. {lockbot-2.7.0 → lockbot-2.8.0}/README.md +0 -0
  17. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/__init__.py +0 -0
  18. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/__init__.py +0 -0
  19. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/__init__.py +0 -0
  20. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/admin/__init__.py +0 -0
  21. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/admin/router.py +0 -0
  22. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/audit/__init__.py +0 -0
  23. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/audit/models.py +0 -0
  24. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/audit/router.py +0 -0
  25. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/audit/service.py +0 -0
  26. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/auth/__init__.py +0 -0
  27. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/auth/dependencies.py +0 -0
  28. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/auth/models.py +0 -0
  29. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/auth/router.py +0 -0
  30. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/auth/schemas.py +0 -0
  31. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/backup/__init__.py +0 -0
  32. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/backup/router.py +0 -0
  33. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/backup/service.py +0 -0
  34. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/bots/__init__.py +0 -0
  35. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/bots/encryption.py +0 -0
  36. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/bots/manager.py +0 -0
  37. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/bots/models.py +0 -0
  38. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/bots/router.py +0 -0
  39. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/bots/webhook_handler.py +0 -0
  40. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/config.py +0 -0
  41. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/database.py +0 -0
  42. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/logs/__init__.py +0 -0
  43. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/main.py +0 -0
  44. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/rate_limit.py +0 -0
  45. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/settings/__init__.py +0 -0
  46. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/settings/models.py +0 -0
  47. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/backend/app/settings/router.py +0 -0
  48. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/__init__.py +0 -0
  49. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/bot_instance.py +0 -0
  50. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/config.py +0 -0
  51. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/device_usage_alert.py +0 -0
  52. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/device_usage_utils.py +0 -0
  53. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/entry.py +0 -0
  54. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/env.py +0 -0
  55. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/handler.py +0 -0
  56. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/i18n/__init__.py +0 -0
  57. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/message_adapter.py +0 -0
  58. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/msg_utils.py +0 -0
  59. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/platforms/__init__.py +0 -0
  60. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/platforms/infoflow.py +0 -0
  61. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/request.py +0 -0
  62. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot/core/scheduler.py +0 -0
  63. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot.egg-info/SOURCES.txt +0 -0
  64. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot.egg-info/dependency_links.txt +0 -0
  65. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot.egg-info/requires.txt +0 -0
  66. {lockbot-2.7.0 → lockbot-2.8.0}/python/lockbot.egg-info/top_level.txt +0 -0
  67. {lockbot-2.7.0 → lockbot-2.8.0}/setup.cfg +0 -0
  68. {lockbot-2.7.0 → lockbot-2.8.0}/tools/create_super_admin.py +0 -0
  69. {lockbot-2.7.0 → lockbot-2.8.0}/tools/gen_keys.py +0 -0
  70. {lockbot-2.7.0 → lockbot-2.8.0}/tools/reset_super_admin_password.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lockbot
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: Cluster resource management bot for IM platforms
5
5
  Author-email: Jianbang Yang <yangjianbang112@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lockbot"
7
- version = "2.7.0"
7
+ version = "2.8.0"
8
8
  description = "Cluster resource management bot for IM platforms"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -4,7 +4,7 @@ Backup scheduler — daemon thread for periodic BOS backups.
4
4
 
5
5
  import logging
6
6
  import threading
7
- from datetime import datetime, timedelta, timezone
7
+ from datetime import datetime, timedelta
8
8
 
9
9
  logger = logging.getLogger(__name__)
10
10
 
@@ -25,7 +25,7 @@ class BackupScheduler:
25
25
  if self._thread and self._thread.is_alive():
26
26
  return
27
27
  self._stop_event.clear()
28
- self._started_at = datetime.now(timezone.utc)
28
+ self._started_at = datetime.now()
29
29
  self._last_error = ""
30
30
  self._thread = threading.Thread(target=self._run, daemon=True, name="backup-scheduler")
31
31
  self._thread.start()
@@ -56,11 +56,11 @@ class BackupScheduler:
56
56
 
57
57
  def _run(self):
58
58
  while not self._stop_event.is_set():
59
- self._last_heartbeat = datetime.now(timezone.utc)
59
+ self._last_heartbeat = datetime.now()
60
60
  try:
61
61
  seconds = self._seconds_until_next()
62
62
  if seconds is not None:
63
- self._next_run_at = datetime.now(timezone.utc) + timedelta(seconds=seconds)
63
+ self._next_run_at = datetime.now() + timedelta(seconds=seconds)
64
64
  else:
65
65
  self._next_run_at = None
66
66
  if seconds is None:
@@ -76,7 +76,9 @@ class BackupScheduler:
76
76
  if triggered or self._stop_event.is_set():
77
77
  continue # Config changed or shutting down, recalculate
78
78
 
79
- # Time to backup
79
+ # Time to backup — skip if last successful backup was recent
80
+ if self._should_skip():
81
+ continue
80
82
  self._do_backup()
81
83
  except Exception as exc:
82
84
  self._last_error = str(exc)
@@ -84,6 +86,30 @@ class BackupScheduler:
84
86
  # Sleep a bit before retry
85
87
  self._stop_event.wait(timeout=60)
86
88
 
89
+ def _should_skip(self) -> bool:
90
+ """Skip if last successful backup was less than 20 hours ago (prevents duplicates on restart)."""
91
+ from lockbot.backend.app.backup.service import get_backup_config
92
+ from lockbot.backend.app.database import SessionLocal
93
+
94
+ db = SessionLocal()
95
+ try:
96
+ config = get_backup_config(db)
97
+ finally:
98
+ db.close()
99
+
100
+ last_time = config.get("backup_last_time", "")
101
+ last_status = config.get("backup_last_status", "")
102
+ if not last_time or last_status != "success":
103
+ return False
104
+ try:
105
+ last_dt = datetime.strptime(last_time, "%Y-%m-%d %H:%M:%S")
106
+ if (datetime.now() - last_dt).total_seconds() < 20 * 3600:
107
+ logger.info("Skipping backup — last success was %s (< 20h ago)", last_time)
108
+ return True
109
+ except ValueError:
110
+ pass
111
+ return False
112
+
87
113
  def _seconds_until_next(self) -> int | None:
88
114
  """Calculate seconds until next backup. Returns None if disabled."""
89
115
  from lockbot.backend.app.backup.service import get_backup_config
@@ -110,12 +136,10 @@ class BackupScheduler:
110
136
  except (ValueError, IndexError):
111
137
  return None
112
138
 
113
- now = datetime.now(timezone.utc)
139
+ # Use local time (server timezone) for scheduling
140
+ now = datetime.now()
114
141
  target = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
115
142
  if target <= now:
116
- # Next day
117
- from datetime import timedelta
118
-
119
143
  target += timedelta(days=1)
120
144
 
121
145
  return int((target - now).total_seconds())
@@ -53,6 +53,10 @@ def _validate_config_overrides(v: dict | None) -> dict | None:
53
53
  if not (lo <= val <= hi):
54
54
  errors.append(f"{key} must be between {lo} and {hi}")
55
55
 
56
+ whitelist = v.get("DURATION_WHITELIST")
57
+ if whitelist is not None and (not isinstance(whitelist, list) or not all(isinstance(u, str) for u in whitelist)):
58
+ errors.append("DURATION_WHITELIST must be a list of strings")
59
+
56
60
  max_dur = v.get("MAX_LOCK_DURATION")
57
61
  default_dur = v.get("DEFAULT_DURATION")
58
62
  if isinstance(max_dur, int) and isinstance(default_dur, int) and max_dur != -1 and default_dur > max_dur:
@@ -121,6 +121,13 @@ class BaseLockBot:
121
121
  """
122
122
  return self.adapter.build_reply("\u274c" + error_msg, [user_id])
123
123
 
124
+ def _whitelist_hint(self):
125
+ """Return whitelist hint string if BOT_OWNER is configured, else empty string."""
126
+ owner = self.config.get_val("BOT_OWNER")
127
+ if not owner:
128
+ return ""
129
+ return t("error.whitelist_hint", config=self.config, owner=owner)
130
+
124
131
  # ------------------------------------------------------ _msg_with_usage
125
132
  def _msg_with_usage(self, msg_key, *, node_key=None, sep="", **kwargs):
126
133
  """Return ``t(msg_key, ...) + sep + self._current_usage(node_key)``."""
@@ -177,6 +177,7 @@ class DeviceBot(BaseLockBot):
177
177
  return error_reply
178
178
 
179
179
  max_dur = self.config.get_val("MAX_LOCK_DURATION")
180
+ whitelist = self.config.get_val("DURATION_WHITELIST") or []
180
181
  with self._lock:
181
182
  timestamp = int(time.time())
182
183
 
@@ -193,7 +194,7 @@ class DeviceBot(BaseLockBot):
193
194
  user_id, self._msg_with_usage("error.device_in_use_or_shared", node_key=node_key)
194
195
  )
195
196
 
196
- if max_dur > 0:
197
+ if max_dur > 0 and user_id not in whitelist:
197
198
  for device in devices:
198
199
  total_duration = duration
199
200
  user_info = find_user_info(device["current_users"], user_id)
@@ -209,6 +210,7 @@ class DeviceBot(BaseLockBot):
209
210
  "error.lock_max_duration_exceeded",
210
211
  config=self.config,
211
212
  max_duration=format_duration(max_dur, config=self.config),
213
+ whitelist_hint=self._whitelist_hint(),
212
214
  ),
213
215
  )
214
216
 
@@ -249,6 +251,7 @@ class DeviceBot(BaseLockBot):
249
251
  return error_reply
250
252
 
251
253
  max_dur = self.config.get_val("MAX_LOCK_DURATION")
254
+ whitelist = self.config.get_val("DURATION_WHITELIST") or []
252
255
  with self._lock:
253
256
  timestamp = int(time.time())
254
257
  for node_key, dev_ids in zip(node_key_list, dev_ids_list, strict=True):
@@ -260,7 +263,7 @@ class DeviceBot(BaseLockBot):
260
263
  user_id, self._msg_with_usage("error.device_exclusive_mode", node_key=node_key)
261
264
  )
262
265
 
263
- if max_dur > 0:
266
+ if max_dur > 0 and user_id not in whitelist:
264
267
  for device in devices:
265
268
  user_info = find_user_info(device["current_users"], user_id)
266
269
  if user_info:
@@ -276,6 +279,7 @@ class DeviceBot(BaseLockBot):
276
279
  "error.slock_max_duration_exceeded",
277
280
  config=self.config,
278
281
  max_duration=format_duration(max_dur, config=self.config),
282
+ whitelist_hint=self._whitelist_hint(),
279
283
  ),
280
284
  )
281
285
 
@@ -27,8 +27,9 @@ MESSAGES = {
27
27
  "error.duration_must_be_positive": "Duration must be greater than 0\n",
28
28
  "error.node_in_use_or_shared": "Node is in use by others or in shared mode\n\n",
29
29
  "error.node_exclusive_mode": "Node is in exclusive mode\n\n",
30
- "error.lock_max_duration_exceeded": "Note: consecutive lock cannot exceed {max_duration}\n\n",
31
- "error.slock_max_duration_exceeded": "Note: consecutive slock cannot exceed {max_duration}\n\n",
30
+ "error.lock_max_duration_exceeded": "Note: consecutive lock cannot exceed {max_duration}\n\n{whitelist_hint}",
31
+ "error.slock_max_duration_exceeded": "Note: consecutive slock cannot exceed {max_duration}\n\n{whitelist_hint}",
32
+ "error.whitelist_hint": "To lock longer, contact @{owner} to be added to the whitelist\n\n",
32
33
  "error.node_not_requested": "You have not requested this node\n",
33
34
  "error.unrecognized_command": "❌ Unrecognized command: {command}",
34
35
  "error.unknown_error": "❌ Unknown error: {command}",
@@ -139,9 +140,9 @@ MESSAGES = {
139
140
  "notify.booking_expired_header": "⚠️ The following bookings have expired:\n",
140
141
  "notify.pending_bookings_header": "🗓️ Pending bookings:\n",
141
142
  "notify.duration_clamped": (
142
- "⚠️ [Lock Duration Adjusted]\n"
143
+ "⚠️ [Lock Duration Adjusted]\n\n"
143
144
  "The admin has set max lock duration to {max_duration}. "
144
- "Your remaining lock time has been automatically shortened.\n"
145
+ "Your remaining lock time has been automatically shortened.\n\n"
145
146
  ),
146
147
  # ── Help: news (inline) ──
147
148
  "help.news_header": "📢 Announcement:\n",
@@ -27,8 +27,9 @@ MESSAGES = {
27
27
  "error.duration_must_be_positive": "【申请资源时间应大于0】\n",
28
28
  "error.node_in_use_or_shared": "【节点正在被他人使用或处于共享状态】\n\n",
29
29
  "error.node_exclusive_mode": "【节点正处于独占状态】\n\n",
30
- "error.lock_max_duration_exceeded": "【注意: 目前禁止连续lock超过{max_duration}】\n\n",
31
- "error.slock_max_duration_exceeded": "【注意: 目前禁止连续slock超过{max_duration}】\n\n",
30
+ "error.lock_max_duration_exceeded": "【注意: 目前禁止连续lock超过{max_duration}】\n\n{whitelist_hint}",
31
+ "error.slock_max_duration_exceeded": "【注意: 目前禁止连续slock超过{max_duration}】\n\n{whitelist_hint}",
32
+ "error.whitelist_hint": "如需长期lock,请联系管理员 @{owner} 申请加入白名单\n\n",
32
33
  "error.node_not_requested": "【你并未申请过该节点资源】\n",
33
34
  "error.unrecognized_command": "❌【未识别的命令】{command}",
34
35
  "error.unknown_error": "❌【未知错误】{command}",
@@ -133,7 +134,8 @@ MESSAGES = {
133
134
  "notify.booking_expired_header": "⚠️ 以下预约已失效,请重新预约:\n",
134
135
  "notify.pending_bookings_header": "🗓️ 目前待抢锁的预约:\n",
135
136
  "notify.duration_clamped": (
136
- "⚠️【锁定时长调整通知】\n管理员已将最大锁定时长调整为 {max_duration},您的锁定剩余时长已被自动缩短,请知悉。\n"
137
+ "⚠️【锁定时长调整通知】\n\n"
138
+ "管理员已将最大锁定时长调整为 {max_duration},您的锁定剩余时长已被自动缩短,请知悉。\n\n"
137
139
  ),
138
140
  # ── Help: news (inline) ──
139
141
  "help.news_header": "📢 公告:\n",
@@ -114,6 +114,7 @@ def create_or_load_device_state(config=None):
114
114
  state_filename = os.path.join(_bot_dir(config), "bot_state.json")
115
115
  cluster_configs = _cfg_get(config, "CLUSTER_CONFIGS")
116
116
  max_duration = _cfg_get(config, "MAX_LOCK_DURATION", -1)
117
+ duration_whitelist = _cfg_get(config, "DURATION_WHITELIST", [])
117
118
 
118
119
  default_state = {
119
120
  node_key: [
@@ -150,7 +151,7 @@ def create_or_load_device_state(config=None):
150
151
  device_status = old_by_id[dev_id]
151
152
  _migrate_status_fields(device_status)
152
153
  _migrate_user_info_fields(device_status["current_users"])
153
- clamped = apply_max_duration_limit(device_status["current_users"], max_duration)
154
+ clamped = apply_max_duration_limit(device_status["current_users"], max_duration, duration_whitelist)
154
155
  clamped_user_ids.update(clamped)
155
156
  default_device.update(**device_status)
156
157
 
@@ -175,6 +176,7 @@ def create_or_load_node_state(config=None):
175
176
  state_filename = os.path.join(_bot_dir(config), "bot_state.json")
176
177
  cluster_configs = _cfg_get(config, "CLUSTER_CONFIGS")
177
178
  max_duration = _cfg_get(config, "MAX_LOCK_DURATION", -1)
179
+ duration_whitelist = _cfg_get(config, "DURATION_WHITELIST", [])
178
180
 
179
181
  default_state = {
180
182
  shortname: {
@@ -202,7 +204,7 @@ def create_or_load_node_state(config=None):
202
204
  node_status.pop("fullname", None)
203
205
  _migrate_user_info_fields(node_status.get("current_users", []))
204
206
  _migrate_user_info_fields(node_status.get("booking_list", []))
205
- clamped = apply_max_duration_limit(node_status.get("current_users", []), max_duration)
207
+ clamped = apply_max_duration_limit(node_status.get("current_users", []), max_duration, duration_whitelist)
206
208
  clamped_user_ids.update(clamped)
207
209
  default_state[node_key].update(**node_status)
208
210
 
@@ -111,6 +111,7 @@ class NodeBot(BaseLockBot):
111
111
  return error_reply
112
112
 
113
113
  max_dur = self.config.get_val("MAX_LOCK_DURATION")
114
+ whitelist = self.config.get_val("DURATION_WHITELIST") or []
114
115
  with self._lock:
115
116
  nodes = [self.state.bot_state[node_key] for node_key in node_keys]
116
117
  if not all(
@@ -122,7 +123,7 @@ class NodeBot(BaseLockBot):
122
123
 
123
124
  timestamp = int(time.time())
124
125
 
125
- if max_dur > 0:
126
+ if max_dur > 0 and user_id not in whitelist:
126
127
  for node in nodes:
127
128
  total_duration = duration
128
129
  user_info = find_user_info(node["current_users"], user_id)
@@ -138,6 +139,7 @@ class NodeBot(BaseLockBot):
138
139
  "error.lock_max_duration_exceeded",
139
140
  config=self.config,
140
141
  max_duration=format_duration(max_dur, config=self.config),
142
+ whitelist_hint=self._whitelist_hint(),
141
143
  ),
142
144
  )
143
145
 
@@ -169,6 +171,7 @@ class NodeBot(BaseLockBot):
169
171
  return error_reply
170
172
 
171
173
  max_dur = self.config.get_val("MAX_LOCK_DURATION")
174
+ whitelist = self.config.get_val("DURATION_WHITELIST") or []
172
175
  with self._lock:
173
176
  nodes = [self.state.bot_state[node_key] for node_key in node_keys]
174
177
  if not all(node["status"] != "exclusive" for node in nodes):
@@ -176,7 +179,7 @@ class NodeBot(BaseLockBot):
176
179
 
177
180
  timestamp = int(time.time())
178
181
 
179
- if max_dur > 0:
182
+ if max_dur > 0 and user_id not in whitelist:
180
183
  for node in nodes:
181
184
  user_info = find_user_info(node["current_users"], user_id)
182
185
  if user_info:
@@ -190,6 +193,7 @@ class NodeBot(BaseLockBot):
190
193
  "error.slock_max_duration_exceeded",
191
194
  config=self.config,
192
195
  max_duration=format_duration(max_dur, config=self.config),
196
+ whitelist_hint=self._whitelist_hint(),
193
197
  )
194
198
  return self.show_error(user_id, msg)
195
199
 
@@ -38,6 +38,7 @@ class QueueBot(NodeBot):
38
38
  return error_reply
39
39
 
40
40
  max_dur = self.config.get_val("MAX_LOCK_DURATION")
41
+ whitelist = self.config.get_val("DURATION_WHITELIST") or []
41
42
  with self._lock:
42
43
  nodes = [self.state.bot_state[node_key] for node_key in node_keys]
43
44
  if not all(
@@ -61,7 +62,7 @@ class QueueBot(NodeBot):
61
62
  users_to_notify = set()
62
63
  users_to_notify.add(user_id)
63
64
 
64
- if max_dur > 0:
65
+ if max_dur > 0 and user_id not in whitelist:
65
66
  for node in nodes:
66
67
  booking_info = find_user_info(node["booking_list"], user_id)
67
68
  if not command_has_duration and booking_info:
@@ -81,6 +82,7 @@ class QueueBot(NodeBot):
81
82
  "error.lock_max_duration_exceeded",
82
83
  config=self.config,
83
84
  max_duration=format_duration(max_dur, config=self.config),
85
+ whitelist_hint=self._whitelist_hint(),
84
86
  ),
85
87
  )
86
88
 
@@ -130,6 +132,7 @@ class QueueBot(NodeBot):
130
132
  return error_reply
131
133
 
132
134
  max_dur = self.config.get_val("MAX_LOCK_DURATION")
135
+ whitelist = self.config.get_val("DURATION_WHITELIST") or []
133
136
  with self._lock:
134
137
  nodes = [self.state.bot_state[node_key] for node_key in node_keys]
135
138
  if any(
@@ -140,13 +143,14 @@ class QueueBot(NodeBot):
140
143
 
141
144
  timestamp = int(time.time())
142
145
 
143
- if max_dur > 0 and duration > max_dur:
146
+ if max_dur > 0 and user_id not in whitelist and duration > max_dur:
144
147
  return self.show_error(
145
148
  user_id,
146
149
  t(
147
150
  "error.lock_max_duration_exceeded",
148
151
  config=self.config,
149
152
  max_duration=format_duration(max_dur, config=self.config),
153
+ whitelist_hint=self._whitelist_hint(),
150
154
  ),
151
155
  )
152
156
 
@@ -170,6 +174,7 @@ class QueueBot(NodeBot):
170
174
  content = t("success.take_success_by", config=self.config, user_id=user_id)
171
175
  content += self._msg_with_usage("label.before_take")
172
176
  max_dur = self.config.get_val("MAX_LOCK_DURATION")
177
+ whitelist = self.config.get_val("DURATION_WHITELIST") or []
173
178
  with self._lock:
174
179
  nodes = [self.state.bot_state[node_key] for node_key in node_keys]
175
180
  if any(find_user_info(node["current_users"], user_id) for node in nodes):
@@ -180,13 +185,14 @@ class QueueBot(NodeBot):
180
185
  users_to_notify.add(user_id)
181
186
  nodes = [self.state.bot_state[node_key] for node_key in node_keys]
182
187
 
183
- if max_dur > 0 and remaining_duration(timestamp, duration) > max_dur:
188
+ if max_dur > 0 and user_id not in whitelist and remaining_duration(timestamp, duration) > max_dur:
184
189
  return self.show_error(
185
190
  user_id,
186
191
  t(
187
192
  "error.lock_max_duration_exceeded",
188
193
  config=self.config,
189
194
  max_duration=format_duration(max_dur, config=self.config),
195
+ whitelist_hint=self._whitelist_hint(),
190
196
  ),
191
197
  )
192
198
 
@@ -71,13 +71,17 @@ def remaining_duration(start_time, duration):
71
71
  return max(duration - elapsed_duration, 0)
72
72
 
73
73
 
74
- def apply_max_duration_limit(user_list, max_duration):
74
+ def apply_max_duration_limit(user_list, max_duration, whitelist=None):
75
75
  """Clamp each user's remaining duration to max_duration in place.
76
76
 
77
77
  Returns a list of user_ids whose durations were clamped.
78
+ Whitelist users are skipped.
78
79
  """
80
+ whitelist = whitelist or []
79
81
  clamped_users = []
80
82
  for user_info in user_list:
83
+ if user_info["user_id"] in whitelist:
84
+ continue
81
85
  final_duration = remaining_duration(user_info["start_time"], user_info["duration"])
82
86
  if max_duration > 0 and final_duration > max_duration:
83
87
  user_info["duration"] = user_info["duration"] - final_duration + max_duration
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lockbot
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: Cluster resource management bot for IM platforms
5
5
  Author-email: Jianbang Yang <yangjianbang112@gmail.com>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes