From 347d1dfe2f1f8a74e281feccf48a73d4fa9899c2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 4 Aug 2026 20:02:42 +0500 Subject: [PATCH] Add platform poster header text settings --- MULTI_PROJECT.md | 2 ++ RAA.md | 14 ++++++-- STATE.md | 10 +++++- .../043_platform_header_footer_text.sql | 35 +++++++++++++++++++ src/vk_parser_app/admin.py | 4 +++ src/vk_parser_app/text_utils.py | 6 ++-- src/vk_parser_app/workers/max_poster.py | 6 ++++ src/vk_parser_app/workers/tg_poster.py | 3 ++ src/vk_parser_app/workers/vk_poster.py | 3 ++ 9 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 db/migrations/043_platform_header_footer_text.sql diff --git a/MULTI_PROJECT.md b/MULTI_PROJECT.md index c831f07..e0a22e5 100644 --- a/MULTI_PROJECT.md +++ b/MULTI_PROJECT.md @@ -62,5 +62,7 @@ The site poster is selected by `site_poster_provider`. A project can disable the - FN-8 Coolify app `n6cnr60anmruiwkiey0pukye` points to `new_vk_parser.git`, branch `main`. - RAA Coolify app `korokrhpoyqxf0nw2y8vk7lp` points to `new_vk_parser.git`, branch `main`. - Both projects should deploy from the same commit SHA. +- Both current Coolify apps run on Docker destination CTID `105`, hostname `raa-fn8`. +- Old CT `107` was removed on 2026-08-04; CT `104` is a separate legacy VK reposting app. - Keep only env and database settings different between projects. - Do not add project-specific defaults to migrations or `.env.example`. diff --git a/RAA.md b/RAA.md index ede876b..c99bf9f 100644 --- a/RAA.md +++ b/RAA.md @@ -1,6 +1,6 @@ # RAA deployment -Last updated: 2026-08-03. +Last updated: 2026-08-04. RAA is not a separate code project anymore. The source of truth for code and documentation is: @@ -23,7 +23,7 @@ continue feature work there. - Coolify git branch: `main` - Postgres container: `m9v14hzfh5mk9kelxzcqvhc6` - Postgres DB/user: `raa_parser` / `raa_parser_user` -- Destination: `vk-parser-lxc`, CTID `105`, IP `192.168.1.105` +- Destination: `raa-fn8`, CTID `105`, IP `192.168.1.105` - Edge proxy: Caddy in CTID `110` proxies `raa.panel.f-n8.ru` to Coolify Traefik at `192.168.1.105:80` - Health check: `https://raa.panel.f-n8.ru/health` @@ -55,6 +55,11 @@ Database settings: - `vk_poster_access_token` is the RAA community token - `site_poster_enabled=false` - `site_poster_provider=""` until a website adapter is configured +- Optional social text headers/footers: + - `tg_poster_header_text` / `tg_poster_footer_text` + - `vk_poster_header_text` / `vk_poster_footer_text` + - `max_poster_header_text` / `max_poster_footer_text` + Empty header/footer settings add nothing. ## Workers @@ -103,6 +108,11 @@ automatic rejection of non-target content. ## Deployment Notes +Legacy CT cleanup: + +- CT `107` (`raa-parser-poster`) was stopped and removed on 2026-08-04. +- CT `104` (`raa-sender`) is not an error: it still runs the legacy VK reposting app (`vk-reposter`) and can legitimately post to its configured Telegram channel. + To deploy code changes for both FN-8 and RAA: 1. Work only in `D:\DEVELOPMENT\new_vk_parser`. diff --git a/STATE.md b/STATE.md index 26cb556..e9d78bf 100644 --- a/STATE.md +++ b/STATE.md @@ -13,7 +13,7 @@ Current runtime is local infrastructure, not the old VPS. - Deploy path: push to `gitea/main` -> Gitea webhook -> Coolify -> Docker build/deploy. - Coolify + Gitea LXC: `192.168.1.135` (CTID 102). - Coolify app UUID: `n6cnr60anmruiwkiey0pukye` (`FN-8 admin`). API token is only in `D:\DEVELOPMENT\.infra.md`; do not copy it into repo docs. -- Application LXC: `192.168.1.105` (CTID 105, `new-vk-parser`). +- Application LXC: `192.168.1.105` (CTID 105, `raa-fn8`). - Public admin URL: `https://panel.f-n8.ru` (also `https://admin.f-n8.ru` when DNS is current). - Coolify repository URL must be reachable from the deployment container; use `http://192.168.1.135:3000/exostring/new_vk_parser.git` or another resolvable Gitea URL, not `http://coolify.lan:3000/...`. - Do not deploy this project through `sw.exostring.xyz`, `109.120.156.205`, `/opt/vk-parser`, or manual `systemctl` unless the user explicitly says the old VPS is being restored. @@ -60,6 +60,7 @@ canonical git repository as FN-8. Code work for both projects must happen in - Coolify app UUID: `korokrhpoyqxf0nw2y8vk7lp` (`RAA admin`). - Public admin URL: `https://raa.panel.f-n8.ru`. - RAA Postgres container: `m9v14hzfh5mk9kelxzcqvhc6`, DB `raa_parser`, user `raa_parser_user`. +- Docker destination/LXC for both FN-8 and RAA: CTID `105`, hostname `raa-fn8`, IP `192.168.1.105`. - Raw Telegram media channel setting: `tg_media_channel_id=-1004312015562` (`VK RAW RAA`). - Final Telegram publication chat setting: `tg_poster_chat_id=-1001303630155` (`Red Airsoft | Страйкбол`). - VK publication owner: `vk_poster_owner_id=-36860851`, `vk_poster_from_group=true`. @@ -70,6 +71,9 @@ canonical git repository as FN-8. Code work for both projects must happen in `ai-qualifier=false`, `ai-writer=false`, `tg-poster=false`, `vk-poster=false`, `site-poster=false`, `tg_poster_enabled=false`, `vk_poster_enabled=false`. - Detailed RAA notes live in `RAA.md`. +- CT `107` (`raa-parser-poster`) was removed on 2026-08-04. CT `104` + (`raa-sender`) remains a valid legacy VK reposting app and is not the shared + RAA parser/writer/poster app. ## Important Text Rules @@ -84,6 +88,10 @@ canonical git repository as FN-8. Code work for both projects must happen in - optional project-wide common tags first, then `#category #source` - Project-wide common tags live in DB setting `publication_common_tags`. FN-8 value: `#новости_снаряжения`. RAA value: `#raa_news #страйкбол`. +- Optional per-platform header/footer settings: + `tg_poster_header_text`, `tg_poster_footer_text`, + `vk_poster_header_text`, `vk_poster_footer_text`, + `max_poster_header_text`, `max_poster_footer_text`. - The editor textarea must show only clean editable text. - The list preview in `/editor` should show the publication preview, including composed hashtags. - `build_publication_text()` in `src/vk_parser_app/text_utils.py` is display/composition helper only. Do not use it before writing `final_text` to DB. diff --git a/db/migrations/043_platform_header_footer_text.sql b/db/migrations/043_platform_header_footer_text.sql new file mode 100644 index 0000000..e5fba9c --- /dev/null +++ b/db/migrations/043_platform_header_footer_text.sql @@ -0,0 +1,35 @@ +INSERT INTO app_settings(key, value_json, value_type, title, description, category) +VALUES + ( + 'tg_poster_header_text', + '""'::jsonb, + 'text', + 'TG текст в начале поста', + 'Текст, который добавляется перед постом только для Telegram. Если пусто, ничего не добавляется. Для ссылок используй HTML: Текст.', + 'TG Poster' + ), + ( + 'vk_poster_header_text', + '""'::jsonb, + 'text', + 'VK текст в начале поста', + 'Текст, который добавляется перед постом только для VK. Если пусто, ничего не добавляется. Можно использовать VK-разметку ссылок: [https://example.com|Текст].', + 'VK Poster' + ), + ( + 'max_poster_header_text', + '""'::jsonb, + 'text', + 'MAX текст в начале поста', + 'Текст, который добавляется перед постом только для MAX. Если пусто, ничего не добавляется.', + 'MAX Poster' + ), + ( + 'max_poster_footer_text', + '""'::jsonb, + 'text', + 'MAX текст перед хэштегами', + 'Текст, который добавляется после поста и перед хэштегами только для MAX.', + 'MAX Poster' + ) +ON CONFLICT (key) DO NOTHING; diff --git a/src/vk_parser_app/admin.py b/src/vk_parser_app/admin.py index 5f88186..78e943c 100644 --- a/src/vk_parser_app/admin.py +++ b/src/vk_parser_app/admin.py @@ -271,6 +271,7 @@ SETTING_ORDER = { "tg_poster_recent_window", "tg_poster_category_repeat_penalty", "tg_poster_source_repeat_penalty", + "tg_poster_header_text", "tg_poster_footer_text", ], "TG Reactor": [ @@ -302,6 +303,7 @@ SETTING_ORDER = { "vk_poster_category_repeat_penalty", "vk_poster_source_repeat_penalty", "vk_poster_dry_run", + "vk_poster_header_text", "vk_poster_footer_text", ], "Publishing": [ @@ -327,6 +329,8 @@ SETTING_ORDER = { "max_poster_auto_reaction", "max_poster_reaction_path_template", "max_poster_dry_run", + "max_poster_header_text", + "max_poster_footer_text", ], "Site Poster": [ "site_poster_provider", diff --git a/src/vk_parser_app/text_utils.py b/src/vk_parser_app/text_utils.py index d71f136..e7ddada 100644 --- a/src/vk_parser_app/text_utils.py +++ b/src/vk_parser_app/text_utils.py @@ -56,6 +56,7 @@ def build_publication_text( source_tag: str, common_tags: object = "", footer_text: str = "", + header_text: str = "", format_title: bool = False, parse_mode: str | None = None, ) -> str: @@ -76,10 +77,11 @@ def build_publication_text( title_idx = idx break + header = str(header_text or "").strip() footer = str(footer_text or "").strip() hashtags = publication_hashtags(category_tag, source_tag, common_tags) if title_idx is None: - return "\n\n".join(part for part in [footer, hashtags] if part) + return "\n\n".join(part for part in [header, footer, hashtags] if part) formatted_lines = [] for idx, line in enumerate(cleaned_lines): @@ -104,7 +106,7 @@ def build_publication_text( raw_body = "\n".join(formatted_lines) # Collapse multiple consecutive blank lines to at most two newlines (\n\n) normalized_body = re.sub(r"\n{3,}", "\n\n", raw_body).strip() - parts = [part for part in [normalized_body, footer, hashtags] if part] + parts = [part for part in [header, normalized_body, footer, hashtags] if part] return "\n\n".join(parts) diff --git a/src/vk_parser_app/workers/max_poster.py b/src/vk_parser_app/workers/max_poster.py index 6661930..7c82ad4 100644 --- a/src/vk_parser_app/workers/max_poster.py +++ b/src/vk_parser_app/workers/max_poster.py @@ -219,6 +219,8 @@ class MAXPoster: self.reaction_path_template = "/messages/{message_id}/reactions" self.dry_run = False self.common_tags = "" + self.header_text = "" + self.footer_text = "" async def init(self) -> None: self.pool = await get_pool() @@ -245,6 +247,8 @@ class MAXPoster: self.reaction_path_template = str(await fetch_setting("max_poster_reaction_path_template", self.reaction_path_template) or "").strip() self.dry_run = await fetch_bool_setting("max_poster_dry_run", False) self.common_tags = str(await fetch_setting("publication_common_tags", "") or "").strip() + self.header_text = str(await fetch_setting("max_poster_header_text", "") or "").strip() + self.footer_text = str(await fetch_setting("max_poster_footer_text", "") or "").strip() if not self.token: raise RuntimeError("max_poster_bot_token is empty") if not self.chat_id: @@ -378,6 +382,8 @@ class MAXPoster: category_tag, source_tag, self.common_tags, + self.footer_text, + self.header_text, ) async def download_media_to_temp(self, session: aiohttp.ClientSession, item: dict[str, Any]) -> Path | None: diff --git a/src/vk_parser_app/workers/tg_poster.py b/src/vk_parser_app/workers/tg_poster.py index 82f1189..866ec99 100644 --- a/src/vk_parser_app/workers/tg_poster.py +++ b/src/vk_parser_app/workers/tg_poster.py @@ -112,6 +112,7 @@ class TelegramPoster: self.category_repeat_penalty = 3.0 self.source_repeat_penalty = 4.0 self.common_tags = "" + self.header_text = "" self.footer_text = "" async def init(self) -> None: @@ -140,6 +141,7 @@ class TelegramPoster: self.category_repeat_penalty = max(0.0, await fetch_float_setting("tg_poster_category_repeat_penalty", 3.0)) self.source_repeat_penalty = max(0.0, await fetch_float_setting("tg_poster_source_repeat_penalty", 4.0)) self.common_tags = str(await fetch_setting("publication_common_tags", "") or "").strip() + self.header_text = str(await fetch_setting("tg_poster_header_text", "") or "").strip() self.footer_text = str(await fetch_setting("tg_poster_footer_text", "") or "").strip() logger.info("TG poster config: chat={} schedule={} caption_limit={}", self.chat_id, self.schedule, self.caption_limit) @@ -335,6 +337,7 @@ class TelegramPoster: source_tag, self.common_tags, self.footer_text, + self.header_text, format_title=True, parse_mode="html", ) diff --git a/src/vk_parser_app/workers/vk_poster.py b/src/vk_parser_app/workers/vk_poster.py index 91e64f8..2c6e926 100644 --- a/src/vk_parser_app/workers/vk_poster.py +++ b/src/vk_parser_app/workers/vk_poster.py @@ -73,6 +73,7 @@ class VKPoster: self.source_repeat_penalty = 4.0 self.dry_run = False self.common_tags = "" + self.header_text = "" self.footer_text = "" async def init(self) -> None: @@ -161,6 +162,7 @@ class VKPoster: self.source_repeat_penalty = max(0.0, await fetch_float_setting("vk_poster_source_repeat_penalty", 4.0)) self.dry_run = await fetch_bool_setting("vk_poster_dry_run", False) self.common_tags = str(await fetch_setting("publication_common_tags", "") or "").strip() + self.header_text = str(await fetch_setting("vk_poster_header_text", "") or "").strip() self.footer_text = str(await fetch_setting("vk_poster_footer_text", "") or "").strip() if not self.token: raise RuntimeError("vk_poster_access_token, VK_GROUP_ACCESS_TOKEN and VK_ACCESS_TOKEN are empty") @@ -302,6 +304,7 @@ class VKPoster: source_tag, self.common_tags, self.footer_text, + self.header_text, )[:VK_MESSAGE_LIMIT] async def media_attachment(self, client: VKAPIClient, item: dict[str, Any]) -> str | None: