# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The gate the management page throws: a command switched off says so. Sits between the dispatcher and the handler, in front of every other gate, and is the one gate not expressed as a decorator. The decorators are chosen at registration time, once per deployment; this one is read from the store on every attempt, because the whole point of the page is that flipping a command off takes effect before the browser has repainted -- no reload, nothing to remember to do. It answers. A silent "off" reads as a bot that is broken, and the same person asks again. "WYŁĄCZONE" once per attempt is the cooldown gate's manners applied to a different reason, and it is filed as a refusal for the same reason a cooldown refusal is: a command people keep asking for while it is off is one worth switching back on. """ from __future__ import annotations import datetime as dt import logging from typing import TYPE_CHECKING, Final from signal_bot.decorators import GATE_OFF from signal_bot.registry import CommandRefusedError from signal_bot.stats.periods import WARSAW if TYPE_CHECKING: from signal_bot.context import CommandContext from signal_bot.registry import Command from signal_bot.store import Store logger = logging.getLogger(__name__) OFF: Final = "BEEP BOOP {trigger} WYŁĄCZONE." OFF_UNTIL: Final = "BEEP BOOP {trigger} WYŁĄCZONE DO {until}." def off_message(trigger: str, disabled_until: float | None, now: float) -> str: """What the group is told, naming the deadline when there is one to name. Warsaw time, like every clock reading the bot prints. A deadline already behind us gets the plain message: the hard switch is what is holding the command off then, and naming a time that has passed reads as a lie. """ if disabled_until is None or disabled_until <= now: return OFF.format(trigger=trigger) at = dt.datetime.fromtimestamp(disabled_until, tz=dt.UTC).astimezone(WARSAW) return OFF_UNTIL.format(trigger=trigger, until=at.strftime("%H:%M")) async def run_unless_off(store: Store, command: Command, ctx: CommandContext) -> None: """Run the handler, or refuse with a word if the operator switched it off.""" setting = store.command_settings.get(command.name) if setting.off_at(ctx.now): logger.info("%s is switched off; refusing %s", command.name, ctx.message.source_name) await ctx.reply(off_message(command.primary, setting.disabled_until, ctx.now)) raise CommandRefusedError(GATE_OFF) await command.handler(ctx)