Add project common publication tags

This commit is contained in:
Your Name
2026-08-03 21:43:56 +05:00
parent 2bcf5f38bf
commit a19cc82168
10 changed files with 143 additions and 26 deletions
+13
View File
@@ -31,6 +31,19 @@ Goal: keep one codebase and run separate deployments for each editorial project.
Sources, target groups/channels, poster tokens, schedules, prompts, categories, and enabled workers should stay in `app_settings`, `sources`, `content_categories`, and `worker_controls`. Sources, target groups/channels, poster tokens, schedules, prompts, categories, and enabled workers should stay in `app_settings`, `sources`, `content_categories`, and `worker_controls`.
Common publication hashtags are also per-project database settings:
- key: `publication_common_tags`
- FN-8: `#новости_снаряжения`
- RAA: `#raa_news #страйкбол`
- The app posts these tags before category/source tags in TG/VK/MAX social posters.
RAA current social targets:
- raw media Telegram channel: `tg_media_channel_id=-1004312015562`
- final Telegram publication channel: `tg_poster_chat_id=-1001303630155`
- VK owner: `vk_poster_owner_id=-36860851`, `vk_poster_from_group=true`
## Poster modules ## Poster modules
The site poster is selected by `site_poster_provider`. A project can disable the worker or leave the provider empty when there is no website poster yet. New website integrations should be added as provider modules behind the same interface instead of branching the worker by project name. The site poster is selected by `site_poster_provider`. A project can disable the worker or leave the provider empty when there is no website poster yet. New website integrations should be added as provider modules behind the same interface instead of branching the worker by project name.
+22 -1
View File
@@ -48,6 +48,25 @@ Current runtime is local infrastructure, not the old VPS.
- New installs default `site-poster` to disabled and must explicitly set provider, - New installs default `site-poster` to disabled and must explicitly set provider,
base URL, secret, and `site_poster_enabled=true`. base URL, secret, and `site_poster_enabled=true`.
## RAA Duplicate
As of 2026-08-03, the RAA duplicate is a separate Coolify app/database using the
same codebase shape.
- Local copy path requested by user: `D:\DEVELOPMENT\raa-parser_poster`.
- Gitea repo: `http://192.168.1.135:3000/exostring/raa-parser_poster.git`.
- 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`.
- 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`.
- `TG_BOT_TOKEN`, `TELEGRAM_API_ID`, and `TELEGRAM_API_HASH` were copied from FN-8 Coolify env.
- Local Bot API app setting: `local_bot_api_url=http://127.0.0.1:8081`.
- As last checked, publication workers were intentionally disabled:
`vk-storage-uploader=false`, `tg-poster=false`, `vk-poster=false`,
`tg_poster_enabled=false`, `vk_poster_enabled=false`.
## Important Text Rules ## Important Text Rules
- AI writer output text must be clean: no physical hashtags at the end. - AI writer output text must be clean: no physical hashtags at the end.
@@ -58,7 +77,9 @@ Current runtime is local infrastructure, not the old VPS.
- Publication preview may show hashtags by composing: - Publication preview may show hashtags by composing:
- clean text - clean text
- blank line - blank line
- `#category #source` - 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 #страйкбол`.
- 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,13 @@
ALTER TABLE content_categories
ADD COLUMN IF NOT EXISTS description TEXT NOT NULL DEFAULT '';
INSERT INTO app_settings(key, value_json, value_type, title, description, category)
VALUES (
'publication_common_tags',
'""'::jsonb,
'text',
'Общие хэштеги публикаций',
'Один или несколько хэштегов, которые добавляются первыми в публикациях соцсетей. Можно писать через пробел, запятую или с новой строки.',
'Publishing'
)
ON CONFLICT (key) DO NOTHING;
+30 -13
View File
@@ -1325,13 +1325,21 @@ async def writer_category_payload() -> list[dict[str, Any]]:
pool = await get_pool() pool = await get_pool()
rows = await pool.fetch( rows = await pool.fetch(
""" """
SELECT sort_order AS id, name, tag SELECT sort_order AS id, name, tag, COALESCE(description, '') AS description
FROM content_categories FROM content_categories
WHERE is_active=TRUE WHERE is_active=TRUE
ORDER BY sort_order, name ORDER BY sort_order, name
""" """
) )
categories = [{"id": int(row["id"]), "name": str(row["name"]), "tag": str(row["tag"])} for row in rows] categories = [
{
"id": int(row["id"]),
"name": str(row["name"]),
"tag": str(row["tag"]),
"description": str(row["description"] or ""),
}
for row in rows
]
if categories: if categories:
return categories return categories
return [ return [
@@ -1344,7 +1352,8 @@ async def category_rows() -> list[dict[str, Any]]:
pool = await get_pool() pool = await get_pool()
rows = await pool.fetch( rows = await pool.fetch(
""" """
SELECT id, name, tag, site_name, site_slug, site_enabled, SELECT id, name, tag, COALESCE(description, '') AS description,
site_name, site_slug, site_enabled,
is_active, sort_order, created_at, updated_at is_active, sort_order, created_at, updated_at
FROM content_categories FROM content_categories
ORDER BY is_active DESC, sort_order, name ORDER BY is_active DESC, sort_order, name
@@ -3630,6 +3639,7 @@ async def category_create(
csrf_token: str = Form(...), csrf_token: str = Form(...),
name: str = Form(...), name: str = Form(...),
tag: str = Form(""), tag: str = Form(""),
description: str = Form(""),
site_name: str = Form(...), site_name: str = Form(...),
site_slug: str = Form(...), site_slug: str = Form(...),
site_enabled: str | None = Form(None), site_enabled: str | None = Form(None),
@@ -3642,6 +3652,7 @@ async def category_create(
if not name: if not name:
return redirect("/workers") return redirect("/workers")
tag = normalize_hash_tag(tag or name, "category") tag = normalize_hash_tag(tag or name, "category")
description = description.strip()
site_name = site_name.strip() site_name = site_name.strip()
site_slug = re.sub(r"[^a-z0-9-]+", "-", site_slug.strip().lower().replace("_", "-")).strip("-") site_slug = re.sub(r"[^a-z0-9-]+", "-", site_slug.strip().lower().replace("_", "-")).strip("-")
if not site_name or not site_slug: if not site_name or not site_slug:
@@ -3650,24 +3661,26 @@ async def category_create(
sort_order = int(await pool.fetchval("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM content_categories") or 1) sort_order = int(await pool.fetchval("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM content_categories") or 1)
await pool.execute( await pool.execute(
""" """
INSERT INTO content_categories(name, tag, sort_order, site_name, site_slug, site_enabled) INSERT INTO content_categories(name, tag, sort_order, description, site_name, site_slug, site_enabled)
VALUES($1, $2, $3, $4, $5, $6) VALUES($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (name) DO UPDATE ON CONFLICT (name) DO UPDATE
SET tag=$2, SET tag=$2,
site_name=$4, description=$4,
site_slug=$5, site_name=$5,
site_enabled=$6, site_slug=$6,
site_enabled=$7,
is_active=TRUE, is_active=TRUE,
updated_at=NOW() updated_at=NOW()
""", """,
name, name,
tag, tag,
sort_order, sort_order,
description,
site_name, site_name,
site_slug, site_slug,
site_enabled is not None, site_enabled is not None,
) )
await audit(user["id"], "category.create", "content_category", None, {"name": name, "tag": tag, "site_name": site_name, "site_slug": site_slug, "site_enabled": site_enabled is not None}) await audit(user["id"], "category.create", "content_category", None, {"name": name, "tag": tag, "description": description, "site_name": site_name, "site_slug": site_slug, "site_enabled": site_enabled is not None})
return redirect("/workers") return redirect("/workers")
@@ -3678,6 +3691,7 @@ async def category_update(
csrf_token: str = Form(...), csrf_token: str = Form(...),
name: str = Form(...), name: str = Form(...),
tag: str = Form(""), tag: str = Form(""),
description: str = Form(""),
site_name: str = Form(...), site_name: str = Form(...),
site_slug: str = Form(...), site_slug: str = Form(...),
site_enabled: str | None = Form(None), site_enabled: str | None = Form(None),
@@ -3690,6 +3704,7 @@ async def category_update(
if not name: if not name:
return redirect("/workers") return redirect("/workers")
tag = normalize_hash_tag(tag or name, "category") tag = normalize_hash_tag(tag or name, "category")
description = description.strip()
site_name = site_name.strip() site_name = site_name.strip()
site_slug = re.sub(r"[^a-z0-9-]+", "-", site_slug.strip().lower().replace("_", "-")).strip("-") site_slug = re.sub(r"[^a-z0-9-]+", "-", site_slug.strip().lower().replace("_", "-")).strip("-")
if not site_name or not site_slug: if not site_name or not site_slug:
@@ -3700,20 +3715,22 @@ async def category_update(
UPDATE content_categories UPDATE content_categories
SET name=$2, SET name=$2,
tag=$3, tag=$3,
site_name=$4, description=$4,
site_slug=$5, site_name=$5,
site_enabled=$6, site_slug=$6,
site_enabled=$7,
updated_at=NOW() updated_at=NOW()
WHERE id=$1 WHERE id=$1
""", """,
category_id, category_id,
name, name,
tag, tag,
description,
site_name, site_name,
site_slug, site_slug,
site_enabled is not None, site_enabled is not None,
) )
await audit(user["id"], "category.update", "content_category", category_id, {"name": name, "tag": tag, "site_name": site_name, "site_slug": site_slug, "site_enabled": site_enabled is not None}) await audit(user["id"], "category.update", "content_category", category_id, {"name": name, "tag": tag, "description": description, "site_name": site_name, "site_slug": site_slug, "site_enabled": site_enabled is not None})
return redirect("/workers") return redirect("/workers")
+6 -1
View File
@@ -73,7 +73,7 @@
</summary> </summary>
<div class="p-4 flex flex-col gap-6 bg-app-bg/30"> <div class="p-4 flex flex-col gap-6 bg-app-bg/30">
<div class="text-sm text-app-textMuted"> <div class="text-sm text-app-textMuted">
Название описывает категорию для AI, тэг используется в соцсетях. Название и описание помогают AI выбрать категорию, тэг используется в соцсетях.
</div> </div>
<form method="post" action="/categories/create" class="flex flex-col gap-4 bg-app-surface p-4 rounded-xl border border-app-border"> <form method="post" action="/categories/create" class="flex flex-col gap-4 bg-app-surface p-4 rounded-xl border border-app-border">
@@ -88,6 +88,10 @@
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Тэг (для хэштегов)</label> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Тэг (для хэштегов)</label>
<input name="tag" class="input w-full"> <input name="tag" class="input w-full">
</div> </div>
<div class="md:col-span-2">
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Описание (для AI)</label>
<textarea name="description" class="textarea w-full min-h-20"></textarea>
</div>
<div> <div>
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">На сайте</label> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">На сайте</label>
<input name="site_name" class="input w-full" required> <input name="site_name" class="input w-full" required>
@@ -127,6 +131,7 @@
<div class="flex flex-col gap-1.5"> <div class="flex flex-col gap-1.5">
<input form="category-update-{{ c.id }}" name="name" value="{{ c.name }}" class="input input-sm w-full bg-app-bg text-white h-7"> <input form="category-update-{{ c.id }}" name="name" value="{{ c.name }}" class="input input-sm w-full bg-app-bg text-white h-7">
<input form="category-update-{{ c.id }}" name="tag" value="{{ c.tag }}" class="input input-sm w-full bg-app-bg text-blue-400 font-mono h-7"> <input form="category-update-{{ c.id }}" name="tag" value="{{ c.tag }}" class="input input-sm w-full bg-app-bg text-blue-400 font-mono h-7">
<textarea form="category-update-{{ c.id }}" name="description" class="textarea textarea-sm w-full bg-app-bg text-app-textMain min-h-16">{{ c.description }}</textarea>
</div> </div>
</td> </td>
<td class="px-3 py-2"> <td class="px-3 py-2">
+20 -3
View File
@@ -23,8 +23,24 @@ def strip_trailing_hashtag_line(text: str) -> str:
return "\n".join(lines).rstrip() return "\n".join(lines).rstrip()
def publication_hashtags(category_tag: str, source_tag: str) -> str: def parse_hash_tags(value: object) -> list[str]:
if isinstance(value, list):
raw_items = [str(item) for item in value]
else:
raw_items = re.split(r"[\s,|]+", str(value or ""))
tags: list[str] = []
seen: set[str] = set()
for item in raw_items:
tag = normalize_hash_tag(item, "")
if tag and tag not in seen:
tags.append(tag)
seen.add(tag)
return tags
def publication_hashtags(category_tag: str, source_tag: str, common_tags: object = "") -> str:
tags = [ tags = [
*parse_hash_tags(common_tags),
normalize_hash_tag(category_tag, "category"), normalize_hash_tag(category_tag, "category"),
normalize_hash_tag(source_tag, "source"), normalize_hash_tag(source_tag, "source"),
] ]
@@ -38,6 +54,7 @@ def build_publication_text(
text: str, text: str,
category_tag: str, category_tag: str,
source_tag: str, source_tag: str,
common_tags: object = "",
format_title: bool = False, format_title: bool = False,
parse_mode: str | None = None, parse_mode: str | None = None,
) -> str: ) -> str:
@@ -59,7 +76,7 @@ def build_publication_text(
break break
if title_idx is None: if title_idx is None:
return publication_hashtags(category_tag, source_tag) return publication_hashtags(category_tag, source_tag, common_tags)
formatted_lines = [] formatted_lines = []
for idx, line in enumerate(cleaned_lines): for idx, line in enumerate(cleaned_lines):
@@ -84,7 +101,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()
hashtags = publication_hashtags(category_tag, source_tag) hashtags = publication_hashtags(category_tag, source_tag, common_tags)
if hashtags: if hashtags:
return f"{normalized_body}\n\n{hashtags}" if normalized_body else hashtags return f"{normalized_body}\n\n{hashtags}" if normalized_body else hashtags
return normalized_body return normalized_body
+13 -5
View File
@@ -16,7 +16,7 @@ from ..constants import WORKER_AI_WRITER
from ..db import fetch_bool_setting, fetch_float_setting, fetch_int_setting, fetch_setting, get_pool from ..db import fetch_bool_setting, fetch_float_setting, fetch_int_setting, fetch_setting, get_pool
from ..heartbeat import HeartbeatReporter from ..heartbeat import HeartbeatReporter
from ..jobs import is_worker_enabled from ..jobs import is_worker_enabled
from ..text_utils import build_publication_text, normalize_hash_tag, parse_categories from ..text_utils import normalize_hash_tag, parse_categories, strip_trailing_hashtag_line
from .ai_alerts import send_ai_worker_error_alert from .ai_alerts import send_ai_worker_error_alert
@@ -53,7 +53,7 @@ OUTPUT SCHEMA (return array matching input order):
Rules: Rules:
- Input is a JSON object with key "posts" containing accepted posts. - Input is a JSON object with key "posts" containing accepted posts.
- Input has key "categories" with objects: id, name, tag. - Input has key "categories" with objects: id, name, tag, description.
- Each post includes producer_name and producer_tag. producer_name can be a manufacturer, shop, or publishing source. - Each post includes producer_name and producer_tag. producer_name can be a manufacturer, shop, or publishing source.
- The "text" field for every rewrite must explicitly mention producer_name at least once. Do not satisfy this only through producer_tag, hashtags, notes, or metadata. - The "text" field for every rewrite must explicitly mention producer_name at least once. Do not satisfy this only through producer_tag, hashtags, notes, or metadata.
- Pick exactly one category from the categories list in input and return its numeric id as category_id. - Pick exactly one category from the categories list in input and return its numeric id as category_id.
@@ -208,7 +208,7 @@ async def load_writer_categories(pool) -> list[dict[str, Any]]:
try: try:
rows = await pool.fetch( rows = await pool.fetch(
""" """
SELECT sort_order AS id, name, tag SELECT sort_order AS id, name, tag, COALESCE(description, '') AS description
FROM content_categories FROM content_categories
WHERE is_active=TRUE WHERE is_active=TRUE
ORDER BY sort_order, name ORDER BY sort_order, name
@@ -216,7 +216,15 @@ async def load_writer_categories(pool) -> list[dict[str, Any]]:
) )
except Exception: except Exception:
rows = [] rows = []
categories = [{"id": int(row["id"]), "name": str(row["name"]), "tag": str(row["tag"])} for row in rows] categories = [
{
"id": int(row["id"]),
"name": str(row["name"]),
"tag": str(row["tag"]),
"description": str(row["description"] or ""),
}
for row in rows
]
if categories: if categories:
return categories return categories
legacy = parse_categories(await fetch_setting("ai_writer_categories", [])) legacy = parse_categories(await fetch_setting("ai_writer_categories", []))
@@ -399,7 +407,7 @@ class AIWriterWorker:
async def apply_rewrites(self, batch_id: int, rewrites: list[dict], model: str, prompt: str) -> None: async def apply_rewrites(self, batch_id: int, rewrites: list[dict], model: str, prompt: str) -> None:
for item in rewrites: for item in rewrites:
final_text = build_publication_text(item["text"], item["category_tag"], item["source_tag"]) final_text = strip_trailing_hashtag_line(item["text"])
editorial_status = "rejected" if item.get("reject_by_category") else "review" editorial_status = "rejected" if item.get("reject_by_category") else "review"
editor_notes = "Отклонено AI: Не целевой контент" if item.get("reject_by_category") else None editor_notes = "Отклонено AI: Не целевой контент" if item.get("reject_by_category") else None
await self.pool.execute( await self.pool.execute(
+8 -1
View File
@@ -218,6 +218,7 @@ class MAXPoster:
self.auto_reaction = "👍" self.auto_reaction = "👍"
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 = ""
async def init(self) -> None: async def init(self) -> None:
self.pool = await get_pool() self.pool = await get_pool()
@@ -243,6 +244,7 @@ class MAXPoster:
self.auto_reaction = str(await fetch_setting("max_poster_auto_reaction", "👍") or "👍").strip() or "👍" self.auto_reaction = str(await fetch_setting("max_poster_auto_reaction", "👍") or "👍").strip() or "👍"
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()
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:
@@ -371,7 +373,12 @@ class MAXPoster:
def build_text(self, post: dict[str, Any]) -> str: def build_text(self, post: dict[str, Any]) -> str:
category_tag = str(post.get("final_category_tag") or post.get("rewrite_category_tag") or post.get("final_category") or post.get("rewrite_category") or "") category_tag = str(post.get("final_category_tag") or post.get("rewrite_category_tag") or post.get("final_category") or post.get("rewrite_category") or "")
source_tag = str(post.get("final_source_tag") or post.get("rewrite_source_tag") or post.get("source_tag") or "") source_tag = str(post.get("final_source_tag") or post.get("rewrite_source_tag") or post.get("source_tag") or "")
return build_publication_text(post.get("final_text") or post.get("rewritten_text") or "", category_tag, source_tag) return build_publication_text(
post.get("final_text") or post.get("rewritten_text") or "",
category_tag,
source_tag,
self.common_tags,
)
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:
media_type = str(item.get("media_type") or "") media_type = str(item.get("media_type") or "")
+10 -1
View File
@@ -111,6 +111,7 @@ class TelegramPoster:
self.recent_window = 20 self.recent_window = 20
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 = ""
async def init(self) -> None: async def init(self) -> None:
self.pool = await get_pool() self.pool = await get_pool()
@@ -137,6 +138,7 @@ class TelegramPoster:
self.recent_window = max(1, await fetch_int_setting("tg_poster_recent_window", 20)) self.recent_window = max(1, await fetch_int_setting("tg_poster_recent_window", 20))
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()
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)
async def close(self) -> None: async def close(self) -> None:
@@ -325,7 +327,14 @@ class TelegramPoster:
def build_text(self, post: dict[str, Any]) -> str: def build_text(self, post: dict[str, Any]) -> str:
category_tag = str(post.get("final_category_tag") or post.get("rewrite_category_tag") or post.get("final_category") or post.get("rewrite_category") or "") category_tag = str(post.get("final_category_tag") or post.get("rewrite_category_tag") or post.get("final_category") or post.get("rewrite_category") or "")
source_tag = str(post.get("final_source_tag") or post.get("rewrite_source_tag") or post.get("source_tag") or "") source_tag = str(post.get("final_source_tag") or post.get("rewrite_source_tag") or post.get("source_tag") or "")
return build_publication_text(post.get("final_text") or post.get("rewritten_text") or "", category_tag, source_tag, format_title=True, parse_mode="html") return build_publication_text(
post.get("final_text") or post.get("rewritten_text") or "",
category_tag,
source_tag,
self.common_tags,
format_title=True,
parse_mode="html",
)
async def send_text(self, text: str) -> list[int]: async def send_text(self, text: str) -> list[int]:
message_ids: list[int] = [] message_ids: list[int] = []
+8 -1
View File
@@ -72,6 +72,7 @@ class VKPoster:
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.dry_run = False self.dry_run = False
self.common_tags = ""
async def init(self) -> None: async def init(self) -> None:
self.pool = await get_pool() self.pool = await get_pool()
@@ -158,6 +159,7 @@ class VKPoster:
self.category_repeat_penalty = max(0.0, await fetch_float_setting("vk_poster_category_repeat_penalty", 3.0)) self.category_repeat_penalty = max(0.0, await fetch_float_setting("vk_poster_category_repeat_penalty", 3.0))
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()
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")
if not self.owner_id: if not self.owner_id:
@@ -292,7 +294,12 @@ class VKPoster:
def build_text(self, post: dict[str, Any]) -> str: def build_text(self, post: dict[str, Any]) -> str:
category_tag = str(post.get("final_category_tag") or post.get("rewrite_category_tag") or post.get("final_category") or post.get("rewrite_category") or "") category_tag = str(post.get("final_category_tag") or post.get("rewrite_category_tag") or post.get("final_category") or post.get("rewrite_category") or "")
source_tag = str(post.get("final_source_tag") or post.get("rewrite_source_tag") or post.get("source_tag") or "") source_tag = str(post.get("final_source_tag") or post.get("rewrite_source_tag") or post.get("source_tag") or "")
return build_publication_text(post.get("final_text") or post.get("rewritten_text") or "", category_tag, source_tag)[:VK_MESSAGE_LIMIT] return build_publication_text(
post.get("final_text") or post.get("rewritten_text") or "",
category_tag,
source_tag,
self.common_tags,
)[: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:
attachment_id = str(item.get("original_attachment_id") or "").strip() attachment_id = str(item.get("original_attachment_id") or "").strip()