Add platform poster header text settings
This commit is contained in:
@@ -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`.
|
- 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`.
|
- RAA Coolify app `korokrhpoyqxf0nw2y8vk7lp` points to `new_vk_parser.git`, branch `main`.
|
||||||
- Both projects should deploy from the same commit SHA.
|
- 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.
|
- Keep only env and database settings different between projects.
|
||||||
- Do not add project-specific defaults to migrations or `.env.example`.
|
- Do not add project-specific defaults to migrations or `.env.example`.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# RAA deployment
|
# 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
|
RAA is not a separate code project anymore. The source of truth for code and
|
||||||
documentation is:
|
documentation is:
|
||||||
@@ -23,7 +23,7 @@ continue feature work there.
|
|||||||
- Coolify git branch: `main`
|
- Coolify git branch: `main`
|
||||||
- Postgres container: `m9v14hzfh5mk9kelxzcqvhc6`
|
- Postgres container: `m9v14hzfh5mk9kelxzcqvhc6`
|
||||||
- Postgres DB/user: `raa_parser` / `raa_parser_user`
|
- 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`
|
- 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`
|
- Health check: `https://raa.panel.f-n8.ru/health`
|
||||||
|
|
||||||
@@ -55,6 +55,11 @@ Database settings:
|
|||||||
- `vk_poster_access_token` is the RAA community token
|
- `vk_poster_access_token` is the RAA community token
|
||||||
- `site_poster_enabled=false`
|
- `site_poster_enabled=false`
|
||||||
- `site_poster_provider=""` until a website adapter is configured
|
- `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
|
## Workers
|
||||||
|
|
||||||
@@ -103,6 +108,11 @@ automatic rejection of non-target content.
|
|||||||
|
|
||||||
## Deployment Notes
|
## 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:
|
To deploy code changes for both FN-8 and RAA:
|
||||||
|
|
||||||
1. Work only in `D:\DEVELOPMENT\new_vk_parser`.
|
1. Work only in `D:\DEVELOPMENT\new_vk_parser`.
|
||||||
|
|||||||
@@ -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.
|
- Deploy path: push to `gitea/main` -> Gitea webhook -> Coolify -> Docker build/deploy.
|
||||||
- Coolify + Gitea LXC: `192.168.1.135` (CTID 102).
|
- 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.
|
- 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).
|
- 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/...`.
|
- 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.
|
- 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`).
|
- Coolify app UUID: `korokrhpoyqxf0nw2y8vk7lp` (`RAA admin`).
|
||||||
- Public admin URL: `https://raa.panel.f-n8.ru`.
|
- Public admin URL: `https://raa.panel.f-n8.ru`.
|
||||||
- RAA Postgres container: `m9v14hzfh5mk9kelxzcqvhc6`, DB `raa_parser`, user `raa_parser_user`.
|
- 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`).
|
- 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 | Страйкбол`).
|
- 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`.
|
- 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`,
|
`ai-qualifier=false`, `ai-writer=false`, `tg-poster=false`, `vk-poster=false`,
|
||||||
`site-poster=false`, `tg_poster_enabled=false`, `vk_poster_enabled=false`.
|
`site-poster=false`, `tg_poster_enabled=false`, `vk_poster_enabled=false`.
|
||||||
- Detailed RAA notes live in `RAA.md`.
|
- 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
|
## 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`
|
- optional project-wide common tags first, then `#category #source`
|
||||||
- Project-wide common tags live in DB setting `publication_common_tags`.
|
- Project-wide common tags live in DB setting `publication_common_tags`.
|
||||||
FN-8 value: `#новости_снаряжения`. RAA value: `#raa_news #страйкбол`.
|
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 editor textarea must show only clean editable text.
|
||||||
- The list preview in `/editor` should show the publication preview, including composed hashtags.
|
- 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.
|
- `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.
|
||||||
|
|||||||
@@ -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: <a href="https://example.com">Текст</a>.',
|
||||||
|
'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;
|
||||||
@@ -271,6 +271,7 @@ SETTING_ORDER = {
|
|||||||
"tg_poster_recent_window",
|
"tg_poster_recent_window",
|
||||||
"tg_poster_category_repeat_penalty",
|
"tg_poster_category_repeat_penalty",
|
||||||
"tg_poster_source_repeat_penalty",
|
"tg_poster_source_repeat_penalty",
|
||||||
|
"tg_poster_header_text",
|
||||||
"tg_poster_footer_text",
|
"tg_poster_footer_text",
|
||||||
],
|
],
|
||||||
"TG Reactor": [
|
"TG Reactor": [
|
||||||
@@ -302,6 +303,7 @@ SETTING_ORDER = {
|
|||||||
"vk_poster_category_repeat_penalty",
|
"vk_poster_category_repeat_penalty",
|
||||||
"vk_poster_source_repeat_penalty",
|
"vk_poster_source_repeat_penalty",
|
||||||
"vk_poster_dry_run",
|
"vk_poster_dry_run",
|
||||||
|
"vk_poster_header_text",
|
||||||
"vk_poster_footer_text",
|
"vk_poster_footer_text",
|
||||||
],
|
],
|
||||||
"Publishing": [
|
"Publishing": [
|
||||||
@@ -327,6 +329,8 @@ SETTING_ORDER = {
|
|||||||
"max_poster_auto_reaction",
|
"max_poster_auto_reaction",
|
||||||
"max_poster_reaction_path_template",
|
"max_poster_reaction_path_template",
|
||||||
"max_poster_dry_run",
|
"max_poster_dry_run",
|
||||||
|
"max_poster_header_text",
|
||||||
|
"max_poster_footer_text",
|
||||||
],
|
],
|
||||||
"Site Poster": [
|
"Site Poster": [
|
||||||
"site_poster_provider",
|
"site_poster_provider",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ def build_publication_text(
|
|||||||
source_tag: str,
|
source_tag: str,
|
||||||
common_tags: object = "",
|
common_tags: object = "",
|
||||||
footer_text: str = "",
|
footer_text: str = "",
|
||||||
|
header_text: str = "",
|
||||||
format_title: bool = False,
|
format_title: bool = False,
|
||||||
parse_mode: str | None = None,
|
parse_mode: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -76,10 +77,11 @@ def build_publication_text(
|
|||||||
title_idx = idx
|
title_idx = idx
|
||||||
break
|
break
|
||||||
|
|
||||||
|
header = str(header_text or "").strip()
|
||||||
footer = str(footer_text or "").strip()
|
footer = str(footer_text or "").strip()
|
||||||
hashtags = publication_hashtags(category_tag, source_tag, common_tags)
|
hashtags = publication_hashtags(category_tag, source_tag, common_tags)
|
||||||
if title_idx is None:
|
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 = []
|
formatted_lines = []
|
||||||
for idx, line in enumerate(cleaned_lines):
|
for idx, line in enumerate(cleaned_lines):
|
||||||
@@ -104,7 +106,7 @@ def build_publication_text(
|
|||||||
raw_body = "\n".join(formatted_lines)
|
raw_body = "\n".join(formatted_lines)
|
||||||
# Collapse multiple consecutive blank lines to at most two newlines (\n\n)
|
# Collapse multiple consecutive blank lines to at most two newlines (\n\n)
|
||||||
normalized_body = re.sub(r"\n{3,}", "\n\n", raw_body).strip()
|
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)
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ class MAXPoster:
|
|||||||
self.reaction_path_template = "/messages/{message_id}/reactions"
|
self.reaction_path_template = "/messages/{message_id}/reactions"
|
||||||
self.dry_run = False
|
self.dry_run = False
|
||||||
self.common_tags = ""
|
self.common_tags = ""
|
||||||
|
self.header_text = ""
|
||||||
|
self.footer_text = ""
|
||||||
|
|
||||||
async def init(self) -> None:
|
async def init(self) -> None:
|
||||||
self.pool = await get_pool()
|
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.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.dry_run = await fetch_bool_setting("max_poster_dry_run", False)
|
||||||
self.common_tags = str(await fetch_setting("publication_common_tags", "") or "").strip()
|
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:
|
if not self.token:
|
||||||
raise RuntimeError("max_poster_bot_token is empty")
|
raise RuntimeError("max_poster_bot_token is empty")
|
||||||
if not self.chat_id:
|
if not self.chat_id:
|
||||||
@@ -378,6 +382,8 @@ class MAXPoster:
|
|||||||
category_tag,
|
category_tag,
|
||||||
source_tag,
|
source_tag,
|
||||||
self.common_tags,
|
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:
|
async def download_media_to_temp(self, session: aiohttp.ClientSession, item: dict[str, Any]) -> Path | None:
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ class TelegramPoster:
|
|||||||
self.category_repeat_penalty = 3.0
|
self.category_repeat_penalty = 3.0
|
||||||
self.source_repeat_penalty = 4.0
|
self.source_repeat_penalty = 4.0
|
||||||
self.common_tags = ""
|
self.common_tags = ""
|
||||||
|
self.header_text = ""
|
||||||
self.footer_text = ""
|
self.footer_text = ""
|
||||||
|
|
||||||
async def init(self) -> None:
|
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.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.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.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()
|
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)
|
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,
|
source_tag,
|
||||||
self.common_tags,
|
self.common_tags,
|
||||||
self.footer_text,
|
self.footer_text,
|
||||||
|
self.header_text,
|
||||||
format_title=True,
|
format_title=True,
|
||||||
parse_mode="html",
|
parse_mode="html",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class VKPoster:
|
|||||||
self.source_repeat_penalty = 4.0
|
self.source_repeat_penalty = 4.0
|
||||||
self.dry_run = False
|
self.dry_run = False
|
||||||
self.common_tags = ""
|
self.common_tags = ""
|
||||||
|
self.header_text = ""
|
||||||
self.footer_text = ""
|
self.footer_text = ""
|
||||||
|
|
||||||
async def init(self) -> None:
|
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.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.dry_run = await fetch_bool_setting("vk_poster_dry_run", False)
|
||||||
self.common_tags = str(await fetch_setting("publication_common_tags", "") or "").strip()
|
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()
|
self.footer_text = str(await fetch_setting("vk_poster_footer_text", "") or "").strip()
|
||||||
if not self.token:
|
if not self.token:
|
||||||
raise RuntimeError("vk_poster_access_token, VK_GROUP_ACCESS_TOKEN and VK_ACCESS_TOKEN are empty")
|
raise RuntimeError("vk_poster_access_token, VK_GROUP_ACCESS_TOKEN and VK_ACCESS_TOKEN are empty")
|
||||||
@@ -302,6 +304,7 @@ class VKPoster:
|
|||||||
source_tag,
|
source_tag,
|
||||||
self.common_tags,
|
self.common_tags,
|
||||||
self.footer_text,
|
self.footer_text,
|
||||||
|
self.header_text,
|
||||||
)[:VK_MESSAGE_LIMIT]
|
)[:VK_MESSAGE_LIMIT]
|
||||||
|
|
||||||
async def media_attachment(self, client: VKAPIClient, item: dict[str, Any]) -> str | None:
|
async def media_attachment(self, client: VKAPIClient, item: dict[str, Any]) -> str | None:
|
||||||
|
|||||||
Reference in New Issue
Block a user