Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0c5cf29f83
|
|||
|
43c38767a1
|
|||
| a68c01a68f | |||
| bdcdb1cb3d | |||
| fe2b80b51f | |||
| b8d5bc747d | |||
|
6f77b7d845
|
|||
| ea286e117d | |||
| 6a050669e8 |
+1
-1
@@ -21,7 +21,7 @@ checkmk/checkmk/*
|
|||||||
downtify/Downtify_downloads
|
downtify/Downtify_downloads
|
||||||
headscale/config/*
|
headscale/config/*
|
||||||
headscale/data/*
|
headscale/data/*
|
||||||
searxng/core-config/settings.yml
|
searxng/core-config/*
|
||||||
|
|
||||||
# Steaming services files
|
# Steaming services files
|
||||||
streaming/jellyfin/*
|
streaming/jellyfin/*
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ services:
|
|||||||
# Prod Router
|
# Prod Router
|
||||||
- "traefik.http.routers.dockmon.rule=Host(`dockmon.forust.xyz`)"
|
- "traefik.http.routers.dockmon.rule=Host(`dockmon.forust.xyz`)"
|
||||||
- "traefik.http.routers.dockmon.entrypoints=websecure"
|
- "traefik.http.routers.dockmon.entrypoints=websecure"
|
||||||
- "traefik.http.routers.dockmon.middlewares=security-chain@file"
|
- "traefik.http.routers.dockmon.middlewares=security-headers@file"
|
||||||
- "traefik.http.routers.dockmon.tls=true"
|
- "traefik.http.routers.dockmon.tls=true"
|
||||||
# Local Router
|
# Local Router
|
||||||
- "traefik.http.routers.dockmon-local.rule=Host(`dockmon.workstation.internal`)"
|
- "traefik.http.routers.dockmon-local.rule=Host(`dockmon.workstation.internal`)"
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
image: gcr.forust.xyz/forust/dtek-notif:latest
|
||||||
|
pull_policy: build
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Kyiv
|
- TZ=Europe/Kyiv
|
||||||
|
|
||||||
dns:
|
dns:
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ services:
|
|||||||
|
|
||||||
session-keeper:
|
session-keeper:
|
||||||
build: ./phpsessid-bot
|
build: ./phpsessid-bot
|
||||||
|
image: gcr.forust.xyz/forust/session-keeper:latest
|
||||||
|
pull_policy: build
|
||||||
env_file: .env
|
env_file: .env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -31,6 +33,8 @@ services:
|
|||||||
|
|
||||||
webinar-checker:
|
webinar-checker:
|
||||||
build: ./webinar-checker
|
build: ./webinar-checker
|
||||||
|
image: gcr.forust.xyz/forust/webinar-checker:latest
|
||||||
|
pull_policy: build
|
||||||
env_file: .env
|
env_file: .env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import logging
|
import logging
|
||||||
import redis
|
import redis
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ChatMember
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ChatMember
|
||||||
from telegram.constants import ChatType
|
from telegram.constants import ChatType
|
||||||
@@ -30,6 +33,7 @@ def _env(key, default=None):
|
|||||||
EDU_BASE = _env('EDU_URL_BASE', 'https://edu.edu.vn.ua')
|
EDU_BASE = _env('EDU_URL_BASE', 'https://edu.edu.vn.ua')
|
||||||
EDU_WEBINAR_PATH = _env('EDU_URL_WEBINAR', '/webinar/useractive')
|
EDU_WEBINAR_PATH = _env('EDU_URL_WEBINAR', '/webinar/useractive')
|
||||||
WEBINAR_URL = f"{EDU_BASE.rstrip('/')}/{EDU_WEBINAR_PATH.lstrip('/')}"
|
WEBINAR_URL = f"{EDU_BASE.rstrip('/')}/{EDU_WEBINAR_PATH.lstrip('/')}"
|
||||||
|
DIARY_URL = f"{EDU_BASE.rstrip('/')}/user/diary"
|
||||||
|
|
||||||
WEBINAR_CHECK_INTERVAL = int(_env('WEBINAR_CHECK_INTERVAL', 60))
|
WEBINAR_CHECK_INTERVAL = int(_env('WEBINAR_CHECK_INTERVAL', 60))
|
||||||
REDIS_HOST = _env('REDIS_HOST', 'redis')
|
REDIS_HOST = _env('REDIS_HOST', 'redis')
|
||||||
@@ -252,6 +256,205 @@ def get_admin_keyboard(user_id: int):
|
|||||||
]
|
]
|
||||||
return InlineKeyboardMarkup(keyboard)
|
return InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
|
# --- Diary Functions ---
|
||||||
|
|
||||||
|
DIARY_MONTH_NAMES = ['', 'Січня', 'Лютого', 'Березня', 'Квітня', 'Травня', 'Червня',
|
||||||
|
'Липня', 'Серпня', 'Вересня', 'Жовтня', 'Листопада', 'Грудня']
|
||||||
|
|
||||||
|
DIARY_WEEKDAYS_SHORT = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Нд']
|
||||||
|
|
||||||
|
def get_diary_keyboard():
|
||||||
|
today = datetime.now()
|
||||||
|
keyboard = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(f"📌 Сьогодні ({today.day}.{today.month:02d})", callback_data="diary_today"),
|
||||||
|
InlineKeyboardButton("📌 Завтра", callback_data="diary_tomorrow"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("📅 Цей тиждень", callback_data="diary_week"),
|
||||||
|
InlineKeyboardButton("📅 Весь місяць", callback_data="diary_month"),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
return InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
|
def _parse_calendar_html(table_html: str) -> tuple:
|
||||||
|
"""Parse calendar HTML table into (month_text, {day_num: {weekday, events}})."""
|
||||||
|
days = {}
|
||||||
|
weekdays = []
|
||||||
|
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', table_html, re.DOTALL)
|
||||||
|
|
||||||
|
month_text = ''
|
||||||
|
for r_idx, row in enumerate(rows):
|
||||||
|
cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', row, re.DOTALL)
|
||||||
|
|
||||||
|
if r_idx == 0:
|
||||||
|
# Month navigation row: extract "Травень 2026" from nav text
|
||||||
|
raw = re.sub(r'<[^>]+>', ' ', row).strip()
|
||||||
|
raw = re.sub(r'\s+', ' ', raw)
|
||||||
|
m = re.search(r'([А-Яа-яіїєґ\']+\s*:?\s*\d{4})', raw)
|
||||||
|
if m:
|
||||||
|
month_text = m.group(1).replace(' : ', ' ').strip()
|
||||||
|
else:
|
||||||
|
month_text = raw
|
||||||
|
elif r_idx == 1:
|
||||||
|
# Day names row
|
||||||
|
for cell in cells:
|
||||||
|
name = re.sub(r'<[^>]+>', '', cell).strip()
|
||||||
|
if name:
|
||||||
|
weekdays.append(name)
|
||||||
|
else:
|
||||||
|
# Data rows: each cell = a day
|
||||||
|
for col_idx, cell in enumerate(cells):
|
||||||
|
# Extract day number — first number in the cell text
|
||||||
|
text = re.sub(r'<[^>]+>', ' ', cell).strip()
|
||||||
|
text = re.sub(r'\s+', ' ', text)
|
||||||
|
dm = re.match(r'(\d+)', text)
|
||||||
|
if not dm:
|
||||||
|
continue
|
||||||
|
day_num = dm.group(1)
|
||||||
|
|
||||||
|
# Extract events: title attribute (full name) of ALL <a> tags inside the cell
|
||||||
|
events = []
|
||||||
|
for a_match in re.finditer(r'<a[^>]*>(.*?)</a>', cell, re.DOTALL):
|
||||||
|
a_tag = a_match.group(0)
|
||||||
|
# Prefer the title attribute (contains full name, not truncated)
|
||||||
|
title_m = re.search(r'title\s*=\s*"([^"]*)"', a_tag)
|
||||||
|
if title_m:
|
||||||
|
et = title_m.group(1).strip()
|
||||||
|
else:
|
||||||
|
et = re.sub(r'<[^>]+>', '', a_match.group(1)).strip()
|
||||||
|
if et:
|
||||||
|
events.append(et)
|
||||||
|
|
||||||
|
weekday = weekdays[col_idx] if col_idx < len(weekdays) else ''
|
||||||
|
days[day_num] = {'weekday': weekday, 'events': events}
|
||||||
|
|
||||||
|
return month_text, days
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_diary_data(phpsessid: str) -> dict | None:
|
||||||
|
logger.info("Fetching diary data via Playwright...")
|
||||||
|
try:
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.connect(PLAYWRIGHT_WS)
|
||||||
|
try:
|
||||||
|
context_browser = await browser.new_context(user_agent=USER_AGENT)
|
||||||
|
await context_browser.add_cookies([{
|
||||||
|
'name': 'PHPSESSID',
|
||||||
|
'value': phpsessid,
|
||||||
|
'domain': 'edu.edu.vn.ua',
|
||||||
|
'path': '/'
|
||||||
|
}])
|
||||||
|
page = await context_browser.new_page()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await page.goto(DIARY_URL, wait_until='domcontentloaded')
|
||||||
|
await page.wait_for_selector('table.calendar', timeout=10000)
|
||||||
|
await page.wait_for_timeout(1500)
|
||||||
|
|
||||||
|
table_html = await page.evaluate("""
|
||||||
|
() => {
|
||||||
|
const t = document.querySelector('table.calendar');
|
||||||
|
return t ? t.outerHTML : null;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
if not table_html:
|
||||||
|
logger.error("table.calendar not found in DOM")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Debug: save HTML for troubleshooting
|
||||||
|
try:
|
||||||
|
with open('/tmp/diary_debug.html', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(table_html)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
month_text, days = _parse_calendar_html(table_html)
|
||||||
|
logger.info(f"Diary parsed: month={month_text!r}, days_with_events={sum(1 for d in days.values() if d['events'])}/{len(days)}")
|
||||||
|
|
||||||
|
return {'monthFullText': month_text, 'days': days}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing diary: {e}")
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
await page.close()
|
||||||
|
await context_browser.close()
|
||||||
|
finally:
|
||||||
|
await browser.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Playwright error in diary fetch: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_diary_month(text: str) -> str:
|
||||||
|
match = re.search(r'([А-Яа-яіїєґ\']+\s*:\s*\d{4})', text)
|
||||||
|
if match:
|
||||||
|
return match.group(1).replace(' : ', ' ').strip()
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
def format_diary_day(data: dict, day_num: int) -> str:
|
||||||
|
days = data.get('days', {})
|
||||||
|
month_str = _parse_diary_month(data.get('monthFullText', ''))
|
||||||
|
day_data = days.get(str(day_num))
|
||||||
|
lines = [f"📅 <b>{day_num} {month_str}</b>", "─" * 18]
|
||||||
|
if not day_data or not day_data.get('events'):
|
||||||
|
lines.append("Немає подій")
|
||||||
|
else:
|
||||||
|
for e in day_data['events']:
|
||||||
|
lines.append(f"📌 {e}")
|
||||||
|
lines.append(f"\n🔗 {DIARY_URL}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def format_diary_week(data: dict, today: datetime) -> str:
|
||||||
|
days = data.get('days', {})
|
||||||
|
month_str = _parse_diary_month(data.get('monthFullText', ''))
|
||||||
|
monday = today - timedelta(days=today.weekday())
|
||||||
|
sunday = monday + timedelta(days=6)
|
||||||
|
lines = [f"📅 <b>Тиждень {monday.day}.{monday.month} – {sunday.day}.{sunday.month}</b>\n"]
|
||||||
|
for i in range(7):
|
||||||
|
d = monday + timedelta(days=i)
|
||||||
|
day_data = days.get(str(d.day))
|
||||||
|
lines.append(f"─ <b>{DIARY_WEEKDAYS_SHORT[i]} {d.day}.{d.month}</b> ─")
|
||||||
|
if not day_data or not day_data.get('events'):
|
||||||
|
lines.append("Немає подій\n")
|
||||||
|
else:
|
||||||
|
for e in day_data['events']:
|
||||||
|
lines.append(f"📌 {e}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"🔗 {DIARY_URL}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def format_diary_month(data: dict) -> str:
|
||||||
|
days = data.get('days', {})
|
||||||
|
month_str = _parse_diary_month(data.get('monthFullText', ''))
|
||||||
|
lines = [f"📅 <b>{month_str}</b>\n"]
|
||||||
|
for day_num in sorted(days.keys(), key=int):
|
||||||
|
day_data = days[day_num]
|
||||||
|
events = day_data.get('events', [])
|
||||||
|
weekday = day_data.get('weekday', '')
|
||||||
|
lines.append(f"─ <b>{weekday} {day_num}</b> ─")
|
||||||
|
if not events:
|
||||||
|
lines.append("Немає подій\n")
|
||||||
|
else:
|
||||||
|
for e in events:
|
||||||
|
lines.append(f"📌 {e}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"🔗 {DIARY_URL}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
async def _get_diary_data(context: ContextTypes.DEFAULT_TYPE) -> dict | None:
|
||||||
|
cached = context.user_data.get('diary_cache')
|
||||||
|
now_ts = time.time()
|
||||||
|
if cached and (now_ts - cached.get('timestamp', 0)) < 300:
|
||||||
|
return cached['data']
|
||||||
|
phpsessid = redis_client.get(KEY_PHPSESSID)
|
||||||
|
if not phpsessid:
|
||||||
|
return None
|
||||||
|
data = await fetch_diary_data(phpsessid)
|
||||||
|
if data:
|
||||||
|
context.user_data['diary_cache'] = {'data': data, 'timestamp': now_ts}
|
||||||
|
return data
|
||||||
|
|
||||||
# --- Command Handlers ---
|
# --- Command Handlers ---
|
||||||
|
|
||||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
@@ -376,6 +579,74 @@ async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
logger.error(f"Failed to clear history: {e}")
|
logger.error(f"Failed to clear history: {e}")
|
||||||
await update.message.reply_text(t(admin_id, 'history_clear_failed'))
|
await update.message.reply_text(t(admin_id, 'history_clear_failed'))
|
||||||
|
|
||||||
|
async def diary_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
user = update.effective_user
|
||||||
|
chat = update.effective_chat
|
||||||
|
if not is_whitelisted(user.id):
|
||||||
|
await update.message.reply_text(t(user.id, 'access_denied'))
|
||||||
|
return
|
||||||
|
await update.message.reply_text(
|
||||||
|
"📅 <b>Щоденник</b> — виберіть період:",
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=get_diary_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def diary_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
query = update.callback_query
|
||||||
|
user_id = query.from_user.id
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
if user_id != ADMIN_ID:
|
||||||
|
if not is_whitelisted(user_id):
|
||||||
|
await query.edit_message_text("⛔ Доступ заборонено.")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = query.data
|
||||||
|
if data == "diary_refresh":
|
||||||
|
context.user_data.pop('diary_cache', None)
|
||||||
|
await query.edit_message_text("🔄 Завантажую щоденник...")
|
||||||
|
diary_data = await _get_diary_data(context)
|
||||||
|
if not diary_data:
|
||||||
|
await query.edit_message_text("❌ Не вдалося завантажити щоденник. Немає сесії або помилка.")
|
||||||
|
return
|
||||||
|
await query.edit_message_text(
|
||||||
|
"📅 <b>Щоденник</b> — виберіть період:",
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=get_diary_keyboard()
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await query.edit_message_text("🔄 Завантажую щоденник...")
|
||||||
|
diary_data = await _get_diary_data(context)
|
||||||
|
if not diary_data:
|
||||||
|
await query.edit_message_text("❌ Не вдалося завантажити щоденник.")
|
||||||
|
return
|
||||||
|
|
||||||
|
today = datetime.now()
|
||||||
|
if data == "diary_today":
|
||||||
|
text = format_diary_day(diary_data, today.day)
|
||||||
|
elif data == "diary_tomorrow":
|
||||||
|
tomorrow = today + timedelta(days=1)
|
||||||
|
if tomorrow.day < today.day:
|
||||||
|
text = "❌ Дані за наступний місяць недоступні. Перейдіть на сайт."
|
||||||
|
else:
|
||||||
|
text = format_diary_day(diary_data, tomorrow.day)
|
||||||
|
elif data == "diary_week":
|
||||||
|
text = format_diary_week(diary_data, today)
|
||||||
|
elif data == "diary_month":
|
||||||
|
text = format_diary_month(diary_data)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(text) > 4096:
|
||||||
|
text = text[:4090] + "\n\n✂️ ...(обрізано)"
|
||||||
|
|
||||||
|
await query.edit_message_text(
|
||||||
|
text,
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=get_diary_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
# --- Admin Callbacks ---
|
# --- Admin Callbacks ---
|
||||||
|
|
||||||
async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
@@ -649,8 +920,10 @@ def main():
|
|||||||
app.add_handler(CommandHandler("adduser", add_user))
|
app.add_handler(CommandHandler("adduser", add_user))
|
||||||
app.add_handler(CommandHandler("removeuser", remove_user))
|
app.add_handler(CommandHandler("removeuser", remove_user))
|
||||||
app.add_handler(CommandHandler("clearhistory", clear_history))
|
app.add_handler(CommandHandler("clearhistory", clear_history))
|
||||||
|
app.add_handler(CommandHandler("diary", diary_command))
|
||||||
|
|
||||||
# Callback handlers - language selection first, then admin panel
|
# Callback handlers - diary first, then language selection, then admin panel
|
||||||
|
app.add_handler(CallbackQueryHandler(diary_callback, pattern="^diary_"))
|
||||||
app.add_handler(CallbackQueryHandler(language_callback, pattern="^lang_"))
|
app.add_handler(CallbackQueryHandler(language_callback, pattern="^lang_"))
|
||||||
app.add_handler(CallbackQueryHandler(admin_callback))
|
app.add_handler(CallbackQueryHandler(admin_callback))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
errorpage:
|
errorpage:
|
||||||
build: .
|
build: .
|
||||||
|
image: gcr.forust.xyz/forust/error-pages:latest
|
||||||
|
pull_policy: build
|
||||||
container_name: error-pages
|
container_name: error-pages
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# ports:
|
# ports:
|
||||||
@@ -11,7 +13,7 @@ services:
|
|||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.services.error-pages.loadbalancer.server.port=80"
|
- "traefik.http.services.error-pages.loadbalancer.server.port=80"
|
||||||
# Error handler middleware
|
# Error handler middleware
|
||||||
- "traefik.http.middlewares.error-pages.errors.status=400-599"
|
- "traefik.http.middlewares.error-pages.errors.status=400,402-599"
|
||||||
- "traefik.http.middlewares.error-pages.errors.service=error-pages"
|
- "traefik.http.middlewares.error-pages.errors.service=error-pages"
|
||||||
- "traefik.http.middlewares.error-pages.errors.query=/{status}.html"
|
- "traefik.http.middlewares.error-pages.errors.query=/{status}.html"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ services:
|
|||||||
- "traefik.tcp.services.gitea.loadbalancer.server.port=22"
|
- "traefik.tcp.services.gitea.loadbalancer.server.port=22"
|
||||||
- "traefik.tcp.routers.gitea.entrypoints=ssh"
|
- "traefik.tcp.routers.gitea.entrypoints=ssh"
|
||||||
- "traefik.tcp.routers.gitea.rule=HostSNI(`*`)"
|
- "traefik.tcp.routers.gitea.rule=HostSNI(`*`)"
|
||||||
|
# Gitea container registry Router
|
||||||
|
- "traefik.http.routers.gitea-registry.rule=Host(`gcr.forust.xyz`) && PathPrefix(`/v2`)"
|
||||||
|
- "traefik.http.routers.gitea-registry.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gitea-registry.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.gitea-registry.tls=true"
|
||||||
ports:
|
ports:
|
||||||
- "2221:22"
|
- "2221:22"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.forust
|
dockerfile: Dockerfile.forust
|
||||||
|
image: gcr.forust.xyz/forust/forust-homepage:latest
|
||||||
|
pull_policy: build
|
||||||
# ports:
|
# ports:
|
||||||
# - "8085:80"
|
# - "8085:80"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -30,6 +32,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.xdfnx
|
dockerfile: Dockerfile.xdfnx
|
||||||
|
image: gcr.forust.xyz/forust/xdfnx-homepage:latest
|
||||||
|
pull_policy: build
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# ports:
|
# ports:
|
||||||
# - "8086:80"
|
# - "8086:80"
|
||||||
@@ -42,7 +46,7 @@ services:
|
|||||||
# Prod Router
|
# Prod Router
|
||||||
- "traefik.http.routers.xdfnx.rule=Host(`xdfnx.cfd`)"
|
- "traefik.http.routers.xdfnx.rule=Host(`xdfnx.cfd`)"
|
||||||
- "traefik.http.routers.xdfnx.entrypoints=websecure"
|
- "traefik.http.routers.xdfnx.entrypoints=websecure"
|
||||||
- "traefik.http.routers.xdfnx.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.xdfnx.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.routers.xdfnx.tls=true"
|
- "traefik.http.routers.xdfnx.tls=true"
|
||||||
# Local Router
|
# Local Router
|
||||||
- "traefik.http.routers.xdfnx-local.rule=Host(`xdfnx.workstation.internal`)"
|
- "traefik.http.routers.xdfnx-local.rule=Host(`xdfnx.workstation.internal`)"
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ tls:
|
|||||||
# Cloudflare Origin CA *.forust.xyz
|
# Cloudflare Origin CA *.forust.xyz
|
||||||
# - certFile: /certs/cloudflare.pem
|
# - certFile: /certs/cloudflare.pem
|
||||||
# keyFile: /certs/cloudflare.key
|
# keyFile: /certs/cloudflare.key
|
||||||
stores:
|
# stores:
|
||||||
default:
|
# default:
|
||||||
defaultCertificate:
|
# defaultCertificate:
|
||||||
# Fallback local certificate
|
# # Fallback local certificate
|
||||||
certFile: /certs/local.pem
|
# certFile: /certs/local.pem
|
||||||
keyFile: /certs/local-key.pem
|
# keyFile: /certs/local-key.pem
|
||||||
|
|
||||||
options:
|
options:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
API_ID=""
|
||||||
|
API_HASH=""
|
||||||
|
STRINGSESSION=""
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# apiflash api key only for webshot plugin
|
||||||
|
APIFLASH_KEY=""
|
||||||
|
# gemini api key only for gemini plugin
|
||||||
|
GEMINI_KEY=""
|
||||||
|
# VT api key only for VirusTotal plugin
|
||||||
|
VT_KEY=""
|
||||||
|
# rmbg api key only for removebg plugin
|
||||||
|
RMBG_KEY=""
|
||||||
|
# cohere api key only for cohere plugin
|
||||||
|
COHERE_KEY=""
|
||||||
|
# sqlite/sqlite3 or mongo/mongodb
|
||||||
|
DATABASE_TYPE=""
|
||||||
|
# file name for sqlite3, database name for mongodb
|
||||||
|
DATABASE_NAME=""
|
||||||
@@ -14,3 +14,6 @@ __pycache__/
|
|||||||
/downloads/
|
/downloads/
|
||||||
/Downloads/
|
/Downloads/
|
||||||
config.ini
|
config.ini
|
||||||
|
|
||||||
|
k8s/*/*secret*.yaml
|
||||||
|
!k8s/account-secrets.yaml.example
|
||||||
@@ -3,8 +3,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: userbot:latest
|
image: gcr.forust.xyz/forust/userbot:latest
|
||||||
pull_policy: never
|
pull_policy: build
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -12,14 +12,14 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./volumes/data_forust:/app/data
|
- ./volumes/data_forust:/app/data
|
||||||
- ./Downloads:/app/downloads
|
- ./Downloads:/app/downloads
|
||||||
- ./volumes/logs_forust-:/app/logs
|
- ./volumes/logs_forust:/app/logs
|
||||||
dns:
|
dns:
|
||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
|
|
||||||
anna:
|
anna:
|
||||||
image: userbot:latest
|
image: gcr.forust.xyz/forust/userbot:latest
|
||||||
pull_policy: never
|
pull_policy: build
|
||||||
depends_on:
|
depends_on:
|
||||||
- forust
|
- forust
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -32,4 +32,4 @@ services:
|
|||||||
- ./volumes/logs_anna:/app/logs
|
- ./volumes/logs_anna:/app/logs
|
||||||
dns:
|
dns:
|
||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: userbot-common-config
|
||||||
|
data:
|
||||||
|
DATABASE_TYPE: ""
|
||||||
|
DATABASE_NAME: ""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- userbots.yaml
|
||||||
|
- common-secret.yaml
|
||||||
|
- common-config.yaml
|
||||||
|
- forust-secrets.yaml
|
||||||
|
- anna-secrets.yaml
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: forust-userbot-deployment
|
||||||
|
labels:
|
||||||
|
app: forust-userbot
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: forust-userbot
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: forust-userbot
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: forust-userbot
|
||||||
|
image: gcr.forust.xyz/forust/userbot:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: userbot-common-secrets
|
||||||
|
- configMapRef:
|
||||||
|
name: userbot-common-config
|
||||||
|
- secretRef:
|
||||||
|
name: userbot-forust-secrets
|
||||||
|
volumeMounts:
|
||||||
|
- name: forust-storage
|
||||||
|
mountPath: /app/data
|
||||||
|
subPath: data
|
||||||
|
- name: forust-storage
|
||||||
|
mountPath: /app/logs
|
||||||
|
subPath: logs
|
||||||
|
volumes:
|
||||||
|
- name: forust-storage
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: forust-pvc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: forust-pvc
|
||||||
|
spec:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: anna-userbot-deployment
|
||||||
|
labels:
|
||||||
|
app: anna-userbot
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: anna-userbot
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: anna-userbot
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: anna-userbot
|
||||||
|
image: gcr.forust.xyz/forust/userbot:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: userbot-common-secrets
|
||||||
|
- configMapRef:
|
||||||
|
name: userbot-common-config
|
||||||
|
- secretRef:
|
||||||
|
name: userbot-anna-secrets
|
||||||
|
volumeMounts:
|
||||||
|
- name: anna-storage
|
||||||
|
mountPath: /app/data
|
||||||
|
subPath: data
|
||||||
|
- name: anna-storage
|
||||||
|
mountPath: /app/logs
|
||||||
|
subPath: logs
|
||||||
|
volumes:
|
||||||
|
- name: anna-storage
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: anna-pvc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: anna-pvc
|
||||||
|
spec:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ../../base
|
||||||
|
|
||||||
|
patches:
|
||||||
|
- path: patch-downloads.yaml
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: forust-userbot-deployment
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: forust-userbot
|
||||||
|
volumeMounts:
|
||||||
|
- name: downloads
|
||||||
|
mountPath: /app/downloads
|
||||||
|
volumes:
|
||||||
|
- name: downloads
|
||||||
|
hostPath:
|
||||||
|
path: /home/user/projects/homelab/userbot/Downloads
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: anna-userbot-deployment
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: anna-userbot
|
||||||
|
volumeMounts:
|
||||||
|
- name: downloads
|
||||||
|
mountPath: /app/downloads
|
||||||
|
volumes:
|
||||||
|
- name: downloads
|
||||||
|
hostPath:
|
||||||
|
path: /home/user/projects/homelab/userbot/Downloads
|
||||||
|
type: DirectoryOrCreate
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ../../base
|
||||||
|
|
||||||
|
patches:
|
||||||
|
- path: patch-downloads.yaml
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: forust-userbot-deployment
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: forust-userbot
|
||||||
|
volumeMounts:
|
||||||
|
- name: downloads
|
||||||
|
mountPath: /app/downloads
|
||||||
|
volumes:
|
||||||
|
- name: downloads
|
||||||
|
hostPath:
|
||||||
|
path: /srv/homelab/userbot/Downloads
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: anna-userbot-deployment
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: anna-userbot
|
||||||
|
volumeMounts:
|
||||||
|
- name: downloads
|
||||||
|
mountPath: /app/downloads
|
||||||
|
volumes:
|
||||||
|
- name: downloads
|
||||||
|
hostPath:
|
||||||
|
path: /srv/homelab/userbot/Downloads
|
||||||
|
type: DirectoryOrCreate
|
||||||
+14
-14
@@ -59,26 +59,26 @@ async def version(client: Client, message: Message):
|
|||||||
|
|
||||||
await message.delete()
|
await message.delete()
|
||||||
|
|
||||||
remote_url = list(gitrepo.remote().urls)[0]
|
if gitrepo is not None:
|
||||||
commit_time = (
|
remote_url = list(gitrepo.remote().urls)[0]
|
||||||
datetime.datetime.fromtimestamp(gitrepo.head.commit.committed_date)
|
commit_time = (
|
||||||
.astimezone(datetime.timezone.utc)
|
datetime.datetime.fromtimestamp(gitrepo.head.commit.committed_date)
|
||||||
.strftime("%Y-%m-%d %H:%M:%S %Z")
|
.astimezone(datetime.timezone.utc)
|
||||||
)
|
.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||||
|
)
|
||||||
|
git_info = (
|
||||||
|
f"\n<b>Branch: <a href={remote_url}/tree/{gitrepo.active_branch}>{gitrepo.active_branch}</a>\n"
|
||||||
|
if gitrepo.active_branch != "master" else "\n"
|
||||||
|
) + f"Commit: <a href={remote_url}/commit/{gitrepo.head.commit.hexsha}>" f"{gitrepo.head.commit.hexsha[:7]}</a> by {gitrepo.head.commit.author.name}\n" f"Commit time: {commit_time}</b>"
|
||||||
|
else:
|
||||||
|
git_info = ""
|
||||||
|
|
||||||
await message.reply(
|
await message.reply(
|
||||||
f"<b>Moon Userbot version: {userbot_version}\n"
|
f"<b>Moon Userbot version: {userbot_version}\n"
|
||||||
f"Changelog </b><i><a href=https://t.me/moonuserbot/{changelog}>in channel</a></i>.<b>\n"
|
f"Changelog </b><i><a href=https://t.me/moonuserbot/{changelog}>in channel</a></i>.<b>\n"
|
||||||
f"Changelog written by </b><i>"
|
f"Changelog written by </b><i>"
|
||||||
f"<a href=https://t.me/Qbtaumai>Abhi</a></i>\n\n"
|
f"<a href=https://t.me/Qbtaumai>Abhi</a></i>\n\n"
|
||||||
+ (
|
f"{git_info}",
|
||||||
f"<b>Branch: <a href={remote_url}/tree/{gitrepo.active_branch}>{gitrepo.active_branch}</a>\n"
|
|
||||||
if gitrepo.active_branch != "master"
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
+ f"Commit: <a href={remote_url}/commit/{gitrepo.head.commit.hexsha}>"
|
|
||||||
f"{gitrepo.head.commit.hexsha[:7]}</a> by {gitrepo.head.commit.author.name}\n"
|
|
||||||
f"Commit time: {commit_time}</b>",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-29
@@ -1,19 +1,3 @@
|
|||||||
# Moon-Userbot - telegram userbot
|
|
||||||
# Copyright (C) 2020-present Moon Userbot Organization
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from sys import version_info
|
from sys import version_info
|
||||||
from .db import db
|
from .db import db
|
||||||
import git
|
import git
|
||||||
@@ -37,19 +21,11 @@ prefix = db.get("core.main", "prefix", ".")
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
gitrepo = git.Repo(".")
|
gitrepo = git.Repo(".")
|
||||||
except git.exc.InvalidGitRepositoryError:
|
except (git.exc.InvalidGitRepositoryError, git.exc.NoSuchPathError):
|
||||||
repo = git.Repo.init()
|
gitrepo = None
|
||||||
origin = repo.create_remote(
|
|
||||||
"origin", "https://github.com/The-MoonTg-project/Moon-Userbot"
|
|
||||||
)
|
|
||||||
origin.fetch()
|
|
||||||
repo.create_head("main", origin.refs.main)
|
|
||||||
repo.heads.main.set_tracking_branch(origin.refs.main)
|
|
||||||
repo.heads.main.checkout(True)
|
|
||||||
gitrepo = git.Repo(".")
|
|
||||||
|
|
||||||
if len(gitrepo.tags) > 0:
|
if gitrepo is not None and len(gitrepo.tags) > 0:
|
||||||
commits_since_tag = list(gitrepo.iter_commits(f"{gitrepo.tags[-1].name}..HEAD"))
|
commits_since_tag = list(gitrepo.iter_commits(f"{gitrepo.tags[-1].name}..HEAD"))
|
||||||
|
userbot_version = f"2.5.{len(commits_since_tag)}"
|
||||||
else:
|
else:
|
||||||
commits_since_tag = []
|
userbot_version = "2.5.0"
|
||||||
userbot_version = f"2.5.{len(commits_since_tag)}"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user