envsbot 2.0.0__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.
- bot/__init__.py +0 -0
- bot/audit.py +29 -0
- bot/connection.py +82 -0
- bot/context.py +5 -0
- bot/dispatch.py +235 -0
- bot/lifecycle.py +814 -0
- bot/messages.py +293 -0
- bot/permissions.py +114 -0
- bot/room_state.py +145 -0
- bot/routing.py +74 -0
- config_sample.py +933 -0
- core_plugins/__init__.py +0 -0
- core_plugins/_admin.py +1070 -0
- core_plugins/_core.py +671 -0
- core_plugins/_reg_profile.py +418 -0
- core_plugins/audit.py +598 -0
- core_plugins/backups.py +435 -0
- core_plugins/config_cmd.py +619 -0
- core_plugins/doctor.py +1342 -0
- core_plugins/help.py +1319 -0
- core_plugins/outbox.py +146 -0
- core_plugins/plugins.py +449 -0
- core_plugins/presence.py +224 -0
- core_plugins/reports.py +184 -0
- core_plugins/rooms/__init__.py +17 -0
- core_plugins/rooms/commands.py +929 -0
- core_plugins/rooms/defaults.py +190 -0
- core_plugins/rooms/invites.py +617 -0
- core_plugins/rooms/lifecycle.py +481 -0
- core_plugins/rooms/permissions.py +58 -0
- core_plugins/rooms/presence.py +238 -0
- core_plugins/rooms/settings.py +143 -0
- core_plugins/rooms/state.py +353 -0
- core_plugins/tasks.py +222 -0
- core_plugins/usage.py +137 -0
- core_plugins/users/__init__.py +21 -0
- core_plugins/users/commands.py +743 -0
- core_plugins/users/formatting.py +71 -0
- core_plugins/users/lookup.py +56 -0
- core_plugins/users/permission_commands.py +261 -0
- core_plugins/users/permissions.py +377 -0
- core_plugins/users/roles.py +69 -0
- core_plugins/users/tracking.py +386 -0
- database/__init__.py +1 -0
- database/audit.py +255 -0
- database/command_usage.py +129 -0
- database/idlerpg.py +876 -0
- database/locking.py +51 -0
- database/manager.py +1040 -0
- database/message_cache.py +180 -0
- database/migrations/__init__.py +274 -0
- database/outbox.py +9 -0
- database/rooms.py +129 -0
- database/users.py +689 -0
- envsbot-2.0.0.dist-info/METADATA +640 -0
- envsbot-2.0.0.dist-info/RECORD +184 -0
- envsbot-2.0.0.dist-info/WHEEL +5 -0
- envsbot-2.0.0.dist-info/entry_points.txt +2 -0
- envsbot-2.0.0.dist-info/licenses/LICENSE +674 -0
- envsbot-2.0.0.dist-info/top_level.txt +8 -0
- envsbot.py +400 -0
- plugins/__init__.py +1 -0
- plugins/birthday_notify.py +827 -0
- plugins/dice.py +245 -0
- plugins/ducks.py +1375 -0
- plugins/idlerpg/__init__.py +17 -0
- plugins/idlerpg/commands.py +1436 -0
- plugins/idlerpg/config.py +116 -0
- plugins/idlerpg/constants.py +270 -0
- plugins/idlerpg/events.py +450 -0
- plugins/idlerpg/export.py +1401 -0
- plugins/idlerpg/formatting.py +262 -0
- plugins/idlerpg/handlers.py +366 -0
- plugins/idlerpg/items.py +465 -0
- plugins/idlerpg/leveling.py +279 -0
- plugins/idlerpg/map.py +241 -0
- plugins/idlerpg/quests.py +483 -0
- plugins/idlerpg/seasons.py +184 -0
- plugins/idlerpg/state.py +833 -0
- plugins/idlerpg/tasks.py +447 -0
- plugins/info.py +1173 -0
- plugins/karma.py +530 -0
- plugins/pin.py +1293 -0
- plugins/poll.py +1158 -0
- plugins/reminder/__init__.py +22 -0
- plugins/reminder/commands.py +449 -0
- plugins/reminder/config.py +13 -0
- plugins/reminder/events.py +113 -0
- plugins/reminder/formatting.py +124 -0
- plugins/reminder/lifecycle.py +117 -0
- plugins/reminder/parsing.py +384 -0
- plugins/reminder/runtime.py +13 -0
- plugins/reminder/store.py +140 -0
- plugins/reminder/tasks.py +308 -0
- plugins/rss/__init__.py +21 -0
- plugins/rss/command_support.py +458 -0
- plugins/rss/commands.py +973 -0
- plugins/rss/config.py +143 -0
- plugins/rss/fetch.py +506 -0
- plugins/rss/formatting.py +566 -0
- plugins/rss/lifecycle.py +118 -0
- plugins/rss/store.py +415 -0
- plugins/rss/subscriptions.py +451 -0
- plugins/rss/tasks.py +318 -0
- plugins/rss/templates.py +518 -0
- plugins/sed.py +726 -0
- plugins/tell.py +337 -0
- plugins/tools.py +806 -0
- plugins/translate.py +916 -0
- plugins/urlcheck.py +647 -0
- plugins/vcard/__init__.py +26 -0
- plugins/vcard/commands.py +511 -0
- plugins/vcard/config.py +24 -0
- plugins/vcard/fetch.py +151 -0
- plugins/vcard/fields.py +105 -0
- plugins/vcard/formatting.py +260 -0
- plugins/vcard/store.py +4 -0
- plugins/vcard/timezone.py +121 -0
- plugins/weather.py +499 -0
- plugins/xkcd.py +1004 -0
- plugins/xmpp.py +1292 -0
- utils/__init__.py +1 -0
- utils/admin_alerts.py +564 -0
- utils/admin_notify.py +56 -0
- utils/admin_reports.py +182 -0
- utils/audit.py +21 -0
- utils/backups.py +1011 -0
- utils/bundled/avatar.jpg +0 -0
- utils/bundled/init_chat_slang.csv +3358 -0
- utils/bundled_assets.py +39 -0
- utils/command.py +518 -0
- utils/command_context.py +22 -0
- utils/command_docs.py +925 -0
- utils/command_execution.py +205 -0
- utils/command_help.py +108 -0
- utils/command_metadata.py +88 -0
- utils/command_registry.py +180 -0
- utils/config/__init__.py +59 -0
- utils/config/defaults.py +58 -0
- utils/config/display.py +116 -0
- utils/config/errors.py +15 -0
- utils/config/loader.py +179 -0
- utils/config/logging_setup.py +77 -0
- utils/config/runtime.py +677 -0
- utils/config/spec.py +400 -0
- utils/config/validation.py +399 -0
- utils/database_cli.py +177 -0
- utils/deploy_systemd_values.py +118 -0
- utils/file_security.py +81 -0
- utils/formatting.py +171 -0
- utils/health.py +380 -0
- utils/http_fetch.py +428 -0
- utils/http_user_agent.py +30 -0
- utils/logging_helpers.py +26 -0
- utils/message_cache.py +574 -0
- utils/outbox.py +443 -0
- utils/performance.py +109 -0
- utils/permissions.py +91 -0
- utils/plugin_manager.py +1407 -0
- utils/plugin_manager_dependencies.py +62 -0
- utils/plugin_manager_diagnostics.py +63 -0
- utils/plugin_manager_discovery.py +77 -0
- utils/plugin_manager_lifecycle.py +48 -0
- utils/plugin_metadata.py +84 -0
- utils/preflight.py +267 -0
- utils/presence_manager.py +148 -0
- utils/python_source.py +45 -0
- utils/rate_limiter.py +256 -0
- utils/redaction.py +21 -0
- utils/release_state.py +56 -0
- utils/room_features.py +601 -0
- utils/runtime_paths.py +56 -0
- utils/runtime_watchdog.py +48 -0
- utils/systemd_deploy.py +220 -0
- utils/task_display.py +100 -0
- utils/task_supervisor.py +436 -0
- utils/time_utils.py +124 -0
- utils/tls_certificate.py +671 -0
- utils/updatecheck.py +197 -0
- utils/url_safety.py +272 -0
- utils/urlcheck_extraction.py +54 -0
- utils/version.py +20 -0
- utils/xmpp_notify.py +253 -0
- vcard_sample.py +30 -0
bot/__init__.py
ADDED
|
File without changes
|
bot/audit.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Audit helper mixin for envsbot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from envs_xmpp_core.security import redact_named, redact_value
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuditMixin:
|
|
14
|
+
"""Best-effort audit event writer."""
|
|
15
|
+
|
|
16
|
+
async def audit(self, event: str, *, actor: Any = None, target: Any = None, details: dict[str, Any] | None = None) -> None:
|
|
17
|
+
"""Write an audit event if the audit log is available."""
|
|
18
|
+
try:
|
|
19
|
+
audit_log = getattr(getattr(self, "db", None), "audit", None)
|
|
20
|
+
if audit_log is None:
|
|
21
|
+
return
|
|
22
|
+
await audit_log.append(
|
|
23
|
+
event,
|
|
24
|
+
actor=redact_named("actor", str(actor)) if actor is not None else None,
|
|
25
|
+
target=redact_named("target", str(target)) if target is not None else None,
|
|
26
|
+
details=redact_value(details or {}),
|
|
27
|
+
)
|
|
28
|
+
except Exception:
|
|
29
|
+
log.debug("[AUDIT] Failed to write audit event", exc_info=True)
|
bot/connection.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""XMPP connection and JID compatibility facade for envsbot."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from envs_xmpp_core.xmpp.connection import (
|
|
10
|
+
connect_kwargs_from_mapping,
|
|
11
|
+
maybe_await,
|
|
12
|
+
)
|
|
13
|
+
from envs_xmpp_core.xmpp.connection import (
|
|
14
|
+
connection_target as _core_connection_target,
|
|
15
|
+
)
|
|
16
|
+
from envs_xmpp_core.xmpp.jid import (
|
|
17
|
+
boundjid_domain,
|
|
18
|
+
build_client_jid,
|
|
19
|
+
configured_jid_domain,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"boundjid_domain",
|
|
26
|
+
"build_client_jid",
|
|
27
|
+
"configured_jid_domain",
|
|
28
|
+
"connect_kwargs",
|
|
29
|
+
"connect_signature_parameters",
|
|
30
|
+
"connect_xmpp",
|
|
31
|
+
"connection_target",
|
|
32
|
+
"get_configured_resource",
|
|
33
|
+
"session_is_ready",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_configured_resource(config: Mapping[str, Any]) -> str | None:
|
|
38
|
+
resource = config.get("resource")
|
|
39
|
+
if resource is None:
|
|
40
|
+
return None
|
|
41
|
+
value = str(resource).strip()
|
|
42
|
+
return value or None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def connect_signature_parameters(connect_method: Any) -> Mapping[str, inspect.Parameter]:
|
|
46
|
+
"""Return inspectable connect() parameters, preserving envsbot monkeypatch hooks."""
|
|
47
|
+
try:
|
|
48
|
+
return inspect.signature(connect_method).parameters
|
|
49
|
+
except (TypeError, ValueError):
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def session_is_ready(xmpp: Any) -> bool:
|
|
54
|
+
marker = getattr(xmpp, "session_ready", None)
|
|
55
|
+
if marker is None:
|
|
56
|
+
return True
|
|
57
|
+
is_set = getattr(marker, "is_set", None)
|
|
58
|
+
return bool(is_set()) if callable(is_set) else True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def connect_kwargs(xmpp: Any, config: Mapping[str, Any]) -> dict[str, Any]:
|
|
62
|
+
return connect_kwargs_from_mapping(
|
|
63
|
+
xmpp,
|
|
64
|
+
config,
|
|
65
|
+
parameters=connect_signature_parameters(xmpp.connect),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def connection_target(kwargs: Mapping[str, Any], config: Mapping[str, Any]) -> tuple[object, object, str]:
|
|
70
|
+
return _core_connection_target(
|
|
71
|
+
kwargs,
|
|
72
|
+
fallback_host=configured_jid_domain(config) or "auto",
|
|
73
|
+
fallback_port="auto",
|
|
74
|
+
direct_tls=bool(config.get("direct_tls", False)),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def connect_xmpp(xmpp: Any, config: Mapping[str, Any]) -> Any:
|
|
79
|
+
kwargs = connect_kwargs(xmpp, config)
|
|
80
|
+
host, port, mode = connection_target(kwargs, config)
|
|
81
|
+
log.info("[XMPP] event=connect target=%s:%s mode=%s", host, port, mode)
|
|
82
|
+
return await maybe_await(xmpp.connect(**kwargs))
|
bot/context.py
ADDED
bot/dispatch.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Command dispatch and sender context resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import slixmpp
|
|
9
|
+
from envs_xmpp_core.xmpp.messaging import is_muc_private_message
|
|
10
|
+
from envs_xmpp_core.xmpp.occupants import find_occupant_by_nick
|
|
11
|
+
|
|
12
|
+
from bot.context import CommandContext
|
|
13
|
+
from bot.permissions import role_bypasses_rate_limit
|
|
14
|
+
from utils.command import Role, is_command_group
|
|
15
|
+
from utils.command_execution import CommandExecutionContext
|
|
16
|
+
from utils.permissions import can_execute_command
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CommandDispatchMixin:
|
|
22
|
+
"""Message context resolution and unified command handling."""
|
|
23
|
+
|
|
24
|
+
presence: Any
|
|
25
|
+
get_user_role: Any
|
|
26
|
+
prefix: str
|
|
27
|
+
reply: Any
|
|
28
|
+
config: dict[str, Any]
|
|
29
|
+
rate_limiter: Any
|
|
30
|
+
command_executor: Any
|
|
31
|
+
|
|
32
|
+
def _joined_room_jids(self) -> set[str]:
|
|
33
|
+
"""Return known joined room bare JIDs from runtime room state."""
|
|
34
|
+
rooms: set[str] = set()
|
|
35
|
+
try:
|
|
36
|
+
rooms.update(str(room) for room in getattr(self.presence, "joined_rooms", {}))
|
|
37
|
+
except Exception:
|
|
38
|
+
log.debug("[MUC] Could not inspect presence joined rooms", exc_info=True)
|
|
39
|
+
try:
|
|
40
|
+
from bot.room_state import JOINED_ROOMS
|
|
41
|
+
|
|
42
|
+
rooms.update(str(room) for room in JOINED_ROOMS)
|
|
43
|
+
except Exception:
|
|
44
|
+
log.debug("[MUC] Could not inspect plugin joined rooms", exc_info=True)
|
|
45
|
+
return rooms
|
|
46
|
+
|
|
47
|
+
def _is_muc_private_message(self, msg: Any) -> bool:
|
|
48
|
+
"""Return True for a private message sent via a MUC occupant JID."""
|
|
49
|
+
try:
|
|
50
|
+
msg_type = msg.get("type", "chat")
|
|
51
|
+
from_jid = msg["from"]
|
|
52
|
+
room = str(from_jid.bare)
|
|
53
|
+
nick = getattr(from_jid, "resource", None)
|
|
54
|
+
except Exception:
|
|
55
|
+
return False
|
|
56
|
+
return is_muc_private_message(msg_type, room, nick, self._joined_room_jids())
|
|
57
|
+
|
|
58
|
+
def _get_message_room_and_nick(self, msg: Any) -> tuple[str | None, str | None]:
|
|
59
|
+
"""Resolve room and nick from a message if possible."""
|
|
60
|
+
room = None
|
|
61
|
+
nick = None
|
|
62
|
+
try:
|
|
63
|
+
from_jid = msg["from"]
|
|
64
|
+
room = from_jid.bare
|
|
65
|
+
nick = msg.get("mucnick") or msg["from"].resource
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
log.debug("[MUC] Could not resolve message room/nick: %s", exc)
|
|
68
|
+
return room, nick
|
|
69
|
+
|
|
70
|
+
def _lookup_muc_occupant_jid(self, room: str | None, nick: str | None) -> Any:
|
|
71
|
+
"""Resolve a MUC occupant's real JID from live MUC state."""
|
|
72
|
+
muc = getattr(self, "plugin", {}).get("xep_0045", None)
|
|
73
|
+
if muc:
|
|
74
|
+
try:
|
|
75
|
+
jid = muc.get_jid_property(room, nick, "jid")
|
|
76
|
+
if jid:
|
|
77
|
+
return jid
|
|
78
|
+
except Exception:
|
|
79
|
+
log.debug("[BOT] Error getting JID from XEP-0045 state", exc_info=True)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
from bot.room_state import JOINED_ROOMS
|
|
83
|
+
|
|
84
|
+
room_data = JOINED_ROOMS.get(room, {}) or {} if room else {}
|
|
85
|
+
room_occupants = room_data.get("nicks", {}) or {}
|
|
86
|
+
occupant = find_occupant_by_nick(
|
|
87
|
+
room_occupants,
|
|
88
|
+
nick,
|
|
89
|
+
room=room or "",
|
|
90
|
+
)
|
|
91
|
+
if occupant is not None and occupant.jid:
|
|
92
|
+
# Preserve the historical full real-JID return value here;
|
|
93
|
+
# sender authorization still normalizes it to bare JID below.
|
|
94
|
+
source = room_occupants.get(occupant.nick, {}) or {}
|
|
95
|
+
return source.get("jid") or occupant.jid
|
|
96
|
+
except Exception:
|
|
97
|
+
log.debug("[BOT] Error getting JID from joined room state", exc_info=True)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def _bare_jid_value(self, jid_value: Any) -> str:
|
|
101
|
+
"""Return a best-effort bare JID string without leaking resources."""
|
|
102
|
+
bare = getattr(jid_value, "bare", None)
|
|
103
|
+
if bare:
|
|
104
|
+
return str(bare)
|
|
105
|
+
return str(jid_value).split("/", 1)[0]
|
|
106
|
+
|
|
107
|
+
def _resolve_sender_jid(self, msg: Any, sender_jid: Any, nick: str | None) -> tuple[str, str | None]:
|
|
108
|
+
"""Resolve the real sender JID for room messages, with fallback."""
|
|
109
|
+
jid = None
|
|
110
|
+
room = None
|
|
111
|
+
msg_type = msg.get("type", "chat")
|
|
112
|
+
|
|
113
|
+
if msg_type in ("chat", "normal") and not self._is_muc_private_message(msg):
|
|
114
|
+
return self._bare_jid_value(sender_jid), None
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
room, nick = self._get_message_room_and_nick(msg)
|
|
118
|
+
jid = self._lookup_muc_occupant_jid(room, nick)
|
|
119
|
+
except Exception:
|
|
120
|
+
log.debug("[BOT] Error getting JID from MUC context", exc_info=True)
|
|
121
|
+
|
|
122
|
+
if jid is None:
|
|
123
|
+
return self._bare_jid_value(sender_jid), room
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
return str(slixmpp.JID(jid).bare), room
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
log.warning("[BOT] Failed to parse resolved JID: %s", exc)
|
|
129
|
+
return str(sender_jid), room
|
|
130
|
+
|
|
131
|
+
def _command_error_message(self, user_role: Role, cmd_name: str, error: Exception) -> str:
|
|
132
|
+
"""Preserve the existing public/internal error wording."""
|
|
133
|
+
if user_role in (Role.OWNER, Role.ADMIN):
|
|
134
|
+
return f"🔴 Command error: {error}"
|
|
135
|
+
return f"🔴 Command '{cmd_name}' failed due to internal error."
|
|
136
|
+
|
|
137
|
+
def _is_loaded_plugin_help_target(self, text: str) -> bool:
|
|
138
|
+
"""Return whether *text* exactly names one loaded plugin."""
|
|
139
|
+
tokens = tuple(part.lower() for part in str(text).split() if part)
|
|
140
|
+
if len(tokens) != 1:
|
|
141
|
+
return False
|
|
142
|
+
try:
|
|
143
|
+
plugins = getattr(getattr(self, "bot_plugins", None), "plugins", {})
|
|
144
|
+
return any(str(name).lower() == tokens[0] for name in plugins)
|
|
145
|
+
except Exception:
|
|
146
|
+
log.debug("[COMMAND] Could not inspect loaded plugin names", exc_info=True)
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
async def build_command_context(
|
|
150
|
+
self,
|
|
151
|
+
body: str,
|
|
152
|
+
sender_jid: Any,
|
|
153
|
+
nick: str | None,
|
|
154
|
+
msg: Any,
|
|
155
|
+
is_room: bool,
|
|
156
|
+
cmd_obj: Any,
|
|
157
|
+
args: list[str],
|
|
158
|
+
) -> CommandContext:
|
|
159
|
+
"""Resolve sender, room and role into a reusable command context."""
|
|
160
|
+
jid, room = self._resolve_sender_jid(msg, sender_jid, nick)
|
|
161
|
+
role = await self.get_user_role(jid, room)
|
|
162
|
+
return CommandContext(
|
|
163
|
+
command_name=cmd_obj.name,
|
|
164
|
+
sender_jid=jid,
|
|
165
|
+
nick=nick,
|
|
166
|
+
room=room,
|
|
167
|
+
is_room=is_room,
|
|
168
|
+
is_muc_pm=self._is_muc_private_message(msg),
|
|
169
|
+
role=role,
|
|
170
|
+
args=tuple(args),
|
|
171
|
+
raw_body=body,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
async def handle_command(self, body: str | None, sender_jid: Any, nick: str | None, msg: Any, is_room: bool) -> None:
|
|
175
|
+
"""Parse and execute a bot command from a message."""
|
|
176
|
+
if not body:
|
|
177
|
+
return
|
|
178
|
+
if not body.startswith(self.prefix):
|
|
179
|
+
return
|
|
180
|
+
if not getattr(self, "accepting_commands", True):
|
|
181
|
+
self.reply(msg, "🔴 Bot is shutting down; please retry shortly.")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
text = body[len(self.prefix):].strip()
|
|
185
|
+
if not text:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
import envsbot as app
|
|
189
|
+
cmd_obj, args = app.resolve_command(text)
|
|
190
|
+
if not cmd_obj and (
|
|
191
|
+
is_command_group(text) or self._is_loaded_plugin_help_target(text)
|
|
192
|
+
):
|
|
193
|
+
cmd_obj, _ = app.resolve_command("help")
|
|
194
|
+
args = text.split()
|
|
195
|
+
if not cmd_obj:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
context = await self.build_command_context(body, sender_jid, nick, msg, is_room, cmd_obj, args)
|
|
199
|
+
|
|
200
|
+
if self.config.get("command_rate_limit_enabled", True) and not role_bypasses_rate_limit(context.role, self.config):
|
|
201
|
+
allowed, retry_after = await self.rate_limiter.allow(context.sender_jid)
|
|
202
|
+
if not allowed:
|
|
203
|
+
if self.rate_limiter.notify_allowed(context.sender_jid):
|
|
204
|
+
log.info(
|
|
205
|
+
"[COMMAND] event=rate_limited actor=%s room=%s retry_after=%.1fs",
|
|
206
|
+
context.sender_jid,
|
|
207
|
+
context.room,
|
|
208
|
+
retry_after,
|
|
209
|
+
)
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
from utils.permissions import configured_room_invite_admin_rooms
|
|
213
|
+
|
|
214
|
+
invite_admin_rooms = configured_room_invite_admin_rooms(self.config)
|
|
215
|
+
|
|
216
|
+
decision = can_execute_command(
|
|
217
|
+
context,
|
|
218
|
+
cmd_obj,
|
|
219
|
+
room_invite_admin_rooms=invite_admin_rooms,
|
|
220
|
+
permission_checker=app.check_permission,
|
|
221
|
+
)
|
|
222
|
+
if not decision.allowed:
|
|
223
|
+
self.reply(msg, decision.reason)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
execution_context = CommandExecutionContext(
|
|
227
|
+
command_name=context.command_name,
|
|
228
|
+
sender_jid=context.sender_jid,
|
|
229
|
+
nick=context.nick,
|
|
230
|
+
room=context.room,
|
|
231
|
+
is_room=context.is_room,
|
|
232
|
+
role=context.role,
|
|
233
|
+
args=context.args,
|
|
234
|
+
)
|
|
235
|
+
await self.command_executor.execute(cmd_obj, execution_context, msg)
|