214
Some checks failed
CI / Lint (ruff + mypy) (push) Failing after 32s
CI / Run tests (push) Has been skipped
CI / Docker build test (push) Successful in 11s

This commit is contained in:
2026-04-02 22:03:20 +07:00
parent 8be26418b1
commit ddebe03f15
3 changed files with 82 additions and 56 deletions

View File

@@ -205,13 +205,32 @@ async def show_actor_status_menu(callback: CallbackQuery, actor: dict[str, Any],
async def update_channel_post(bot: Bot, app_config: dict, state_storage: JsonStateStorage, settings) -> None:
state = state_storage.load()
text = build_channel_text(app_config, state)
await bot.edit_message_text(
chat_id=settings.channel_id,
message_id=settings.channel_message_id,
text=text,
parse_mode=ParseMode.HTML,
disable_web_page_preview=False,
)
try:
await bot.edit_message_text(
chat_id=settings.channel_id,
message_id=settings.channel_message_id,
text=text,
parse_mode=ParseMode.HTML,
disable_web_page_preview=False,
)
except TelegramBadRequest as exc:
if "Invalid custom emoji identifier specified" not in str(exc):
raise
template = state.get("template", {}).get("text", "")
if not template:
raise
state["template"]["text"] = sanitize_template_html(template)
state_storage.save(state)
fallback_text = build_channel_text(app_config, state)
await bot.edit_message_text(
chat_id=settings.channel_id,
message_id=settings.channel_message_id,
text=fallback_text,
parse_mode=ParseMode.HTML,
disable_web_page_preview=False,
)
async def apply_status_update(

View File

@@ -4,14 +4,8 @@ import re
from html import escape
DEFAULT_STATUS_LABELS = {
"open": "исполняет роль",
"backstage": "в закулисье",
"delay": "задержки",
"rest": "антракт",
}
ACTOR_PLACEHOLDER_RE = re.compile(r"\{\{\s*actor\s*:\s*([a-z0-9_\-]+)\s*\}\}", re.IGNORECASE)
PLAIN_LINK_RE = re.compile(r"(?P<label>[^\n<>()]+?) \((?P<url>https?://[^\s)]+)\)")
def build_hidden_link(config: dict) -> str:
@@ -22,39 +16,38 @@ def build_hidden_link(config: dict) -> str:
return f'<a href="{escape(url, quote=True)}">{invisible}</a>'
def build_actor_line(actor: dict, state: dict, config: dict) -> str:
def convert_plain_links_to_html(template: str) -> str:
def repl(match: re.Match[str]) -> str:
label = match.group("label").rstrip()
url = match.group("url")
if "<a " in label:
return match.group(0)
return f'<a href="{escape(url, quote=True)}">{escape(label)}</a>'
return PLAIN_LINK_RE.sub(repl, template)
def build_actor_phrase(actor: dict, state: dict) -> str:
actor_state = state.get("actors", {})
status_labels = {**DEFAULT_STATUS_LABELS, **config.get("status_labels", {})}
current = actor_state.get(actor["key"], {})
status = current.get("status", actor.get("default_status", "backstage"))
phrase = current.get("phrase", actor.get("phrases", {}).get(status, ""))
label = status_labels.get(status, status)
return current.get("phrase", actor.get("phrases", {}).get(status, ""))
def build_actor_line(actor: dict, state: dict) -> str:
phrase = build_actor_phrase(actor, state)
display_name = actor.get("display_html", escape(actor["display_name"]))
meta = actor.get("meta_html", escape(actor["pronouns"]))
emoji = actor.get("emoji_html", escape(actor.get("emoji", "")))
line = (
return (
f'{emoji} '
f'<a href="{escape(actor["link"], quote=True)}">{display_name}</a>'
f"{meta}{escape(label)}."
f"{meta}{escape(phrase)}"
)
if phrase:
line = f"{line}\n {escape(phrase)}"
return line
def build_actor_fragment(actor: dict, state: dict, config: dict) -> str:
actor_state = state.get("actors", {})
status_labels = {**DEFAULT_STATUS_LABELS, **config.get("status_labels", {})}
current = actor_state.get(actor["key"], {})
status = current.get("status", actor.get("default_status", "backstage"))
phrase = current.get("phrase", actor.get("phrases", {}).get(status, ""))
label = status_labels.get(status, status)
fragment = f"{escape(label)}."
if phrase:
fragment = f"{fragment}\n {escape(phrase)}"
return fragment
def build_actor_fragment(actor: dict, state: dict) -> str:
return escape(build_actor_phrase(actor, state))
def build_actor_lines(config: dict, state: dict, skip_keys: set[str] | None = None) -> str:
@@ -64,7 +57,7 @@ def build_actor_lines(config: dict, state: dict, skip_keys: set[str] | None = No
for actor in config["actors"]:
if actor["key"] in skip_keys:
continue
actor_lines.append(build_actor_line(actor, state, config))
actor_lines.append(build_actor_line(actor, state))
actor_lines.extend(config.get("static_actor_lines_html", []))
return "\n".join(actor_lines)
@@ -88,19 +81,15 @@ def replace_actor_placeholders(template: str, config: dict, state: dict) -> tupl
line_text = template[line_start:line_end]
if line_text.strip().lower() == match.group(0).strip().lower():
return build_actor_line(actor, state, config)
return build_actor_line(actor, state)
return build_actor_fragment(actor, state, config)
return build_actor_fragment(actor, state)
return ACTOR_PLACEHOLDER_RE.sub(repl, template), used_keys
def build_default_template(config: dict) -> str:
blocks = []
hidden = build_hidden_link(config)
if hidden:
blocks.append(hidden)
for key in ("header_html", "intro_links_html", "projects_block_html", "actors_title_html"):
value = config.get(key, "").strip()
if value:
@@ -118,14 +107,14 @@ def build_default_template(config: dict) -> str:
def build_channel_text(config: dict, state: dict) -> str:
template = state.get("template", {}).get("text") or config.get("template_text") or build_default_template(config)
template = convert_plain_links_to_html(template)
template, used_keys = replace_actor_placeholders(template, config, state)
actors_block = build_actor_lines(config, state, skip_keys=used_keys)
hidden_link = build_hidden_link(config)
text = template.replace("{{actors}}", actors_block)
text = text.replace("{{hidden_link}}", hidden_link)
if "{{hidden_link}}" not in template and hidden_link:
text = f"{hidden_link}\n{text}"
text = text.replace("{{hidden_link}}", "")
if hidden_link:
text = f"{hidden_link}{text}"
return text