Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b7c0df586 | |||
| fb2f420520 | |||
| 60c7d4ff17 | |||
| d20ec696a3 | |||
| c030b4cffa | |||
| e0f7ab6561 | |||
| f0682319a7 | |||
| dbd3f36f76 | |||
| 4471da827c | |||
| 163ea867ce | |||
| dc75dbaf7c | |||
| 17cae71952 | |||
| f4c695f95e | |||
| 6bdb16ad21 | |||
| 6eb62f41cc | |||
| 586f0e3f7d | |||
| 0e46193f53 | |||
| aed28ed15c | |||
| f3b73bae11 | |||
| be049cfa0d | |||
| 4f9e7ea990 | |||
| c31369f2b7 | |||
| 3bfcd6edf4 | |||
| 504cbc81a0 | |||
| aa7239ee83 | |||
| 6d9427cf2a | |||
| 70d60ed40a | |||
| aca6824309 | |||
| d48a01775d | |||
| 99d50b43fe | |||
| 23e955bde7 | |||
| 7d7a0e5c8a | |||
| a767107277 | |||
| e68e37c285 | |||
| fc6a11397b | |||
| d776124f26 | |||
| 8a0ed85e7c | |||
| 15f0f35ce4 | |||
| 1ec145d4cf | |||
| 8b6815f314 | |||
| 2d05a1911c | |||
| 6f2f70ad09 | |||
| 6cb49be951 | |||
| 3bcabcce4b | |||
| a54b7b000f | |||
| 5b1fa11b5e | |||
| 380288d103 | |||
| 8fae8bf75d | |||
| d4e0e7f37a | |||
| cb92bff0c5 | |||
| bfbe841154 | |||
| 7cbc5bf4f3 | |||
| 258ed04f98 | |||
| 7ee0a1e7a2 | |||
| 739915c451 | |||
| 68b5467a37 | |||
| 61e6b2b5c7 | |||
| 272666d2fd | |||
| aca485836b | |||
| 02671b9117 | |||
| 1c1170ca96 | |||
| 3eab3b254a | |||
| 5d9fc18ba5 | |||
| 03d39f8a32 | |||
| 7b60630d76 | |||
| cc20d848f8 | |||
| 1040e4fdf7 | |||
| 7af5af0630 | |||
| 8f9be8a478 | |||
| d1adaa54b1 | |||
| b3b0d5b553 | |||
| d22ef0cb44 | |||
| 7c29d74c72 | |||
| b2ae170ea0 | |||
| 2c2474165b | |||
| 155bb8d02b | |||
| ed1b41ca1d | |||
| 9bb5f070e3 | |||
| b238e9ccf2 | |||
| 09a05a5ad5 | |||
| 5b05207985 | |||
| 319cf0ea1f | |||
| 53a0460476 | |||
| 13340b6ddc | |||
| ed20fb6e2a | |||
| 4fe36be5ea | |||
| 6f8b780906 | |||
| 1f9a1c2c62 | |||
| b20c012268 | |||
| b4bce72611 | |||
| e7d21bfe90 | |||
| 0f75fe02c2 | |||
| 2d895fe2bd | |||
| 45b0859ab5 | |||
| 45021933a6 | |||
| d25570e293 | |||
| 6843befac3 | |||
| 0dfb09590d | |||
| 625eb9561b | |||
| b367b64879 |
@@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#===============================
|
||||||
|
# Basic auth credentials
|
||||||
|
#===============================
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#===============================
|
||||||
|
|
||||||
|
#===============================================
|
||||||
|
#BEGIN TRAEFIK ENVIRONMENT VARIABLES ===========
|
||||||
|
#===============================================
|
||||||
|
|
||||||
|
#===============================================
|
||||||
|
# General Traefik Environment Variables
|
||||||
|
#===============================================
|
||||||
|
HOST=hostname
|
||||||
|
EMAIL=your@email.here
|
||||||
|
CF_DNS_API_TOKEN=API_TOKEN_HERE
|
||||||
|
CF_EMAIL=your_cloudflare@email.here
|
||||||
|
TZ=Europe/Berlin
|
||||||
|
|
||||||
|
#===============================================
|
||||||
|
# Dockmon Traefik Configuration File
|
||||||
|
#===============================================
|
||||||
|
DOCKMON_APPNAME=dockmon
|
||||||
|
DOCKMON_SUBDOMEN=dockmon
|
||||||
|
#===============================================
|
||||||
|
# Dashboard Traefik Environment Variables
|
||||||
|
#===============================================
|
||||||
|
DASHBOARD_APPNAME=traefik
|
||||||
|
DASHBOARD_SUBDOMEN=traefik
|
||||||
|
#===============================================
|
||||||
|
# Watercrawl Traefik Environment Variables
|
||||||
|
#===============================================
|
||||||
|
WATERCRAWL_APPNAME=watercrawl
|
||||||
|
WATERCRAWL_SUBDOMEN=watercrawl
|
||||||
|
#===============================================
|
||||||
|
# n8n Traefik Environment Variables
|
||||||
|
#===============================================
|
||||||
|
N8N_APPNAME=n8n
|
||||||
|
N8N_SUBDOMEN=n8n
|
||||||
|
#===============================================
|
||||||
|
# Glance Traefik Environment Variables
|
||||||
|
#===============================================
|
||||||
|
GLANCE_APPNAME=glance
|
||||||
|
GLANCE_SUBDOMEN=glance
|
||||||
|
#===============================================
|
||||||
|
# AdGuard Traefik Environment Variables
|
||||||
|
#===============================================
|
||||||
|
ADGUARD_APPNAME=adguard
|
||||||
|
ADGUARD_SUBDOMEN=adguard
|
||||||
|
#===============================================
|
||||||
|
# Portainer Traefik Environment Variables
|
||||||
|
#===============================================
|
||||||
|
PORTAINER_APPNAME=portainer
|
||||||
|
PORTAINER_SUBDOMEN=portainer
|
||||||
|
#===============================================
|
||||||
|
# Nextcloud Traefik Environment Variables
|
||||||
|
#===============================================
|
||||||
|
NEXTCLOUD_APPNAME=nextcloud
|
||||||
|
NEXTCLOUD_SUBDOMEN=nextcloud
|
||||||
|
#===============================================
|
||||||
|
# Aio Traefik Environment Variables
|
||||||
|
#===============================================
|
||||||
|
NEXTCLOUD_AIO_APPNAME=nextcloud-aio
|
||||||
|
NEXTCLOUD_AIO_SUBDOMEN=nextcloud-aio
|
||||||
|
# END OF TRAEFIK ENVIRONMENT VARIABLES
|
||||||
|
#===============================================
|
||||||
+59
-2
@@ -6,23 +6,80 @@ sync.ffs_lock
|
|||||||
.env
|
.env
|
||||||
.env.anna
|
.env.anna
|
||||||
.env.forust
|
.env.forust
|
||||||
|
.env.*
|
||||||
|
!.env.*example
|
||||||
|
|
||||||
# Volumes and data directories
|
# Volumes and data directories
|
||||||
gitea/gitea-db/*
|
gitea/gitea-db/
|
||||||
gitea/gitea-data/*
|
gitea/gitea-data/*
|
||||||
|
gitea/*runner/*
|
||||||
n8n/n8n-data/*
|
n8n/n8n-data/*
|
||||||
n8n/n8n-node-data/*
|
n8n/n8n-node-data/*
|
||||||
adguardhome/data/*
|
adguardhome/data/*
|
||||||
dockmon/data/*
|
dockmon/data/*
|
||||||
portainer/portainer_data/*
|
portainer/portainer_data/*
|
||||||
metube/MeTube_downloads
|
metube/MeTube_downloads
|
||||||
|
uptime-kuma/data/
|
||||||
|
termix/termix-data/*
|
||||||
|
cfddns/config.json
|
||||||
|
checkmk/checkmk/*
|
||||||
|
downtify/Downtify_downloads
|
||||||
|
|
||||||
|
# Steaming services files
|
||||||
|
streaming/jellyfin/*
|
||||||
|
streaming/jellyseerr/*
|
||||||
|
streaming/sonarr/*
|
||||||
|
streaming/radarr/*
|
||||||
|
streaming/data/*
|
||||||
|
streaming/qbittorrent/*
|
||||||
|
streaming/prowlarr/*
|
||||||
|
|
||||||
|
# Homepage
|
||||||
|
homepages/forust_files/assets/images/team/*
|
||||||
|
|
||||||
|
|
||||||
# Traefik files
|
# Traefik files
|
||||||
traefik/letsencrypt/acme.json
|
traefik/letsencrypt/acme.json
|
||||||
traefik/logs/*
|
traefik/logs/*
|
||||||
traefik/certs/*
|
traefik/certs/*
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
monitoring/prometheus.yml
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
.python-version
|
.python-version
|
||||||
.venv/
|
|
||||||
venv/
|
venv/
|
||||||
|
pyc
|
||||||
|
unknown_errors.txt
|
||||||
|
moonlogs.txt
|
||||||
|
thumb.jpg
|
||||||
|
antipm_pic.jpg
|
||||||
|
musicbot/
|
||||||
|
.trunk/
|
||||||
|
previous_profiles/
|
||||||
|
.python-version
|
||||||
|
/modules/__pycache__/
|
||||||
|
__pycache__/
|
||||||
|
*.session
|
||||||
|
*.session-old
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
*-journal
|
||||||
|
/venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# DataSecurity
|
||||||
|
replacements.txt
|
||||||
|
|
||||||
|
# Vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
edu_master/temp/
|
||||||
|
|||||||
@@ -15,14 +15,36 @@ services:
|
|||||||
- ./data/work:/opt/adguardhome/work
|
- ./data/work:/opt/adguardhome/work
|
||||||
- ./data/conf:/opt/adguardhome/conf
|
- ./data/conf:/opt/adguardhome/conf
|
||||||
networks:
|
networks:
|
||||||
- traefik-proxy
|
- proxy
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.adguard.rule=Host(`adguard.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.adguard.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.adguard.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.adguard.service=adguard"
|
||||||
|
- "traefik.http.routers.adguard.tls=true"
|
||||||
|
- "traefik.http.services.adguard.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.adguard-local.rule=Host(`adguard.workstation.internal`) || Host(`adguard.internal`)"
|
||||||
|
- "traefik.http.routers.adguard-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.adguard-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.adguard-local.service=adguard"
|
||||||
|
- "traefik.http.routers.adguard-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.adguard-dev.rule=Host(`adguard.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.adguard-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.adguard-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.adguard-dev.service=adguard"
|
||||||
|
- "traefik.http.routers.adguard-dev.tls=true"
|
||||||
|
|
||||||
- glance.name=adguard
|
- glance.name=adguard
|
||||||
# - glance.icon=si:adguard
|
|
||||||
- glance.url=https://adguard.forust.xyz/
|
- glance.url=https://adguard.forust.xyz/
|
||||||
- glance.description=AdGuard Home is a network-wide software for blocking ads.
|
- glance.description=AdGuard Home is a network-wide software for blocking ads.
|
||||||
networks:
|
networks:
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# ===================================
|
||||||
|
# Authentification app (authentik)
|
||||||
|
|
||||||
|
# PostgresQL conf
|
||||||
|
PG_PASS=change_this_cuz_its_ur_db_pass
|
||||||
|
PG_USER=authentik # it's okay
|
||||||
|
|
||||||
|
# Image Settings
|
||||||
|
AUTHENTIK_IMAGE=ghcr.io/goauthentik/server
|
||||||
|
AUTHENTIK_TAG=2025.10.2
|
||||||
|
|
||||||
|
# Networking
|
||||||
|
PORT_HTTP=9000
|
||||||
|
PORT_HTTPS=9443 # btw likely already used by portainer
|
||||||
|
|
||||||
|
AUTHENTIK_SECRET_KEY=super_secret_super_scary_authenik_key
|
||||||
|
|
||||||
|
AUTHENTIK_BOOTSTRAP_PASSWORD=pls_change_this
|
||||||
|
|
||||||
|
AUTHENTIK_ERROR_REPORTING__ENABLED=true # Or false to turn off
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
services:
|
||||||
|
postgresql:
|
||||||
|
image: docker.io/library/postgres:15-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${PG_DB:-authentik}
|
||||||
|
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
|
||||||
|
POSTGRES_USER: ${PG_USER:-authentik}
|
||||||
|
healthcheck:
|
||||||
|
interval: 30s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
|
||||||
|
timeout: 5s
|
||||||
|
volumes:
|
||||||
|
- database:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- authentik
|
||||||
|
|
||||||
|
server:
|
||||||
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.2}
|
||||||
|
command: server
|
||||||
|
container_name: authentik-server
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- ${PORT_HTTP:-9000}:9000
|
||||||
|
- ${PORT_HTTPS:-9443}:9443
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||||
|
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||||
|
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
|
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||||
|
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=proxy"
|
||||||
|
# Services
|
||||||
|
# - "traefik.http.services.authentik-server.loadbalancer.server.port=9443"
|
||||||
|
- "traefik.http.services.authentik-server.loadbalancer.server.port=9000"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.authentik-server.rule=Host(`auth.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.authentik-server.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.authentik-server.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.authentik-server.service=authentik-server"
|
||||||
|
- "traefik.http.routers.authentik-server.tls=true"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.authentik-server-local.rule=Host(`auth.workstation.internal`) || Host(`auth-dashboard.internal`)"
|
||||||
|
- "traefik.http.routers.authentik-server-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.authentik-server-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.authentik-server-local.service=authentik-server"
|
||||||
|
- "traefik.http.routers.authentik-server-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.authentik-server-dev.rule=Host(`auth.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.authentik-server-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.authentik-server-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.authentik-server-dev.service=authentik-server"
|
||||||
|
- "traefik.http.routers.authentik-server-dev.tls=true"
|
||||||
|
volumes:
|
||||||
|
- ./media:/media
|
||||||
|
- ./custom-templates:/templates
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
- authentik
|
||||||
|
depends_on:
|
||||||
|
postgresql:
|
||||||
|
condition: service_healthy
|
||||||
|
worker:
|
||||||
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.2}
|
||||||
|
restart: unless-stopped
|
||||||
|
user: root
|
||||||
|
command: worker
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||||
|
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||||
|
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
|
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||||
|
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./media:/media
|
||||||
|
- ./certs:/certs
|
||||||
|
- ./custom-templates:/templates
|
||||||
|
networks:
|
||||||
|
- authentik
|
||||||
|
depends_on:
|
||||||
|
postgresql:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
database:
|
||||||
|
driver: local
|
||||||
|
networks:
|
||||||
|
authentik:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
cloudflare-ddns:
|
||||||
|
image: timothyjmiller/cloudflare-ddns:latest
|
||||||
|
container_name: cloudflare-ddns
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
network_mode: 'host'
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/config.json
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"cloudflare": [
|
||||||
|
{
|
||||||
|
"authentication": {
|
||||||
|
"api_token": "API_TOKEN"
|
||||||
|
},
|
||||||
|
"api_key": {
|
||||||
|
"api_key": "api_key_here",
|
||||||
|
"account_email": "your_email_here"
|
||||||
|
}
|
||||||
|
"zone_id": "your_zone-id",
|
||||||
|
"subdomains": [
|
||||||
|
{ "name": "", "proxied": true },
|
||||||
|
{ "name": "www", "proxied": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a": true,
|
||||||
|
"aaaa": false,
|
||||||
|
"purgeUnknownRecords": false,
|
||||||
|
"ttl": 300
|
||||||
|
}
|
||||||
+34
-10
@@ -11,20 +11,44 @@ services:
|
|||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-k", "-f", "https://localhost:443/health"]
|
test: [ "CMD", "curl", "-k", "-f", "https://localhost:443/health" ]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
networks:
|
networks:
|
||||||
- traefik-proxy
|
- proxy
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.docker.network=proxy"
|
||||||
- glance.name=dockmon
|
|
||||||
# - glance.icon=sh:dockmon
|
# Prod Router
|
||||||
- glance.url=https://dockmon.forust.xyz/
|
- "traefik.http.routers.dockmon.rule=Host(`dockmon.forust.xyz`)"
|
||||||
- glance.description=Dockmon is a lightweight Docker container monitoring and management tool with a user-friendly web interface.
|
- "traefik.http.routers.dockmon.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.dockmon.middlewares=security-chain@file"
|
||||||
|
- "traefik.http.routers.dockmon.service=dockmon"
|
||||||
|
- "traefik.http.routers.dockmon.tls=true"
|
||||||
|
- "traefik.http.services.dockmon.loadbalancer.server.port=443"
|
||||||
|
- "traefik.http.services.dockmon.loadbalancer.server.scheme=https"
|
||||||
|
- "traefik.http.services.dockmon.loadbalancer.serverstransport=insecureTransport@file"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.dockmon-local.rule=Host(`dockmon.workstation.internal`) || Host(`dockmon.internal`)"
|
||||||
|
- "traefik.http.routers.dockmon-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.dockmon-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.dockmon-local.service=dockmon"
|
||||||
|
- "traefik.http.routers.dockmon-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.dockmon-dev.rule=Host(`dockmon.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.dockmon-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.dockmon-dev.middlewares=security-chain@file"
|
||||||
|
- "traefik.http.routers.dockmon-dev.service=dockmon"
|
||||||
|
- "traefik.http.routers.dockmon-dev.tls=true"
|
||||||
|
|
||||||
|
- glance.name=dockmon
|
||||||
|
- glance.url=https://dockmon.forust.xyz/
|
||||||
|
- glance.description=Dockmon is a lightweight Docker container monitoring and management tool with a user-friendly web interface.
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
services:
|
||||||
|
downtify:
|
||||||
|
container_name: downtify
|
||||||
|
image: ghcr.io/henriquesebastiao/downtify:latest
|
||||||
|
# ports:
|
||||||
|
# - '7077:8000'
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.services.downtify.loadbalancer.server.port=8000
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- traefik.http.routers.downtify.rule=Host(`downtify.forust.xyz`)
|
||||||
|
- traefik.http.routers.downtify.entrypoints=websecure
|
||||||
|
- traefik.http.routers.downtify.middlewares=security-chain@file
|
||||||
|
- traefik.http.routers.downtify.service=downtify
|
||||||
|
- traefik.http.routers.downtify.tls=true
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- traefik.http.routers.downtify-local.rule=Host(`downtify.workstation.internal`) || Host(`downtify.internal`)
|
||||||
|
- traefik.http.routers.downtify-local.entrypoints=websecure
|
||||||
|
- traefik.http.routers.downtify-local.middlewares=security-headers@file
|
||||||
|
- traefik.http.routers.downtify-local.service=downtify
|
||||||
|
- traefik.http.routers.downtify-local.tls=true
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- traefik.http.routers.downtify-dev.rule=Host(`downtify.gigaforust.internal`)
|
||||||
|
- traefik.http.routers.downtify-dev.entrypoints=websecure
|
||||||
|
- traefik.http.routers.downtify-dev.middlewares=security-chain@file
|
||||||
|
- traefik.http.routers.downtify-dev.service=downtify
|
||||||
|
- traefik.http.routers.downtify-dev.tls=true
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ./Downtify_downloads:/downloads
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
EDU_LOGIN=your_edu_login_here
|
||||||
|
EDU_PASSWORD=your_edu_password_here
|
||||||
|
EDU_URL_LOGIN=https://edu.edu.vn.ua/user/login
|
||||||
|
EDU_URL_VERIFY=https://edu.edu.vn.ua/course/userlist
|
||||||
|
PHPSESSID_INTERVAL=10
|
||||||
|
USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"
|
||||||
|
WEBINAR_URL=https://edu.edu.vn.ua/webinar/useractive
|
||||||
|
WEBINAR_CHECK_INTERVAL=60
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
PLAYWRIGHT_WS=ws://playwright-service:3000/ws
|
||||||
|
WEBINAR_TELEGRAM_TOKEN=your_telegram_bot_token_here
|
||||||
|
WEBINAR_ADMIN_ID=123456789
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
ARG VERSION
|
|
||||||
# Use the official WaterCrawl image as the base image
|
|
||||||
FROM watercrawl/watercrawl:${VERSION:-v0.10.2}
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /var/www
|
|
||||||
|
|
||||||
# Copy the extra requirements file
|
|
||||||
COPY extra_requirements.txt /var/www/extra_requirements.txt
|
|
||||||
|
|
||||||
# Install any additional packages
|
|
||||||
RUN poetry run pip install -r /var/www/extra_requirements.txt
|
|
||||||
|
|
||||||
# The rest of the configuration is inherited from the base image
|
|
||||||
# The entrypoint and command should be defined in docker-compose.yml
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Add your additional Python packages here, one per line
|
|
||||||
+32
-249
@@ -1,262 +1,45 @@
|
|||||||
x-app: &app
|
|
||||||
build:
|
|
||||||
context: ./backend/
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
- VERSION=${VERSION:-v0.10.2}
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
dns:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 1.1.1.1
|
|
||||||
environment:
|
|
||||||
- SECRET_KEY=${SECRET_KEY:-django-insecure-el4wo4a4--=f0+ag#omp@^w4eq^8v4(scda&1a(td_y2@=sh6&}
|
|
||||||
- API_ENCRYPTION_KEY=${API_ENCRYPTION_KEY:-8zSd6JIuC7ovfZ4AoxG_XmhubW6CPnQWW7Qe_4TD1TQ=}
|
|
||||||
- DEBUG=${DEBUG:-True}
|
|
||||||
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-*}
|
|
||||||
- LANGUAGE_CODE=${LANGUAGE_CODE:-en-us}
|
|
||||||
- TIME_ZONE=${TIME_ZONE:-UTC}
|
|
||||||
- USE_I18N=${USE_I18N:-True}
|
|
||||||
- USE_TZ=${USE_TZ:-True}
|
|
||||||
- STATIC_ROOT=${STATIC_ROOT:-storage/static/}
|
|
||||||
- MEDIA_ROOT=${MEDIA_ROOT:-storage/media/}
|
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
||||||
- REDIS_URL=${REDIS_URL:-redis://redis:6379/1}
|
|
||||||
- DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@${POSTGRES_HOST:-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
|
|
||||||
- CELERY_BROKER_URL=${CELERY_BROKER_URL:-redis://redis:6379/0}
|
|
||||||
- CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND:-django-db}
|
|
||||||
- REDIS_LOCKER_URL=${REDIS_LOCKER_URL:-redis://redis:6379/3}
|
|
||||||
- MINIO_ENDPOINT=minio:9000
|
|
||||||
- MINIO_EXTERNAL_ENDPOINT=nginx
|
|
||||||
- MINIO_REGION=us-east-1
|
|
||||||
- MINIO_ACCESS_KEY=minio
|
|
||||||
- MINIO_SECRET_KEY=minio123
|
|
||||||
- MINIO_USE_HTTPS=False
|
|
||||||
- MINIO_EXTERNAL_ENDPOINT_USE_HTTPS=False
|
|
||||||
- MINIO_URL_EXPIRY_HOURS=7
|
|
||||||
- MINIO_PRIVATE_BUCKET=private
|
|
||||||
- MINIO_PUBLIC_BUCKET=public
|
|
||||||
- CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS:-}
|
|
||||||
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-}
|
|
||||||
- CORS_ALLOWED_ORIGIN_REGEXES=${CORS_ALLOWED_ORIGIN_REGEXES:-}
|
|
||||||
- CORS_ALLOW_ALL_ORIGINS=${CORS_ALLOW_ALL_ORIGINS:-False}
|
|
||||||
- FRONTEND_URL=${FRONTEND_URL:-http://localhost}
|
|
||||||
- IS_LOGIN_ACTIVE=${IS_LOGIN_ACTIVE:-True}
|
|
||||||
- IS_SIGNUP_ACTIVE=${IS_SIGNUP_ACTIVE:-True}
|
|
||||||
- IS_GITHUB_LOGIN_ACTIVE=${IS_GITHUB_LOGIN_ACTIVE:-True}
|
|
||||||
- IS_GOOGLE_LOGIN_ACTIVE=${IS_GOOGLE_LOGIN_ACTIVE:-True}
|
|
||||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
|
|
||||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
|
|
||||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
|
|
||||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
|
||||||
- ACCESS_TOKEN_LIFETIME_MINUTES=${ACCESS_TOKEN_LIFETIME_MINUTES:-5}
|
|
||||||
- REFRESH_TOKEN_LIFETIME_DAYS=${REFRESH_TOKEN_LIFETIME_DAYS:-30}
|
|
||||||
- EMAIL_BACKEND=${EMAIL_BACKEND:-django.core.mail.backends.smtp.EmailBackend}
|
|
||||||
- EMAIL_HOST=${EMAIL_HOST:-}
|
|
||||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
|
||||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-True}
|
|
||||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-}
|
|
||||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-}
|
|
||||||
- DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL:-}
|
|
||||||
- SCRAPY_USER_AGENT=${SCRAPY_USER_AGENT:-WaterCrawl/0.1 (+https://github.com/watercrawl/watercrawl)}
|
|
||||||
- SCRAPY_ROBOTSTXT_OBEY=${SCRAPY_ROBOTSTXT_OBEY:-True}
|
|
||||||
- SCRAPY_CONCURRENT_REQUESTS=${SCRAPY_CONCURRENT_REQUESTS:-16}
|
|
||||||
- SCRAPY_DOWNLOAD_DELAY=${SCRAPY_DOWNLOAD_DELAY:-0}
|
|
||||||
- SCRAPY_CONCURRENT_REQUESTS_PER_DOMAIN=${SCRAPY_CONCURRENT_REQUESTS_PER_DOMAIN:-4}
|
|
||||||
- SCRAPY_CONCURRENT_REQUESTS_PER_IP=${SCRAPY_CONCURRENT_REQUESTS_PER_IP:-4}
|
|
||||||
- SCRAPY_COOKIES_ENABLED=${SCRAPY_COOKIES_ENABLED:-False}
|
|
||||||
- SCRAPY_HTTPCACHE_ENABLED=${SCRAPY_HTTPCACHE_ENABLED:-True}
|
|
||||||
- SCRAPY_HTTPCACHE_EXPIRATION_SECS=${SCRAPY_HTTPCACHE_EXPIRATION_SECS:-3600}
|
|
||||||
- SCRAPY_HTTPCACHE_DIR=${SCRAPY_HTTPCACHE_DIR:-httpcache}
|
|
||||||
- SCRAPY_LOG_LEVEL=${SCRAPY_LOG_LEVEL:-ERROR}
|
|
||||||
- SCRAPY_GOOGLE_API_KEY=${SCRAPY_GOOGLE_API_KEY:-}
|
|
||||||
- SCRAPY_GOOGLE_CSE_ID=${SCRAPY_GOOGLE_CSE_ID:-}
|
|
||||||
- SCRAPY_MAX_NUMBER_OF_SITEMAP_URLS=${SCRAPY_MAX_NUMBER_OF_SITEMAP_URLS:-20000}
|
|
||||||
- SCRAPY_SITEMAP_CRAWL_PAGE_LIMIT=${SCRAPY_SITEMAP_CRAWL_PAGE_LIMIT:-100}
|
|
||||||
- PLAYWRIGHT_SERVER=${PLAYWRIGHT_SERVER:-http://playwright:8000}
|
|
||||||
- PLAYWRIGHT_API_KEY=${PLAYWRIGHT_API_KEY:-your-secret-api-key}
|
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
|
||||||
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-}
|
|
||||||
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-}
|
|
||||||
- GOOGLE_ANALYTICS_ID=${GOOGLE_ANALYTICS_ID:-}
|
|
||||||
- IS_ENTERPRISE_MODE_ACTIVE=${IS_ENTERPRISE_MODE_ACTIVE:-False}
|
|
||||||
- MAX_CRAWL_DEPTH=${MAX_CRAWL_DEPTH:--1}
|
|
||||||
- CAPTURE_USAGE_HISTORY=${CAPTURE_USAGE_HISTORY:-True}
|
|
||||||
- MCP_SERVER=${MCP_SERVER:-http://localhost/sse}
|
|
||||||
networks:
|
|
||||||
- traefik-proxy
|
|
||||||
- default
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
x-frontend: &frontend
|
|
||||||
image: watercrawl/frontend:${VERSION:-v0.10.2}
|
|
||||||
environment:
|
|
||||||
- VITE_API_BASE_URL=${API_BASE_URL:-http://localhost/api}
|
|
||||||
depends_on:
|
|
||||||
- app
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nginx:
|
redis:
|
||||||
image: nginx:alpine
|
image: redis:alpine
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf.template
|
|
||||||
- ./nginx/entrypoint.sh:/entrypoint.sh
|
|
||||||
environment:
|
|
||||||
- MINIO_PRIVATE_BUCKET=${MINIO_PRIVATE_BUCKET:-private}
|
|
||||||
- MINIO_PUBLIC_BUCKET=${MINIO_PUBLIC_BUCKET:-public}
|
|
||||||
command: ["/bin/sh", "/entrypoint.sh"]
|
|
||||||
depends_on:
|
|
||||||
- app
|
|
||||||
- frontend
|
|
||||||
- minio
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- traefik-proxy
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network=traefik-proxy"
|
|
||||||
|
|
||||||
app:
|
|
||||||
<<: *app
|
|
||||||
command: [ "gunicorn", "-b", "0.0.0.0:9000", "-w", "2", "watercrawl.wsgi:application", "--access-logfile", "-", "--error-logfile", "-", "--timeout", "60" ]
|
|
||||||
|
|
||||||
celery:
|
|
||||||
<<: *app
|
|
||||||
command: [ "celery", "-A", "watercrawl", "worker", "-l", "info", "-S", "django" ]
|
|
||||||
dns:
|
|
||||||
- 1.1.1.1
|
|
||||||
- 8.8.8.8
|
|
||||||
|
|
||||||
celery-beat:
|
|
||||||
<<: *app
|
|
||||||
command: [ "celery", "-A", "watercrawl", "beat", "-l", "info", "-S", "django" ]
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
<<: *frontend
|
|
||||||
command: [ "npm", "run", "serve" ]
|
|
||||||
|
|
||||||
minio:
|
|
||||||
image: minio/minio:RELEASE.2024-11-07T00-52-20Z
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/minio-data:/data
|
- redis-data:/data
|
||||||
command: server /data --console-address ":9001"
|
|
||||||
environment:
|
|
||||||
- MINIO_BROWSER_REDIRECT_URL=${MINIO_BROWSER_REDIRECT_URL:-http://localhost/minio-console/}
|
|
||||||
- MINIO_SERVER_URL=${MINIO_SERVER_URL:-http://localhost/}
|
|
||||||
- MINIO_ROOT_USER=${MINIO_ACCESS_KEY:-minio}
|
|
||||||
- MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY:-minio123}
|
|
||||||
|
|
||||||
playwright:
|
|
||||||
image: watercrawl/playwright:1.1
|
|
||||||
restart: unless-stopped
|
|
||||||
user: root
|
|
||||||
environment:
|
|
||||||
- AUTH_API_KEY=${PLAYWRIGHT_API_KEY:-your-secret-api-key}
|
|
||||||
- PORT=${PLAYWRIGHT_PORT:-8000}
|
|
||||||
- HOST=${PLAYWRIGHT_HOST:-0.0.0.0}
|
|
||||||
dns:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 1.1.1.1
|
|
||||||
networks:
|
|
||||||
- traefik-proxy
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:17.2-alpine3.21
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
|
|
||||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
|
||||||
- POSTGRES_DB=${POSTGRES_DB:-postgres}
|
|
||||||
volumes:
|
|
||||||
- ./volumes/postgres-db:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready" ]
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
interval: 10s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
mcp:
|
playwright-service:
|
||||||
image: watercrawl/mcp:v1.2.0
|
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: [ "sse", "--base-url", "http://app:9000", '--port', '3000', '--endpoint', '/sse' ]
|
command: npx -y playwright@1.56.0 run-server --port 3000 --path /ws
|
||||||
networks:
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
redis:
|
session-keeper:
|
||||||
image: redis:latest
|
build: ./phpsessid-bot
|
||||||
|
env_file: .env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "redis-cli -h redis EXISTS EDU_PHPSESSID | grep -q 1" ]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
llm:
|
webinar-checker:
|
||||||
image: ollama/ollama:latest
|
build: ./webinar-checker
|
||||||
|
env_file: .env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
depends_on:
|
||||||
- ./volumes/ollama-models:/root/.ollama
|
redis:
|
||||||
environment:
|
condition: service_healthy
|
||||||
- OLLAMA_DISABLE_TELEMETRY=true
|
session-keeper:
|
||||||
- OLLAMA_KEEP_ALIVE=5m
|
condition: service_healthy
|
||||||
- OLLAMA_HOST=0.0.0.0:11434
|
playwright-service:
|
||||||
- OLLAMA_NUM_PARALLEL=1
|
condition: service_started
|
||||||
- OLLAMA_MAX_LOADED_MODELS=1
|
|
||||||
dns:
|
|
||||||
- 1.1.1.1
|
|
||||||
- 8.8.8.8
|
|
||||||
networks:
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
# docker exec -it edu_master-llm-1 ollama pull neural-chat:7b-q4
|
|
||||||
# docker exec -it edu_master-llm-1 ollama pull mistral:7b-q4
|
|
||||||
|
|
||||||
# lessons-bot:
|
|
||||||
# build:
|
|
||||||
# context: edu_master/lessons_bot/
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
# restart: unless-stopped
|
|
||||||
# environment:
|
|
||||||
# - LESSONS_BOT_TOKEN=${LESSONS_BOT_TOKEN}
|
|
||||||
# - N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://n8n:5678/webhook-test/get-lessons}
|
|
||||||
# - N8N_SECRET=${N8N_SECRET:-your-secret-token-here}
|
|
||||||
# - WATERCRAWL_API_URL=${WATERCRAWL_API_URL:-http://app:9000/api}
|
|
||||||
# - PHPSESSID_BOT_URL=${PHPSESSID_BOT_URL:-http://phpsessid-bot:5000}
|
|
||||||
# - EDU_HOST=${EDU_HOST:-edu.edu.vn.ua}
|
|
||||||
# depends_on:
|
|
||||||
# - n8n
|
|
||||||
# - app
|
|
||||||
# - phpsessid-bot
|
|
||||||
# dns:
|
|
||||||
# - 1.1.1.1
|
|
||||||
# - 8.8.8.8
|
|
||||||
# networks:
|
|
||||||
# - default
|
|
||||||
|
|
||||||
phpsessid-bot:
|
|
||||||
build:
|
|
||||||
context: ./phpsessid_bot/
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- EDU_HOST=${EDU_HOST:-edu.edu.vn.ua}
|
|
||||||
- EDU_LOGIN=${EDU_LOGIN}
|
|
||||||
- EDU_PASSWORD=${EDU_PASSWORD}
|
|
||||||
- BOT_PORT=${PHPSESSID_BOT_PORT:-5000}
|
|
||||||
- BOT_HOST=${PHPSESSID_BOT_HOST:-0.0.0.0}
|
|
||||||
dns:
|
|
||||||
- 1.1.1.1
|
|
||||||
- 8.8.8.8
|
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
n8n_data:
|
redis-data:
|
||||||
postgres-db:
|
|
||||||
minio-data:
|
|
||||||
ollama-models:
|
|
||||||
lmstudio_data:
|
|
||||||
networks:
|
|
||||||
traefik-proxy:
|
|
||||||
external: true
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Установка зависимостей
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Копирование кода
|
|
||||||
COPY config.py .
|
|
||||||
COPY utils.py .
|
|
||||||
COPY handlers.py .
|
|
||||||
COPY main.py .
|
|
||||||
|
|
||||||
# Запуск бота
|
|
||||||
CMD ["python", "-u", "main.py"]
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import os
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Telegram
|
|
||||||
BOT_TOKEN = os.getenv('LESSONS_BOT_TOKEN')
|
|
||||||
|
|
||||||
# n8n
|
|
||||||
N8N_WEBHOOK_URL = os.getenv('N8N_WEBHOOK_URL', 'http://n8n:5678/webhook/homework-check')
|
|
||||||
N8N_SECRET = os.getenv('N8N_SECRET', 'your-secret-token-here')
|
|
||||||
|
|
||||||
# WaterCrawl API
|
|
||||||
WATERCRAWL_API_URL = os.getenv('WATERCRAWL_API_URL', 'http://app:9000/api')
|
|
||||||
|
|
||||||
# PHPSESSID Bot
|
|
||||||
PHPSESSID_BOT_URL = os.getenv('PHPSESSID_BOT_URL', 'http://phpsessid-bot:5000')
|
|
||||||
|
|
||||||
# EDU site
|
|
||||||
EDU_HOST = os.getenv('EDU_HOST', 'edu.edu.vn.ua')
|
|
||||||
EDU_WEBINAR_URL = f'https://{EDU_HOST}/webinar/useractive'
|
|
||||||
|
|
||||||
# Playwright
|
|
||||||
PLAYWRIGHT_SERVER = os.getenv('PLAYWRIGHT_SERVER', 'http://playwright:8000')
|
|
||||||
PLAYWRIGHT_API_KEY = os.getenv('PLAYWRIGHT_API_KEY', 'your-secret-api-key')
|
|
||||||
WEBINAR_WAIT_TIME = int(os.getenv('WEBINAR_WAIT_TIME', '3')) # Секунды ожидания загрузки
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import logging
|
|
||||||
import requests
|
|
||||||
from telegram import Update
|
|
||||||
from telegram.ext import ContextTypes
|
|
||||||
import config
|
|
||||||
from utils import fetch_webinars, format_webinar_message
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Команда /start"""
|
|
||||||
welcome_message = """
|
|
||||||
Привет! Я бот для проверки домашних заданий и вебинаров.
|
|
||||||
|
|
||||||
<b>Команды:</b>
|
|
||||||
/check - Проверить несделанные уроки
|
|
||||||
/webinar - Проверить активные онлайн уроки
|
|
||||||
/help - Помощь
|
|
||||||
"""
|
|
||||||
await update.message.reply_text(welcome_message, parse_mode='HTML')
|
|
||||||
|
|
||||||
|
|
||||||
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Команда /help"""
|
|
||||||
help_text = """
|
|
||||||
<b>Как пользоваться ботом:</b>
|
|
||||||
|
|
||||||
<b>/check</b> - Проверка домашних заданий
|
|
||||||
- Поиск несделанных уроков
|
|
||||||
|
|
||||||
⏱ Проверка занимает 10-30 секунд
|
|
||||||
|
|
||||||
<b>/webinar</b> - Активные онлайн уроки
|
|
||||||
- Проверка активных вебинаровв
|
|
||||||
⏱ Проверка занимает 3-5 секунд
|
|
||||||
"""
|
|
||||||
await update.message.reply_text(help_text, parse_mode='HTML')
|
|
||||||
|
|
||||||
|
|
||||||
async def check_homework(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Команда /check - запускает проверку уроков"""
|
|
||||||
chat_id = update.effective_chat.id
|
|
||||||
user_id = update.effective_user.id
|
|
||||||
username = update.effective_user.username or "unknown"
|
|
||||||
|
|
||||||
# Отправляем уведомление что начали работу
|
|
||||||
status_message = await update.message.reply_text("Запускаю проверку уроков...")
|
|
||||||
|
|
||||||
# Формируем данные для n8n
|
|
||||||
payload = {
|
|
||||||
"chat_id": chat_id,
|
|
||||||
"user_id": user_id,
|
|
||||||
"username": username,
|
|
||||||
"timestamp": update.message.date.isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {config.N8N_SECRET}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info(f"Sending request to n8n for user {user_id}")
|
|
||||||
|
|
||||||
# Отправляем запрос в n8n
|
|
||||||
response = requests.post(
|
|
||||||
config.N8N_WEBHOOK_URL,
|
|
||||||
json=payload,
|
|
||||||
headers=headers,
|
|
||||||
timeout=5 # Короткий таймаут т.к. это асинхронный запрос
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
await status_message.edit_text(
|
|
||||||
"✅ Запрос принят!\n"
|
|
||||||
"🔄 Парсинг сайта и анализ данных...\n"
|
|
||||||
"⏱ Это займет 10-30 секунд"
|
|
||||||
)
|
|
||||||
logger.info(f"Request accepted for user {user_id}")
|
|
||||||
else:
|
|
||||||
await status_message.edit_text(
|
|
||||||
f"Ошибка при отправке запроса. Функция в разработке\n"
|
|
||||||
f"Код: {response.status_code}"
|
|
||||||
)
|
|
||||||
logger.error(f"n8n returned status {response.status_code}")
|
|
||||||
|
|
||||||
except requests.Timeout:
|
|
||||||
await status_message.edit_text("⏱ Запрос обрабатывается (таймаут соединения)")
|
|
||||||
logger.warning(f"Timeout for user {user_id}")
|
|
||||||
except Exception as e:
|
|
||||||
await status_message.edit_text(f"❌ Ошибка: {str(e)}")
|
|
||||||
logger.error(f"Error for user {user_id}: {e}", exc_info=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def check_webinar(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Команда /webinar - проверяет активные онлайн уроки"""
|
|
||||||
user_id = update.effective_user.id
|
|
||||||
|
|
||||||
# Отправляем уведомление что начали работу
|
|
||||||
status_message = await update.message.reply_text("Проверяю активные вебинары...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info(f"Checking webinars for user {user_id}")
|
|
||||||
|
|
||||||
# Получаем список вебинаров
|
|
||||||
webinars = fetch_webinars()
|
|
||||||
|
|
||||||
if webinars is None:
|
|
||||||
await status_message.edit_text(
|
|
||||||
"❌ Не удалось получить информацию о вебинарах\n"
|
|
||||||
"Попробуйте позже или обратитесь к администратору\n"
|
|
||||||
"|@MrForust|mr.forust| Либо же прямо сюда."
|
|
||||||
)
|
|
||||||
logger.error(f"Failed to fetch webinars for user {user_id}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Форматируем и отправляем результат
|
|
||||||
message = format_webinar_message(webinars)
|
|
||||||
await status_message.edit_text(message, parse_mode='HTML', disable_web_page_preview=True)
|
|
||||||
|
|
||||||
logger.info(f"Webinar check completed for user {user_id}: found {len(webinars)} webinars")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await status_message.edit_text(f"❌ Ошибка: {str(e)}")
|
|
||||||
logger.error(f"Error checking webinars for user {user_id}: {e}", exc_info=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Обработчик ошибок"""
|
|
||||||
logger.error(f"Update {update} caused error {context.error}", exc_info=context.error)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import logging
|
|
||||||
from telegram import Update
|
|
||||||
from telegram.ext import Application, CommandHandler
|
|
||||||
import config
|
|
||||||
from handlers import start, help_command, check_homework, check_webinar, error_handler
|
|
||||||
|
|
||||||
# Настройка логирования
|
|
||||||
logging.basicConfig(
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
level=logging.INFO
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Запуск бота"""
|
|
||||||
if not config.BOT_TOKEN:
|
|
||||||
logger.error("LESSONS_BOT_TOKEN not set!")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Создаем приложение
|
|
||||||
application = Application.builder().token(config.BOT_TOKEN).build()
|
|
||||||
|
|
||||||
# Регистрируем обработчики команд
|
|
||||||
application.add_handler(CommandHandler("start", start))
|
|
||||||
application.add_handler(CommandHandler("help", help_command))
|
|
||||||
application.add_handler(CommandHandler("check", check_homework))
|
|
||||||
application.add_handler(CommandHandler("webinar", check_webinar))
|
|
||||||
|
|
||||||
# Регистрируем обработчик ошибок
|
|
||||||
application.add_error_handler(error_handler)
|
|
||||||
|
|
||||||
# Запускаем бота
|
|
||||||
logger.info("Lessons Bot started!")
|
|
||||||
logger.info(f"PHPSESSID Bot URL: {config.PHPSESSID_BOT_URL}")
|
|
||||||
logger.info(f"n8n Webhook URL: {config.N8N_WEBHOOK_URL}")
|
|
||||||
|
|
||||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
python-telegram-bot==20.7
|
|
||||||
requests==2.31.0
|
|
||||||
beautifulsoup4==4.12.2
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
lxml==4.9.3
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
import logging
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from typing import Optional, Dict, List
|
|
||||||
import config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_phpsessid() -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Получает валидный PHPSESSID через phpsessid-bot
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: PHPSESSID или None в случае ошибки
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
url = f"{config.PHPSESSID_BOT_URL}/get-session"
|
|
||||||
logger.info(f"Requesting PHPSESSID from {url}")
|
|
||||||
|
|
||||||
response = requests.post(url, timeout=10)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
if data.get('success'):
|
|
||||||
phpsessid = data.get('phpsessid')
|
|
||||||
logger.info(f"Got PHPSESSID: {phpsessid[:10]}...")
|
|
||||||
return phpsessid
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to get PHPSESSID: {data.get('error')}")
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
logger.error(f"PHPSESSID bot returned status {response.status_code}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting PHPSESSID: {e}", exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_webinar_table(html_content: str) -> List[Dict[str, str]]:
|
|
||||||
"""
|
|
||||||
Парсит таблицу с вебинарами
|
|
||||||
|
|
||||||
Args:
|
|
||||||
html_content: HTML контент страницы
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict]: Список вебинаров или пустой список
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
soup = BeautifulSoup(html_content, 'html.parser')
|
|
||||||
|
|
||||||
# Находим таблицу с вебинарами
|
|
||||||
meetings_div = soup.find('div', {'id': 'meetings'})
|
|
||||||
if not meetings_div:
|
|
||||||
logger.warning("meetings div not found")
|
|
||||||
return []
|
|
||||||
|
|
||||||
table = meetings_div.find('table', {'class': 'table table-zebra'})
|
|
||||||
if not table:
|
|
||||||
logger.warning("table not found")
|
|
||||||
return []
|
|
||||||
|
|
||||||
tbody = table.find('tbody')
|
|
||||||
if not tbody:
|
|
||||||
logger.warning("tbody not found")
|
|
||||||
return []
|
|
||||||
|
|
||||||
rows = tbody.find_all('tr')
|
|
||||||
if not rows:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Проверяем на сообщение "Жодного онлайн уроку зараз"
|
|
||||||
first_row = rows[0]
|
|
||||||
td = first_row.find('td')
|
|
||||||
if td and 'Жодного онлайн уроку зараз' in td.get_text(strip=True):
|
|
||||||
logger.info("No webinars available")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Парсим активные вебинары
|
|
||||||
webinars = []
|
|
||||||
for row in rows:
|
|
||||||
tds = row.find_all('td')
|
|
||||||
if len(tds) >= 4:
|
|
||||||
webinar = {
|
|
||||||
'topic': tds[0].get_text(strip=True),
|
|
||||||
'course': tds[1].get_text(strip=True),
|
|
||||||
'teacher': tds[2].get_text(strip=True),
|
|
||||||
'join_link': tds[3].find('a')['href'] if tds[3].find('a') else ''
|
|
||||||
}
|
|
||||||
webinars.append(webinar)
|
|
||||||
|
|
||||||
logger.info(f"Parsed {len(webinars)} webinars")
|
|
||||||
return webinars
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error parsing webinar table: {e}", exc_info=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_webinars_with_playwright() -> Optional[List[Dict[str, str]]]:
|
|
||||||
"""
|
|
||||||
Получает список активных вебинаров используя Playwright для динамического контента
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict]: Список вебинаров или None в случае ошибки
|
|
||||||
"""
|
|
||||||
# Получаем PHPSESSID
|
|
||||||
phpsessid = get_phpsessid()
|
|
||||||
if not phpsessid:
|
|
||||||
logger.error("Failed to get PHPSESSID")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Подготавливаем cookies для Playwright
|
|
||||||
cookies = [
|
|
||||||
{
|
|
||||||
'name': 'PHPSESSID',
|
|
||||||
'value': phpsessid,
|
|
||||||
'domain': config.EDU_HOST,
|
|
||||||
'path': '/'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Запрос к Playwright серверу
|
|
||||||
playwright_request = {
|
|
||||||
'url': config.EDU_WEBINAR_URL,
|
|
||||||
'cookies': cookies,
|
|
||||||
'wait_until': 'networkidle', # Ждем пока сеть успокоится
|
|
||||||
'wait_time': config.WEBINAR_WAIT_TIME * 1000, # Дополнительное ожидание в миллисекундах
|
|
||||||
'user_agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'Bearer {config.PLAYWRIGHT_API_KEY}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(f"Fetching webinars via Playwright from {config.EDU_WEBINAR_URL}")
|
|
||||||
logger.info(f"Will wait {config.WEBINAR_WAIT_TIME} seconds for dynamic content")
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"{config.PLAYWRIGHT_SERVER}/render",
|
|
||||||
json=playwright_request,
|
|
||||||
headers=headers,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
logger.error(f"Playwright server returned status {response.status_code}")
|
|
||||||
logger.error(f"Response: {response.text}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = response.json()
|
|
||||||
html_content = result.get('html', '')
|
|
||||||
|
|
||||||
if not html_content:
|
|
||||||
logger.error("No HTML content in Playwright response")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Парсим таблицу
|
|
||||||
webinars = parse_webinar_table(html_content)
|
|
||||||
return webinars
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching webinars via Playwright: {e}", exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_webinars() -> Optional[List[Dict[str, str]]]:
|
|
||||||
"""
|
|
||||||
Получает список активных вебинаров
|
|
||||||
Сначала пробует через Playwright (для динамического контента),
|
|
||||||
при неудаче - через обычный requests
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict]: Список вебинаров или None в случае ошибки
|
|
||||||
"""
|
|
||||||
# Пробуем через Playwright
|
|
||||||
logger.info("Attempting to fetch via Playwright for dynamic content")
|
|
||||||
webinars = fetch_webinars_with_playwright()
|
|
||||||
|
|
||||||
if webinars is not None:
|
|
||||||
return webinars
|
|
||||||
|
|
||||||
# Fallback на обычный requests
|
|
||||||
logger.warning("Playwright fetch failed, falling back to simple requests")
|
|
||||||
|
|
||||||
# Получаем PHPSESSID
|
|
||||||
phpsessid = get_phpsessid()
|
|
||||||
if not phpsessid:
|
|
||||||
logger.error("Failed to get PHPSESSID")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Запрашиваем страницу с вебинарами
|
|
||||||
try:
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
||||||
'Accept-Language': 'ru-RU,ru;q=0.9,uk;q=0.8',
|
|
||||||
'Referer': f'https://{config.EDU_HOST}/'
|
|
||||||
}
|
|
||||||
|
|
||||||
cookies = {
|
|
||||||
'PHPSESSID': phpsessid
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(f"Fetching webinars from {config.EDU_WEBINAR_URL}")
|
|
||||||
response = requests.get(
|
|
||||||
config.EDU_WEBINAR_URL,
|
|
||||||
headers=headers,
|
|
||||||
cookies=cookies,
|
|
||||||
timeout=15
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
logger.error(f"Failed to fetch webinars page: {response.status_code}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Парсим таблицу
|
|
||||||
webinars = parse_webinar_table(response.text)
|
|
||||||
return webinars
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching webinars: {e}", exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def format_webinar_message(webinars: List[Dict[str, str]]) -> str:
|
|
||||||
"""
|
|
||||||
Форматирует список вебинаров для отправки в Telegram
|
|
||||||
|
|
||||||
Args:
|
|
||||||
webinars: Список вебинаров
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Отформатированное сообщение
|
|
||||||
"""
|
|
||||||
if not webinars:
|
|
||||||
return "📭 Жодного онлайн уроку зараз"
|
|
||||||
|
|
||||||
message = "🎓 <b>Активні онлайн уроки:</b>\n\n"
|
|
||||||
|
|
||||||
for i, webinar in enumerate(webinars, 1):
|
|
||||||
message += f"<b>{i}. {webinar['topic']}</b>\n"
|
|
||||||
message += f"📚 Курс: {webinar['course']}\n"
|
|
||||||
message += f"👨🏫 Вчитель: {webinar['teacher']}\n"
|
|
||||||
|
|
||||||
if webinar['join_link']:
|
|
||||||
full_link = webinar['join_link']
|
|
||||||
if not full_link.startswith('http'):
|
|
||||||
full_link = f"https://{config.EDU_HOST}{webinar['join_link']}"
|
|
||||||
message += f"🔗 <a href='{full_link}'>Увійти до уроку</a>\n"
|
|
||||||
|
|
||||||
message += "\n"
|
|
||||||
|
|
||||||
return message
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Replace environment variables in the Nginx configuration template
|
|
||||||
envsubst '${MINIO_PRIVATE_BUCKET} ${MINIO_PUBLIC_BUCKET}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# Start Nginx
|
|
||||||
exec nginx -g 'daemon off;'
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
client_max_body_size 100M;
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
location / {
|
|
||||||
proxy_pass http://frontend:80;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://app:9000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# MCP
|
|
||||||
location ~ ^/(sse|messages) {
|
|
||||||
proxy_pass http://mcp:3000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
|
|
||||||
# Important SSE settings
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection "";
|
|
||||||
|
|
||||||
# Disable buffering so events are sent immediately
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_cache off;
|
|
||||||
|
|
||||||
# Increase timeouts so connection stays open
|
|
||||||
proxy_read_timeout 3600s;
|
|
||||||
proxy_send_timeout 3600s;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# MinIO private bucket
|
|
||||||
location /${MINIO_PRIVATE_BUCKET}/ {
|
|
||||||
proxy_pass http://minio:9000/${MINIO_PRIVATE_BUCKET}/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
|
|
||||||
# MinIO public bucket
|
|
||||||
location /${MINIO_PUBLIC_BUCKET}/ {
|
|
||||||
proxy_pass http://minio:9000/${MINIO_PUBLIC_BUCKET}/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
|
|
||||||
# MinIO API - for direct S3 operations
|
|
||||||
# location /minio/api/ {
|
|
||||||
# proxy_pass http://minio:9000/;
|
|
||||||
# proxy_set_header Host $host;
|
|
||||||
# proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
# proxy_buffering off;
|
|
||||||
# }
|
|
||||||
|
|
||||||
# MinIO Console
|
|
||||||
location /minio-console/ {
|
|
||||||
proxy_pass http://minio:9001/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
# Rewrite location headers
|
|
||||||
proxy_redirect / /minio-console/;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y redis-tools && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install requests redis
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Run the bot
|
||||||
|
CMD ["python", "bot.py"]
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import redis
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Load configuration
|
||||||
|
LOGIN = os.getenv('EDU_LOGIN')
|
||||||
|
PASSWORD = os.getenv('EDU_PASSWORD')
|
||||||
|
URL_LOGIN = os.getenv('EDU_URL_LOGIN', 'https://edu.edu.vn.ua/user/login')
|
||||||
|
URL_VERIFY = os.getenv('EDU_URL_VERIFY', 'https://edu.edu.vn.ua/course/userlist')
|
||||||
|
INTERVAL = int(os.getenv('PHPSESSID_INTERVAL', 10))
|
||||||
|
USER_AGENT = os.getenv('USER_AGENT', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36')
|
||||||
|
REDIS_HOST = os.getenv('REDIS_HOST', 'redis')
|
||||||
|
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
||||||
|
|
||||||
|
SUCCESS_FILE = '/tmp/last_success'
|
||||||
|
|
||||||
|
def touch_success_file():
|
||||||
|
"""Updates the timestamp of the success file for healthchecks."""
|
||||||
|
try:
|
||||||
|
with open(SUCCESS_FILE, 'w') as f:
|
||||||
|
f.write(str(datetime.now().timestamp()))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to touch success file: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.info("Starting Session Keeper Bot")
|
||||||
|
|
||||||
|
# Connect to Redis
|
||||||
|
try:
|
||||||
|
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
|
||||||
|
redis_client.ping()
|
||||||
|
logger.info(f"Connected to Redis at {REDIS_HOST}:{REDIS_PORT}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to connect to Redis: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
# Set headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
|
'Cache-Control': 'max-age=0',
|
||||||
|
'Upgrade-Insecure-Requests': '1',
|
||||||
|
'Sec-Fetch-Site': 'same-origin',
|
||||||
|
'Sec-Fetch-Mode': 'navigate',
|
||||||
|
'Sec-Fetch-User': '?1',
|
||||||
|
'Sec-Fetch-Dest': 'document',
|
||||||
|
'Sec-Ch-Ua': '"Not_A Brand";v="99", "Chromium";v="142"',
|
||||||
|
'Sec-Ch-Ua-Mobile': '?0',
|
||||||
|
'Sec-Ch-Ua-Platform': '"Linux"',
|
||||||
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
|
'Priority': 'u=0, i'
|
||||||
|
}
|
||||||
|
session.headers.update(headers)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
logger.info("Attempting login...")
|
||||||
|
|
||||||
|
# Login payload
|
||||||
|
payload = {
|
||||||
|
'login': LOGIN,
|
||||||
|
'password': PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
# Perform Login
|
||||||
|
# Note: The user request shows a POST to /user/login with form data
|
||||||
|
# We need to make sure we handle the PHPSESSID correctly.
|
||||||
|
# If we already have a PHPSESSID, requests will send it.
|
||||||
|
|
||||||
|
login_response = session.post(URL_LOGIN, data=payload, allow_redirects=True)
|
||||||
|
|
||||||
|
logger.info(f"Login Response Status: {login_response.status_code}")
|
||||||
|
logger.info(f"Cookies after login: {session.cookies.get_dict()}")
|
||||||
|
|
||||||
|
# Verify Session
|
||||||
|
logger.info("Verifying session...")
|
||||||
|
verify_response = session.get(URL_VERIFY, allow_redirects=False)
|
||||||
|
|
||||||
|
logger.info(f"Verify Response Status: {verify_response.status_code}")
|
||||||
|
|
||||||
|
if verify_response.status_code == 200:
|
||||||
|
logger.info("Session verification SUCCESS (200 OK).")
|
||||||
|
touch_success_file()
|
||||||
|
|
||||||
|
# Save PHPSESSID to Redis
|
||||||
|
phpsessid = session.cookies.get('PHPSESSID')
|
||||||
|
if phpsessid:
|
||||||
|
try:
|
||||||
|
redis_client.set('EDU_PHPSESSID', phpsessid)
|
||||||
|
logger.info(f"Saved PHPSESSID to Redis: {phpsessid}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save PHPSESSID to Redis: {e}")
|
||||||
|
elif verify_response.status_code == 302:
|
||||||
|
logger.warning("Session verification FAILED (302 Redirect). Session might be invalid.")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Session verification returned unexpected status: {verify_response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An error occurred: {e}")
|
||||||
|
|
||||||
|
logger.info(f"Sleeping for {INTERVAL} minutes...")
|
||||||
|
time.sleep(INTERVAL * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Устанавливаем зависимости
|
|
||||||
RUN pip install --no-cache-dir flask requests
|
|
||||||
|
|
||||||
# Копируем код бота
|
|
||||||
COPY main.py .
|
|
||||||
|
|
||||||
# Открываем порт
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Запускаем бот
|
|
||||||
CMD ["python", "-u", "main.py"]
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import os
|
|
||||||
import logging
|
|
||||||
from flask import Flask, request, jsonify
|
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Настройка логирования
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# Конфигурация из переменных окружения
|
|
||||||
EDU_HOST = os.getenv('EDU_HOST', 'edu.edu.vn.ua')
|
|
||||||
EDU_LOGIN = os.getenv('EDU_LOGIN', '')
|
|
||||||
EDU_PASSWORD = os.getenv('EDU_PASSWORD', '')
|
|
||||||
BOT_PORT = int(os.getenv('BOT_PORT', '5000'))
|
|
||||||
BOT_HOST = os.getenv('BOT_HOST', '0.0.0.0')
|
|
||||||
|
|
||||||
# Кэш для хранения актуальной сессии
|
|
||||||
session_cache = {
|
|
||||||
'phpsessid': None,
|
|
||||||
'expires_at': None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def login_and_get_session():
|
|
||||||
"""
|
|
||||||
Выполняет логин и возвращает новый PHPSESSID
|
|
||||||
"""
|
|
||||||
url = f"https://{EDU_HOST}/user/login"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Cache-Control': 'max-age=0',
|
|
||||||
'Sec-Ch-Ua': '"Chromium";v="141", "Not?A_Brand";v="8"',
|
|
||||||
'Sec-Ch-Ua-Mobile': '?0',
|
|
||||||
'Sec-Ch-Ua-Platform': '"Linux"',
|
|
||||||
'Accept-Language': 'ru-RU,ru;q=0.9',
|
|
||||||
'Origin': f'https://{EDU_HOST}',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Upgrade-Insecure-Requests': '1',
|
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
||||||
'Sec-Fetch-Site': 'same-origin',
|
|
||||||
'Sec-Fetch-Mode': 'navigate',
|
|
||||||
'Sec-Fetch-User': '?1',
|
|
||||||
'Sec-Fetch-Dest': 'document',
|
|
||||||
'Referer': f'https://{EDU_HOST}/',
|
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
|
||||||
'Priority': 'u=0, i'
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'login': EDU_LOGIN,
|
|
||||||
'password': EDU_PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info(f"Attempting login to {url}")
|
|
||||||
response = requests.post(
|
|
||||||
url,
|
|
||||||
data=data,
|
|
||||||
headers=headers,
|
|
||||||
allow_redirects=False,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
# Получаем PHPSESSID из cookies
|
|
||||||
phpsessid = response.cookies.get('PHPSESSID')
|
|
||||||
|
|
||||||
if phpsessid:
|
|
||||||
logger.info(f"Login successful, got PHPSESSID: {phpsessid[:10]}...")
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'phpsessid': phpsessid,
|
|
||||||
'status_code': response.status_code
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
logger.warning(f"Login failed: no PHPSESSID in response. Status: {response.status_code}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'error': 'No PHPSESSID in response',
|
|
||||||
'status_code': response.status_code
|
|
||||||
}
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logger.error(f"Login request failed: {str(e)}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def validate_phpsessid(phpsessid):
|
|
||||||
"""
|
|
||||||
Проверяет валидность существующего PHPSESSID
|
|
||||||
"""
|
|
||||||
url = f"https://{EDU_HOST}/"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
|
||||||
}
|
|
||||||
|
|
||||||
cookies = {
|
|
||||||
'PHPSESSID': phpsessid
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers, cookies=cookies, timeout=10)
|
|
||||||
|
|
||||||
# Проверяем, не редиректит ли на страницу логина
|
|
||||||
is_valid = response.status_code == 200 and '/user/login' not in response.url
|
|
||||||
|
|
||||||
return {
|
|
||||||
'valid': is_valid,
|
|
||||||
'status_code': response.status_code,
|
|
||||||
'url': response.url
|
|
||||||
}
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logger.error(f"Validation request failed: {str(e)}")
|
|
||||||
return {
|
|
||||||
'valid': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
|
||||||
def health():
|
|
||||||
"""Health check endpoint"""
|
|
||||||
return jsonify({'status': 'ok', 'timestamp': datetime.now().isoformat()})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/get-session', methods=['POST', 'GET'])
|
|
||||||
def get_session():
|
|
||||||
"""
|
|
||||||
Основной endpoint для получения валидного PHPSESSID
|
|
||||||
Возвращает кэшированную сессию или создает новую
|
|
||||||
"""
|
|
||||||
result = login_and_get_session()
|
|
||||||
|
|
||||||
if result['success']:
|
|
||||||
session_cache['phpsessid'] = result['phpsessid']
|
|
||||||
session_cache['last_updated'] = datetime.now().isoformat()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'phpsessid': result['phpsessid'],
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': result.get('error', 'Login failed'),
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/validate-session', methods=['POST'])
|
|
||||||
def validate_session():
|
|
||||||
"""
|
|
||||||
Проверяет валидность переданного PHPSESSID
|
|
||||||
"""
|
|
||||||
data = request.get_json() or {}
|
|
||||||
phpsessid = data.get('phpsessid') or request.args.get('phpsessid')
|
|
||||||
|
|
||||||
if not phpsessid:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'PHPSESSID not provided'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
validation_result = validate_phpsessid(phpsessid)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'valid': validation_result.get('valid', False),
|
|
||||||
'details': validation_result,
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/refresh-session', methods=['POST', 'GET'])
|
|
||||||
def refresh_session():
|
|
||||||
"""
|
|
||||||
Принудительно обновляет сессию
|
|
||||||
"""
|
|
||||||
result = login_and_get_session()
|
|
||||||
|
|
||||||
if result['success']:
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'phpsessid': result['phpsessid'],
|
|
||||||
'message': 'Session refreshed successfully',
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': result.get('error', 'Failed to refresh session'),
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if not EDU_LOGIN or not EDU_PASSWORD:
|
|
||||||
logger.error("EDU_LOGIN and EDU_PASSWORD must be set!")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
logger.info(f"Starting PHPSESSID validator bot on {BOT_HOST}:{BOT_PORT}")
|
|
||||||
logger.info(f"Target host: {EDU_HOST}")
|
|
||||||
|
|
||||||
app.run(host=BOT_HOST, port=BOT_PORT, debug=False)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
flask==3.0.0
|
|
||||||
requests==2.31.0
|
|
||||||
Werkzeug==3.0.1
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install --upgrade pip && pip install playwright==1.56.0 redis requests "python-telegram-bot[job-queue]"
|
||||||
|
|
||||||
|
COPY checker.py .
|
||||||
|
|
||||||
|
CMD ["python", "checker.py"]
|
||||||
@@ -0,0 +1,589 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import redis
|
||||||
|
import json
|
||||||
|
|
||||||
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
WEBINAR_URL = os.getenv('WEBINAR_URL', 'https://edu.edu.vn.ua/webinar/useractive')
|
||||||
|
WEBINAR_CHECK_INTERVAL = int(os.getenv('WEBINAR_CHECK_INTERVAL', 60))
|
||||||
|
REDIS_HOST = os.getenv('REDIS_HOST', 'redis')
|
||||||
|
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
||||||
|
PLAYWRIGHT_WS = os.getenv('PLAYWRIGHT_WS', 'ws://playwright-service:3000/ws')
|
||||||
|
USER_AGENT = os.getenv('USER_AGENT', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36')
|
||||||
|
WEBINAR_TELEGRAM_TOKEN = os.getenv('WEBINAR_TELEGRAM_TOKEN')
|
||||||
|
ADMIN_ID = int(os.getenv('WEBINAR_ADMIN_ID', '0'))
|
||||||
|
|
||||||
|
# Redis Keys
|
||||||
|
KEY_WHITELIST = "bot:whitelist"
|
||||||
|
KEY_WHITELIST_ENABLED = "bot:whitelist_enabled"
|
||||||
|
KEY_SUBSCRIBERS = "bot:subscribers"
|
||||||
|
KEY_PHPSESSID = "EDU_PHPSESSID"
|
||||||
|
KEY_WEBINAR_HISTORY = "bot:webinar_history" # Stores last 5 webinars
|
||||||
|
|
||||||
|
# Initialize Redis
|
||||||
|
try:
|
||||||
|
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
|
||||||
|
redis_client.ping()
|
||||||
|
logger.info(f"Connected to Redis at {REDIS_HOST}:{REDIS_PORT}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to connect to Redis: {e}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# --- Translations ---
|
||||||
|
|
||||||
|
TRANSLATIONS = {
|
||||||
|
'ru': {
|
||||||
|
'welcome': "👋 Привет, {name}!\n\nЯ бот-уведомитель о вебинарах. Я буду сообщать вам, когда появится новый вебинар.\nВы подписаны на уведомления.",
|
||||||
|
'welcome_admin': "\n\n👑 <b>Режим администратора активен</b>",
|
||||||
|
'access_denied': "⛔ Доступ запрещен. Вас нет в белом списке.",
|
||||||
|
'help_title': "🤖 <b>Помощь по боту</b>\n\n",
|
||||||
|
'help_commands': "/start - Подписаться на уведомления\n/help - Показать это сообщение\n/language - Сменить язык",
|
||||||
|
'help_admin': "\n<b>Команды администратора:</b>\n/adduser [user_id] - Добавить пользователя в белый список\n/removeuser [user_id] - Удалить пользователя из белого списка\nИли используйте панель ниже для управления настройками.",
|
||||||
|
'admin_only': "⛔ Только для администратора!",
|
||||||
|
'user_added': "✅ Пользователь {user_id} добавлен в белый список",
|
||||||
|
'user_removed': "✅ Пользователь {user_id} удален из белого списка",
|
||||||
|
'user_not_in_whitelist': "⚠️ Пользователь {user_id} не был в белом списке",
|
||||||
|
'cannot_remove_admin': "❌ Невозможно удалить администратора из белого списка",
|
||||||
|
'invalid_user_id': "❌ Неверный ID пользователя. Должно быть число.",
|
||||||
|
'usage_adduser': "Использование: /adduser [user_id]",
|
||||||
|
'usage_removeuser': "Использование: /removeuser [user_id]",
|
||||||
|
'whitelist_enabled': "✅ Белый список включен",
|
||||||
|
'whitelist_disabled': "✅ Белый список отключен",
|
||||||
|
'whitelist_title': "📋 <b>Белый список:</b>\n",
|
||||||
|
'subscribers_title': "👥 <b>Подписчики:</b>\n",
|
||||||
|
'empty': "Пусто",
|
||||||
|
'force_check_running': "🔄 Запускаю проверку...",
|
||||||
|
'check_failed': "❌ Проверка не удалась. Смотрите логи.",
|
||||||
|
'check_completed_none': "✅ Проверка завершена. Вебинаров не найдено.",
|
||||||
|
'check_completed': "✅ Проверка завершена. Найдено {count} вебинар(ов)!",
|
||||||
|
'toggle_whitelist_disable': "🔒 Отключить белый список",
|
||||||
|
'toggle_whitelist_enable': "🔓 Включить белый список",
|
||||||
|
'view_whitelist': "📋 Посмотреть белый список",
|
||||||
|
'view_subscribers': "👥 Посмотреть подписчиков",
|
||||||
|
'force_check': "🔄 Принудительная проверка",
|
||||||
|
'webinar_found': "🎓 <b>Новый вебинар!</b>\n\n",
|
||||||
|
'webinar_item': "📌 <b>{name}</b>\n🔗 https://edu.edu.vn.ua{url}",
|
||||||
|
'select_language': "🌐 <b>Выберите язык / Оберіть мову / Select language:</b>",
|
||||||
|
'language_changed': "✅ Язык изменен на русский",
|
||||||
|
'flag_ru': "🇷🇺 Русский",
|
||||||
|
'flag_uk': "🇺🇦 Українська",
|
||||||
|
'flag_en': "🇬🇧 English",
|
||||||
|
},
|
||||||
|
'uk': {
|
||||||
|
'welcome': "👋 Привіт, {name}!\n\nЯ бот-сповіщувач про вебінари. Я повідомлятиму вас, коли з'явиться новий вебінар.\nВи підписані на сповіщення.",
|
||||||
|
'welcome_admin': "\n\n👑 <b>Режим адміністратора активний</b>",
|
||||||
|
'access_denied': "⛔ Доступ заборонено. Вас немає в білому списку.",
|
||||||
|
'help_title': "🤖 <b>Довідка по боту</b>\n\n",
|
||||||
|
'help_commands': "/start - Підписатися на сповіщення\n/help - Показати це повідомлення\n/language - Змінити мову",
|
||||||
|
'help_admin': "\n<b>Команди адміністратора:</b>\n/adduser [user_id] - Додати користувача до білого списку\n/removeuser [user_id] - Видалити користувача з білого списку\nАбо використовуйте панель нижче для керування налаштуваннями.",
|
||||||
|
'admin_only': "⛔ Тільки для адміністратора!",
|
||||||
|
'user_added': "✅ Користувач {user_id} доданий до білого списку",
|
||||||
|
'user_removed': "✅ Користувач {user_id} видалений з білого списку",
|
||||||
|
'user_not_in_whitelist': "⚠️ Користувач {user_id} не був у білому списку",
|
||||||
|
'cannot_remove_admin': "❌ Неможливо видалити адміністратора з білого списку",
|
||||||
|
'invalid_user_id': "❌ Невірний ID користувача. Має бути число.",
|
||||||
|
'usage_adduser': "Використання: /adduser [user_id]",
|
||||||
|
'usage_removeuser': "Використання: /removeuser [user_id]",
|
||||||
|
'whitelist_enabled': "✅ Білий список увімкнено",
|
||||||
|
'whitelist_disabled': "✅ Білий список вимкнено",
|
||||||
|
'whitelist_title': "📋 <b>Білий список:</b>\n",
|
||||||
|
'subscribers_title': "👥 <b>Підписники:</b>\n",
|
||||||
|
'empty': "Порожньо",
|
||||||
|
'force_check_running': "🔄 Запускаю перевірку...",
|
||||||
|
'check_failed': "❌ Перевірка не вдалася. Дивіться логи.",
|
||||||
|
'check_completed_none': "✅ Перевірка завершена. Вебінарів не знайдено.",
|
||||||
|
'check_completed': "✅ Перевірка завершена. Знайдено {count} вебінар(ів)!",
|
||||||
|
'toggle_whitelist_disable': "🔒 Вимкнути білий список",
|
||||||
|
'toggle_whitelist_enable': "🔓 Увімкнути білий список",
|
||||||
|
'view_whitelist': "📋 Переглянути білий список",
|
||||||
|
'view_subscribers': "👥 Переглянути підписників",
|
||||||
|
'force_check': "🔄 Примусова перевірка",
|
||||||
|
'webinar_found': "🎓 <b>Новий вебінар!</b>\n\n",
|
||||||
|
'webinar_item': "📌 <b>{name}</b>\n🔗 https://edu.edu.vn.ua{url}",
|
||||||
|
'select_language': "🌐 <b>Виберіть мову / Выберите язык / Select language:</b>",
|
||||||
|
'language_changed': "✅ Мову змінено на українську",
|
||||||
|
'flag_ru': "🇷🇺 Русский",
|
||||||
|
'flag_uk': "🇺🇦 Українська",
|
||||||
|
'flag_en': "🇬🇧 English",
|
||||||
|
},
|
||||||
|
'en': {
|
||||||
|
'welcome': "👋 Hello, {name}!\n\nI am the Webinar Checker Bot. I will notify you when a new webinar appears.\nYou have been subscribed to notifications.",
|
||||||
|
'welcome_admin': "\n\n👑 <b>Admin Mode Active</b>",
|
||||||
|
'access_denied': "⛔ Access denied. You are not on the whitelist.",
|
||||||
|
'help_title': "🤖 <b>Bot Help</b>\n\n",
|
||||||
|
'help_commands': "/start - Subscribe to notifications\n/help - Show this message\n/language - Change language",
|
||||||
|
'help_admin': "\n<b>Admin Commands:</b>\n/adduser [user_id] - Add user to whitelist\n/removeuser [user_id] - Remove user from whitelist\nOr use the panel below to manage settings.",
|
||||||
|
'admin_only': "⛔ Admin only!",
|
||||||
|
'user_added': "✅ User {user_id} added to whitelist",
|
||||||
|
'user_removed': "✅ User {user_id} removed from whitelist",
|
||||||
|
'user_not_in_whitelist': "⚠️ User {user_id} was not in whitelist",
|
||||||
|
'cannot_remove_admin': "❌ Cannot remove admin from whitelist",
|
||||||
|
'invalid_user_id': "❌ Invalid user ID. Must be a number.",
|
||||||
|
'usage_adduser': "Usage: /adduser [user_id]",
|
||||||
|
'usage_removeuser': "Usage: /removeuser [user_id]",
|
||||||
|
'whitelist_enabled': "✅ Whitelist Enabled",
|
||||||
|
'whitelist_disabled': "✅ Whitelist Disabled",
|
||||||
|
'whitelist_title': "📋 <b>Whitelist:</b>\n",
|
||||||
|
'subscribers_title': "👥 <b>Subscribers:</b>\n",
|
||||||
|
'empty': "Empty",
|
||||||
|
'force_check_running': "🔄 Running immediate check...",
|
||||||
|
'check_failed': "❌ Check failed. See logs for details.",
|
||||||
|
'check_completed_none': "✅ Check completed. No webinars found.",
|
||||||
|
'check_completed': "✅ Check completed. Found {count} webinar(s)!",
|
||||||
|
'toggle_whitelist_disable': "🔒 Disable Whitelist",
|
||||||
|
'toggle_whitelist_enable': "🔓 Enable Whitelist",
|
||||||
|
'view_whitelist': "📋 View Whitelist",
|
||||||
|
'view_subscribers': "👥 View Subscribers",
|
||||||
|
'force_check': "🔄 Force Check",
|
||||||
|
'webinar_found': "🎓 <b>New webinar found!</b>\n\n",
|
||||||
|
'webinar_item': "📌 <b>{name}</b>\n🔗 https://edu.edu.vn.ua{url}",
|
||||||
|
'select_language': "🌐 <b>Select language / Виберіть мову / Выберите язык:</b>",
|
||||||
|
'language_changed': "✅ Language changed to English",
|
||||||
|
'flag_ru': "🇷🇺 Русский",
|
||||||
|
'flag_uk': "🇺🇦 Українська",
|
||||||
|
'flag_en': "🇬🇧 English",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Language Helper Functions ---
|
||||||
|
|
||||||
|
def get_user_language(user_id: int) -> str:
|
||||||
|
"""Get user's preferred language from Redis. Default: Ukrainian."""
|
||||||
|
lang = redis_client.get(f"user:{user_id}:language")
|
||||||
|
return lang if lang in ['ru', 'uk', 'en'] else 'uk'
|
||||||
|
|
||||||
|
def set_user_language(user_id: int, lang: str):
|
||||||
|
"""Save user's language preference to Redis."""
|
||||||
|
if lang in ['ru', 'uk', 'en']:
|
||||||
|
redis_client.set(f"user:{user_id}:language", lang)
|
||||||
|
logger.info(f"User {user_id} language set to {lang}")
|
||||||
|
|
||||||
|
def t(user_id: int, key: str, **kwargs) -> str:
|
||||||
|
"""Translate message for user with optional formatting."""
|
||||||
|
lang = get_user_language(user_id)
|
||||||
|
message = TRANSLATIONS.get(lang, TRANSLATIONS['uk']).get(key, key)
|
||||||
|
if kwargs:
|
||||||
|
return message.format(**kwargs)
|
||||||
|
return message
|
||||||
|
|
||||||
|
def get_language_keyboard():
|
||||||
|
"""Generate language selection keyboard."""
|
||||||
|
keyboard = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🇷🇺 Русский", callback_data="lang_ru"),
|
||||||
|
InlineKeyboardButton("🇺🇦 Українська", callback_data="lang_uk"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🇬🇧 English", callback_data="lang_en"),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
return InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
|
# --- Helper Functions ---
|
||||||
|
|
||||||
|
def is_whitelisted(user_id: int) -> bool:
|
||||||
|
"""Check if user is allowed to use the bot."""
|
||||||
|
if user_id == ADMIN_ID:
|
||||||
|
return True
|
||||||
|
|
||||||
|
enabled = redis_client.get(KEY_WHITELIST_ENABLED)
|
||||||
|
if enabled == "0": # Whitelist disabled
|
||||||
|
return True
|
||||||
|
|
||||||
|
return redis_client.sismember(KEY_WHITELIST, str(user_id))
|
||||||
|
|
||||||
|
def get_admin_keyboard(user_id: int):
|
||||||
|
"""Generate admin panel keyboard."""
|
||||||
|
whitelist_enabled = redis_client.get(KEY_WHITELIST_ENABLED) != "0"
|
||||||
|
toggle_text = t(user_id, 'toggle_whitelist_disable') if whitelist_enabled else t(user_id, 'toggle_whitelist_enable')
|
||||||
|
|
||||||
|
keyboard = [
|
||||||
|
[InlineKeyboardButton(toggle_text, callback_data="toggle_whitelist")],
|
||||||
|
[InlineKeyboardButton(t(user_id, 'view_whitelist'), callback_data="view_whitelist")],
|
||||||
|
[InlineKeyboardButton(t(user_id, 'view_subscribers'), callback_data="view_subscribers")],
|
||||||
|
[InlineKeyboardButton(t(user_id, 'force_check'), callback_data="force_check")]
|
||||||
|
]
|
||||||
|
return InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
|
# --- Command Handlers ---
|
||||||
|
|
||||||
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Handle /start command."""
|
||||||
|
user = update.effective_user
|
||||||
|
logger.info(f"User {user.id} ({user.username}) started the bot.")
|
||||||
|
|
||||||
|
if not is_whitelisted(user.id):
|
||||||
|
await update.message.reply_text(t(user.id, 'access_denied'))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add to subscribers
|
||||||
|
redis_client.sadd(KEY_SUBSCRIBERS, user.id)
|
||||||
|
|
||||||
|
msg = t(user.id, 'welcome', name=user.first_name)
|
||||||
|
|
||||||
|
if user.id == ADMIN_ID:
|
||||||
|
msg += t(user.id, 'welcome_admin')
|
||||||
|
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user.id))
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(msg, parse_mode='HTML')
|
||||||
|
|
||||||
|
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Handle /help command."""
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
msg = t(user_id, 'help_title') + t(user_id, 'help_commands')
|
||||||
|
|
||||||
|
if user_id == ADMIN_ID:
|
||||||
|
msg += t(user_id, 'help_admin')
|
||||||
|
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user_id))
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(msg, parse_mode='HTML')
|
||||||
|
|
||||||
|
async def language_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Handle /language command."""
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
await update.message.reply_text(
|
||||||
|
t(user_id, 'select_language'),
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=get_language_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def add_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Add user to whitelist (admin only)."""
|
||||||
|
admin_id = update.effective_user.id
|
||||||
|
if admin_id != ADMIN_ID:
|
||||||
|
await update.message.reply_text(t(admin_id, 'admin_only'))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not context.args:
|
||||||
|
await update.message.reply_text(t(admin_id, 'usage_adduser'))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = int(context.args[0])
|
||||||
|
redis_client.sadd(KEY_WHITELIST, str(user_id))
|
||||||
|
await update.message.reply_text(t(admin_id, 'user_added', user_id=user_id))
|
||||||
|
logger.info(f"Admin added user {user_id} to whitelist")
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text(t(admin_id, 'invalid_user_id'))
|
||||||
|
|
||||||
|
async def remove_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Remove user from whitelist (admin only)."""
|
||||||
|
admin_id = update.effective_user.id
|
||||||
|
if admin_id != ADMIN_ID:
|
||||||
|
await update.message.reply_text(t(admin_id, 'admin_only'))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not context.args:
|
||||||
|
await update.message.reply_text(t(admin_id, 'usage_removeuser'))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = int(context.args[0])
|
||||||
|
if str(user_id) == str(ADMIN_ID):
|
||||||
|
await update.message.reply_text(t(admin_id, 'cannot_remove_admin'))
|
||||||
|
return
|
||||||
|
|
||||||
|
removed = redis_client.srem(KEY_WHITELIST, str(user_id))
|
||||||
|
if removed:
|
||||||
|
await update.message.reply_text(t(admin_id, 'user_removed', user_id=user_id))
|
||||||
|
logger.info(f"Admin removed user {user_id} from whitelist")
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(t(admin_id, 'user_not_in_whitelist', user_id=user_id))
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text(t(admin_id, 'invalid_user_id'))
|
||||||
|
|
||||||
|
# --- Admin Callbacks ---
|
||||||
|
|
||||||
|
async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Handle admin panel button clicks."""
|
||||||
|
query = update.callback_query
|
||||||
|
user_id = query.from_user.id
|
||||||
|
|
||||||
|
if user_id != ADMIN_ID:
|
||||||
|
await query.answer(t(user_id, 'admin_only'), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await query.answer()
|
||||||
|
data = query.data
|
||||||
|
|
||||||
|
if data == "toggle_whitelist":
|
||||||
|
current = redis_client.get(KEY_WHITELIST_ENABLED)
|
||||||
|
new_state = "0" if current != "0" else "1"
|
||||||
|
redis_client.set(KEY_WHITELIST_ENABLED, new_state)
|
||||||
|
state_text = t(user_id, 'whitelist_disabled') if new_state == "0" else t(user_id, 'whitelist_enabled')
|
||||||
|
await query.edit_message_reply_markup(reply_markup=get_admin_keyboard(user_id))
|
||||||
|
await query.message.reply_text(state_text)
|
||||||
|
|
||||||
|
elif data == "view_whitelist":
|
||||||
|
members = redis_client.smembers(KEY_WHITELIST)
|
||||||
|
msg = t(user_id, 'whitelist_title') + ("\n".join(members) if members else t(user_id, 'empty'))
|
||||||
|
await query.message.reply_text(msg, parse_mode='HTML')
|
||||||
|
|
||||||
|
elif data == "view_subscribers":
|
||||||
|
subs = redis_client.smembers(KEY_SUBSCRIBERS)
|
||||||
|
msg = t(user_id, 'subscribers_title') + ("\n".join(subs) if subs else t(user_id, 'empty'))
|
||||||
|
await query.message.reply_text(msg, parse_mode='HTML')
|
||||||
|
|
||||||
|
elif data == "force_check":
|
||||||
|
await query.message.reply_text(t(user_id, 'force_check_running'))
|
||||||
|
result = await check_webinars_job(context)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
await query.message.reply_text(t(user_id, 'check_failed'))
|
||||||
|
elif result == 0:
|
||||||
|
await query.message.reply_text(t(user_id, 'check_completed_none'))
|
||||||
|
else:
|
||||||
|
await query.message.reply_text(t(user_id, 'check_completed', count=result))
|
||||||
|
|
||||||
|
async def language_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Handle language selection button clicks."""
|
||||||
|
query = update.callback_query
|
||||||
|
user_id = query.from_user.id
|
||||||
|
data = query.data
|
||||||
|
|
||||||
|
if data.startswith("lang_"):
|
||||||
|
lang = data.split("_")[1]
|
||||||
|
set_user_language(user_id, lang)
|
||||||
|
await query.answer()
|
||||||
|
await query.edit_message_text(
|
||||||
|
t(user_id, 'language_changed'),
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Webinar Checking Job ---
|
||||||
|
|
||||||
|
def get_webinar_key(name: str, url: str) -> str:
|
||||||
|
"""Generate unique key for a webinar based on name and URL."""
|
||||||
|
return f"{name}|{url}"
|
||||||
|
|
||||||
|
def get_stored_webinars() -> list:
|
||||||
|
"""Get list of stored webinar keys from Redis."""
|
||||||
|
data = redis_client.get(KEY_WEBINAR_HISTORY)
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
return json.loads(data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to parse webinar history: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def store_webinars(webinar_keys: list):
|
||||||
|
"""Store up to 5 most recent webinar keys in Redis."""
|
||||||
|
# Keep only last 5
|
||||||
|
webinar_keys = webinar_keys[-5:]
|
||||||
|
try:
|
||||||
|
redis_client.set(KEY_WEBINAR_HISTORY, json.dumps(webinar_keys))
|
||||||
|
logger.info(f"Stored {len(webinar_keys)} webinar(s) in history")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to store webinar history: {e}")
|
||||||
|
|
||||||
|
async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Background job to check for webinars using Async Playwright.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Number of webinars found, or None if check failed
|
||||||
|
"""
|
||||||
|
logger.info("Running webinar check...")
|
||||||
|
|
||||||
|
phpsessid = redis_client.get(KEY_PHPSESSID)
|
||||||
|
if not phpsessid:
|
||||||
|
logger.warning("PHPSESSID missing. Skipping check.")
|
||||||
|
# --- DEBUG LOGGING ---
|
||||||
|
try:
|
||||||
|
with open('phpsessid_missing.log', 'a') as f:
|
||||||
|
f.write(f"[{os.getcwd()}] PHPSESSID missing at {context.job.last_run: %Y-%m-%d %H:%M:%S}\n")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to write PHPSESSID debug log: {e}")
|
||||||
|
# ---------------------
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_webinars = [] # List of dicts with name, url, and formatted text
|
||||||
|
content = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with async_playwright() as p:
|
||||||
|
# Connect to remote Playwright service
|
||||||
|
browser = await p.chromium.connect(PLAYWRIGHT_WS)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create browser context with user agent
|
||||||
|
context_browser = await browser.new_context(user_agent=USER_AGENT)
|
||||||
|
|
||||||
|
# Add PHPSESSID cookie
|
||||||
|
await context_browser.add_cookies([{
|
||||||
|
'name': 'PHPSESSID',
|
||||||
|
'value': phpsessid,
|
||||||
|
'domain': 'edu.edu.vn.ua',
|
||||||
|
'path': '/'
|
||||||
|
}])
|
||||||
|
|
||||||
|
# Create new page
|
||||||
|
page = await context_browser.new_page()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Navigate to webinar page
|
||||||
|
await page.goto(WEBINAR_URL, wait_until='domcontentloaded')
|
||||||
|
|
||||||
|
# Wait for the table to load
|
||||||
|
await page.wait_for_selector('#meetings table', timeout=10000)
|
||||||
|
await page.wait_for_timeout(2000)
|
||||||
|
|
||||||
|
# Get page content
|
||||||
|
content = await page.content()
|
||||||
|
|
||||||
|
# Check if "no webinar" message is present
|
||||||
|
if "Жодного онлайн уроку зараз" not in content:
|
||||||
|
logger.info("!!! WEBINAR FOUND !!!")
|
||||||
|
|
||||||
|
# Extract webinar details from table rows
|
||||||
|
rows = page.locator('#meetings table tbody tr')
|
||||||
|
count = await rows.count()
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
row = rows.nth(i)
|
||||||
|
text = await row.inner_text()
|
||||||
|
|
||||||
|
if "Жодного онлайн уроку зараз" not in text:
|
||||||
|
# Extract name (topic) from first column
|
||||||
|
name_elem = row.locator('td').nth(0)
|
||||||
|
name = await name_elem.inner_text()
|
||||||
|
name = name.strip()
|
||||||
|
|
||||||
|
# Extract join URL from fourth column
|
||||||
|
url_elem = row.locator('td').nth(3).locator('a[href*="/webinar/join/"]').first
|
||||||
|
url = await url_elem.get_attribute('href')
|
||||||
|
|
||||||
|
if name and url:
|
||||||
|
current_webinars.append({
|
||||||
|
'name': name,
|
||||||
|
'url': url,
|
||||||
|
'text': text.strip()
|
||||||
|
})
|
||||||
|
logger.info(f"Found webinar: {name} -> {url}")
|
||||||
|
else:
|
||||||
|
logger.info("No webinars found (expected message present)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking page: {e}. Saving content for debug.")
|
||||||
|
# If page content is available, save it on error
|
||||||
|
try:
|
||||||
|
if page and not content:
|
||||||
|
content = await page.content()
|
||||||
|
except Exception:
|
||||||
|
pass # Ignore error during content retrieval on check error
|
||||||
|
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
await page.close()
|
||||||
|
await context_browser.close()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Playwright error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# --- DEBUG LOGGING (Saving last response content) ---
|
||||||
|
if not current_webinars and content: #if no webinars found, save the page content
|
||||||
|
try:
|
||||||
|
with open('response.html', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
logger.info("Saved page content to response.html for debug.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to write debug HTML: {e}")
|
||||||
|
# -----------------------------------------------------
|
||||||
|
|
||||||
|
# Check for NEW webinars and notify
|
||||||
|
if current_webinars:
|
||||||
|
# Get stored webinar history
|
||||||
|
stored_keys = get_stored_webinars()
|
||||||
|
logger.info(f"Stored webinar keys: {stored_keys}")
|
||||||
|
|
||||||
|
# Find new webinars (not in history)
|
||||||
|
new_webinars = []
|
||||||
|
current_keys = []
|
||||||
|
|
||||||
|
for webinar in current_webinars:
|
||||||
|
key = get_webinar_key(webinar['name'], webinar['url'])
|
||||||
|
current_keys.append(key)
|
||||||
|
|
||||||
|
if key not in stored_keys:
|
||||||
|
new_webinars.append(webinar)
|
||||||
|
logger.info(f"NEW webinar detected: {webinar['name']}")
|
||||||
|
|
||||||
|
# Update stored history with current webinars
|
||||||
|
# Merge old and new, keeping only last 5
|
||||||
|
updated_keys = stored_keys + [k for k in current_keys if k not in stored_keys]
|
||||||
|
store_webinars(updated_keys)
|
||||||
|
|
||||||
|
# Notify subscribers ONLY about NEW webinars
|
||||||
|
if new_webinars:
|
||||||
|
subscribers = redis_client.smembers(KEY_SUBSCRIBERS)
|
||||||
|
logger.info(f"Sending notification about {len(new_webinars)} new webinar(s) to {len(subscribers)} subscriber(s)")
|
||||||
|
|
||||||
|
for sub_id in subscribers:
|
||||||
|
try:
|
||||||
|
# Build message in user's language
|
||||||
|
webinar_items = "\n\n".join([
|
||||||
|
t(int(sub_id), 'webinar_item', name=w['name'], url=w['url'])
|
||||||
|
for w in new_webinars
|
||||||
|
])
|
||||||
|
message = t(int(sub_id), 'webinar_found') + webinar_items
|
||||||
|
|
||||||
|
await context.bot.send_message(chat_id=sub_id, text=message, parse_mode='HTML')
|
||||||
|
logger.info(f"Notification sent to {sub_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to send to {sub_id}: {e}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Found {len(current_webinars)} webinar(s), but all are already known")
|
||||||
|
|
||||||
|
return len(current_webinars)
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not WEBINAR_TELEGRAM_TOKEN:
|
||||||
|
logger.error("WEBINAR_TELEGRAM_TOKEN is missing!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set default whitelist state if not set
|
||||||
|
if not redis_client.exists(KEY_WHITELIST_ENABLED):
|
||||||
|
redis_client.set(KEY_WHITELIST_ENABLED, "1") # Enabled by default
|
||||||
|
|
||||||
|
# Add admin to whitelist
|
||||||
|
if ADMIN_ID:
|
||||||
|
redis_client.sadd(KEY_WHITELIST, str(ADMIN_ID))
|
||||||
|
|
||||||
|
app = Application.builder().token(WEBINAR_TELEGRAM_TOKEN).build()
|
||||||
|
|
||||||
|
# Handlers
|
||||||
|
app.add_handler(CommandHandler("start", start))
|
||||||
|
app.add_handler(CommandHandler("help", help_command))
|
||||||
|
app.add_handler(CommandHandler("language", language_command))
|
||||||
|
app.add_handler(CommandHandler("adduser", add_user))
|
||||||
|
app.add_handler(CommandHandler("removeuser", remove_user))
|
||||||
|
|
||||||
|
# Callback handlers - language selection first, then admin panel
|
||||||
|
app.add_handler(CallbackQueryHandler(language_callback, pattern="^lang_"))
|
||||||
|
app.add_handler(CallbackQueryHandler(admin_callback))
|
||||||
|
|
||||||
|
# Job Queue
|
||||||
|
job_queue = app.job_queue
|
||||||
|
job_queue.run_repeating(check_webinars_job, interval=WEBINAR_CHECK_INTERVAL, first=10)
|
||||||
|
|
||||||
|
logger.info("Bot started polling...")
|
||||||
|
app.run_polling()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+5
-1
@@ -1,3 +1,7 @@
|
|||||||
GITEA_POSTGRES_USER=
|
GITEA_POSTGRES_USER=
|
||||||
GITEA_POSTGRES_PASSWORD=
|
GITEA_POSTGRES_PASSWORD=
|
||||||
GITEA_POSTGRES_DB=gitea
|
GITEA_POSTGRES_DB=gitea
|
||||||
|
BASIC-RUNNER_TOKEN=
|
||||||
|
GITEA_SMTP_PASS=
|
||||||
|
MAILER_ADDR=
|
||||||
|
SERVICE_EMAIL=email.used.by.services@domain.tld
|
||||||
+59
-5
@@ -13,23 +13,77 @@ services:
|
|||||||
- GITEA__database__NAME=gitea
|
- GITEA__database__NAME=gitea
|
||||||
#Server
|
#Server
|
||||||
- GITEA__server__ROOT_URL=https://gitea.forust.xyz
|
- GITEA__server__ROOT_URL=https://gitea.forust.xyz
|
||||||
|
- GITEA__server__SSH_DOMAIN=gitssh.forust.xyz
|
||||||
- GITEA__server__SSH_PORT=2221
|
- GITEA__server__SSH_PORT=2221
|
||||||
|
# Mailer
|
||||||
|
- GITEA__mailer__ENABLED=true
|
||||||
|
- GITEA__mailer__FROM=${SERVICE_EMAIL}
|
||||||
|
- GITEA__mailer__SMTP_ADDR=${MAILER_ADDR}:465
|
||||||
|
- GITEA__mailer__USER=${SERVICE_EMAIL}
|
||||||
|
- GITEA__mailer__PASSWD=${GITEA_SMTP_PASS}
|
||||||
|
- GITEA__mailer__PROTOCOL=SMTP
|
||||||
|
- GITEA__service__REGISTER_EMAIL_CONFIRM=true
|
||||||
|
- GITEA__service__ENABLE_NOTIFY_MAIL=true
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- gitea-db
|
- gitea-db
|
||||||
- traefik-proxy
|
- proxy
|
||||||
volumes:
|
volumes:
|
||||||
- ./gitea-data:/data
|
- ./gitea-data:/data
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.gitea.rule=Host(`gitea.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.gitea.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gitea.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.gitea.service=gitea"
|
||||||
|
- "traefik.http.routers.gitea.tls=true"
|
||||||
|
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.gitea-local.rule=Host(`gitea.workstation.internal`) || Host(`gitea.internal`)"
|
||||||
|
- "traefik.http.routers.gitea-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gitea-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.gitea-local.service=gitea"
|
||||||
|
- "traefik.http.routers.gitea-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.gitea-dev.rule=Host(`gitea.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.gitea-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gitea-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.gitea-dev.service=gitea"
|
||||||
|
- "traefik.http.routers.gitea-dev.tls=true"
|
||||||
|
- "traefik.tcp.routers.gitea.entrypoints=ssh"
|
||||||
|
- "traefik.tcp.routers.gitea.rule=HostSNI(`*`)"
|
||||||
|
- "traefik.tcp.services.gitea.loadbalancer.server.port=22"
|
||||||
ports:
|
ports:
|
||||||
- "2221:22"
|
- "2221:22"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
||||||
|
runner:
|
||||||
|
image: gitea/act_runner:0.2.11
|
||||||
|
container_name: gitea-runner
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- server
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
networks:
|
||||||
|
- gitea-db
|
||||||
|
environment:
|
||||||
|
- GITEA_INSTANCE_URL=http://server:3000
|
||||||
|
- GITEA_RUNNER_REGISTRATION_TOKEN=${BASIC-RUNNER_TOKEN}
|
||||||
|
- GITEA_RUNNER_NAME=basic-runner
|
||||||
|
- GITEA_RUNNER_LABELS=docker:docker://node:20-bookworm,ubuntu-latest:docker://node:20-bookworm
|
||||||
|
volumes:
|
||||||
|
- ./gitea-runner:/data
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: docker.io/library/postgres:14
|
image: docker.io/library/postgres:14
|
||||||
restart: always
|
restart: always
|
||||||
@@ -41,9 +95,9 @@ services:
|
|||||||
- gitea-db
|
- gitea-db
|
||||||
volumes:
|
volumes:
|
||||||
- ./gitea-db/:/var/lib/postgresql/data
|
- ./gitea-db/:/var/lib/postgresql/data
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
gitea-db:
|
gitea-db:
|
||||||
external: false
|
external: false
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
+22
-4
@@ -10,13 +10,31 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
env_file: .env
|
env_file: .env
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.glance.rule=Host(`glance.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.glance.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.glance.middlewares=security-chain@file"
|
||||||
|
- "traefik.http.routers.glance.tls=true"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.glance-local.rule=Host(`glance.workstation.internal`) || Host(`glance.internal`)"
|
||||||
|
- "traefik.http.routers.glance-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.glance-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.glance-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.glance-dev.rule=Host(`glance.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.glance-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.glance-dev.middlewares=security-chain@file"
|
||||||
|
- "traefik.http.routers.glance-dev.tls=true"
|
||||||
networks:
|
networks:
|
||||||
- traefik-proxy
|
- proxy
|
||||||
dns:
|
dns:
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
networks:
|
networks:
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# everyone use that
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
|
||||||
|
COPY ./forust_files /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
# Start
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# everyone use that
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
|
||||||
|
COPY ./xdfnx_files /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
# Start
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
services:
|
||||||
|
forust:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.forust
|
||||||
|
ports:
|
||||||
|
- "8085:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./forust_files:/usr/share/nginx/html
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Services
|
||||||
|
- "traefik.http.services.forust-homepage.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.forust-homepage.rule=Host(`forust.xyz`)"
|
||||||
|
- "traefik.http.routers.forust-homepage.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.forust-homepage.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.forust-homepage.service=forust-homepage"
|
||||||
|
- "traefik.http.routers.forust-homepage.tls=true"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.forust-homepage-local.rule=Host(`landing.workstation.internal`) || Host(`landing.internal`)"
|
||||||
|
- "traefik.http.routers.forust-homepage-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.forust-homepage-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.forust-homepage-local.service=forust-homepage"
|
||||||
|
- "traefik.http.routers.forust-homepage-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.forust-homepage-dev.rule=Host(`landing.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.forust-homepage-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.forust-homepage-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.forust-homepage-dev.service=forust-homepage"
|
||||||
|
- "traefik.http.routers.forust-homepage-dev.tls=true"
|
||||||
|
|
||||||
|
xdfnx:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.xdfnx
|
||||||
|
ports:
|
||||||
|
- "8086:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./xdfnx_files:/usr/share/nginx/html
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Services
|
||||||
|
- "traefik.http.services.xdfnx-homepage.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.xdfnx.rule=Host(`xdfnx.cfd`)"
|
||||||
|
- "traefik.http.routers.xdfnx.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.xdfnx.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.xdfnx.service=xdfnx-homepage"
|
||||||
|
- "traefik.http.routers.xdfnx.tls=true"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.xdfnx-local.rule=Host(`xdfnx.workstation.internal`) || Host(`xdfnx.internal`)"
|
||||||
|
- "traefik.http.routers.xdfnx-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.xdfnx-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.xdfnx-local.service=xdfnx-homepage"
|
||||||
|
- "traefik.http.routers.xdfnx-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.xdfnx-dev.rule=Host(`xdfnx.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.xdfnx-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.xdfnx-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.xdfnx-dev.service=xdfnx-homepage"
|
||||||
|
- "traefik.http.routers.xdfnx-dev.tls=true"
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
/* hidden in a plain sight? */
|
||||||
|
:root {
|
||||||
|
--bg-color: #050505;
|
||||||
|
--text-color: #e0e0e0;
|
||||||
|
--accent: #ffffff;
|
||||||
|
--dim: #666666;
|
||||||
|
--font-mono: 'Courier New', Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid var(--dim);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
background-color: var(--text-color);
|
||||||
|
color: var(--bg-color);
|
||||||
|
border-color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TEXT */
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -2px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--dim);
|
||||||
|
display: inline-block;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--dim);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px dashed var(--dim);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SECTIONS */
|
||||||
|
section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LISTS */
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-list li {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STACK GRID */
|
||||||
|
.grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special .level {
|
||||||
|
color: var(--text-color);
|
||||||
|
text-shadow: 1px 0 0 red, -1px 0 0 blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* my dudes */
|
||||||
|
.team-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member {
|
||||||
|
text-align: center;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background-color: #222;
|
||||||
|
border: 2px solid var(--text-color);
|
||||||
|
margin: 0 auto 10px auto;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if no avatar added: */
|
||||||
|
.placeholder::before {
|
||||||
|
content: "?";
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* REPOS */
|
||||||
|
.repo-list li {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOOTER */
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
/* flag{why-are-you-here?} */
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
/* SMTH RESPONSIVE */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.grid-2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>MrForust // XRock</title>
|
||||||
|
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1 class="glitch" data-text="MrForust">Mr-Forust</h1>
|
||||||
|
<p class="subtitle">> CTF Player / XRock_Team / Just Signal.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<section id="socials">
|
||||||
|
<h2>./socials</h2>
|
||||||
|
<ul class="link-list">
|
||||||
|
<li>
|
||||||
|
<i class="fab fa-github"></i>
|
||||||
|
<a href="https://github.com/mr-forust" target="_blank">github/mr-forust</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fas fa-flag"></i>
|
||||||
|
<a href="https://tryhackme.com/p/MrForust" target="_blank">tryhackme/MrForust</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fab fa-telegram-plane"></i>
|
||||||
|
<a href="https://t.me/MrForust" target="_blank">telegram/MrForust</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fab fa-discord"></i>
|
||||||
|
<span>discord/mr.forust</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
<a href="mailto:forust@forust.xyz">forust@forust.xyz</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="stack">
|
||||||
|
<h2>./skills_and_tools</h2>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>ArchLinux # btw</span>
|
||||||
|
<span class="level">[#######...]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>Docker Compose</span> <span class="level">[#####.....]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>Web Pentest</span> <span class="level">[#####.....]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>Burpsuite</span> <span class="level">[#####.....]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>Docker</span> <span class="level">[####......]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>Steganography</span> <span class="level">[####......]</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>Cryptography</span> <span class="level">[####......]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>OSINT</span> <span class="level">[####......]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>Python</span> <span class="level">[###.......]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>HTML</span> <span class="level">[###.......]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>Bash</span> <span class="level">[##........]</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<span>Golang</span> <span class="level">[#.........]</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="team">
|
||||||
|
<h2>./xrock_team</h2>
|
||||||
|
<p class="comment"># It's a select caste. Cybershamans. Cryptoanarchists. Shadows on the net..</p>
|
||||||
|
|
||||||
|
<div class="team-grid">
|
||||||
|
<div class="member">
|
||||||
|
<div class="avatar"
|
||||||
|
style="background-image: url('assets/images/team/mrforust.jpg'); background-size: cover; background-position: center;">
|
||||||
|
</div>
|
||||||
|
<a href="https://github.com/mr-forust" target="_blank">MrForust</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="member">
|
||||||
|
<div class="avatar"
|
||||||
|
style="background-image: url('assets/images/team/anna.jpg'); background-size: cover; background-position: center;">
|
||||||
|
</div>
|
||||||
|
<a href="" target="_blank">Anna~</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="member">
|
||||||
|
<div class="avatar"
|
||||||
|
style="background-image: url('assets/images/team/chernuha.jpg'); background-size: cover; background-position: center;">
|
||||||
|
</div>
|
||||||
|
<a href="https://chernuha.space" target="_blank">Chernuha</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="member">
|
||||||
|
<div class="avatar"
|
||||||
|
style="background-image: url('assets/images/team/hudan.jpg'); background-size: cover; background-position: center;">
|
||||||
|
</div>
|
||||||
|
<a href="https://hudan.xyz" target="_blank">p1ngvi</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="member">
|
||||||
|
<div class="avatar"
|
||||||
|
style="background-image: url('assets/images/team/xdfnx.jpg'); background-size: cover; background-position: center;">
|
||||||
|
</div>
|
||||||
|
<a href="https://xdfnx.cfd" target="_blank">xdfnx</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="repos">
|
||||||
|
<h2>./favorite_repos</h2>
|
||||||
|
<ul class="repo-list">
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/TDesktop-x64/tdesktop" target="_blank">TDesktop-x64/tdesktop</a>
|
||||||
|
<span class="comment">// unofficial telegram client with some additions</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/traefik/traefik" target="_blank">traefik/traefik</a>
|
||||||
|
<span class="comment">// beloved reverse-proxy</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/unhappychoice/gitlogue" target="_blank">unhappychoice/gitlogue</a>
|
||||||
|
<span class="comment">// nice git log visualizer</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/mountain-loop/yaak" target="_blank">mountain-loop/yaak</a>
|
||||||
|
<span class="comment">// modern, fancy api client</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/pear-devs/pear-desktop" target="_blank">pear-devs/pear-desktop</a>
|
||||||
|
<span class="comment">// music client with a lot of features</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/epi052/feroxbuster" target="_blank">epi052/feroxbuster</a>
|
||||||
|
<span class="comment">// directory discovery</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>root@xrock:~$ cat <a href="miku.html">./miku</a></p>
|
||||||
|
<p>miku?</p>
|
||||||
|
<p>root@xrock:~$ shutdown -h now</p>
|
||||||
|
<p>© XRock - Just Signal.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>miku?</title>
|
||||||
|
</head>
|
||||||
|
<body bgcolor="black" text="grey">
|
||||||
|
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠋⠉⠉⠀⠀⠀⠀⠀⠀⠛⠘⠛⠛⠛⠿⠿⠿⠿⠿⣽⠙⣛⡛⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠋⠁⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠈⠙⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢋⣰⢋⣴⠻⣿⡟⢹⡟⠋⠀⠀⠀⠀⢀⣠⣴⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣶⣿⣿⣿⣷⣶⣶⣦⣤⣄⣀⠀⠀⠉⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⡀⠏⣰⠟⠁⡰⢋⠀⠀⠀⠀⠀⢀⣠⣾⣿⣿⣿⡿⠟⠛⣛⣽⣯⣿⣽⣯⣿⣟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣖⡄⠀⠀⠉⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⣠⠾⠋⠀⢊⡴⠋⠀⠀⠀⣀⣴⣿⣿⣿⡿⠋⢀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣿⣿⣽⣿⣯⣿⣯⣿⣽⣯⣿⢿⣷⣷⣄⠀⠀⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⢠⠞⠁⠀⠀⣰⠋⠀⠀⢀⣤⣾⣿⣿⣿⠿⣋⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣝⣛⡫⠷⣿⡿⣯⣿⢿⣺⣄⠀⠈⠋⠙⣿⡻⣧⠹⣏⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀⢀⡴⠋⠀⠀⢠⡾⠁⠀⢀⣴⣿⣿⣿⣿⣿⣿⣾⠿⠛⣩⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣭⣛⣿⣿⣻⣿⣷⣤⠀⠀⠘⢷⠘⣧⠹⣆⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⠀⢀⡰⠍⠀⠀⢀⣴⡟⠁⢀⣶⣿⣿⣿⣿⣿⣿⠟⣋⠴⢂⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⠹⣦⠘⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠀⠀⢀⠀⠀⠀⢰⣾⠏⢀⣴⣾⣿⣿⡿⣻⡿⢛⣡⠞⣁⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣻⣽⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⢠⡀⠁⠘⢧⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⠀⠀⠠⠀⠀⠀⣿⠏⣴⣿⣿⣿⡿⣋⣾⠟⣱⠋⢂⣶⣿⣿⣿⣿⣿⣿⡿⣯⣿⣽⢿⡾⣟⣿⡽⣾⢽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠱⠀⠀⠈⢧⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡋⠀⠠⢀⠏⠀⠀⠀⠀⠀⠀⢋⣾⣿⣿⣿⣿⣷⠟⡡⠞⢁⣼⣿⣿⣿⣿⣿⢟⣿⢿⣽⣟⡷⣯⣟⡿⣽⢯⡿⣽⢊⢷⣻⣟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣽⣿⣟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡂⠀⠀⠀⢫⠄⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⡈⠁⡌⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⠟⢁⢮⡱⣳⢾⣟⠋⣿⣿⡿⠃⣼⢯⡟⣾⡽⣳⠶⣭⠿⣽⣛⡞⣷⡈⢎⢷⡹⣯⣟⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⣿⣿⣯⣿⣿⣿⣿⣿⣿⣟⣿⣿⣿⣿⣿⣿⣿⣷⡆⠀⠀⠀⠫⡄⠀⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⡔⠀⢈⠆⠀⠀⠀⣠⣾⣿⣿⣿⣿⢛⢋⡰⣋⢮⡱⢏⡾⠍⣰⣿⡿⠁⡼⣏⠷⡘⢼⣓⠧⡓⡜⣯⢳⣭⣛⢶⡁⢊⠶⣙⢧⡻⣷⣻⣯⣿⢿⣷⣿⢿⣯⣿⢿⣽⣾⢿⣳⣿⣽⡿⣿⣿⡿⣟⣾⣽⣻⢿⣿⣿⣿⣿⣧⠀⠀⠀⠄⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡧⠀⡘⠀⡀⠌⡐⠀⠀⣼⣿⣿⣿⣿⣿⠃⢀⠦⡓⢭⡲⢭⣓⠞⢠⣿⡟⢀⣼⢳⡍⠧⡑⠈⡝⠦⡑⡍⡖⣏⢶⡩⢞⠁⡄⠘⡱⢎⡵⢫⢷⣻⣽⣿⣻⣾⢿⣽⡾⣟⣯⡿⣟⣿⣳⣯⢿⡟⣿⣿⣟⡿⣞⡿⣯⣟⣿⢿⣿⣯⡀⠀⢀⠂⠀⠀⠟⡛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠠⠘⠰⢀⡘⠔⢀⣾⣿⣿⣿⣿⣿⠃⠀⡎⠵⣙⠶⣹⢣⡞⠁⣾⡿⢁⡾⣡⠗⡬⠑⢀⡆⡙⢆⠱⡘⠼⣘⠦⣝⢪⠀⣗⡀⠙⡼⣘⢏⡞⡵⣛⡾⣽⢯⣟⠾⡽⣭⢿⣹⢟⣷⡻⣎⢯⢳⡌⢿⣾⣟⣯⢳⡝⣺⠽⣻⣽⣻⡇⠀⠀⠀⠀⢰⡄⠸⡈⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠂⡍⡖⠀⠌⣠⣿⣿⣿⡿⣿⡿⠃⠀⡜⣌⠳⣬⢛⡴⢫⡜⢸⣿⠃⡼⣱⢃⠞⡠⢁⣾⠁⢹⠠⢃⢌⠣⡝⣚⢬⢣⠀⣿⣦⠀⠰⡩⠞⡼⣱⢫⡝⣧⢟⡾⢫⡕⣣⢏⢞⡹⢲⡝⡜⣊⠖⠲⠈⢿⣾⣻⠸⡜⣥⠫⣝⡷⣟⡧⠀⠀⠀⠀⠰⣃⠐⢠⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢀⠱⢠⠄⠈⣴⣿⣿⣿⠟⣼⡿⢡⠃⡜⡰⢊⢷⡸⣍⢞⡣⠄⣿⣏⠴⣣⠧⣉⠎⢁⣼⣯⠇⠰⣉⠌⡄⠳⣌⠧⢎⢧⠀⣿⡽⣷⠀⠈⠽⡰⢣⠳⣜⢬⢫⡜⣳⠸⣑⢎⠎⡕⢣⠜⡰⢡⠎⣡⢃⠘⣿⣽⡆⢹⠰⣍⢲⡹⣟⣿⠀⠀⠀⢀⡃⠲⠀⢠⠂⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠄⡂⡜⢀⣾⣿⣿⣿⢋⣾⣟⢠⠃⡸⢰⡑⢯⡒⢷⣘⢮⡑⢸⣯⢏⡞⣡⠞⡤⠁⣾⠋⣾⠡⠐⡥⢂⠈⡱⣈⢮⡙⢦⠐⣏⠻⣿⣷⣤⠈⠱⣋⠳⣜⢪⡓⡼⣡⠳⣉⢎⢎⡙⠦⡙⡔⠣⢎⠔⡈⠆⢹⣯⣧⠈⡳⢌⠦⣱⣛⣮⠀⠀⠀⠆⡌⢡⠃⠄⢂⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠆⡑⢠⣾⣿⣿⡻⢃⣾⣿⠋⠆⠰⣁⠦⣙⠦⣏⠳⡜⢦⠁⣾⣏⠞⡴⢱⢊⠀⣼⢏⣾⣿⡅⠈⠴⠈⠐⢠⡑⢦⡙⣧⠈⣟⣦⠹⣿⣿⣄⠀⠉⠷⣌⠳⡜⣥⢃⠯⣜⣊⢦⡙⢦⠱⣌⠳⡌⢎⠱⡈⠄⢻⣿⡄⡹⢌⠲⡡⢞⡵⠀⠀⠀⡇⠄⡃⠥⢈⠄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⡈⠂⣴⣟⠫⡍⠰⢁⣾⣿⠃⡜⢀⠣⣐⠣⣍⠞⣌⢳⢩⢚⠀⡿⣌⠏⡼⢡⠂⠰⣿⡾⢿⣿⣇⠀⠓⡄⡄⠂⡜⢢⡝⡶⠐⣸⣿⣷⡜⢿⣿⣷⡠⠘⢌⡳⢍⠶⣩⠞⡴⣊⢦⡙⢆⡳⢌⠣⡜⣌⠲⡁⠎⠈⣿⣷⡱⢋⡒⠥⣫⠖⠀⠠⢌⡓⠌⡰⢁⠎⡐⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⢀⡾⢋⡌⢣⠌⢁⣿⡿⠁⡜⠀⢢⠑⡄⠳⣈⠞⡌⢎⠦⡉⢸⡗⡭⢊⡵⠁⣰⣶⣤⣤⣌⡙⠉⠷⠈⠄⢣⠐⣈⠳⣜⣳⠀⣿⣿⣿⣿⣦⡙⣿⣷⡀⠈⠼⣩⠞⣥⢛⡴⣍⠶⣉⠦⣑⠮⣑⢮⣐⢣⠱⡈⠅⠸⣷⡏⡵⢘⡐⢧⣻⠀⡡⢂⡝⢠⠐⡩⠐⢠⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠂⡰⢋⠔⠣⠘⢠⠃⣾⡟⠄⡐⢀⡘⠄⠣⢌⠱⠠⢎⡐⢣⡘⠁⣾⢸⡱⢋⠄⣘⣿⣿⣿⣿⣿⣿⣿⡦⠀⠁⢸⡄⢠⢋⢾⣱⠠⣿⣿⣿⣿⣿⣿⣌⢻⣿⣤⠈⢱⡚⢦⡛⣶⡩⢞⢥⠓⣜⢢⡍⠶⣨⢆⢣⡑⠌⡀⢿⣷⢡⠣⡘⠦⣏⠔⡁⠢⣜⠠⠂⢥⠃⠄⠂⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠅⢀⠰⠌⡌⢁⠊⠄⣸⡟⠀⡐⠀⡄⢢⢉⠒⡌⢂⠇⢢⠘⢤⠘⢰⠋⣦⡙⠂⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⡄⠘⢸⣧⠀⡚⣼⢳⠀⣿⣿⣿⣿⣿⣿⣿⡷⢈⢁⣤⠀⠍⡧⡝⢦⠻⣍⢮⡙⣤⠣⡜⢳⡰⢎⠲⣌⠒⡀⠘⣿⣎⠱⣀⠛⡼⢐⡈⠔⣌⠂⡉⠔⡨⢈⠄⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠃⠈⠐⠈⠆⢠⠂⠌⡐⢰⡿⠂⡐⠀⠰⡈⠆⡌⠒⡌⢌⡘⠤⡉⢆⠁⣼⣙⢦⠙⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠈⣿⡇⠘⡴⣻⠀⣹⣿⣿⣿⣿⣿⣿⣶⣾⣷⣿⡖⠀⠱⣩⠏⣝⢮⠳⡜⢤⠓⣍⠲⣱⢪⡑⢆⠣⡔⠁⢹⣎⠧⢠⠙⣞⠀⢎⠐⣌⠂⣁⠚⠄⢢⠀⢽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠗⢀⣠⣶⠁⠌⠂⡌⢀⠂⢠⡿⠁⠀⠀⠀⣎⠡⡘⢠⠃⢌⠢⠄⡃⠜⡠⢀⡿⣜⠋⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⢿⣿⡆⢱⢻⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⣄⠑⢮⡱⢎⠿⣜⠦⡙⢤⠛⣤⠓⡼⣈⠇⣆⠡⠀⢻⡜⡠⢙⡼⠈⠆⡌⣰⠡⠠⠘⡌⠠⠂⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⢛⣡⣴⣿⣿⡏⠐⢨⠀⠆⠠⠁⣼⠃⠀⠀⠀⡜⢄⠢⠑⡄⢊⠄⡃⠜⡠⢃⠔⠰⣯⠍⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣾⣿⣿⣀⠹⡇⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡦⡀⠹⢎⡝⣮⢧⠙⣢⠙⣤⠛⣔⢣⡚⢤⢃⠅⡈⠞⠤⡁⢞⡥⠘⡄⢲⠀⡅⢣⠐⡡⠁⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⡈⠄⡘⢠⠁⢸⠇⠀⠀⠀⣼⠱⡈⠄⡡⠐⠌⢂⡘⠤⠑⠢⠀⣼⠇⠀⠀⠉⠙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣀⠉⢞⡬⣛⣇⠰⣉⠦⡹⢌⡖⡍⣆⠫⡔⡀⢈⠱⢈⠺⣄⠱⣌⠲⢁⠰⢀⠇⠄⡡⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠠⠐⢡⡘⠤⢀⡏⠀⠀⠀⣸⡱⢂⡁⠂⠄⡑⠈⡄⠰⡈⢡⠁⠐⡟⢀⣶⣤⣄⣀⠀⠀⠈⠙⠿⣿⣿⡙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⢀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡈⠲⡝⡾⣇⠈⡖⣩⠲⣌⠳⣌⢳⣜⡰⠀⠄⠊⣜⣣⢘⡧⡘⠄⢂⠡⢊⡐⠐⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠠⣉⠐⢆⠂⡼⠀⠀⠀⣼⢣⢇⠃⠠⢈⠐⠠⠁⢄⠃⠤⠁⠌⡀⢡⣿⣿⣿⣿⣿⣿⣷⣦⣄⠀⠀⠙⠻⢷⣬⣙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⣿⣿⣿⣿⠿⠟⠛⠛⠁⠀⠘⠵⣻⡄⠸⣄⠳⣌⠳⣌⠧⣿⢰⢃⠘⡀⠰⣧⢸⠲⡍⠂⡌⢐⡃⢄⡉⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⢂⠤⡙⡄⢰⠁⠀⠀⢬⡷⣩⠎⡄⠁⠄⡈⠄⡁⢂⠌⠄⣉⠰⠀⢠⣿⡟⠉⠉⠉⠉⠀⠁⠈⠁⠀⠀⠀⠀⠈⣹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⢡⣶⠾⠟⠋⠉⠀⠀⠀⢀⣀⣀⣠⣤⣤⡀⠓⠋⡀⢨⠓⣌⠳⣌⢳⡌⢠⡋⠄⠀⠐⣽⢸⠣⡅⢃⠐⢂⡅⢂⠰⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⡌⢂⡜⠀⠃⠀⠀⡜⢲⡝⠂⣁⡀⠈⡀⠄⡐⠐⡠⢈⠂⢄⠒⠀⠈⣿⣷⣀⣠⣤⣤⣶⣶⣶⣶⣶⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⠁⠀⠀⠀⠀⠀⠶⠾⣿⣿⣿⣿⣿⣿⣿⣿⠄⠀⠱⠀⡹⢄⣛⠈⣁⡘⢂⣇⠈⠆⠀⢘⣯⠃⡜⣀⠊⢄⡒⠈⡔⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⡐⢌⠒⠆⡈⠀⢠⠘⣌⢻⠌⣸⣿⠇⠀⢀⠐⠠⢁⡐⠠⠌⠂⢌⠀⠀⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣄⣀⠀⠀⠀⠉⠙⠻⢿⣿⡏⠀⠐⡀⠅⢀⠣⠎⠐⣿⣧⡀⢺⡀⠌⢡⠘⣮⠑⡌⠤⠈⡔⢨⢁⠰⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⡰⢈⢜⡂⠀⢀⡎⠴⡨⡇⢰⣿⣿⡁⠀⠀⠌⡐⠠⠀⡡⠘⡈⠤⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⣀⣠⣿⡟⠀⡈⠄⡐⠌⡄⢀⠩⠀⢿⣿⡄⠠⡇⠈⠄⠢⣝⠢⠜⡠⢃⡐⢃⠄⢊⠁⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠠⡐⠡⡸⠀⢠⢃⡜⢰⠱⠇⣼⣿⣿⠄⠀⠈⠄⠠⠁⠠⢂⠡⡐⠐⡀⢠⣶⡎⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠠⠀⢂⠄⠃⠀⣬⣂⠀⢼⣿⣧⠀⢳⠈⠌⢡⢘⡇⡱⠄⡃⠌⡆⡘⠤⠁⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⡰⢁⠣⡅⡘⢄⠺⡄⢣⠛⢰⣿⣿⣿⠀⠀⠌⠠⠁⣴⣆⠁⢂⠔⠡⠀⣿⣿⣿⡸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀⠀⡅⠀⢠⣄⡀⢈⣿⣿⣄⠈⣿⣿⣄⠈⠧⡘⢀⠎⡖⢱⡈⠔⡡⢚⡀⢆⡁⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡯⠀⡔⢡⢊⡵⢈⠜⣸⠐⣣⠁⣾⣿⣿⠿⠀⠀⠌⢀⣾⣿⡏⠀⠌⡠⢁⠀⠻⣿⣿⣷⡜⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢋⣄⠀⡘⠀⠠⠘⢻⡻⣿⣿⣿⣿⠆⣿⣿⣧⠀⢃⠆⡡⢂⢝⠢⠜⡠⢁⠧⡈⢄⠒⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠰⡈⢆⣩⠆⣅⠚⣤⠙⡄⠸⣽⣿⣿⠁⠀⠀⣠⣿⣿⡿⠰⠀⢂⡁⢂⠀⣆⡈⠻⣿⣿⣦⡹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⣻⢖⡶⣹⢖⡶⣤⠦⠄⣹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣡⣿⡟⠠⠁⣀⠃⢀⢸⣿⣿⣿⣿⣿⡇⢿⣿⣿⡄⠈⢆⠡⢂⠌⣇⠓⠤⡉⢖⠡⠊⡌⢈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠅⢢⠑⡌⡲⢌⡄⣋⠴⣉⠀⣩⣿⣿⡏⠀⢀⣴⣿⣿⣿⢁⣿⡅⢀⠒⠠⢀⣿⣷⣆⡈⠛⠿⠛⠛⠻⠿⠿⠿⢿⣿⣿⣿⣿⣷⡈⠻⣜⣧⣻⡜⡧⠋⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⣋⣴⣿⡿⠀⠀⠐⠤⠁⣾⣇⢹⣿⡿⢿⣿⣿⢸⣿⣿⣯⠀⢨⠂⡅⢊⡼⡘⠤⡑⢎⠰⢡⠘⡀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⢢⡉⠴⣙⠤⡘⢤⡓⠌⢠⣾⣿⣿⠀⣠⣾⣿⣿⣿⠇⣾⣿⣯⠀⠌⡡⠠⣿⣿⣿⣿⣦⣄⣀⠀⠂⠀⠀⠀⠀⠀⠀⠀⠉⠛⢿⣷⣬⣌⣁⣭⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⣋⣥⣾⣿⣿⣿⢃⡆⠀⢩⢀⣾⣿⣿⡌⢿⣿⣎⣿⣿⡄⣿⣿⣿⡆⢀⠳⣸⠠⡹⢄⠣⡘⢼⢀⠣⡘⡀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡷⢀⠣⡘⢔⡫⢰⡉⢦⠍⠂⣸⣿⣿⡿⠁⣾⡟⣽⣿⡟⣰⣿⣿⣿⡀⢂⠡⢐⣿⣿⣿⣿⣿⣿⣿⣷⣤⠀⠀⠀⠋⠀⠀⠂⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋⣁⣀⣀⣉⣙⣛⣻⣿⣧⣞⠀⠨⢀⣾⣿⣿⣿⣿⡘⣿⣿⡜⣿⣷⠸⣿⣿⣷⡀⠱⢸⠅⣟⠄⢣⠑⢮⠠⢃⠴⠁⢺⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢈⣒⣉⠆⠛⣀⣈⣐⢃⣴⣿⣿⣿⠅⢘⣿⣽⢿⡿⢠⣿⣿⣿⣿⣧⠀⠆⢈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣶⢢⣤⡠⢤⣤⣤⡴⢿⣿⣿⣿⣿⣿⣿⡿⠿⣛⣭⠇⣠⡴⣹⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠀⢠⣾⣿⣿⣿⣿⣿⣧⢹⣿⣷⣽⣿⡆⢿⣿⣿⣷⡄⢚⣉⣈⠋⠠⣍⣺⠀⠉⠦⣉⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠠⠚⣃⣋⣰⣿⣿⣿⣟⣡⣿⣿⣿⣿⣿⣿⣿⡤⣶⢀⣾⣿⣿⣿⣿⣿⣂⠨⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣬⠛⡓⢦⡙⠻⣿⣷⣬⣍⣛⣭⣵⣶⣾⡿⢋⣡⠞⣉⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⡆⢩⣦⣛⣿⣿⣾⣿⣿⣿⣧⡙⢿⣿⣯⡑⣈⣑⣋⠳⢢⡀⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⡄⢰⣿⣿⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡔⡌⢿⣽⣿⣿⣿⣿⣿⣿⠄⠠⣿⣿⣿⣿⣿⣿⣿⣿⡿⢃⡿⣷⣶⠉⢢⡽⣷⣬⣙⠻⢿⣿⣿⠟⢋⣡⣴⢟⣡⡾⣽⣦⢿⣝⡻⣿⣿⣿⣿⣿⣿⠛⢀⣾⣿⣿⣿⣿⣿⣿⣿⡿⠻⠉⣄⣾⣿⣿⣿⣿⣿⣿⣿⣿⣼⣿⣿⣷⡘⣿⣿⡗⠀⢷⡀⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⣇⢸⡁⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⢽⠃⠙⢿⣿⣿⣿⣷⠀⢿⣿⣿⣿⣿⡿⠛⠁⣶⢏⣼⣿⣿⣷⣦⣿⣿⣿⣿⡗⢢⡙⣡⢾⣿⣿⣿⣿⣿⣿⣿⣿⣷⣟⣿⡈⠻⠿⣿⣿⣿⣷⣾⣿⣿⣿⣿⣿⡿⠟⠡⡄⣊⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡗⠠⣸⠇⢹⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⣿⠇⠸⣷⡀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣀⠸⣦⣈⠻⢿⣿⣧⣿⣿⣿⣿⣿⠀⠘⡇⢻⣼⣿⣿⣿⣿⣿⣿⣿⣿⠿⢿⣦⠙⢹⠏⣿⣿⣿⣿⣿⣿⠟⢿⣿⣿⣿⠀⠳⣀⢻⣿⣿⣿⣿⣿⣿⣿⠿⢋⣤⡞⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀⣰⡿⢀⢸⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⠙⢷⡄⠹⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⠀⠈⠙⢷⣆⡙⢿⣿⣿⣿⣿⣿⠁⢠⣆⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠗⣀⣄⣤⣤⣌⡐⢼⣿⣿⡿⠁⢰⡦⢙⠿⡿⢀⣃⠈⣿⣿⣿⣿⣿⣿⠛⣡⡶⠟⠁⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⢀⣼⠟⠀⠠⠈⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⠈⠛⢷⣄⡙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡗⠀⠀⠀⠀⠙⢷⣆⠹⣿⣿⡿⠃⠄⣸⡆⠀⢻⣿⣿⣿⣿⣿⣿⡟⠁⢰⡿⣯⢿⡽⣯⢿⡀⢻⣿⠁⢠⣧⣶⡏⣰⠁⢸⣇⠀⠈⣿⣿⣿⠏⢀⣶⠟⠁⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠉⣀⣴⠿⠉⠀⠀⠀⠁⠸⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠉⠻⢶⣄⡙⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠙⣷⡌⠻⣧⠐⣰⣿⣷⠀⠈⢿⣿⣿⣿⡿⠋⡁⠔⢺⠿⣽⢫⡽⣯⣟⠲⠄⡙⠿⣦⣍⡛⣰⠏⠀⣿⣿⡄⠀⢨⡿⠃⣴⡟⠁⠀⠀⠀⠀⠀⠀⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠏⢁⣁⣴⠞⠋⠁⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠷⣦⣄⡉⠛⠻⢿⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣄⠁⣰⣾⣿⣿⣧⠀⠈⠉⢉⣀⣴⡾⣷⡀⠈⢻⣓⣯⣙⡳⠃⣀⣶⣌⣐⠠⢠⡉⢀⠀⣶⡿⣿⣿⣄⠘⠁⣼⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⢯⢿⣿⡿⡿⠻⠛⢉⣀⣤⡾⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠻⠶⣤⡈⠉⠙⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢁⠂⠀⣿⠀⠙⣿⣿⣽⣿⣿⣿⣷⣾⡿⣷⣻⢷⣻⠀⢠⣟⡾⣽⢣⠀⣾⣿⣿⣻⣷⣶⣶⣿⣯⣿⣷⢿⣿⡟⠀⢸⠃⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⢀⣠⡴⠚⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿⣿⣿
|
||||||
|
⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠙⠲⢍⡖⣆⠦⠤⠤⠤⢠⣄⡌⣤⢤⣤⣙⠖⢠⣿⣿⣻⣾⣿⣿⣿⣷⣿⣳⣽⣻⣿⠇⢸⣞⡽⣽⢫⠐⠿⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⡃⠐⣮⣤⢤⠤⣤⠤⣄⠤⠤⢤⢒⡤⢎⡜⠣⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿
|
||||||
|
⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠓⠉⠓⠾⠜⠻⠞⠙⠁⣰⣾⣿⣿⣽⣿⣿⣿⣿⣿⣷⣿⣞⣿⣿⠀⢸⣎⣟⢮⣏⠀⢺⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣀⠙⠋⠛⠲⠛⠜⠉⠛⠈⠁⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣿⣿⣿
|
||||||
|
⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢺⠀⣿⣿⣿⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣿⠀⢸⡮⣝⡞⡼⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⢸⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿
|
||||||
|
⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⡿⣼⡹⣜⡱⠂⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡅⠈⢸⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿
|
||||||
|
⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡿⢃⠀⡿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢠⢳⢧⡝⢦⡹⡁⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠄⢈⠻⣿⣧⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡰⢻⣿⣿⠇⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⢰⣋⢶⡹⢎⡵⣁⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣝⢿⣿⣿⡄⠈⠰⣿⣿⣿⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣰⣿⣿⡘⣿⠅⠠⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠸⡜⡶⣩⢳⢎⡵⢸⣿⣿⣿⡏⠹⠿⠿⠟⠻⠛⡟⠻⠟⠀⣿⣿⣷⡀⠠⠙⣿⡏⣸⣿⣶⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⡇⠝⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠂⢼⡹⣱⢏⡾⣘⠖⠘⣿⣿⣿⣿⣦⣉⣉⣭⣭⣤⣭⣤⣈⣸⣿⣿⣿⣧⠀⠘⢿⢀⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⠀⠊⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⢧⡛⡽⢪⠵⣩⠞⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠹⣿⣿⣿⣿⣆⠈⠀⢸⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⡿⠂⠀⢸⣿⣿⣿⣟⡳⢯⣛⠿⣻⢿⡿⣿⠻⣟⣿⣿⣿⣿⠀⣎⠵⣩⢳⡙⢦⢫⠄⢻⣿⣿⣿⣿⢿⣻⡟⣿⢫⠟⡵⢪⢇⣹⣿⣿⣿⣿⡇⢈⠹⣿⣿⣿⣿⣿⣿⣿⣷⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠀⣠⣾⢿⣿⡗⢮⡑⢣⠘⡬⢑⠮⡑⢎⡱⢎⡞⡽⢹⡃⠀⡎⢧⠣⢧⣝⢣⢏⠆⡸⣏⠿⣱⢎⢳⡘⡜⢢⢋⡜⢌⡑⢊⠴⢻⣿⣿⣿⡇⠈⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
File diff suppressed because it is too large
Load Diff
+27
-5
@@ -4,7 +4,7 @@ services:
|
|||||||
# container_name: metube
|
# container_name: metube
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# ports:
|
# ports:
|
||||||
# - "8081:8081"
|
# - "8081:8081"
|
||||||
environment:
|
environment:
|
||||||
- DOWNLOAD_MODE=limited
|
- DOWNLOAD_MODE=limited
|
||||||
- MAX_CONCURRENT_DOWNLOADS=3
|
- MAX_CONCURRENT_DOWNLOADS=3
|
||||||
@@ -14,10 +14,32 @@ services:
|
|||||||
- ./MeTube_downloads:/downloads
|
- ./MeTube_downloads:/downloads
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.metube.rule=Host(`metube.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.metube.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.metube.middlewares=security-chain@file"
|
||||||
|
- "traefik.http.routers.metube.service=metube"
|
||||||
|
- "traefik.http.routers.metube.tls=true"
|
||||||
|
- "traefik.http.services.metube.loadbalancer.server.port=8081"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.metube-local.rule=Host(`metube.workstation.internal`) || Host(`metube.internal`)"
|
||||||
|
- "traefik.http.routers.metube-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.metube-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.metube-local.service=metube"
|
||||||
|
- "traefik.http.routers.metube-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.metube-dev.rule=Host(`metube.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.metube-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.metube-dev.middlewares=security-chain@file"
|
||||||
|
- "traefik.http.routers.metube-dev.service=metube"
|
||||||
|
- "traefik.http.routers.metube-dev.tls=true"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- traefik-proxy
|
- proxy
|
||||||
networks:
|
networks:
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
+32
-9
@@ -23,17 +23,40 @@ services:
|
|||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
networks:
|
networks:
|
||||||
- traefik-proxy
|
- proxy
|
||||||
- n8n
|
- n8n
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.docker.network=proxy"
|
||||||
- glance.name=n8n
|
|
||||||
- glance.icon=si:n8n
|
# Prod Router
|
||||||
- glance.url=https://n8n.forust.xyz/
|
- "traefik.http.routers.n8n.rule=Host(`n8n.forust.xyz`)"
|
||||||
- glance.description=n8n is a workflow automation tool that enables you to connect various apps and services to automate tasks and processes.
|
- "traefik.http.routers.n8n.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.n8n.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.n8n.service=n8n"
|
||||||
|
- "traefik.http.routers.n8n.tls=true"
|
||||||
|
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.n8n-local.rule=Host(`n8n.workstation.internal`) || Host(`n8n.internal`)"
|
||||||
|
- "traefik.http.routers.n8n-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.n8n-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.n8n-local.service=n8n"
|
||||||
|
- "traefik.http.routers.n8n-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.n8n-dev.rule=Host(`n8n.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.n8n-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.n8n-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.n8n-dev.service=n8n"
|
||||||
|
- "traefik.http.routers.n8n-dev.tls=true"
|
||||||
|
|
||||||
|
- glance.name=n8n
|
||||||
|
- glance.icon=si:n8n
|
||||||
|
- glance.url=https://n8n.forust.xyz/
|
||||||
|
- glance.description=n8n is a workflow automation tool that enables you to connect various apps and services to automate tasks and processes.
|
||||||
networks:
|
networks:
|
||||||
n8n:
|
n8n:
|
||||||
external: false
|
external: false
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
+62
-34
@@ -1,58 +1,86 @@
|
|||||||
services:
|
services:
|
||||||
nextcloud-aio-mastercontainer:
|
nextcloud-aio-mastercontainer:
|
||||||
image: ghcr.io/nextcloud-releases/all-in-one:latest # This is the container image used. You can switch to ghcr.io/nextcloud-releases/all-in-one:beta if you want to help testing new releases. See https://github.com/nextcloud/all-in-one#how-to-switch-the-channel
|
image: ghcr.io/nextcloud-releases/all-in-one:beta
|
||||||
init: true # This setting makes sure that signals from main process inside the container are correctly forwarded to children. See https://docs.docker.com/reference/compose-file/services/#init
|
init: true # This setting makes sure that signals from main process inside the container are correctly forwarded to children. See https://docs.docker.com/reference/compose-file/services/#init
|
||||||
restart: unless-stopped # This makes sure that the container starts always together with the host OS. See https://docs.docker.com/reference/compose-file/services/#restart
|
restart: unless-stopped
|
||||||
container_name: nextcloud-aio-mastercontainer # This line is not allowed to be changed as otherwise AIO will not work correctly
|
container_name: nextcloud-aio-mastercontainer # Do not change
|
||||||
volumes:
|
volumes:
|
||||||
- nextcloud_aio_mastercontainer:/mnt/docker-aio-config # This line is not allowed to be changed as otherwise the built-in backup solution will not work
|
- nextcloud_aio_mastercontainer:/mnt/docker-aio-config # Do not change (backup)
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro # May be changed on macOS, Windows or docker rootless. See the applicable documentation. If adjusting, don't forget to also set 'WATCHTOWER_DOCKER_SOCKET_PATH'!
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
# network_mode: bridge # This adds the container to the same network as docker run would do. Comment this line and uncomment the line below and the networks section at the end of the file if you want to define a custom MTU size for the docker network
|
networks:
|
||||||
networks:
|
|
||||||
- nextcloud-aio
|
- nextcloud-aio
|
||||||
- traefik-proxy # Optional: Connects the mastercontainer to the traefik-proxy network in order to make the built-in reverse proxy detection work.
|
- proxy # Optional: Connects the mastercontainer to the proxy network in order to make the built-in reverse proxy detection work. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
|
||||||
# ports:
|
# ports:
|
||||||
# - 8081:80 # Can be removed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
|
# - 8081:80 # may be removed if under reverse-proxy
|
||||||
# - 8888:8080 # This is the AIO interface, served via https and self-signed certificate. See https://github.com/nextcloud/all-in-one#explanation-of-used-ports
|
# - 8443:8443
|
||||||
# - 8443:8443 # Can be removed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
|
# - 8888:8080 # AIO
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.docker.network=proxy"
|
||||||
- glance.name=Nextcloud
|
|
||||||
# - glance.icon=si:nextcloud
|
|
||||||
- glance.url=https://nextcloud.forust.xyz/
|
|
||||||
- glance.description=Nextcloud is a suite of client-server software for creating and using file hosting services.
|
|
||||||
|
|
||||||
|
# AIO Services configuration
|
||||||
|
- "traefik.http.services.nextcloud-aio.loadbalancer.server.port=8080"
|
||||||
|
- "traefik.http.services.nextcloud-aio.loadbalancer.server.scheme=https"
|
||||||
|
- "traefik.http.services.nextcloud-aio.loadbalancer.serverstransport=insecureTransport@file"
|
||||||
|
|
||||||
environment: # Is needed when using any of the options below
|
# Prod Router
|
||||||
AIO_DISABLE_BACKUP_SECTION: false # Setting this to true allows to hide the backup section in the AIO interface. See https://github.com/nextcloud/all-in-one#how-to-disable-the-backup-section
|
# - "traefik.http.routers.nextcloud-aio.rule=Host(`nextcloud-aio.forust.xyz`)"
|
||||||
|
# - "traefik.http.routers.nextcloud-aio.entrypoints=websecure"
|
||||||
|
# - "traefik.http.routers.nextcloud-aio.middlewares=security-headers"
|
||||||
|
# - "traefik.http.routers.nextcloud-aio.service=nextcloud-aio"
|
||||||
|
# - "traefik.http.routers.nextcloud-aio.tls=true"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.nextcloud-aio-local.rule=Host(`nextcloud-aio.workstation.internal`) || Host(`nextcloud-aio.internal`)"
|
||||||
|
- "traefik.http.routers.nextcloud-aio-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.nextcloud-aio-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.nextcloud-aio-local.service=nextcloud-aio"
|
||||||
|
- "traefik.http.routers.nextcloud-aio-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.nextcloud-aio-dev.rule=Host(`nextcloud-aio.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.nextcloud-aio-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.nextcloud-aio-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.nextcloud-aio-dev.service=nextcloud-aio"
|
||||||
|
- "traefik.http.routers.nextcloud-aio-dev.tls=true"
|
||||||
|
|
||||||
|
# Glanceapp/glance config
|
||||||
|
- glance.name=Nextcloud
|
||||||
|
# - glance.icon=si:nextcloud
|
||||||
|
- glance.url=https://nextcloud.forust.xyz/
|
||||||
|
- glance.description=Nextcloud is a suite of client-server software for creating and using file hosting services.
|
||||||
|
|
||||||
|
environment:
|
||||||
|
AIO_DISABLE_BACKUP_SECTION: false
|
||||||
APACHE_PORT: 11000 # Is needed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
|
APACHE_PORT: 11000 # Is needed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
|
||||||
APACHE_IP_BINDING: 0.0.0.0 # Should be set when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) that is running on the same host. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
|
APACHE_IP_BINDING: 0.0.0.0 # Configure when going with reverse-proxy https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
|
||||||
APACHE_ADDITIONAL_NETWORK: traefik-proxy # (Optional) Connect the apache container to an additional docker network. Needed when behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) running in a different docker network on same server. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
|
APACHE_ADDITIONAL_NETWORK: proxy # (Optional) Connect the apache container to an additional docker network. When going with reverse-proxy. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
|
||||||
BORG_RETENTION_POLICY: --keep-within=7d --keep-weekly=4 --keep-monthly=6 # Allows to adjust borgs retention policy. See https://github.com/nextcloud/all-in-one#how-to-adjust-borgs-retention-policy
|
BORG_RETENTION_POLICY: --keep-within=7d --keep-weekly=4 --keep-monthly=6 # Backup retention See https://github.com/nextcloud/all-in-one#how-to-adjust-borgs-retention-policy
|
||||||
COLLABORA_SECCOMP_DISABLED: false # Setting this to true allows to disable Collabora's Seccomp feature. See https://github.com/nextcloud/all-in-one#how-to-disable-collaboras-seccomp-feature
|
COLLABORA_SECCOMP_DISABLED: false # Setting this to true allows to disable Collabora's Seccomp feature. See https://github.com/nextcloud/all-in-one#how-to-disable-collaboras-seccomp-feature
|
||||||
FULLTEXTSEARCH_JAVA_OPTIONS: "-Xms1024M -Xmx1024M" # Allows to adjust the fulltextsearch java options. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-fulltextsearch-java-options
|
FULLTEXTSEARCH_JAVA_OPTIONS: "-Xms1024M -Xmx1024M" # adjust fulltextsearch java options. https://github.com/nextcloud/all-in-one#how-to-adjust-the-fulltextsearch-java-options
|
||||||
NEXTCLOUD_DATADIR: /media/forust/nextcloud/ncdata # Allows to set the host directory for Nextcloud's datadir. ⚠️⚠️⚠️ Warning: do not set or adjust this value after the initial Nextcloud installation is done! See https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir
|
NEXTCLOUD_DATADIR: /media/forust/nextcloud/ncdata # Allows to set the host directory for Nextcloud's datadir. ⚠️⚠️⚠️ Warning: do not set or adjust this value after the initial Nextcloud installation is done! See https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir
|
||||||
NEXTCLOUD_MOUNT: /media/forust/nextcloud # Allows the Nextcloud container to access the chosen directory on the host. See https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host
|
NEXTCLOUD_MOUNT: /mnt/ # Allows the Nextcloud container to access the chosen directory on the host. See https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host
|
||||||
NEXTCLOUD_UPLOAD_LIMIT: 16G # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-upload-limit-for-nextcloud
|
NEXTCLOUD_UPLOAD_LIMIT: 16G # https://github.com/nextcloud/all-in-one#how-to-adjust-the-upload-limit-for-nextcloud
|
||||||
NEXTCLOUD_MAX_TIME: 3600 # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-max-execution-time-for-nextcloud
|
NEXTCLOUD_MAX_TIME: 7200 # Max uploading time See https://github.com/nextcloud/all-in-one#how-to-adjust-the-max-execution-time-for-nextcloud
|
||||||
NEXTCLOUD_MEMORY_LIMIT: 512M # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-php-memory-limit-for-nextcloud
|
NEXTCLOUD_MEMORY_LIMIT: 512M # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-php-memory-limit-for-nextcloud
|
||||||
# NEXTCLOUD_TRUSTED_CACERTS_DIR: /path/to/my/cacerts # CA certificates in this directory will be trusted by the OS of the nextcloud container (Useful e.g. for LDAPS) See https://github.com/nextcloud/all-in-one#how-to-trust-user-defined-certification-authorities-ca
|
# NEXTCLOUD_TRUSTED_CACERTS_DIR: /path/to/my/cacerts # CA certificates will be trusted by the OS of the nextcloud container See https://github.com/nextcloud/all-in-one#how-to-trust-user-defined-certification-authorities-ca
|
||||||
NEXTCLOUD_STARTUP_APPS: deck twofactor_totp tasks calendar contacts notes # Allows to modify the Nextcloud apps that are installed on starting AIO the first time. See https://github.com/nextcloud/all-in-one#how-to-change-the-nextcloud-apps-that-are-installed-on-the-first-startup
|
NEXTCLOUD_STARTUP_APPS: deck twofactor_totp tasks calendar contacts notes # Allows to modify the Nextcloud apps that are installed on starting AIO the first time. See https://github.com/nextcloud/all-in-one#how-to-change-the-nextcloud-apps-that-are-installed-on-the-first-startup
|
||||||
NEXTCLOUD_ADDITIONAL_APKS: imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-os-packages-permanently-to-the-nextcloud-container
|
NEXTCLOUD_ADDITIONAL_APKS: imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-os-packages-permanently-to-the-nextcloud-container
|
||||||
NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS: imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-php-extensions-permanently-to-the-nextcloud-container
|
NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS: imagick # dditional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-php-extensions-permanently-to-the-nextcloud-container
|
||||||
NEXTCLOUD_ENABLE_DRI_DEVICE: true # This allows to enable the /dev/dri device for containers that profit from it. ⚠️⚠️⚠️ Warning: this only works if the '/dev/dri' device is present on the host! If it should not exist on your host, don't set this to true as otherwise the Nextcloud container will fail to start! See https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud
|
NEXTCLOUD_ENABLE_DRI_DEVICE: true # This allows to enable the /dev/dri device for containers that profit from it. ⚠️⚠️⚠️ Warning: this only works if the '/dev/dri' device is present on the host! If it should not exist on your host, don't set this to true as otherwise the Nextcloud container will fail to start! See https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud
|
||||||
# NEXTCLOUD_KEEP_DISABLED_APPS: false # Setting this to true will keep Nextcloud apps that are disabled in the AIO interface and not uninstall them if they should be installed. See https://github.com/nextcloud/all-in-one#how-to-keep-disabled-apps
|
# NEXTCLOUD_KEEP_DISABLED_APPS: false # Setting this to true will keep Nextcloud apps that are disabled in the AIO interface and not uninstall them if they should be installed. See https://github.com/nextcloud/all-in-one#how-to-keep-disabled-apps
|
||||||
SKIP_DOMAIN_VALIDATION: false # This should only be set to true if things are correctly configured. See https://github.com/nextcloud/all-in-one?tab=readme-ov-file#how-to-skip-the-domain-validation
|
SKIP_DOMAIN_VALIDATION: true # This should only be set to true if things are correctly configured. See https://github.com/nextcloud/all-in-one?tab=readme-ov-file#how-to-skip-the-domain-validation
|
||||||
# TALK_PORT: 3478 # This a-llows to adjust the port that the talk container is using which is exposed on the host. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port
|
# TALK_PORT: 3478 # This a-llows to adjust the port that the talk container is using which is exposed on the host. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port
|
||||||
# WATCHTOWER_DOCKER_SOCKET_PATH: /var/run/docker.sock # Needs to be specified if the docker socket on the host is not located in the default '/var/run/docker.sock'. Otherwise mastercontainer updates will fail. For macos it needs to be '/var/run/docker.sock'
|
# WATCHTOWER_DOCKER_SOCKET_PATH: /var/run/docker.sock # Needs to be specified if the docker socket on the host is not located in the default '/var/run/docker.sock'. For macos it needs to be '/var/run/docker.sock'
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
nextcloud-aio:
|
nextcloud-aio:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
external: false
|
external: false
|
||||||
volumes: # If you want to store the data on a different drive, see https://github.com/nextcloud/all-in-one#how-to-store-the-filesinstallation-on-a-separate-drive
|
volumes:
|
||||||
|
# If you want to store the data on a different drive, see https://github.com/nextcloud/all-in-one#how-to-store-the-filesinstallation-on-a-separate-drive
|
||||||
nextcloud_aio_mastercontainer:
|
nextcloud_aio_mastercontainer:
|
||||||
name: nextcloud_aio_mastercontainer # This line is not allowed to be changed as otherwise the built-in backup solution will not work
|
name: nextcloud_aio_mastercontainer # Do not change
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
PENPOT_SECRET_KEY=somesecretfdsfsdfds
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
## Common flags:
|
||||||
|
# demo-users
|
||||||
|
# email-verification
|
||||||
|
# log-emails
|
||||||
|
# log-invitation-tokens
|
||||||
|
# login-with-github
|
||||||
|
# login-with-gitlab
|
||||||
|
# login-with-google
|
||||||
|
# login-with-ldap
|
||||||
|
# login-with-oidc
|
||||||
|
# login-with-password
|
||||||
|
# prepl-server
|
||||||
|
# registration
|
||||||
|
# secure-session-cookies
|
||||||
|
# smtp
|
||||||
|
# smtp-debug
|
||||||
|
# telemetry
|
||||||
|
# webhooks
|
||||||
|
##
|
||||||
|
## You can read more about all available flags and other
|
||||||
|
## environment variables here:
|
||||||
|
## https://help.penpot.app/technical-guide/configuration/#penpot-configuration
|
||||||
|
#
|
||||||
|
# WARNING: if you're exposing Penpot to the internet, you should remove the flags
|
||||||
|
# 'disable-secure-session-cookies' and 'disable-email-verification'
|
||||||
|
x-flags: &penpot-flags
|
||||||
|
PENPOT_FLAGS: disable-email-verification enable-prepl-server disable-secure-session-cookies login-with-github
|
||||||
|
|
||||||
|
x-uri: &penpot-public-uri
|
||||||
|
PENPOT_PUBLIC_URI: http://localhost:9001
|
||||||
|
|
||||||
|
x-body-size:
|
||||||
|
# Max body size (30MiB); Used for plain requests, should never be
|
||||||
|
# greater than multi-part size
|
||||||
|
&penpot-http-body-size
|
||||||
|
PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
|
||||||
|
|
||||||
|
# Max multipart body size (350MiB)
|
||||||
|
PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600
|
||||||
|
|
||||||
|
## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems
|
||||||
|
## (eg http sessions, or invitations) are derived.
|
||||||
|
##
|
||||||
|
## We recommend to use a trully randomly generated
|
||||||
|
## 512 bits base64 encoded string here. You can generate one with:
|
||||||
|
##
|
||||||
|
## python3 -c "import secrets; print(secrets.token_urlsafe(64))"
|
||||||
|
x-secret-key: &penpot-secret-key
|
||||||
|
PENPOT_SECRET_KEY: ${PENPOT_SECRET_KEY}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
penpot:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
penpot_postgres_v15:
|
||||||
|
penpot_assets:
|
||||||
|
penpot_traefik:
|
||||||
|
# penpot_minio:
|
||||||
|
|
||||||
|
services:
|
||||||
|
## Traefik service declaration example. Consider using it if you are going to expose
|
||||||
|
## penpot to the internet, or a different host than `localhost`.
|
||||||
|
|
||||||
|
# traefik:
|
||||||
|
# image: traefik:v3.3
|
||||||
|
# networks:
|
||||||
|
# - penpot
|
||||||
|
# command:
|
||||||
|
# - "--api.insecure=true"
|
||||||
|
# - "--entryPoints.web.address=:80"
|
||||||
|
# - "--providers.docker=true"
|
||||||
|
# - "--providers.docker.exposedbydefault=false"
|
||||||
|
# - "--entryPoints.websecure.address=:443"
|
||||||
|
# - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
||||||
|
# - "--certificatesresolvers.letsencrypt.acme.email=<EMAIL_ADDRESS>"
|
||||||
|
# - "--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json"
|
||||||
|
# volumes:
|
||||||
|
# - "penpot_traefik:/traefik"
|
||||||
|
# - "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
|
# ports:
|
||||||
|
# - "80:80"
|
||||||
|
# - "443:443"
|
||||||
|
|
||||||
|
penpot-frontend:
|
||||||
|
image: "penpotapp/frontend:${PENPOT_VERSION:-latest}"
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 9001:8080
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- penpot_assets:/opt/data/assets
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- penpot-backend
|
||||||
|
- penpot-exporter
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- penpot
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.penpot.rule=Host(`penpot.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.penpot.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.penpot.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.penpot.service=penpot"
|
||||||
|
- "traefik.http.routers.penpot.tls=true"
|
||||||
|
- "traefik.http.services.penpot.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.penpot-local.rule=Host(`penpot.workstation.internal`) || Host(`penpot.internal`)"
|
||||||
|
- "traefik.http.routers.penpot-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.penpot-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.penpot-local.service=penpot"
|
||||||
|
- "traefik.http.routers.penpot-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.penpot-dev.rule=Host(`penpot.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.penpot-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.penpot-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.penpot-dev.service=penpot"
|
||||||
|
- "traefik.http.routers.penpot-dev.tls=true"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
<<: [ *penpot-flags, *penpot-http-body-size ]
|
||||||
|
|
||||||
|
penpot-backend:
|
||||||
|
image: "penpotapp/backend:${PENPOT_VERSION:-latest}"
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- penpot_assets:/opt/data/assets
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
penpot-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
penpot-valkey:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- penpot
|
||||||
|
|
||||||
|
## Configuration envronment variables for the backend container.
|
||||||
|
|
||||||
|
environment:
|
||||||
|
<<: [ *penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key ]
|
||||||
|
|
||||||
|
## The PREPL host. Mainly used for external programatic access to penpot backend
|
||||||
|
## (example: admin). By default it will listen on `localhost` but if you are going to use
|
||||||
|
## the `admin`, you will need to uncomment this and set the host to `0.0.0.0`.
|
||||||
|
|
||||||
|
# PENPOT_PREPL_HOST: 0.0.0.0
|
||||||
|
|
||||||
|
## Database connection parameters. Don't touch them unless you are using custom
|
||||||
|
## postgresql connection parameters.
|
||||||
|
|
||||||
|
PENPOT_DATABASE_URI: postgresql://penpot-postgres/penpot
|
||||||
|
PENPOT_DATABASE_USERNAME: penpot
|
||||||
|
PENPOT_DATABASE_PASSWORD: penpot
|
||||||
|
|
||||||
|
## Valkey (or previously redis) is used for the websockets notifications. Don't touch
|
||||||
|
## unless the valkey container has different parameters or different name.
|
||||||
|
|
||||||
|
PENPOT_REDIS_URI: redis://penpot-valkey/0
|
||||||
|
|
||||||
|
## Default configuration for assets storage: using filesystem based with all files
|
||||||
|
## stored in a docker volume.
|
||||||
|
|
||||||
|
PENPOT_ASSETS_STORAGE_BACKEND: assets-fs
|
||||||
|
PENPOT_STORAGE_ASSETS_FS_DIRECTORY: /opt/data/assets
|
||||||
|
|
||||||
|
## Also can be configured to to use a S3 compatible storage
|
||||||
|
## service like MiniIO. Look below for minio service setup.
|
||||||
|
|
||||||
|
# AWS_ACCESS_KEY_ID: <KEY_ID>
|
||||||
|
# AWS_SECRET_ACCESS_KEY: <ACCESS_KEY>
|
||||||
|
# PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
|
||||||
|
# PENPOT_STORAGE_ASSETS_S3_ENDPOINT: http://penpot-minio:9000
|
||||||
|
# PENPOT_STORAGE_ASSETS_S3_BUCKET: <BUKET_NAME>
|
||||||
|
|
||||||
|
## Telemetry. When enabled, a periodical process will send anonymous data about this
|
||||||
|
## instance. Telemetry data will enable us to learn how the application is used,
|
||||||
|
## based on real scenarios. If you want to help us, please leave it enabled. You can
|
||||||
|
## audit what data we send with the code available on github.
|
||||||
|
|
||||||
|
PENPOT_TELEMETRY_ENABLED: true
|
||||||
|
PENPOT_TELEMETRY_REFERER: compose
|
||||||
|
# PENPOT_SMTP_DEFAULT_FROM: no-reply@example.com
|
||||||
|
# PENPOT_SMTP_DEFAULT_REPLY_TO: no-reply@example.com
|
||||||
|
# PENPOT_SMTP_HOST: penpot-mailcatch
|
||||||
|
# PENPOT_SMTP_PORT: 1025
|
||||||
|
# PENPOT_SMTP_USERNAME:
|
||||||
|
# PENPOT_SMTP_PASSWORD:
|
||||||
|
# PENPOT_SMTP_TLS: false
|
||||||
|
# PENPOT_SMTP_SSL: false
|
||||||
|
|
||||||
|
penpot-exporter:
|
||||||
|
image: "penpotapp/exporter:${PENPOT_VERSION:-latest}"
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
penpot-valkey:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- penpot
|
||||||
|
|
||||||
|
environment:
|
||||||
|
<<: [ *penpot-secret-key ]
|
||||||
|
# Don't touch it; this uses an internal docker network to
|
||||||
|
# communicate with the frontend.
|
||||||
|
PENPOT_PUBLIC_URI: http://penpot-frontend:8080
|
||||||
|
|
||||||
|
## Valkey (or previously Redis) is used for the websockets notifications.
|
||||||
|
PENPOT_REDIS_URI: redis://penpot-valkey/0
|
||||||
|
|
||||||
|
penpot-postgres:
|
||||||
|
image: "postgres:15"
|
||||||
|
restart: always
|
||||||
|
stop_signal: SIGINT
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "pg_isready -U penpot" ]
|
||||||
|
interval: 2s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 2s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- penpot_postgres_v15:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- penpot
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- POSTGRES_INITDB_ARGS=--data-checksums
|
||||||
|
- POSTGRES_DB=penpot
|
||||||
|
- POSTGRES_USER=penpot
|
||||||
|
- POSTGRES_PASSWORD=penpot
|
||||||
|
|
||||||
|
penpot-valkey:
|
||||||
|
image: valkey/valkey:8.1
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "valkey-cli ping | grep PONG" ]
|
||||||
|
interval: 1s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 3s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- penpot
|
||||||
|
|
||||||
|
environment:
|
||||||
|
# You can increase the max memory size if you have sufficient resources,
|
||||||
|
# although this should not be necessary.
|
||||||
|
- VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu
|
||||||
|
## A mailcatch service, used as temporal SMTP server. You can access via HTTP to the
|
||||||
|
## port 1080 for read all emails the penpot platform has sent. Should be only used as a
|
||||||
|
## temporal solution while no real SMTP provider is configured.
|
||||||
|
|
||||||
|
# penpot-mailcatch:
|
||||||
|
# image: sj26/mailcatcher:latest
|
||||||
|
# restart: always
|
||||||
|
# expose:
|
||||||
|
# - '1025'
|
||||||
|
# ports:
|
||||||
|
# - "1080:1080"
|
||||||
|
# networks:
|
||||||
|
# - penpot
|
||||||
|
|
||||||
|
## Example configuration of MiniIO (S3 compatible object storage service); If you don't
|
||||||
|
## have preference, then just use filesystem, this is here just for the completeness.
|
||||||
|
|
||||||
|
# minio:
|
||||||
|
# image: "minio/minio:latest"
|
||||||
|
# command: minio server /mnt/data --console-address ":9001"
|
||||||
|
# restart: always
|
||||||
|
#
|
||||||
|
# volumes:
|
||||||
|
# - "penpot_minio:/mnt/data"
|
||||||
|
#
|
||||||
|
# environment:
|
||||||
|
# - MINIO_ROOT_USER=minioadmin
|
||||||
|
# - MINIO_ROOT_PASSWORD=minioadmin
|
||||||
|
#
|
||||||
|
# ports:
|
||||||
|
# - 9000:9000
|
||||||
|
# - 9001:9001
|
||||||
+29
-5
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
portainer:
|
portainer:
|
||||||
container_name: portainer
|
container_name: portainer
|
||||||
image: portainer/portainer-ce:lts
|
image: portainer/portainer-ce:latest
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
@@ -10,12 +10,36 @@ services:
|
|||||||
- 9443:9443
|
- 9443:9443
|
||||||
# - 8000:8000 # Remove if you do not intend to use Edge Agents
|
# - 8000:8000 # Remove if you do not intend to use Edge Agents
|
||||||
networks:
|
networks:
|
||||||
- traefik-proxy
|
- proxy
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.portainer.rule=Host(`portainer.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.portainer.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.portainer.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.portainer.service=portainer"
|
||||||
|
- "traefik.http.routers.portainer.tls=true"
|
||||||
|
- "traefik.http.services.portainer.loadbalancer.server.port=9443"
|
||||||
|
- "traefik.http.services.portainer.loadbalancer.server.scheme=https"
|
||||||
|
- "traefik.http.services.portainer.loadbalancer.serverstransport=insecureTransport@file"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.portainer-local.rule=Host(`portainer.workstation.internal`) || Host(`portainer.internal`)"
|
||||||
|
- "traefik.http.routers.portainer-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.portainer-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.portainer-local.service=portainer"
|
||||||
|
- "traefik.http.routers.portainer-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.portainer-dev.rule=Host(`portainer.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.portainer-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.portainer-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.portainer-dev.service=portainer"
|
||||||
|
- "traefik.http.routers.portainer-dev.tls=true"
|
||||||
|
|
||||||
- glance.name=Portainer
|
- glance.name=Portainer
|
||||||
# - glance.icon=si:portainer
|
|
||||||
- glance.url=https://portainer.forust.xyz/
|
- glance.url=https://portainer.forust.xyz/
|
||||||
- glance.description=Portainer is a lightweight management UI which allows you to easily manage your Docker environments.
|
- glance.description=Portainer is a lightweight management UI which allows you to easily manage your Docker environments.
|
||||||
|
|
||||||
@@ -26,5 +50,5 @@ volumes:
|
|||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
name: portainer_network
|
name: portainer_network
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
services:
|
||||||
|
termix:
|
||||||
|
image: ghcr.io/lukegus/termix:latest
|
||||||
|
container_name: termix
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3331:8080"
|
||||||
|
volumes:
|
||||||
|
- ./termix-data:/app/data
|
||||||
|
environment:
|
||||||
|
PORT: "8080"
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.termix.rule=Host(`termix.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.termix.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.termix.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.termix.service=termix"
|
||||||
|
- "traefik.http.routers.termix.tls=true"
|
||||||
|
- "traefik.http.services.termix.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.termix-local.rule=Host(`termix.workstation.internal`) || Host(`termix.internal`)"
|
||||||
|
- "traefik.http.routers.termix-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.termix-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.termix-local.service=termix"
|
||||||
|
- "traefik.http.routers.termix-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.termix-dev.rule=Host(`termix.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.termix-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.termix-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.termix-dev.service=termix"
|
||||||
|
- "traefik.http.routers.termix-dev.tls=true"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
termix-data:
|
||||||
|
driver: local
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# ===================================
|
||||||
|
# Traefik envs
|
||||||
|
EMAIL=bobrovod@national.shitposting.agency
|
||||||
+42
-21
@@ -1,20 +1,20 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.5
|
image: traefik:latest
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
# API
|
# API
|
||||||
- "--api.insecure=false"
|
- "--api.insecure=true"
|
||||||
- "--api.dashboard=true"
|
- "--api.dashboard=true"
|
||||||
|
|
||||||
# Providers
|
# Providers
|
||||||
- "--providers.docker=true"
|
- "--providers.docker=true"
|
||||||
- "--providers.docker.exposedbydefault=false"
|
- "--providers.docker.exposedbydefault=false"
|
||||||
- "--providers.docker.network=traefik-proxy"
|
- "--providers.docker.network=proxy"
|
||||||
- "--providers.file.directory=/etc/traefik/dynamic"
|
- "--providers.file.directory=/etc/traefik/dynamic"
|
||||||
- "--providers.file.watch=true"
|
- "--providers.file.watch=true"
|
||||||
|
|
||||||
# EntryPoints
|
# EntryPoints
|
||||||
- "--entryPoints.web.address=:80"
|
- "--entryPoints.web.address=:80"
|
||||||
- "--entryPoints.websecure.address=:443"
|
- "--entryPoints.websecure.address=:443"
|
||||||
@@ -22,48 +22,69 @@ services:
|
|||||||
- "--entryPoints.web.http.redirections.entryPoint.to=websecure"
|
- "--entryPoints.web.http.redirections.entryPoint.to=websecure"
|
||||||
- "--entryPoints.web.http.redirections.entryPoint.scheme=https"
|
- "--entryPoints.web.http.redirections.entryPoint.scheme=https"
|
||||||
- "--entryPoints.ssh.address=:2221"
|
- "--entryPoints.ssh.address=:2221"
|
||||||
|
|
||||||
# Let's Encrypt STAGING. CURRENTLY USING CF ORIGIN CA INSTEAD
|
# Let's Encrypt STAGING. CURRENTLY USING CF ORIGIN CA INSTEAD
|
||||||
# - "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}"
|
# - "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}"
|
||||||
# - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
# - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||||
# - "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
|
# - "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||||
# - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
|
# - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
|
||||||
# - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
# - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
||||||
|
|
||||||
# Cloudflare
|
# Cloudflare
|
||||||
- "--entryPoints.web.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22"
|
- "--entryPoints.web.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22"
|
||||||
- "--entryPoints.websecure.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22"
|
- "--entryPoints.websecure.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22"
|
||||||
|
# # Logging
|
||||||
# Logging
|
# - "--log.level=INFO"
|
||||||
- "--log.level=INFO"
|
# - "--log.filePath=/var/log/traefik/traefik.log"
|
||||||
- "--log.filePath=/var/log/traefik/traefik.log"
|
# - "--accesslog=true"
|
||||||
- "--accesslog=true"
|
# - "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Prod Router (Dash)
|
||||||
|
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.traefik-dashboard.middlewares=security-chain@file"
|
||||||
|
- "traefik.http.routers.traefik-dashboard.service=api@internal"
|
||||||
|
- "traefik.http.routers.traefik-dashboard.tls=true"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.traefik-dashboard-local.rule=Host(`traefik.workstation.internal`) || Host(`traefik.internal`)"
|
||||||
|
- "traefik.http.routers.traefik-dashboard-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.traefik-dashboard-local.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.traefik-dashboard-local.service=api@internal"
|
||||||
|
- "traefik.http.routers.traefik-dashboard-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.traefik-dashboard-dev.rule=Host(`traefik.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.traefik-dashboard-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.traefik-dashboard-dev.middlewares=security-chain@file"
|
||||||
|
- "traefik.http.routers.traefik-dashboard-dev.service=api@internal"
|
||||||
|
- "traefik.http.routers.traefik-dashboard-dev.tls=true"
|
||||||
|
|
||||||
- glance.name=Traefik
|
- glance.name=Traefik
|
||||||
- glance.url=https://traefik.forust.xyz/
|
- glance.url=https://traefik.forust.xyz/
|
||||||
- glance.description=Traefik is a modern reverse proxy and load balancer
|
- glance.description=Traefik is a modern reverse proxy and load balancer
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- ./dynamic:/etc/traefik/dynamic:ro
|
- ./dynamic:/etc/traefik/dynamic:ro
|
||||||
- ./certs:/certs:ro
|
- ./certs:/certs:ro
|
||||||
- ./logs:/var/log/traefik
|
- ./logs:/var/log/traefik
|
||||||
# - ./traefik/letsencrypt:/letsencrypt
|
# - ./traefik/letsencrypt:/letsencrypt
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- traefik-proxy
|
- proxy
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Bratislava
|
- TZ=Europe/Bratislava
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
@@ -1,298 +1,4 @@
|
|||||||
http:
|
http:
|
||||||
routers:
|
|
||||||
# Traefik Dashboard (ADMIN, local only)
|
|
||||||
traefik-dashboard:
|
|
||||||
rule: "Host(`traefik.workstation`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: api@internal
|
|
||||||
middlewares:
|
|
||||||
# - auth
|
|
||||||
- security-headers
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
# Nextcloud AIO Interface (ADMIN, local is better)
|
|
||||||
nextcloud-aio:
|
|
||||||
rule: "Host(`nextcloud-aio.workstation`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: nextcloud-aio
|
|
||||||
middlewares:
|
|
||||||
- security-headers
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
# MeTube
|
|
||||||
metube:
|
|
||||||
rule: "Host(`metube.forust.xyz`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: metube
|
|
||||||
middlewares:
|
|
||||||
- metube-auth
|
|
||||||
- security-headers
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
# Portainer (public (account required))
|
|
||||||
portainer:
|
|
||||||
rule: "Host(`portainer.forust.xyz`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: portainer
|
|
||||||
middlewares:
|
|
||||||
- security-headers
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
# AdGuard Home (public (account required))
|
|
||||||
adguard:
|
|
||||||
rule: "Host(`adguard.forust.xyz`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: adguard
|
|
||||||
middlewares:
|
|
||||||
- security-headers
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
|
|
||||||
# Nextcloud Main (public (account required))
|
|
||||||
nextcloud:
|
|
||||||
rule: "Host(`nextcloud.forust.xyz`)"
|
|
||||||
entrypoints:
|
|
||||||
- websecure
|
|
||||||
service: nextcloud
|
|
||||||
middlewares:
|
|
||||||
- nextcloud-chain
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
# Dockmon (public (account required))
|
|
||||||
dockmon:
|
|
||||||
rule: "Host(`dockmon.forust.xyz`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: dockmon
|
|
||||||
middlewares:
|
|
||||||
# - dockmon-auth
|
|
||||||
- security-headers
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
# Gitea (public (account required))
|
|
||||||
gitea:
|
|
||||||
rule: "Host(`gitea.forust.xyz`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
- ssh
|
|
||||||
service: gitea
|
|
||||||
middlewares:
|
|
||||||
- security-headers
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
# # Glance (local only)
|
|
||||||
# glance:
|
|
||||||
# rule: "Host(`glance.workstation`)"
|
|
||||||
# entryPoints:
|
|
||||||
# - websecure
|
|
||||||
# service: glance
|
|
||||||
# middlewares:
|
|
||||||
# - security-headers
|
|
||||||
# tls: {}
|
|
||||||
|
|
||||||
# Watercrawl (local only)
|
|
||||||
watercrawl:
|
|
||||||
rule: "Host(`watercrawl.workstation`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: watercrawl
|
|
||||||
middlewares:
|
|
||||||
- security-headers
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
# N8N (local only)
|
|
||||||
n8n:
|
|
||||||
rule: "Host(`n8n.workstation`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: n8n
|
|
||||||
middlewares:
|
|
||||||
- security-headers
|
|
||||||
tls: {}
|
|
||||||
|
|
||||||
|
|
||||||
services:
|
|
||||||
# Portainer
|
|
||||||
portainer:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "https://portainer:9443"
|
|
||||||
serversTransport: insecureTransport
|
|
||||||
|
|
||||||
# AdGuard Home
|
|
||||||
adguard:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://adguardhome:3000"
|
|
||||||
|
|
||||||
# Nextcloud AIO Interface
|
|
||||||
nextcloud-aio:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "https://nextcloud-aio-mastercontainer:8080"
|
|
||||||
serversTransport: insecureTransport
|
|
||||||
|
|
||||||
# Nextcloud Main
|
|
||||||
nextcloud:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://nextcloud-aio-apache:11000"
|
|
||||||
|
|
||||||
# Dockmon
|
|
||||||
dockmon:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "https://dockmon:443"
|
|
||||||
serversTransport: insecureTransport
|
|
||||||
|
|
||||||
# # Glance
|
|
||||||
# glance:
|
|
||||||
# loadBalancer:
|
|
||||||
# servers:
|
|
||||||
# - url: "http://glance:8080"
|
|
||||||
|
|
||||||
# Watercrawl
|
|
||||||
watercrawl:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://nginx:80"
|
|
||||||
|
|
||||||
# N8N
|
|
||||||
n8n:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://n8n:5678"
|
|
||||||
|
|
||||||
# Gitea
|
|
||||||
gitea:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://gitea:3000"
|
|
||||||
# MeTube
|
|
||||||
metube:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://metube:8081"
|
|
||||||
|
|
||||||
serversTransports:
|
serversTransports:
|
||||||
insecureTransport:
|
insecureTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|
||||||
middlewares:
|
|
||||||
# HTTPS Redirect
|
|
||||||
redirect-https:
|
|
||||||
redirectScheme:
|
|
||||||
scheme: https
|
|
||||||
permanent: true
|
|
||||||
|
|
||||||
# Metube Basic Auth
|
|
||||||
metube-auth:
|
|
||||||
basicAuth:
|
|
||||||
users:
|
|
||||||
- "admin:$2y$05$3Q6gyLFW3NFNp4C6elnyfupntqB6VNB/tcIAeo8NEzvaqPxOjN0iC"
|
|
||||||
# - "jeepik:$2y$05$tIKrmhd7SYOe6yImRRAfpen7hpVdF8PnSbgBTCDZ.GI0Djx.Le2bq"
|
|
||||||
realm: "MeTube Access"
|
|
||||||
|
|
||||||
# Basic Auth Traefik Dashboard
|
|
||||||
auth:
|
|
||||||
basicAuth:
|
|
||||||
users:
|
|
||||||
- "admin:$$apr1$$57E60OUM$$JoYwmLr/uZKaTy6U4IQd9."
|
|
||||||
# - "jeepik:$2y$05$Q8QqJwSjpycVYONyk4id/.rDFApW9oL8tycRMlGttNySsDv71Rnsu"
|
|
||||||
# - "vv:"
|
|
||||||
realm: "Traefik Dashboard"
|
|
||||||
|
|
||||||
# Basic Auth Dockmon
|
|
||||||
dockmon-auth:
|
|
||||||
basicAuth:
|
|
||||||
users:
|
|
||||||
- "admin:$$apr1$$.bCpmIHl$$dxPEKdw5aZLAwo8wUz52b1"
|
|
||||||
realm: "Dockmon Access"
|
|
||||||
|
|
||||||
# Cloudflare IP Whitelist
|
|
||||||
cloudflare-ipwhitelist:
|
|
||||||
ipWhiteList:
|
|
||||||
sourceRange:
|
|
||||||
- "173.245.48.0/20"
|
|
||||||
- "103.21.244.0/22"
|
|
||||||
- "103.22.200.0/22"
|
|
||||||
- "103.31.4.0/22"
|
|
||||||
- "141.101.64.0/18"
|
|
||||||
- "108.162.192.0/18"
|
|
||||||
- "190.93.240.0/20"
|
|
||||||
- "188.114.96.0/20"
|
|
||||||
- "197.234.240.0/22"
|
|
||||||
- "198.41.128.0/17"
|
|
||||||
- "162.158.0.0/15"
|
|
||||||
- "104.16.0.0/13"
|
|
||||||
- "104.24.0.0/14"
|
|
||||||
- "172.64.0.0/13"
|
|
||||||
- "131.0.72.0/22"
|
|
||||||
|
|
||||||
# Security Headers
|
|
||||||
security-headers:
|
|
||||||
headers:
|
|
||||||
browserXssFilter: true
|
|
||||||
contentTypeNosniff: true
|
|
||||||
forceSTSHeader: true
|
|
||||||
stsIncludeSubdomains: true
|
|
||||||
stsPreload: true
|
|
||||||
stsSeconds: 31536000
|
|
||||||
customFrameOptionsValue: "SAMEORIGIN"
|
|
||||||
customResponseHeaders:
|
|
||||||
X-Content-Type-Options: "nosniff"
|
|
||||||
Referrer-Policy: "strict-origin-when-cross-origin"
|
|
||||||
|
|
||||||
# Nextcloud specific headers
|
|
||||||
nextcloud-secure-headers:
|
|
||||||
headers:
|
|
||||||
hostsProxyHeaders:
|
|
||||||
- "X-Forwarded-Host"
|
|
||||||
- "X-Forwarded-Proto"
|
|
||||||
referrerPolicy: "same-origin"
|
|
||||||
customFrameOptionsValue: "SAMEORIGIN"
|
|
||||||
|
|
||||||
# Rate limiting
|
|
||||||
rate-limit:
|
|
||||||
rateLimit:
|
|
||||||
average: 100
|
|
||||||
burst: 50
|
|
||||||
period: 1m
|
|
||||||
|
|
||||||
# Nextcloud chain
|
|
||||||
nextcloud-chain:
|
|
||||||
chain:
|
|
||||||
middlewares:
|
|
||||||
- nextcloud-secure-headers
|
|
||||||
- security-headers
|
|
||||||
|
|
||||||
# TLS Configuration
|
|
||||||
tls:
|
|
||||||
certificates:
|
|
||||||
# Cloudflare Origin CA *.forust.xyz
|
|
||||||
- certFile: /certs/cloudflare.pem
|
|
||||||
keyFile: /certs/cloudflare.key
|
|
||||||
# Local certificate
|
|
||||||
- certFile: /certs/workstation+1.pem
|
|
||||||
keyFile: /certs/workstation+1-key.pem
|
|
||||||
|
|
||||||
stores:
|
|
||||||
default:
|
|
||||||
defaultCertificate:
|
|
||||||
# Fallback local certificate
|
|
||||||
certFile: /certs/workstation+1.pem
|
|
||||||
keyFile: /certs/workstation+1-key.pem
|
|
||||||
|
|
||||||
options:
|
|
||||||
default:
|
|
||||||
minVersion: VersionTLS12
|
|
||||||
sniStrict: true
|
|
||||||
cipherSuites:
|
|
||||||
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
|
||||||
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
|
||||||
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
# HTTPS Redirect
|
||||||
|
redirect-https:
|
||||||
|
redirectScheme:
|
||||||
|
scheme: https
|
||||||
|
permanent: true
|
||||||
|
|
||||||
|
# Metube Basic Auth
|
||||||
|
metube-auth:
|
||||||
|
basicAuth:
|
||||||
|
users:
|
||||||
|
- "vv:$2y$05$JdT8AGUO9bd.E/PiCmKaoOJS1RFlXkrrmZ5mJ4f8/a1bEW39L3FbS"
|
||||||
|
|
||||||
|
realm: "MeTube Access"
|
||||||
|
|
||||||
|
# Basic Auth Traefik Dashboard
|
||||||
|
auth:
|
||||||
|
basicAuth:
|
||||||
|
users:
|
||||||
|
- "admin:$2y$05$.CKDD82sNUxcpaRrLbHuK.dopqt1fgurc2yfTKAT5OFzT7RvPrJHK"
|
||||||
|
realm: "Traefik Dashboard"
|
||||||
|
|
||||||
|
# Basic Auth Dockmon
|
||||||
|
dockmon-auth:
|
||||||
|
basicAuth:
|
||||||
|
users:
|
||||||
|
- "admin:$2y$05$.CKDD82sNUxcpaRrLbHuK.dopqt1fgurc2yfTKAT5OFzT7RvPrJHK"
|
||||||
|
realm: "Dockmon Access"
|
||||||
|
|
||||||
|
# Cloudflare IP Whitelist
|
||||||
|
cloudflare-ipwhitelist:
|
||||||
|
ipWhiteList:
|
||||||
|
sourceRange:
|
||||||
|
- "173.245.48.0/20"
|
||||||
|
- "103.21.244.0/22"
|
||||||
|
- "103.22.200.0/22"
|
||||||
|
- "103.31.4.0/22"
|
||||||
|
- "141.101.64.0/18"
|
||||||
|
- "108.162.192.0/18"
|
||||||
|
- "190.93.240.0/20"
|
||||||
|
- "188.114.96.0/20"
|
||||||
|
- "197.234.240.0/22"
|
||||||
|
- "198.41.128.0/17"
|
||||||
|
- "162.158.0.0/15"
|
||||||
|
- "104.16.0.0/13"
|
||||||
|
- "104.24.0.0/14"
|
||||||
|
- "172.64.0.0/13"
|
||||||
|
- "131.0.72.0/22"
|
||||||
|
|
||||||
|
# Authentik + secure headers
|
||||||
|
security-chain:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- authentik@file
|
||||||
|
- security-headers@file
|
||||||
|
authentik:
|
||||||
|
forwardAuth:
|
||||||
|
address: "http://authentik-server:9000/outpost.goauthentik.io/auth/traefik"
|
||||||
|
trustForwardHeader: true
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-authentik-username
|
||||||
|
- X-authentik-groups
|
||||||
|
- X-authentik-email
|
||||||
|
- X-authentik-name
|
||||||
|
- X-authentik-uid
|
||||||
|
- X-authentik-jwt
|
||||||
|
- X-authentik-meta-jwks
|
||||||
|
- X-authentik-meta-outpost
|
||||||
|
- X-authentik-meta-provider
|
||||||
|
- X-authentik-meta-app
|
||||||
|
- X-authentik-meta-version
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
security-headers:
|
||||||
|
headers:
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
customFrameOptionsValue: "SAMEORIGIN"
|
||||||
|
customResponseHeaders:
|
||||||
|
X-Content-Type-Options: "nosniff"
|
||||||
|
Referrer-Policy: "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
# Nextcloud specific headers
|
||||||
|
nextcloud-secure-headers:
|
||||||
|
headers:
|
||||||
|
hostsProxyHeaders:
|
||||||
|
- "X-Forwarded-Host"
|
||||||
|
- "X-Forwarded-Proto"
|
||||||
|
referrerPolicy: "same-origin"
|
||||||
|
customFrameOptionsValue: "SAMEORIGIN"
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
rate-limit:
|
||||||
|
rateLimit:
|
||||||
|
average: 100
|
||||||
|
burst: 50
|
||||||
|
period: 1m
|
||||||
|
|
||||||
|
# Nextcloud chain
|
||||||
|
nextcloud-chain:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- nextcloud-secure-headers@file
|
||||||
|
- security-headers@file
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
# Nextcloud prod
|
||||||
|
nextcloud:
|
||||||
|
rule: "Host(`nextcloud.forust.xyz`)"
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
service: nextcloud
|
||||||
|
middlewares:
|
||||||
|
- nextcloud-chain
|
||||||
|
tls: {}
|
||||||
|
|
||||||
|
# Nextcloud dev
|
||||||
|
nextcloud-local:
|
||||||
|
rule: "Host(`nextcloud.workstation.internal`)"
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
- web
|
||||||
|
service: nextcloud
|
||||||
|
middlewares:
|
||||||
|
- nextcloud-chain
|
||||||
|
tls: {}
|
||||||
|
|
||||||
|
# Nextcloud dev
|
||||||
|
nextcloud-dev:
|
||||||
|
rule: "Host(`nextcloud.gigaforust.internal`)"
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
- web
|
||||||
|
service: nextcloud
|
||||||
|
middlewares:
|
||||||
|
- nextcloud-chain
|
||||||
|
tls: {}
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Nextcloud Main
|
||||||
|
nextcloud:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://nextcloud-aio-apache:11000"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# TLS Configuration
|
||||||
|
tls:
|
||||||
|
certificates:
|
||||||
|
# Cloudflare Origin CA *.forust.xyz
|
||||||
|
- certFile: /certs/cloudflare.pem
|
||||||
|
keyFile: /certs/cloudflare.key
|
||||||
|
- certFile: /certs/xdfnx.pem
|
||||||
|
keyFile: /certs/xdfnx.key
|
||||||
|
stores:
|
||||||
|
default:
|
||||||
|
defaultCertificate:
|
||||||
|
# Fallback local certificate
|
||||||
|
certFile: /certs/gigaforust.internal+1.pem
|
||||||
|
keyFile: /certs/gigaforust.internal+1-key.pem
|
||||||
|
|
||||||
|
options:
|
||||||
|
default:
|
||||||
|
minVersion: VersionTLS12
|
||||||
|
sniStrict: false
|
||||||
|
cipherSuites:
|
||||||
|
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||||
|
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||||
|
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
uptime-kuma:
|
||||||
|
image: louislam/uptime-kuma:2
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: uptime-kuma
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
ports:
|
||||||
|
# <Host Port>:<Container Port>
|
||||||
|
- "3001:3001"
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.uptime-kuma.rule=Host(`uptime.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.uptime-kuma.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.uptime-kuma.tls=true"
|
||||||
|
- "traefik.http.services.uptime-kuma.loadbalancer.server.port=3001"
|
||||||
|
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.uptime-kuma-local.rule=Host(`uptime.workstation.internal`) || Host(`uptime.internal`)"
|
||||||
|
- "traefik.http.routers.uptime-kuma-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.uptime-kuma-local.tls=true"
|
||||||
|
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.uptime-kuma-dev.rule=Host(`uptime.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.uptime-kuma-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.uptime-kuma-dev.tls=true"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
+1
-10
@@ -1,5 +1,6 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.gitattributes
|
.gitattributes
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
/modules/__pycache__/
|
/modules/__pycache__/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -12,14 +13,4 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
/downloads/
|
/downloads/
|
||||||
/Downloads/
|
/Downloads/
|
||||||
/modules/custom_modules
|
|
||||||
.idea
|
|
||||||
config.ini
|
config.ini
|
||||||
unknown_errors.txt
|
|
||||||
moonlogs.txt
|
|
||||||
thumb.jpg
|
|
||||||
antipm_pic.jpg
|
|
||||||
musicbot/
|
|
||||||
.trunk/
|
|
||||||
previous_profiles/
|
|
||||||
.python-version
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
userbot_forust:
|
forust:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: userbot_forust
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -16,11 +15,10 @@ services:
|
|||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
|
|
||||||
userbot_anna:
|
anna:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: userbot_anna
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -31,27 +29,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
|
||||||
develop:
|
|
||||||
watch:
|
|
||||||
- action: sync
|
|
||||||
path: modules
|
|
||||||
target: /app/modules
|
|
||||||
- action: sync
|
|
||||||
path: utils
|
|
||||||
target: /app/utils
|
|
||||||
- action: sync
|
|
||||||
path: main.py
|
|
||||||
target: /app/main.py
|
|
||||||
- action: rebuild
|
|
||||||
path: .env
|
|
||||||
- action: rebuild
|
|
||||||
path: .env.anna
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
downloads:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
userbot_network:
|
|
||||||
driver: bridge
|
|
||||||
Reference in New Issue
Block a user