lockbot 2.7.1__tar.gz → 2.9.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.
- {lockbot-2.7.1/python/lockbot.egg-info → lockbot-2.9.0}/PKG-INFO +1 -1
- {lockbot-2.7.1 → lockbot-2.9.0}/pyproject.toml +1 -1
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/bots/router.py +34 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/bots/schemas.py +15 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/base_bot.py +19 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/device_bot.py +9 -3
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/device_usage_utils.py +3 -2
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/i18n/en.py +5 -4
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/i18n/zh.py +5 -3
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/io.py +4 -2
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/node_bot.py +11 -6
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/queue_bot.py +9 -3
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/utils.py +6 -1
- {lockbot-2.7.1 → lockbot-2.9.0/python/lockbot.egg-info}/PKG-INFO +1 -1
- {lockbot-2.7.1 → lockbot-2.9.0}/LICENSE +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/MANIFEST.in +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/README.md +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/admin/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/admin/router.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/audit/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/audit/models.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/audit/router.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/audit/service.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/auth/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/auth/dependencies.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/auth/models.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/auth/router.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/auth/schemas.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/backup/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/backup/router.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/backup/scheduler.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/backup/service.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/bots/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/bots/encryption.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/bots/manager.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/bots/models.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/bots/webhook_handler.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/config.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/database.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/logs/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/main.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/rate_limit.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/settings/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/settings/models.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/backend/app/settings/router.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/bot_instance.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/config.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/device_usage_alert.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/entry.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/env.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/handler.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/i18n/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/message_adapter.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/msg_utils.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/platforms/__init__.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/platforms/infoflow.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/request.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot/core/scheduler.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot.egg-info/SOURCES.txt +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot.egg-info/dependency_links.txt +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot.egg-info/requires.txt +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/python/lockbot.egg-info/top_level.txt +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/setup.cfg +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/tools/create_super_admin.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/tools/gen_keys.py +0 -0
- {lockbot-2.7.1 → lockbot-2.9.0}/tools/reset_super_admin_password.py +0 -0
|
@@ -127,6 +127,33 @@ def _normalize_cluster_configs(cc):
|
|
|
127
127
|
return cc
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
def _get_node_keys(cc) -> set:
|
|
131
|
+
"""Extract node key set from cluster_configs (any format)."""
|
|
132
|
+
if _is_device_array_format(cc):
|
|
133
|
+
return {item["node_key"] for item in cc}
|
|
134
|
+
if isinstance(cc, dict):
|
|
135
|
+
return set(cc.keys())
|
|
136
|
+
if isinstance(cc, list):
|
|
137
|
+
return set(cc)
|
|
138
|
+
return set()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _validate_node_aliases(config_overrides, cluster_configs):
|
|
142
|
+
"""Raise HTTPException if NODE_ALIASES contains keys not in cluster_configs."""
|
|
143
|
+
if not config_overrides:
|
|
144
|
+
return
|
|
145
|
+
aliases = config_overrides.get("NODE_ALIASES")
|
|
146
|
+
if not aliases:
|
|
147
|
+
return
|
|
148
|
+
valid_keys = _get_node_keys(cluster_configs)
|
|
149
|
+
invalid = set(aliases.keys()) - valid_keys
|
|
150
|
+
if invalid:
|
|
151
|
+
raise HTTPException(
|
|
152
|
+
status_code=422,
|
|
153
|
+
detail=f"NODE_ALIASES contains keys not in cluster_configs: {sorted(invalid)}",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
130
157
|
def _device_dict_to_array(d: dict) -> list:
|
|
131
158
|
"""Convert DEVICE cluster_configs from dict to ordered array for API response.
|
|
132
159
|
|
|
@@ -214,6 +241,8 @@ def create_bot(
|
|
|
214
241
|
if body.bot_type.upper() not in VALID_BOT_TYPES:
|
|
215
242
|
raise HTTPException(status_code=422, detail=f"Invalid bot_type, must be one of {VALID_BOT_TYPES}")
|
|
216
243
|
|
|
244
|
+
_validate_node_aliases(body.config_overrides, body.cluster_configs)
|
|
245
|
+
|
|
217
246
|
exists = (
|
|
218
247
|
db.query(Bot)
|
|
219
248
|
.filter(
|
|
@@ -349,6 +378,11 @@ def update_bot(
|
|
|
349
378
|
if user.role != "super_admin" and bot.user_id != user.id:
|
|
350
379
|
raise HTTPException(status_code=403, detail="Cannot edit another user's bot")
|
|
351
380
|
|
|
381
|
+
# Validate NODE_ALIASES keys against cluster_configs
|
|
382
|
+
if body.config_overrides and body.config_overrides.get("NODE_ALIASES"):
|
|
383
|
+
cc = body.cluster_configs if body.cluster_configs is not None else json.loads(bot.cluster_configs)
|
|
384
|
+
_validate_node_aliases(body.config_overrides, cc)
|
|
385
|
+
|
|
352
386
|
changes = {}
|
|
353
387
|
if body.name is not None:
|
|
354
388
|
# Check for duplicate name (excluding current bot and deleted bots)
|
|
@@ -53,6 +53,21 @@ 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
|
+
|
|
60
|
+
node_aliases = v.get("NODE_ALIASES")
|
|
61
|
+
if node_aliases is not None and (
|
|
62
|
+
not isinstance(node_aliases, dict)
|
|
63
|
+
or not all(isinstance(k, str) and isinstance(a, str) and len(a) <= 15 for k, a in node_aliases.items())
|
|
64
|
+
):
|
|
65
|
+
errors.append("NODE_ALIASES must be a dict of {string: string} with alias length <= 15")
|
|
66
|
+
|
|
67
|
+
show_alias = v.get("SHOW_NODE_ALIAS")
|
|
68
|
+
if show_alias is not None and not isinstance(show_alias, bool):
|
|
69
|
+
errors.append("SHOW_NODE_ALIAS must be a boolean")
|
|
70
|
+
|
|
56
71
|
max_dur = v.get("MAX_LOCK_DURATION")
|
|
57
72
|
default_dur = v.get("DEFAULT_DURATION")
|
|
58
73
|
if isinstance(max_dur, int) and isinstance(default_dur, int) and max_dur != -1 and default_dur > max_dur:
|
|
@@ -121,6 +121,25 @@ class BaseLockBot:
|
|
|
121
121
|
"""
|
|
122
122
|
return self.adapter.build_reply("\u274c" + error_msg, [user_id])
|
|
123
123
|
|
|
124
|
+
@property
|
|
125
|
+
def _duration_whitelist(self):
|
|
126
|
+
return self.config.get_val("DURATION_WHITELIST") or []
|
|
127
|
+
|
|
128
|
+
def _display_node(self, node_key):
|
|
129
|
+
"""Return node_key with alias suffix if SHOW_NODE_ALIAS is enabled."""
|
|
130
|
+
if not self.config.get_val("SHOW_NODE_ALIAS"):
|
|
131
|
+
return node_key
|
|
132
|
+
aliases = self.config.get_val("NODE_ALIASES") or {}
|
|
133
|
+
alias = aliases.get(node_key)
|
|
134
|
+
return f"{node_key}「{alias}」" if alias else node_key
|
|
135
|
+
|
|
136
|
+
def _whitelist_hint(self):
|
|
137
|
+
"""Return whitelist hint string if BOT_OWNER is configured, else empty string."""
|
|
138
|
+
owner = self.config.get_val("BOT_OWNER")
|
|
139
|
+
if not owner:
|
|
140
|
+
return ""
|
|
141
|
+
return t("error.whitelist_hint", config=self.config, owner=owner)
|
|
142
|
+
|
|
124
143
|
# ------------------------------------------------------ _msg_with_usage
|
|
125
144
|
def _msg_with_usage(self, msg_key, *, node_key=None, sep="", **kwargs):
|
|
126
145
|
"""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._duration_whitelist
|
|
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._duration_whitelist
|
|
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
|
|
|
@@ -593,4 +597,6 @@ class DeviceBot(BaseLockBot):
|
|
|
593
597
|
"""
|
|
594
598
|
Get a text description of the current cluster device usage (supports merged display and heterogeneous hints).
|
|
595
599
|
"""
|
|
596
|
-
return get_current_usage(
|
|
600
|
+
return get_current_usage(
|
|
601
|
+
node_filter, self.state.bot_state, {}, config=self.config, display_node=self._display_node
|
|
602
|
+
)
|
|
@@ -157,7 +157,7 @@ def render_device_lines(node_status, grouped_usage, idle_groups, config=None):
|
|
|
157
157
|
return lines
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
def get_current_usage(node_filter, bot_state, monitor_status, config=None):
|
|
160
|
+
def get_current_usage(node_filter, bot_state, monitor_status, config=None, display_node=None):
|
|
161
161
|
"""
|
|
162
162
|
Get current device usage info including idle, locked, and in-use devices.
|
|
163
163
|
"""
|
|
@@ -169,7 +169,8 @@ def get_current_usage(node_filter, bot_state, monitor_status, config=None):
|
|
|
169
169
|
or node_key == node_filter
|
|
170
170
|
or (isinstance(node_filter, list) and node_key in node_filter)
|
|
171
171
|
):
|
|
172
|
-
|
|
172
|
+
shown_name = display_node(node_key) if display_node else node_key
|
|
173
|
+
usage_info += t("device_usage.node_header", config=config, node_key=shown_name)
|
|
173
174
|
|
|
174
175
|
grouped_usage = group_locked_devices(node_status)
|
|
175
176
|
shown_indices = set()
|
|
@@ -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
|
|
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._duration_whitelist
|
|
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._duration_whitelist
|
|
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
|
|
|
@@ -393,7 +397,7 @@ class NodeBot(BaseLockBot):
|
|
|
393
397
|
|
|
394
398
|
uid = user_info["user_id"] + format_access_mode(node["status"], config=self.config)
|
|
395
399
|
duration = format_duration(remaining_time, config=self.config)
|
|
396
|
-
alert_info += f"{node_key} {uid} {duration}\n"
|
|
400
|
+
alert_info += f"{self._display_node(node_key)} {uid} {duration}\n"
|
|
397
401
|
|
|
398
402
|
if EARLY_NOTIFY and not user_info["is_notified"] and 0 < remaining_time <= TIME_ALERT:
|
|
399
403
|
trigger_time_alert = True
|
|
@@ -403,7 +407,7 @@ class NodeBot(BaseLockBot):
|
|
|
403
407
|
|
|
404
408
|
uid = user_info["user_id"] + format_access_mode(node["status"], config=self.config)
|
|
405
409
|
duration = format_duration(remaining_time, config=self.config)
|
|
406
|
-
alert_info += f"{node_key} {uid} {duration}\n"
|
|
410
|
+
alert_info += f"{self._display_node(node_key)} {uid} {duration}\n"
|
|
407
411
|
|
|
408
412
|
for user_id in removed_users_id:
|
|
409
413
|
remove_user_info(node["current_users"], user_id)
|
|
@@ -449,11 +453,12 @@ class NodeBot(BaseLockBot):
|
|
|
449
453
|
usage_info = ""
|
|
450
454
|
for node_key, node_status in nodes.items():
|
|
451
455
|
if node_filter is None or node_key == node_filter:
|
|
456
|
+
display_name = self._display_node(node_key)
|
|
452
457
|
if node_status["status"] == "idle":
|
|
453
|
-
usage_info += "{:} {}\n".format(
|
|
458
|
+
usage_info += "{:} {}\n".format(display_name, t("status.idle", config=self.config))
|
|
454
459
|
else:
|
|
455
460
|
for user_idx, user_info in enumerate(node_status["current_users"]):
|
|
456
|
-
node_name = f"{
|
|
461
|
+
node_name = f"{display_name}" if user_idx == 0 else ""
|
|
457
462
|
uid = user_info["user_id"] + format_access_mode(node_status["status"], config=self.config)
|
|
458
463
|
duration = format_duration(
|
|
459
464
|
remaining_duration(user_info["start_time"], user_info["duration"]), config=self.config
|
|
@@ -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._duration_whitelist
|
|
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._duration_whitelist
|
|
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._duration_whitelist
|
|
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,18 @@ 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
|
+
if whitelist is None:
|
|
81
|
+
whitelist = []
|
|
79
82
|
clamped_users = []
|
|
80
83
|
for user_info in user_list:
|
|
84
|
+
if user_info["user_id"] in whitelist:
|
|
85
|
+
continue
|
|
81
86
|
final_duration = remaining_duration(user_info["start_time"], user_info["duration"])
|
|
82
87
|
if max_duration > 0 and final_duration > max_duration:
|
|
83
88
|
user_info["duration"] = user_info["duration"] - final_duration + max_duration
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|