Make project defaults reusable
This commit is contained in:
+1
-1
@@ -14,7 +14,7 @@ DB_PASSWORD=change-me
|
|||||||
VK_ACCESS_TOKEN=
|
VK_ACCESS_TOKEN=
|
||||||
VK_GROUP_ACCESS_TOKEN=
|
VK_GROUP_ACCESS_TOKEN=
|
||||||
VK_API_VERSION=5.199
|
VK_API_VERSION=5.199
|
||||||
VK_STORAGE_GROUP_ID=239548476
|
VK_STORAGE_GROUP_ID=0
|
||||||
|
|
||||||
TG_BOT_TOKEN=
|
TG_BOT_TOKEN=
|
||||||
TG_MEDIA_CHANNEL_ID=
|
TG_MEDIA_CHANNEL_ID=
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ INSERT INTO app_settings(key, value_json, value_type, title, description, catego
|
|||||||
VALUES
|
VALUES
|
||||||
('tg_poster_enabled', 'false'::jsonb, 'bool', 'TG poster enabled', 'Double safety flag for publishing accepted posts to Telegram.', 'TG Poster'),
|
('tg_poster_enabled', 'false'::jsonb, 'bool', 'TG poster enabled', 'Double safety flag for publishing accepted posts to Telegram.', 'TG Poster'),
|
||||||
('tg_poster_bot_token', '""'::jsonb, 'secret', 'TG poster bot token', 'Bot token for final Telegram publication. For test can be the same as upload bot token.', 'TG Poster'),
|
('tg_poster_bot_token', '""'::jsonb, 'secret', 'TG poster bot token', 'Bot token for final Telegram publication. For test can be the same as upload bot token.', 'TG Poster'),
|
||||||
('tg_poster_chat_id', '"-1003712724327"'::jsonb, 'str', 'TG publication chat id', 'Target Telegram channel/group id. Supports chat_id:thread_id for topics.', 'TG Poster'),
|
('tg_poster_chat_id', '""'::jsonb, 'str', 'TG publication chat id', 'Target Telegram channel/group id. Supports chat_id:thread_id for topics.', 'TG Poster'),
|
||||||
('tg_poster_schedule_json', '[{"id":"morning","time":"10:00","count":1,"enabled":true},{"id":"evening","time":"18:00","count":1,"enabled":true}]'::jsonb, 'json', 'TG publication schedule', 'EKB time schedule rows: id, time HH:MM, count, enabled.', 'TG Poster'),
|
('tg_poster_schedule_json', '[{"id":"morning","time":"10:00","count":1,"enabled":true},{"id":"evening","time":"18:00","count":1,"enabled":true}]'::jsonb, 'json', 'TG publication schedule', 'EKB time schedule rows: id, time HH:MM, count, enabled.', 'TG Poster'),
|
||||||
('tg_poster_interval_sec', '60'::jsonb, 'int', 'TG poster interval, sec', 'Pause between poster checks.', 'TG Poster'),
|
('tg_poster_interval_sec', '60'::jsonb, 'int', 'TG poster interval, sec', 'Pause between poster checks.', 'TG Poster'),
|
||||||
('tg_poster_media_group_max_items', '10'::jsonb, 'int', 'TG media group max items', 'Telegram media group supports 2..10 media items.', 'TG Poster'),
|
('tg_poster_media_group_max_items', '10'::jsonb, 'int', 'TG media group max items', 'Telegram media group supports 2..10 media items.', 'TG Poster'),
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
INSERT INTO worker_controls(name, enabled, settings_json)
|
INSERT INTO worker_controls(name, enabled, settings_json)
|
||||||
VALUES ('site-poster', TRUE, '{}'::jsonb)
|
VALUES ('site-poster', FALSE, '{}'::jsonb)
|
||||||
ON CONFLICT (name) DO NOTHING;
|
ON CONFLICT (name) DO NOTHING;
|
||||||
|
|
||||||
INSERT INTO app_settings(key, value_json, value_type, title, description, category)
|
INSERT INTO app_settings(key, value_json, value_type, title, description, category)
|
||||||
VALUES
|
VALUES
|
||||||
('site_poster_enabled', 'true'::jsonb, 'bool', 'Site poster enabled', 'Double safety flag for automatic Ghost publishing.', 'Site Poster'),
|
('site_poster_enabled', 'false'::jsonb, 'bool', 'Site poster enabled', 'Double safety flag for automatic site publishing.', 'Site Poster'),
|
||||||
|
('site_poster_provider', '""'::jsonb, 'str', 'Site provider', 'Publish adapter: ghost now; wordpress can be added as a code adapter later.', 'Site Poster'),
|
||||||
|
('site_poster_base_url', '""'::jsonb, 'str', 'Site API base URL', 'Public/admin base URL for the selected site provider.', 'Site Poster'),
|
||||||
|
('site_poster_secret_file', '""'::jsonb, 'str', 'Site secret file', 'Server path to the selected provider API secret. Keep empty in git.', 'Site Poster'),
|
||||||
|
('site_poster_fallback_title', '"Новость"'::jsonb, 'str', 'Fallback article title', 'Title used when post text has no first line.', 'Site Poster'),
|
||||||
('site_poster_interval_sec', '300'::jsonb, 'int', 'Site poster interval, sec', 'Pause between automatic queue checks.', 'Site Poster'),
|
('site_poster_interval_sec', '300'::jsonb, 'int', 'Site poster interval, sec', 'Pause between automatic queue checks.', 'Site Poster'),
|
||||||
('site_poster_photo_batch_size', '10'::jsonb, 'int', 'Photo batch size', 'Maximum photo-ready posts per automatic run.', 'Site Poster'),
|
('site_poster_photo_batch_size', '10'::jsonb, 'int', 'Photo batch size', 'Maximum photo-ready posts per automatic run.', 'Site Poster'),
|
||||||
('site_poster_video_batch_size', '1'::jsonb, 'int', 'Video batch size', 'Maximum video-only posts per automatic run.', 'Site Poster'),
|
('site_poster_video_batch_size', '1'::jsonb, 'int', 'Video batch size', 'Maximum video-only posts per automatic run.', 'Site Poster')
|
||||||
('site_poster_ghost_url', '"https://f-n8.ru"'::jsonb, 'str', 'Ghost URL', 'Target Ghost public URL.', 'Site Poster'),
|
|
||||||
('site_poster_key_file', '"/opt/vk-parser/.site-poster-key"'::jsonb, 'str', 'Ghost key file', 'Server path to the Ghost Admin API key.', 'Site Poster')
|
|
||||||
ON CONFLICT (key) DO NOTHING;
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
INSERT INTO app_settings(key, value_json, value_type, title, description, category)
|
||||||
|
VALUES
|
||||||
|
('site_poster_provider', '""'::jsonb, 'str', 'Site provider', 'Publish adapter: ghost now; wordpress can be added as a code adapter later.', 'Site Poster'),
|
||||||
|
('site_poster_base_url', '""'::jsonb, 'str', 'Site API base URL', 'Public/admin base URL for the selected site provider.', 'Site Poster'),
|
||||||
|
('site_poster_secret_file', '""'::jsonb, 'str', 'Site secret file', 'Server path to the selected provider API secret. Keep empty in git.', 'Site Poster'),
|
||||||
|
('site_poster_fallback_title', '"Новость"'::jsonb, 'str', 'Fallback article title', 'Title used when post text has no first line.', 'Site Poster')
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
UPDATE app_settings
|
||||||
|
SET value_json='"ghost"'::jsonb,
|
||||||
|
updated_at=NOW()
|
||||||
|
WHERE key='site_poster_provider'
|
||||||
|
AND COALESCE(value_json #>> '{}', '') = ''
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM app_settings legacy
|
||||||
|
WHERE legacy.key='site_poster_ghost_url'
|
||||||
|
AND COALESCE(legacy.value_json #>> '{}', '') <> ''
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE app_settings target
|
||||||
|
SET value_json=to_jsonb(legacy.value_json #>> '{}'),
|
||||||
|
updated_at=NOW()
|
||||||
|
FROM app_settings legacy
|
||||||
|
WHERE target.key='site_poster_base_url'
|
||||||
|
AND legacy.key='site_poster_ghost_url'
|
||||||
|
AND COALESCE(target.value_json #>> '{}', '') = ''
|
||||||
|
AND COALESCE(legacy.value_json #>> '{}', '') <> '';
|
||||||
|
|
||||||
|
UPDATE app_settings target
|
||||||
|
SET value_json=to_jsonb(legacy.value_json #>> '{}'),
|
||||||
|
updated_at=NOW()
|
||||||
|
FROM app_settings legacy
|
||||||
|
WHERE target.key='site_poster_secret_file'
|
||||||
|
AND legacy.key='site_poster_key_file'
|
||||||
|
AND COALESCE(target.value_json #>> '{}', '') = ''
|
||||||
|
AND COALESCE(legacy.value_json #>> '{}', '') <> '';
|
||||||
@@ -42,8 +42,9 @@ def parse_args() -> argparse.Namespace:
|
|||||||
group.add_argument("--ids", help="Comma-separated raw_post ids")
|
group.add_argument("--ids", help="Comma-separated raw_post ids")
|
||||||
group.add_argument("--all-ready", action="store_true", help="Publish every photo-ready post not yet published to site")
|
group.add_argument("--all-ready", action="store_true", help="Publish every photo-ready post not yet published to site")
|
||||||
group.add_argument("--all-ready-videos", action="store_true", help="Publish video-only ready posts")
|
group.add_argument("--all-ready-videos", action="store_true", help="Publish video-only ready posts")
|
||||||
parser.add_argument("--ghost-url", default="https://f-n8.ru")
|
parser.add_argument("--ghost-url", required=True)
|
||||||
parser.add_argument("--key-file", default="/opt/vk-parser/.site-poster-key")
|
parser.add_argument("--key-file", default="")
|
||||||
|
parser.add_argument("--fallback-title", default="Новость")
|
||||||
parser.add_argument("--limit", type=int, default=0, help="Maximum posts to publish in this run; 0 means unlimited")
|
parser.add_argument("--limit", type=int, default=0, help="Maximum posts to publish in this run; 0 means unlimited")
|
||||||
parser.add_argument("--retry-failed", action="store_true", help="Include previous site publish_failed rows")
|
parser.add_argument("--retry-failed", action="store_true", help="Include previous site publish_failed rows")
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
@@ -63,11 +64,11 @@ def ghost_token(key: str) -> str:
|
|||||||
return f"{header}.{payload}.{signature}"
|
return f"{header}.{payload}.{signature}"
|
||||||
|
|
||||||
|
|
||||||
def split_post_text(value: str) -> tuple[str, str]:
|
def split_post_text(value: str, fallback_title: str) -> tuple[str, str]:
|
||||||
lines = [line.strip() for line in str(value or "").replace("\r", "").split("\n")]
|
lines = [line.strip() for line in str(value or "").replace("\r", "").split("\n")]
|
||||||
while lines and not lines[0]:
|
while lines and not lines[0]:
|
||||||
lines.pop(0)
|
lines.pop(0)
|
||||||
title = lines.pop(0) if lines else "Новость Forma N8"
|
title = lines.pop(0) if lines else fallback_title
|
||||||
while lines and (not lines[0] or SEPARATOR_RE.fullmatch(lines[0])):
|
while lines and (not lines[0] or SEPARATOR_RE.fullmatch(lines[0])):
|
||||||
lines.pop(0)
|
lines.pop(0)
|
||||||
while lines and (not lines[-1] or HASHTAG_LINE_RE.fullmatch(lines[-1])):
|
while lines and (not lines[-1] or HASHTAG_LINE_RE.fullmatch(lines[-1])):
|
||||||
@@ -296,6 +297,8 @@ async def main() -> None:
|
|||||||
args = parse_args()
|
args = parse_args()
|
||||||
key = os.environ.get("SITE_POSTER_GHOST_ADMIN_KEY", "").strip()
|
key = os.environ.get("SITE_POSTER_GHOST_ADMIN_KEY", "").strip()
|
||||||
if not key:
|
if not key:
|
||||||
|
if not args.key_file:
|
||||||
|
raise RuntimeError("SITE_POSTER_GHOST_ADMIN_KEY or --key-file is required")
|
||||||
key = Path(args.key_file).read_text(encoding="utf-8").strip()
|
key = Path(args.key_file).read_text(encoding="utf-8").strip()
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
if args.all_ready or args.all_ready_videos:
|
if args.all_ready or args.all_ready_videos:
|
||||||
@@ -452,7 +455,7 @@ async def main() -> None:
|
|||||||
feature = uploaded_photos[0] if uploaded_photos else thumbnail
|
feature = uploaded_photos[0] if uploaded_photos else thumbnail
|
||||||
if not feature:
|
if not feature:
|
||||||
raise RuntimeError("no downloadable site media")
|
raise RuntimeError("no downloadable site media")
|
||||||
title, body = split_post_text(post.get("final_text") or post.get("rewritten_text") or "")
|
title, body = split_post_text(post.get("final_text") or post.get("rewritten_text") or "", args.fallback_title)
|
||||||
producer = str(post.get("source_name") or "").strip()
|
producer = str(post.get("source_name") or "").strip()
|
||||||
content = gallery_html(uploaded_photos[1:], title) + videos_html(uploaded_videos, title, feature["url"]) + paragraphs(body)
|
content = gallery_html(uploaded_photos[1:], title) + videos_html(uploaded_videos, title, feature["url"]) + paragraphs(body)
|
||||||
attachments = uploaded_photos + uploaded_videos + ([thumbnail] if thumbnail else [])
|
attachments = uploaded_photos + uploaded_videos + ([thumbnail] if thumbnail else [])
|
||||||
@@ -513,7 +516,7 @@ def run_locked() -> None:
|
|||||||
if fcntl is None:
|
if fcntl is None:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
return
|
return
|
||||||
with Path("/tmp/forma-n8-site-poster.lock").open("w") as lock_file:
|
with Path("/tmp/site-poster.lock").open("w") as lock_file:
|
||||||
try:
|
try:
|
||||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
except BlockingIOError:
|
except BlockingIOError:
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import {createHmac} from "node:crypto";
|
import {createHmac} from "node:crypto";
|
||||||
import {readFileSync} from "node:fs";
|
import {readFileSync} from "node:fs";
|
||||||
|
|
||||||
const baseUrl = "https://f-n8.ru";
|
const baseUrl = (process.env.GHOST_URL || "").replace(/\/$/, "");
|
||||||
const [keyId, secret] = readFileSync("/root/ghost-admin-api-key", "utf8").trim().split(":");
|
const keyFile = process.env.GHOST_ADMIN_KEY_FILE || "";
|
||||||
|
if (!baseUrl || !keyFile) {
|
||||||
|
throw new Error("GHOST_URL and GHOST_ADMIN_KEY_FILE are required");
|
||||||
|
}
|
||||||
|
const [keyId, secret] = readFileSync(keyFile, "utf8").trim().split(":");
|
||||||
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const unsigned = `${encode({alg: "HS256", typ: "JWT", kid: keyId})}.${encode({iat: now, exp: now + 300, aud: "/admin/"})}`;
|
const unsigned = `${encode({alg: "HS256", typ: "JWT", kid: keyId})}.${encode({iat: now, exp: now + 300, aud: "/admin/"})}`;
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ PROVIDER_OPTIONS = [
|
|||||||
{"value": "openai_compatible", "label": "OpenAI-compatible API"},
|
{"value": "openai_compatible", "label": "OpenAI-compatible API"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SITE_POSTER_PROVIDER_OPTIONS = [
|
||||||
|
{"value": "", "label": "Не выбран"},
|
||||||
|
{"value": "ghost", "label": "Ghost"},
|
||||||
|
]
|
||||||
|
|
||||||
PROMPT_HINTS = {
|
PROMPT_HINTS = {
|
||||||
"ai_qualifier_prompt": {
|
"ai_qualifier_prompt": {
|
||||||
"title": "Свободная инструкция квалификатора",
|
"title": "Свободная инструкция квалификатора",
|
||||||
@@ -284,6 +289,10 @@ SETTING_ORDER = {
|
|||||||
"max_poster_dry_run",
|
"max_poster_dry_run",
|
||||||
],
|
],
|
||||||
"Site Poster": [
|
"Site Poster": [
|
||||||
|
"site_poster_provider",
|
||||||
|
"site_poster_base_url",
|
||||||
|
"site_poster_secret_file",
|
||||||
|
"site_poster_fallback_title",
|
||||||
"site_poster_interval_sec",
|
"site_poster_interval_sec",
|
||||||
"site_poster_photo_batch_size",
|
"site_poster_photo_batch_size",
|
||||||
"site_poster_video_batch_size",
|
"site_poster_video_batch_size",
|
||||||
@@ -450,6 +459,7 @@ def base_context(request: Request, user: dict | None, **extra: Any) -> dict[str,
|
|||||||
"app_env": settings.app_env,
|
"app_env": settings.app_env,
|
||||||
"app_title": settings.display_admin_title,
|
"app_title": settings.display_admin_title,
|
||||||
"site_title": settings.display_site_title,
|
"site_title": settings.display_site_title,
|
||||||
|
"site_poster_provider_options": SITE_POSTER_PROVIDER_OPTIONS,
|
||||||
}
|
}
|
||||||
ctx.update(extra)
|
ctx.update(extra)
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -325,6 +325,13 @@
|
|||||||
<option value="{{ p.value }}" {% if s.value_json == p.value %}selected{% endif %}>{{ p.label }}</option>
|
<option value="{{ p.value }}" {% if s.value_json == p.value %}selected{% endif %}>{{ p.label }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{% elif s.key == "site_poster_provider" %}
|
||||||
|
<select name="value" class="select max-w-sm w-full">
|
||||||
|
{% for p in site_poster_provider_options %}
|
||||||
|
<option value="{{ p.value }}" {% if s.value_json == p.value %}selected{% endif %}>{{ p.label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
{% elif s.key == "ai_qualifier_model" %}
|
{% elif s.key == "ai_qualifier_model" %}
|
||||||
<select name="value" class="select max-w-sm w-full">
|
<select name="value" class="select max-w-sm w-full">
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ class DailyReportWorker:
|
|||||||
report_date = data["date"].strftime("%d.%m.%Y")
|
report_date = data["date"].strftime("%d.%m.%Y")
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
[
|
[
|
||||||
f"Ежедневный отчет N8 Parser за {report_date}",
|
f"Ежедневный отчет {settings.display_site_title} за {report_date}",
|
||||||
"",
|
"",
|
||||||
"Сбор:",
|
"Сбор:",
|
||||||
f"raw собрано: {fmt_int(data['raw_collected'])}",
|
f"raw собрано: {fmt_int(data['raw_collected'])}",
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ class SitePoster:
|
|||||||
self.interval_sec = 300
|
self.interval_sec = 300
|
||||||
self.photo_batch_size = 10
|
self.photo_batch_size = 10
|
||||||
self.video_batch_size = 1
|
self.video_batch_size = 1
|
||||||
self.ghost_url = "https://f-n8.ru"
|
self.provider = ""
|
||||||
self.key_file = "/opt/vk-parser/.site-poster-key"
|
self.base_url = ""
|
||||||
|
self.secret_file = ""
|
||||||
|
self.fallback_title = "Новость"
|
||||||
|
|
||||||
async def init(self) -> None:
|
async def init(self) -> None:
|
||||||
self.pool = await get_pool()
|
self.pool = await get_pool()
|
||||||
@@ -32,25 +34,48 @@ class SitePoster:
|
|||||||
self.interval_sec = max(30, await fetch_int_setting("site_poster_interval_sec", 300))
|
self.interval_sec = max(30, await fetch_int_setting("site_poster_interval_sec", 300))
|
||||||
self.photo_batch_size = max(0, await fetch_int_setting("site_poster_photo_batch_size", 10))
|
self.photo_batch_size = max(0, await fetch_int_setting("site_poster_photo_batch_size", 10))
|
||||||
self.video_batch_size = max(0, await fetch_int_setting("site_poster_video_batch_size", 1))
|
self.video_batch_size = max(0, await fetch_int_setting("site_poster_video_batch_size", 1))
|
||||||
self.ghost_url = str(await fetch_setting("site_poster_ghost_url", "https://f-n8.ru") or "https://f-n8.ru").rstrip("/")
|
legacy_ghost_url = str(await fetch_setting("site_poster_ghost_url", "") or "").strip()
|
||||||
self.key_file = str(await fetch_setting("site_poster_key_file", "/opt/vk-parser/.site-poster-key") or "").strip()
|
legacy_key_file = str(await fetch_setting("site_poster_key_file", "") or "").strip()
|
||||||
if not self.key_file:
|
self.provider = str(await fetch_setting("site_poster_provider", "") or "").strip().lower()
|
||||||
raise RuntimeError("site_poster_key_file is empty")
|
if not self.provider and legacy_ghost_url:
|
||||||
|
self.provider = "ghost"
|
||||||
|
self.base_url = str(
|
||||||
|
await fetch_setting("site_poster_base_url", "")
|
||||||
|
or legacy_ghost_url
|
||||||
|
or ""
|
||||||
|
).rstrip("/")
|
||||||
|
self.secret_file = str(
|
||||||
|
await fetch_setting("site_poster_secret_file", "")
|
||||||
|
or legacy_key_file
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
self.fallback_title = str(await fetch_setting("site_poster_fallback_title", "Новость") or "Новость").strip()
|
||||||
|
|
||||||
async def publish_mode(self, flag: str, limit: int) -> tuple[int, int]:
|
def build_publish_command(self, flag: str, limit: int) -> list[str]:
|
||||||
if limit < 1:
|
if self.provider != "ghost":
|
||||||
return 0, 0
|
raise RuntimeError(f"unsupported site_poster_provider: {self.provider or '<empty>'}")
|
||||||
command = [
|
if not self.base_url:
|
||||||
|
raise RuntimeError("site_poster_base_url is empty")
|
||||||
|
if not self.secret_file and not os.environ.get("SITE_POSTER_GHOST_ADMIN_KEY", "").strip():
|
||||||
|
raise RuntimeError("site_poster_secret_file and SITE_POSTER_GHOST_ADMIN_KEY are empty")
|
||||||
|
return [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
str(self.project_root / "scripts" / "publish_site_batch.py"),
|
str(self.project_root / "scripts" / "publish_site_batch.py"),
|
||||||
flag,
|
flag,
|
||||||
"--limit",
|
"--limit",
|
||||||
str(limit),
|
str(limit),
|
||||||
"--ghost-url",
|
"--ghost-url",
|
||||||
self.ghost_url,
|
self.base_url,
|
||||||
"--key-file",
|
"--key-file",
|
||||||
self.key_file,
|
self.secret_file,
|
||||||
|
"--fallback-title",
|
||||||
|
self.fallback_title or "Новость",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def publish_mode(self, flag: str, limit: int) -> tuple[int, int]:
|
||||||
|
if limit < 1:
|
||||||
|
return 0, 0
|
||||||
|
command = self.build_publish_command(flag, limit)
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PYTHONPATH"] = str(self.project_root / "src")
|
env["PYTHONPATH"] = str(self.project_root / "src")
|
||||||
env["PYTHONUNBUFFERED"] = "1"
|
env["PYTHONUNBUFFERED"] = "1"
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class TelegramPoster:
|
|||||||
await self.bot.session.close()
|
await self.bot.session.close()
|
||||||
self.bot = Bot(token=token)
|
self.bot = Bot(token=token)
|
||||||
self.bot_token = token
|
self.bot_token = token
|
||||||
self.chat_id, self.thread_id = parse_topic(str(await fetch_setting("tg_poster_chat_id", "-1003712724327") or ""))
|
self.chat_id, self.thread_id = parse_topic(str(await fetch_setting("tg_poster_chat_id", "") or ""))
|
||||||
self.schedule = self.normalize_schedule(await fetch_setting("tg_poster_schedule_json", []))
|
self.schedule = self.normalize_schedule(await fetch_setting("tg_poster_schedule_json", []))
|
||||||
self.media_group_max_items = max(1, min(MAX_MEDIA_GROUP, await fetch_int_setting("tg_poster_media_group_max_items", 10)))
|
self.media_group_max_items = max(1, min(MAX_MEDIA_GROUP, await fetch_int_setting("tg_poster_media_group_max_items", 10)))
|
||||||
self.caption_limit = max(128, min(1024, await fetch_int_setting("tg_poster_caption_limit", 1024)))
|
self.caption_limit = max(128, min(1024, await fetch_int_setting("tg_poster_caption_limit", 1024)))
|
||||||
|
|||||||
Reference in New Issue
Block a user