Compare commits

..

32 Commits

Author SHA1 Message Date
forust fc83176522 chore(userbot): optimize Dockerfile with multi-stage build and static ffmpeg
Deploy to Server / deploy (push) Has been cancelled
- Multi-stage build: pip deps built in separate stage
- Static ffmpeg binary instead of apt package (avoid 200+ deps)
- Keep only git, mediainfo, wget via apt
2026-06-19 14:56:37 +02:00
forust 0506aaaac8 chore(userbot): apply ruff check --fix and ruff format
- ruff check --fix: 210 auto-fixed errors (import sorting, trailing
  whitespace, unused imports, f-string fixups, deprecated annotations)
- ruff format: 104 files reformatted to consistent style
- 268 non-auto-fixable issues remain (S113 requests timeout, etc.)
2026-06-19 12:19:01 +02:00
forust bd9724da69 Revert "chore: switch yaml formatter to prettier, disable yaml-language-server validation (yamllint covers it)"
This reverts commit 3bad433f1a.
2026-06-19 12:15:14 +02:00
forust 3bad433f1a chore: switch yaml formatter to prettier, disable yaml-language-server validation (yamllint covers it) 2026-06-19 12:11:58 +02:00
forust 2bd7a5176f Revert "chore: disable yaml-language-server formatting (handled by prettier), set yamlVersion 1.2"
This reverts commit a9ff01261b.
2026-06-19 12:08:42 +02:00
forust a9ff01261b chore: disable yaml-language-server formatting (handled by prettier), set yamlVersion 1.2 2026-06-19 12:06:16 +02:00
forust 1362ebc3c2 chore(userbot): apply prettier formatting across manifests 2026-06-19 12:03:13 +02:00
forust 2de6131ba7 chore: apply prettier formatting across compose files and configs 2026-06-19 12:03:11 +02:00
forust 1762962f32 chore(k8s): fix indentation in ingress manifests (3-space -> 2-space) 2026-06-19 12:03:02 +02:00
forust 7fb9e46c05 chore: remove singleQuote from prettier config to preserve original quotes 2026-06-19 12:02:59 +02:00
forust b33488342a chore: add truthy allowed-values to yamllint config 2026-06-19 12:01:11 +02:00
forust d53b14b1de chore: apply yaml lint fixes across compose files
- Fix trailing whitespace in compose files
- Add missing final newlines (EOF)
- Fix indentation in dockmon (3-space -> 2-space) and glance monitor.yml
- Align comments consistently
2026-06-19 11:57:14 +02:00
forust a6a6d933da chore: add linter configs (editorconfig, hadolint, markdownlint, prettier, yamllint, ruff, zed) 2026-06-19 11:57:04 +02:00
forust 0803f3efff chore(k8s): returned to Host || Host standart instead of regexp.
Deploy to Server / deploy (push) Has been cancelled
Yaml lint (yamllint)
2026-06-18 21:02:40 +02:00
forust ec0420962b hotfix: update headscale config for v0.29.0 breaking changes
- ephemeral_node_inactivity_timeout → node.ephemeral.inactivity_timeout
- randomize_client_port → randomizeClientPort in policy file
2026-06-18 14:26:58 +02:00
forust b407202e53 chore(k8s): imagepullpolicy always for homepages
Deploy to Server / deploy (push) Has been cancelled
tools column on the forust.xyz landing
2026-06-17 01:53:00 +02:00
forust b9b8474455 feat(k8s): protect all prod routers with crowdsec middleware 2026-06-17 01:50:33 +02:00
forust 76853637bc feat: traefik prometheus metrics 2026-06-17 01:41:45 +02:00
forust dac3777fc4 fix: authentik k8s url 2026-06-17 01:41:13 +02:00
forust bb5a3697f2 feat: bentopdf k8s manifests 2026-06-16 13:35:03 +02:00
forust e73aacb900 feat: add bentopdf pdf suite to converters stack 2026-06-16 12:36:31 +02:00
forust ea483da645 feat: kube-prometeus-stack 2026-06-16 12:36:31 +02:00
forust 85d35f86a7 feat: switch adguard dns to a dedicated metallb IP 2026-06-16 12:36:31 +02:00
forust 3d03ab1ea4 fix: priority for homepage's router 2026-06-16 12:36:26 +02:00
forust 4ca3ccdad3 chore(k8s): router rewrite
- returned to Host matcher instead of Hostregexp
- switched dockercompose labels to letsencrypt
- renamed DoH route
2026-06-16 12:34:15 +02:00
forust 10e26cda72 feat: crowdsec
- moved bouncer tokens to secrets
- experimental host ssh log parsing
- moved crowdsec to it's own directory
2026-06-16 00:23:29 +02:00
forust c648dfd147 chore(k8s): renamed gitea ingressroute to ingress
Deploy to Server / deploy (push) Has been cancelled
2026-06-14 23:24:24 +02:00
forust 2f97821dc6 feat: crowdsec lapi helm
Deploy to Server / deploy (push) Has been cancelled
2026-06-14 23:21:37 +02:00
forust 3d78b90f3a updated kener to v4.1.0 2026-06-14 23:02:35 +02:00
forust 3dc8228e22 matched dns record and the searxng's route 2026-06-14 23:01:58 +02:00
forust 93171ad8e6 fixed and tweaked glance app 2026-06-14 23:00:29 +02:00
forust dca7ad0902 fix: race condition convertx and homepage
typo in glance stripprefix
glance placed at /glance
2026-06-14 22:48:50 +02:00
191 changed files with 5297 additions and 5691 deletions
+24
View File
@@ -0,0 +1,24 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yml,yaml}]
indent_size = 2
[*.{json,jsonc}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.py]
indent_size = 4
[{Makefile,makefile}]
indent_style = tab
+1 -1
View File
@@ -36,4 +36,4 @@ jobs:
else
echo ">>> Skipping $dir: no compose file found"
fi
done
done
+2
View File
@@ -104,4 +104,6 @@ temp/*
*/k8s/*secret*
!*/k8s/*secret*.example
traefik/k8s/local-tls.yaml
converters/k8s/config.yaml
convertx/k8s/config.yaml
traefik/k8s/crowdsec-middleware.yaml
+10
View File
@@ -0,0 +1,10 @@
ignored:
- DL3008
- DL3042
- DL3018
- DL3059
trustedRegistries:
- docker.io
- ghcr.io
- quay.io
- gcr.forust.xyz
+8
View File
@@ -0,0 +1,8 @@
{
"default": true,
"MD013": false,
"MD024": false,
"MD033": false,
"MD041": false,
"MD046": false
}
+7
View File
@@ -0,0 +1,7 @@
bracketSameLine: true
htmlWhitespaceSensitivity: css
printWidth: 120
tabWidth: 2
trailingComma: all
proseWrap: preserve
endOfLine: lf
+22
View File
@@ -0,0 +1,22 @@
extends: default
rules:
comments:
min-spaces-from-content: 1
comments-indentation: false
document-start: disable
line-length: disable
braces:
min-spaces-inside: 0
max-spaces-inside: 1
brackets:
min-spaces-inside: 0
max-spaces-inside: 1
indentation:
spaces: 2
indent-sequences: consistent
truthy:
allowed-values:
- "true"
- "false"
- "on"
+39
View File
@@ -0,0 +1,39 @@
{
"tab_size": 2,
"soft_wrap": "prefer_line",
"preferred_line_length": 120,
"format_on_save": "on",
"languages": {
"YAML": {
"tab_size": 2,
"hard_tabs": false,
"format_on_save": "on",
"formatter": {
"language_server": { "name": "yaml-language-server" }
}
},
"Python": {
"tab_size": 4,
"format_on_save": "on",
"formatter": {
"language_server": { "name": "ruff" }
}
}
},
"lsp": {
"yaml-language-server": {
"settings": {
"yaml": {
"schemas": {
"kubernetes": ["**/k8s/*.yaml", "**/k8s/*.yml"]
},
"validate": true,
"completion": true,
"format": {
"enable": true
}
}
}
}
}
}
+6 -6
View File
@@ -17,11 +17,11 @@ services:
labels:
- "traefik.enable=true"
- "traefik.http.services.adguard.loadbalancer.server.port=3000"
# Prod Router
- "traefik.http.routers.adguard.rule=Host(`dns.forust.xyz`) || Host(`adguard.forust.xyz`)"
- "traefik.http.routers.adguard.entrypoints=websecure"
- "traefik.http.routers.adguard.tls=true"
- "traefik.http.routers.adguard.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.adguard-local.rule=Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`)"
- "traefik.http.routers.adguard-local.entrypoints=websecure"
@@ -31,10 +31,10 @@ services:
- "traefik.http.routers.adguard-dev.entrypoints=websecure"
- "traefik.http.routers.adguard-dev.tls=true"
# DoH Router
- "traefik.http.routers.dns.rule=(Host(`dns.forust.xyz`) && PathPrefix(`/dns-query`))"
- "traefik.http.routers.dns.entrypoints=websecure"
- "traefik.http.routers.dns.tls.certresolver=letsencrypt"
- "traefik.http.routers.dns-over-https.rule=(Host(`dns.forust.xyz` || Host(`adguard.forust.xyz`)) && PathPrefix(`/dns-query`))"
- "traefik.http.routers.dns-over-https.entrypoints=websecure"
- "traefik.http.routers.dns-over-https.tls.certresolver=letsencrypt"
# Glance Metadata
- glance.name=adguard
- glance.url=https://adguard.forust.xyz/
+26
View File
@@ -1,5 +1,31 @@
apiVersion: v1
kind: Service
metadata:
name: adguard-lb-service
namespace: adguard
annotations:
metallb.io/loadBalancerIPs: "192.168.80.3"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: adguard
ports:
- name: dns-udp
port: 53
targetPort: 53
protocol: UDP
- name: dns-tcp
port: 53
targetPort: 53
protocol: TCP
- name: dot
port: 853
targetPort: 853
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: adguard-service
namespace: adguard
+7 -17
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(adguard|dns)\.forust\.xyz$`)
- match: Host(`adguard.forust.xyz`) || Host(`dns.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: adguard-service
port: 3000
@@ -24,7 +26,7 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(adguard|dns)\.(workstation|gigaforust)\.internal$`)
- match: Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`) || Host(`adguard.gigaforust.internal`) || Host(`dns.gigaforust.internal`)
kind: Rule
services:
- name: adguard-service
@@ -39,24 +41,12 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(adguard|dns)\.forust\.xyz$`) && PathPrefix(`/dns-query`)
- match: (Host(`adguard.forust.xyz`) || Host(`dns.forust.xyz`)) && PathPrefix(`/dns-query`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: adguard-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: adguard-dot
namespace: adguard
spec:
entryPoints:
- dot
routes:
- match: HostSNI(`*`)
services:
- name: adguard-service
port: 853
+1 -1
View File
@@ -47,7 +47,7 @@ services:
# Prod Router
- "traefik.http.routers.authentik-server.rule=Host(`auth.forust.xyz`)"
- "traefik.http.routers.authentik-server.entrypoints=websecure"
- "traefik.http.routers.authentik-server.tls=true"
- "traefik.http.routers.authentik-server.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.authentik-server-local.rule=Host(`auth.workstation.internal`)"
- "traefik.http.routers.authentik-server-local.entrypoints=websecure"
+4 -3
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^auth\.forust\.xyz$`)
- match: Host(`auth.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: authentik-server-service
port: 9000
@@ -24,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^auth\.(workstation|gigaforust)\.internal$`)
- match: Host(`auth.workstation.internal`) || Host(`auth.gigaforust.internal`)
kind: Rule
services:
- name: authentik-server-service
port: 9000
+1 -1
View File
@@ -5,7 +5,7 @@ services:
restart: unless-stopped
security_opt:
- no-new-privileges:true
network_mode: 'host'
network_mode: "host"
# https://github.com/timothymiller/cloudflare-ddns#-quick-start
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:?Cloudflare API token is required}
+1 -1
View File
@@ -21,7 +21,7 @@ services:
# Prod Router
- "traefik.http.routers.checkmk.rule=Host(`cmk.forust.xyz`)"
- "traefik.http.routers.checkmk.entrypoints=websecure"
- "traefik.http.routers.checkmk.tls=true"
- "traefik.http.routers.checkmk.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.checkmk-local.rule=Host(`cmk.workstation.internal`)"
- "traefik.http.routers.checkmk-local.entrypoints=websecure"
+4 -3
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^cmk\.forust\.xyz$`)
- match: Host(`cmk.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
services:
- name: checkmk-service
port: 5000
@@ -24,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^cmk\.(workstation|gigaforust)\.internal$`)
- match: Host(`cmk.workstation.internal`) || Host(`cmk.gigaforust.internal`)
kind: Rule
services:
- name: checkmk-service
port: 5000
+9
View File
@@ -0,0 +1,9 @@
ACCOUNT_REGISTRATION=false
HTTP_ALLOWED=false
ALLOW_UNAUTHENTICAED=false
AUTO_DELETE_EVERY_N_HOURS=24
WEBROOT=/convert
HIDE_HISTORY=false
LANGUAGE=en
UNAUTHED_USER_SHARING=false
MAX_CONVERT_PROCESS=0
+70
View File
@@ -0,0 +1,70 @@
services:
convertx:
container_name: convertx
image: ghcr.io/c4illin/convertx:latest
restart: unless-stopped
ports:
- "9992:3000"
# https://github.com/C4illin/ConvertX#environment-variables
environment:
- JWT_SECRET=$(JWT_SECRET)
- ACCOUNT_REGISTRATION=$(ACCOUNT_REGISTRATION:-false)
- HTTP_ALLOWED=$(HTTP_ALLOWED:-false)
- ALLOW_UNAUTHENTICATED=$(ALLOW_UNAUTHENTICATED:-false)
- AUTO_DELETE_EVERY_N_HOURS=$(AUTO_DELETE_EVERY_N_HOURS:-24)
- WEBROOT=$(WEBROOT)
- HIDE_HISTORY=$(HIDE_HISTORY:-false)
- LANGUAGE=$(LANGUAGE:-en)
- UNAUTHENTICATED_USER_SHARING=$(UNAUTHENTICATED_USER_SHARING:-false)
- MAX_CONVERT_PROCESS=$(MAX_CONVERT_PROCESS:-0)
labels:
- "traefik.enable=true"
- "traefik.http.services.convertx.loadbalancer.server.port=3000"
# Prod Router
- "traefik.http.routers.convertx.rule=(Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/convert`)"
- "traefik.http.routers.convertx.entrypoints=websecure"
- "traefik.http.routers.convertx.priority=50"
- "traefik.http.routers.convertx.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.convertx-local.rule=Host(`workstation.internal`) && PathPrefix(`/convert`)"
- "traefik.http.routers.convertx-local.entrypoints=websecure"
- "traefik.http.routers.convertx-local.priority=50"
- "traefik.http.routers.convertx-local.tls=true"
# Dev Router
- "traefik.http.routers.convertx-dev.rule=Host(`gigaforust.internal`) && PathPrefix(`/convert`)"
- "traefik.http.routers.convertx-dev.entrypoints=websecure"
- "traefik.http.routers.convertx-dev.priority=50"
- "traefik.http.routers.convertx-dev.tls=true"
networks:
- proxy
volumes:
- data:/app/data
bentopdf:
container_name: bentopdf
image: bentopdf/bentopdf:latest
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.services.bentopdf.loadbalancer.server.port=8080"
# Prod router
- "traefik.http.routers.bentopdf.rule=Host(`pdf.forust.xyz`)"
- "traefik.http.routers.bentopdf.entrypoints=websecure"
- "traefik.http.routers.bentopdf.tls.certresolver=letsencrypt"
- "traefik.http.routers.bentopdf.tls=true"
# Local router
- "traefik.http.routers.bentopdf-local.rule=Host(`pdf.wokstation.internal`)"
- "traefik.http.routers.bentopdf-local.entrypoints=websecure"
- "traefik.http.routers.bentopdf-local.tls=true"
# Dev router
- "traefik.http.routers.bentopdf-dev.rule=Host(`pdf.gigaforust.internal`)"
- "traefik.http.routers.bentopdf-dev.entrypoints=websecure"
- "traefik.http.routers.bentopdf-dev.tls=true"
networks:
- proxy
networks:
proxy:
external: true
volumes:
data:
+42
View File
@@ -0,0 +1,42 @@
kind: Service
apiVersion: v1
metadata:
name: bentopdf-service
namespace: converters
spec:
selector:
app: bentopdf
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bentopdf-deployment
namespace: converters
spec:
replicas: 2
selector:
matchLabels:
app: bentopdf
template:
metadata:
labels:
app: bentopdf
spec:
containers:
- image: bentopdf/bentopdf:latest
imagePullPolicy: Always
name: bentopdf
ports:
- containerPort: 8080
resources:
requests:
memory: "50Mi"
cpu: "50m"
ephemeral-storage: "100Mi"
limits:
memory: "700Mi"
cpu: "700m"
ephemeral-storage: "5Gi"
@@ -3,7 +3,7 @@ apiVersion: v1
kind: ConfigMap
metadata:
name: convertx-config
namespace: convertx
namespace: converters
data:
ACCOUNT_REGISTRATION: "false"
HTTP_ALLOWED: "false"
@@ -2,7 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: convertx-service
namespace: convertx
namespace: converters
spec:
selector:
app: convertx
@@ -14,7 +14,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: convertx-deployment
namespace: convertx
namespace: converters
spec:
replicas: 1
selector:
@@ -54,7 +54,7 @@ apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: convertx-pvc
namespace: convertx
namespace: converters
spec:
accessModes:
- ReadWriteOnce
+65
View File
@@ -0,0 +1,65 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: convertx-prod
namespace: converters
spec:
entryPoints:
- websecure
routes:
- match: (Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/convert`)
kind: Rule
priority: 50
services:
- name: convertx-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: convertx-local
namespace: converters
spec:
entryPoints:
- websecure
routes:
- match: (Host(`workstation.internal`) || Host(`gigaforust.internal`)) && PathPrefix(`/convert`)
kind: Rule
priority: 50
services:
- name: convertx-service
port: 3000
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: bentopdf-prod
namespace: converters
spec:
entryPoints:
- websecure
routes:
- match: Host(`pdf.forust.xyz`)
kind: Rule
services:
- name: bentopdf-service
port: 8080
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: bentopdf-local
namespace: converters
spec:
entryPoints:
- websecure
routes:
- match: Host(`pdf.workstation.internal`) || Host(`pdf.gigaforust.internal`)
kind: Rule
services:
- name: bentopdf-service
port: 8080
@@ -1,4 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: convertx
name: converters
@@ -2,6 +2,7 @@ apiVersion: v1
kind: Secret
metadata:
name: convertx-secrets
namespace: converters
type: Opaque
stringData:
jwt-secret: ""
-43
View File
@@ -1,43 +0,0 @@
services:
convertx:
container_name: convertx
image: ghcr.io/c4illin/convertx:latest
restart: unless-stopped
ports:
- "9992:3000"
# https://github.com/C4illin/ConvertX#environment-variables
environment:
- JWT_SECRET=$(JWT_SECRET)
- ACCOUNT_REGISTRATION=$(ACCOUNT_REGISTRATION:-false)
- HTTP_ALLOWED=$(HTTP_ALLOWED:-false)
- ALLOW_UNAUTHENTICATED=$(ALLOW_UNAUTHENTICATED:-false)
- AUTO_DELETE_EVERY_N_HOURS=$(AUTO_DELETE_EVERY_N_HOURS:-24)
- WEBROOT=$(WEBROOT)
- HIDE_HISTORY=$(HIDE_HISTORY:-false)
- LANGUAGE=$(LANGUAGE:-en)
- UNAUTHENTICATED_USER_SHARING=$(UNAUTHENTICATED_USER_SHARING:-false)
- MAX_CONVERT_PROCESS=$(MAX_CONVERT_PROCESS:-0)
labels:
- "traefik.enable=true"
- "traefik.http.services.convertx.loadbalancer.server.port=3000"
# Prod Router
- "traefik.http.routers.convertx.rule=Host(`convert.forust.xyz`)"
- "traefik.http.routers.convertx.entrypoints=websecure"
- "traefik.http.routers.convertx.tls=true"
# Local Router
- "traefik.http.routers.convertx-local.rule=Host(`convert.workstation.internal`)"
- "traefik.http.routers.convertx-local.entrypoints=websecure"
- "traefik.http.routers.convertx-local.tls=true"
# Dev Router
- "traefik.http.routers.convertx-dev.rule=Host(`convertx.gigaforust.internal`)"
- "traefik.http.routers.convertx-dev.entrypoints=websecure"
- "traefik.http.routers.convertx-dev.tls=true"
networks:
- proxy
volumes:
- data:/app/data
networks:
proxy:
external: true
volumes:
data:
-32
View File
@@ -1,32 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: convertx-prod
namespace: convertx
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^forust\.xyz$`) && PathPrefix(`/convert`)
kind: Rule
services:
- name: convertx-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: convertx-local
namespace: convertx
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(workstation|gigaforust)\.internal$`) && PathPrefix(`/convert`)
kind: Rule
services:
- name: convertx-service
port: 3000
+14
View File
@@ -0,0 +1,14 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: crowdsec-bouncer
namespace: crowdsec
spec:
plugin:
crowdsec-bouncer:
enabled: true
LogLevel: INFO
CrowdsecMode: live
CrowdsecLapiScheme: http
CrowdsecLapiHost: crowdsec-service.crowdsec.svc.cluster.local:8080
CrowdsecLapiKeyFile: "/etc/traefik/secrets/traefik-api-key"
+57
View File
@@ -0,0 +1,57 @@
container_runtime: containerd
agent:
env:
- name: COLLECTIONS
value: "crowdsecurity/traefik crowdsecurity/base-http-scenarios crowdsecurity/sshd"
extraVolumes:
- name: journal-dir
hostPath:
path: /var/log/journal
type: DirectoryOrCreate
- name: run-journal-dir
hostPath:
path: /run/log/journal
type: DirectoryOrCreate
extraVolumeMounts:
- name: journal-dir
mountPath: /var/log/journal
readOnly: true
- name: run-journal-dir
mountPath: /run/log/journal
readOnly: true
acquisition:
- namespace: traefik
podName: "*traefik*"
program: traefik
poll_without_inotify: true
acquisitionCustom: |
- source: journalctl
journalctl_filter:
- _SYSTEMD_UNIT=sshd.service
labels:
type: syslog
resources:
requests:
cpu: 50m
memory: 100Mi
limits:
cpu: 200m
memory: 500Mi
lapi:
env:
- name: COLLECTIONS
value: "crowdsecurity/traefik crowdsecurity/base-http-scenarios crowdsecurity/sshd"
service:
type: NodePort
nodePort: 30011
resources:
requests:
cpu: 50m
memory: 150Mi
limits:
cpu: 200m
memory: 500Mi
+41 -41
View File
@@ -1,46 +1,46 @@
services:
dockmon:
image: darthnorse/dockmon:latest
container_name: dockmon
restart: unless-stopped
# ports:
# - 8000:443
volumes:
- data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: [ "CMD", "curl", "-k", "-f", "https://localhost:443/health" ]
interval: 30s
timeout: 10s
retries: 3
labels:
- "traefik.enable=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"
dockmon:
image: darthnorse/dockmon:latest
container_name: dockmon
restart: unless-stopped
# ports:
# - 8000:443
volumes:
- data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "curl", "-k", "-f", "https://localhost:443/health"]
interval: 30s
timeout: 10s
retries: 3
labels:
- "traefik.enable=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"
# Prod Router
- "traefik.http.routers.dockmon.rule=Host(`dockmon.forust.xyz`)"
- "traefik.http.routers.dockmon.entrypoints=websecure"
- "traefik.http.routers.dockmon.middlewares=security-headers@file"
- "traefik.http.routers.dockmon.tls=true"
# Local Router
- "traefik.http.routers.dockmon-local.rule=Host(`dockmon.workstation.internal`)"
- "traefik.http.routers.dockmon-local.entrypoints=websecure"
- "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.tls=true"
# Prod Router
- "traefik.http.routers.dockmon.rule=Host(`dockmon.forust.xyz`)"
- "traefik.http.routers.dockmon.entrypoints=websecure"
- "traefik.http.routers.dockmon.middlewares=security-headers@file"
- "traefik.http.routers.dockmon.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.dockmon-local.rule=Host(`dockmon.workstation.internal`)"
- "traefik.http.routers.dockmon-local.entrypoints=websecure"
- "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.tls=true"
# Glance Metadata
- 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:
- proxy
# Glance Metadata
- 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:
- proxy
volumes:
data:
data:
networks:
proxy:
external: true
proxy:
external: true
+3 -3
View File
@@ -15,9 +15,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^dockmon\.forust\.xyz$`)
- match: Host(`dockmon.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: security-headers@file
services:
- name: dockmon-service
@@ -35,10 +36,9 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^dockmon\.(workstation|gigaforust)\.internal$`)
- match: Host(`dockmon.workstation.internal`) || Host(`dockmon.gigaforust.internal`)
kind: Rule
services:
- name: dockmon-service
port: 443
serversTransport: dockmon-transport
+2 -2
View File
@@ -11,11 +11,11 @@ services:
- "traefik.enable=true"
- "traefik.http.services.downtify.loadbalancer.server.port=8000"
# Prod Router
# 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.tls=true"
- "traefik.http.routers.downtify.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.downtify-local.rule=Host(`downtify.workstation.internal`)"
- "traefik.http.routers.downtify-local.entrypoints=websecure"
+3 -3
View File
@@ -7,9 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^downtify\.forust\.xyz$`)
- match: Host(`downtify.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: security-chain@file
services:
- name: downtify-service
@@ -26,9 +27,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^downtify\.(workstation|gigaforust)\.internal$`)
- match: Host(`downtify.workstation.internal`) || Host(`downtify.gigaforust.internal`)
kind: Rule
services:
- name: downtify-service
port: 8000
+2 -2
View File
@@ -5,7 +5,7 @@ services:
volumes:
- redis-data:/data
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
@@ -25,7 +25,7 @@ services:
redis:
condition: service_healthy
healthcheck:
test: [ "CMD-SHELL", "redis-cli -h redis EXISTS EDU_PHPSESSID | grep -q 1" ]
test: ["CMD-SHELL", "redis-cli -h redis EXISTS EDU_PHPSESSID | grep -q 1"]
interval: 30s
timeout: 5s
retries: 10
+2 -3
View File
@@ -12,7 +12,7 @@ services:
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=gitea
- GITEA__database__NAME=gitea
#Server
# Server
- GITEA__server__ROOT_URL=https://gitea.forust.xyz
- GITEA__server__SSH_DOMAIN=gitssh.forust.xyz
- GITEA__server__SSH_PORT=2221
@@ -36,7 +36,7 @@ services:
# Prod Router
- "traefik.http.routers.gitea.rule=Host(`gitea.forust.xyz`)"
- "traefik.http.routers.gitea.entrypoints=websecure"
- "traefik.http.routers.gitea.tls=true"
- "traefik.http.routers.gitea.tls.certresolver"
# Local Router
- "traefik.http.routers.gitea-local.rule=Host(`gitea.workstation.internal`)"
- "traefik.http.routers.gitea-local.entrypoints=websecure"
@@ -53,7 +53,6 @@ services:
- "traefik.http.routers.gitea-registry.rule=Host(`gcr.forust.xyz`) && PathPrefix(`/v2`)"
- "traefik.http.routers.gitea-registry.entrypoints=websecure"
- "traefik.http.routers.gitea-registry.tls.certresolver=letsencrypt"
- "traefik.http.routers.gitea-registry.tls=true"
ports:
- "2221:22"
networks:
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^gitea\.forust\.xyz$`)
- match: Host(`gitea.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: gitea-service
port: 3000
@@ -24,12 +26,11 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^gitea\.(workstation|gigaforust)\.internal$`)
- match: Host(`gitea.workstation.internal`) || Host(`gitea.gigaforust.internal`)
kind: Rule
services:
- name: gitea-service
port: 3000
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
+8 -5
View File
@@ -15,19 +15,22 @@ services:
- "traefik.services.glance.loadbalancer.server.port=8080"
# Prod Router
- "traefik.http.routers.glance.rule=Host(`glance.forust.xyz`)"
- "traefik.http.routers.glance.rule=(Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/glance`)"
- "traefik.http.routers.glance.entrypoints=websecure"
- "traefik.http.routers.glance.tls=true"
- "traefik.http.routers.glance.priority=50"
- "traefik.http.routers.glance.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.glance-local.rule=Host(`glance.workstation.internal`)"
- "traefik.http.routers.glance-local.rule=Host(`workstation.internal`) && PathPrefix(`/glance`)"
- "traefik.http.routers.glance-local.entrypoints=websecure"
- "traefik.http.routers.glance-local.priority=50"
- "traefik.http.routers.glance-local.tls=true"
# Dev Router
- "traefik.http.routers.glance-dev.rule=Host(`glance.gigaforust.internal`)"
- "traefik.http.routers.glance-dev.rule=Host(`gigaforust.internal`) && PathPrefix(`/glance`)"
- "traefik.http.routers.glance-dev.entrypoints=websecure"
- "traefik.http.routers.glance-dev.priority=50"
- "traefik.http.routers.glance-dev.tls=true"
networks:
- proxy
networks:
proxy:
external: true
external: true
-1
View File
@@ -64,7 +64,6 @@
name: Apple
- symbol: MSFT
name: Microsoft
- type: releases
cache: 1d
+11 -13
View File
@@ -1,15 +1,13 @@
- name: Monitoring
columns:
- size: small
widgets:
- type: dns-stats
service: adguard
url: http://adguardhome:3000
username: forust
password: ${ADGUARD_PASSWORD}
- size: full
widgets:
- type: docker-containers
hide-by-default: false
- size: small
widgets:
- type: dns-stats
service: adguard
url: http://adguardhome:3000
username: forust
password: ${ADGUARD_PASSWORD}
- size: full
widgets:
- type: docker-containers
hide-by-default: false
+2
View File
@@ -56,6 +56,8 @@ data:
glance.yml: |
server:
assets-path: /app/assets
proxied: true
base-url: /glance
theme:
# Перевели #050505 и #e0e0e0 в формат HSL для Glance
background-color: 0 0 2 # Истинно черный фон
+1 -1
View File
@@ -56,7 +56,7 @@ spec:
readOnly: true
resources:
requests:
memory: "10Mi"
memory: "30Mi"
cpu: "20m"
limits:
memory: "100Mi"
+17 -5
View File
@@ -7,10 +7,11 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^forust\.xyz$`)
- match: (Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/glance`)
kind: Rule
priority: 50
middlewares:
- name: glance-strupprefix
- name: glance-stripprefix
services:
- name: glance-service
port: 8080
@@ -26,10 +27,21 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^glance\.(workstation|gigaforust)\.internal$`)
- match: (Host(`glance.workstation.internal`) || Host(`glance.gigaforust.internal`)) && PathPrefix(`/glance`)
kind: Rule
middlewares:
- name: glance-stripprefix
priority: 50
services:
- name: glance-service
port: 8080
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: glance-stripprefix
namespace: glance
spec:
stripPrefix:
prefixes:
- /glance
+3 -3
View File
@@ -24,7 +24,7 @@ services:
- "traefik.http.routers.headscale.rule=Host(`hs.forust.xyz`)"
- "traefik.http.routers.headscale.entrypoints=websecure"
- "traefik.http.routers.headscale.service=headscale"
- "traefik.http.routers.headscale.tls=true"
- "traefik.http.routers.headscale.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.headscale-local.rule=Host(`hs.workstation.internal`)"
- "traefik.http.routers.headscale-local.entrypoints=websecure"
@@ -57,7 +57,7 @@ services:
container_name: headplane
restart: unless-stopped
ports:
- '13000:3000'
- "13000:3000"
volumes:
- ./config/headplane.yaml:/etc/headplane/config.yaml
- ./config/headscale.yaml:/etc/headscale/config.yaml
@@ -78,7 +78,7 @@ services:
- "traefik.http.routers.headscale-ui.rule=Host(`hs.forust.xyz`) && PathPrefix(`/admin`)"
- "traefik.http.routers.headscale-ui.entrypoints=websecure"
- "traefik.http.routers.headscale-ui.middlewares=security-chain@file"
- "traefik.http.routers.headscale-ui.tls=true"
- "traefik.http.routers.headscale-ui.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.headscale-ui-local.rule=Host(`hs.workstation.internal`) && PathPrefix(`/admin`)"
- "traefik.http.routers.headscale-ui-local.entrypoints=websecure"
+3 -2
View File
@@ -33,7 +33,9 @@ derp:
auto_update_enabled: true
update_frequency: 24h
disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m
node:
ephemeral:
inactivity_timeout: 30m
database:
type: sqlite
debug: false
@@ -76,4 +78,3 @@ unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
enabled: false
randomize_client_port: false
+2 -1
View File
@@ -8,7 +8,8 @@
"tagOwners": {},
"hosts": {},
"acls": [
{
{
"randomizeClientPort": false,
"#ha-meta": {
"name": "users",
"open": true
+11 -6
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.forust\.xyz$`)
- match: Host(`hs.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
services:
- name: headscale-server-external
port: 8080
@@ -24,7 +26,7 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.(workstation|gigaforust)\.internal$`)
- match: Host(`hs.workstation.internal`) || Host(`hs.gigaforust.internal`)
kind: Rule
services:
- name: headscale-server-external
@@ -40,9 +42,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.forust\.xyz$`) && PathPrefix(`/admin`)
- match: Host(`hs.forust.xyz`) && PathPrefix(`/admin`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: security-chain@file
services:
- name: headscale-ui-external
@@ -75,13 +78,17 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.forust\.xyz$`) && PathPrefix(`/metrics`)
- match: Host(`hs.forust.xyz`) && PathPrefix(`/metrics`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
services:
- name: headscale-server-external
port: 9090
tls:
certResolver: letsencrypt
domains:
- main: hs.forust.xyz
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
@@ -97,5 +104,3 @@ spec:
services:
- name: headscale-server-external
port: 9090
+4 -2
View File
@@ -19,14 +19,17 @@ services:
# Prod Router
- "traefik.http.routers.forust-homepage.rule=Host(`forust.xyz`) || Host(`www.forust.xyz`)"
- "traefik.http.routers.forust-homepage.entrypoints=websecure"
- "traefik.http.routers.forust-homepage.tls=true"
- "traefik.http.routers.forust-homepage.priority=10"
- "traefik.http.routers.forust-homepage.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.forust-homepage-local.rule=Host(`landing.workstation.internal`)"
- "traefik.http.routers.forust-homepage-local.entrypoints=websecure"
- "traefik.http.routers.forust-homepage-local.priority=10"
- "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.priority=10"
- "traefik.http.routers.forust-homepage-dev.tls=true"
xdfnx:
build:
@@ -47,7 +50,6 @@ services:
- "traefik.http.routers.xdfnx.rule=Host(`xdfnx.cfd`) || Host(`www.xdfnx.cfd`)"
- "traefik.http.routers.xdfnx.entrypoints=websecure"
- "traefik.http.routers.xdfnx.tls.certresolver=letsencrypt"
- "traefik.http.routers.xdfnx.tls=true"
# Local Router
- "traefik.http.routers.xdfnx-local.rule=Host(`xdfnx.workstation.internal`)"
- "traefik.http.routers.xdfnx-local.entrypoints=websecure"
+57 -41
View File
@@ -20,49 +20,61 @@
<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:contact@forust.xyz">mail/contact@forust.xyz</a>
</li>
<li>
<i class="fa fa-pie-chart"></i>
<a href="https://forust.xyz/glance">forust/dashboard</a>
</li>
<li>
<i class="fa fa-refresh"></i>
<a href="https://forust.xyz/convert">forust/converter</a>
</li>
<li>
<i class="fa-solid fa-key"></i>
<a href=".well-known/pgp-key.asc">security/PGP Key</a>
</li>
<li>
<p class="comment"># PGP Key Fingerprint: A777 7CB7 D9C4 0A97 443D CCF0 7A3D A455 F820 5B82</p>
</li>
</ul>
</section>
<div class="grid-2">
<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:contact@forust.xyz">mail/contact@forust.xyz</a>
</li>
<li>
<i class="fa-solid fa-key"></i>
<a href=".well-known/pgp-key.asc">security/PGP Key</a>
</li>
<li>
<p class="comment"># PGP Key Fingerprint: A777 7CB7 D9C4 0A97 443D CCF0 7A3D A455 F820 5B82</p>
</li>
</ul>
</section>
<section id="tools">
<h2>./tools</h2>
<ul class="link-list">
<li>
<i class="fa fa-pie-chart"></i>
<a href="https://forust.xyz/glance" target="_blank">forust/dashboard</a>
</li>
<li>
<i class="fa fa-refresh"></i>
<a href="https://forust.xyz/convert" target="_blank">forust/converter</a>
</li>
<li>
<i class="fa-solid fa-file-pdf"></i>
<a href="https://pdf.forust.xyz" target="_blank">pdf.forust.xyz</a>
</li>
</ul>
</section>
</div>
<section id="stack">
<h2>./skills_and_tools</h2>
<h2>./skills</h2>
<div class="grid-2">
<div>
<div class="skill-item">
@@ -168,6 +180,10 @@
<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/tstack/lnav" target="_blank">tstack/lnav</a>
<span class="comment">// powerful log reader</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>
+2
View File
@@ -28,6 +28,7 @@ spec:
containers:
- name: forust-homepage
image: gcr.forust.xyz/forust/forust-homepage:latest
imagePullPolicy: Always
ports:
- containerPort: 80
resources:
@@ -68,6 +69,7 @@ spec:
containers:
- name: xdfnx-homepage
image: gcr.forust.xyz/forust/xdfnx-homepage:latest
imagePullPolicy: Always
ports:
- containerPort: 80
resources:
+10 -5
View File
@@ -7,8 +7,11 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(forust\.xyz|www\.forust\.xyz)$`)
- match: Host(`forust.xyz`) || Host(`www.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
priority: 10
services:
- name: forust-homepage-service
port: 80
@@ -24,8 +27,9 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^landing\.(workstation|gigaforust)\.internal$`)
- match: Host(`landing.workstation.internal`) || Host(`landing.gigaforust.internal`)
kind: Rule
priority: 10
services:
- name: forust-homepage-service
port: 80
@@ -39,8 +43,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(xdfnx\.cfd|www\.xdfnx\.cfd)$`)
- match: Host(`xdfnx.cfd`) || Host(`www.xdfnx.cfd`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
services:
- name: xdfnx-homepage-service
port: 80
@@ -56,9 +62,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^xdfnx\.(workstation|gigaforust)\.internal$`)
- match: Host(`xdfnx.workstation.internal`) || Host(`xdfnx.gigaforust.internal`)
kind: Rule
services:
- name: xdfnx-homepage-service
port: 80
+2 -2
View File
@@ -23,7 +23,7 @@ services:
# Prod Router
- "traefik.http.routers.kener.rule=Host(`status.forust.xyz`)"
- "traefik.http.routers.kener.entrypoints=websecure"
- "traefik.http.routers.kener.tls=true"
- "traefik.http.routers.kener.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.kener-local.rule=Host(`status.workstation.internal`)"
- "traefik.http.routers.kener-local.entrypoints=websecure"
@@ -42,7 +42,7 @@ services:
volumes:
- redis:/data
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
test: ["CMD", "redis-cli", "ping"]
interval: 6s
timeout: 5s
retries: 5
+4 -3
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^status\.forust\.xyz$`)
- match: Host(`status.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
services:
- name: kener-service
port: 3000
@@ -24,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^status\.(workstation|gigaforust)\.internal$`)
- match: Host(`status.workstation.internal`) || Host(`status.gigaforust.internal`)
kind: Rule
services:
- name: kener-service
port: 3000
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
spec:
containers:
- name: kener
image: rajnandan1/kener:4.0.23
image: rajnandan1/kener:4.1.0
envFrom:
- configMapRef:
name: kener-config
+1 -1
View File
@@ -20,7 +20,7 @@ services:
- "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.tls=true"
- "traefik.http.routers.metube.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.metube-local.rule=Host(`metube.workstation.internal`)"
- "traefik.http.routers.metube-local.entrypoints=websecure"
+2 -3
View File
@@ -7,7 +7,7 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^metube\.forust\.xyz$`)
- match: Host(`metube.forust.xyz`)
kind: Rule
middlewares:
- name: security-headers@file
@@ -26,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^metube\.(workstation|gigaforust)\.internal$`)
- match: Host(`metube.workstation.internal`) || Host(`metube.gigaforust.internal`)
kind: Rule
services:
- name: metube-service
port: 8081
+2 -2
View File
@@ -32,7 +32,7 @@ services:
# Prod Router
- "traefik.http.routers.n8n.rule=Host(`n8n.forust.xyz`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls=true"
- "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.n8n-local.rule=Host(`n8n.workstation.internal`)"
- "traefik.http.routers.n8n-local.entrypoints=websecure"
@@ -54,4 +54,4 @@ networks:
volumes:
node-data:
files:
files:
+4 -3
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^n8n\.forust\.xyz$`)
- match: Host(`n8n.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: n8n-service
port: 5678
@@ -24,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^n8n\.(workstation|gigaforust)\.internal$`)
- match: Host(`n8n.workstation.internal`) || Host(`n8n.gigaforust.internal`)
kind: Rule
services:
- name: n8n-service
port: 5678
+2 -3
View File
@@ -16,7 +16,7 @@ services:
# Prod Router
- "traefik.http.routers.netronome.rule=Host(`nm.forust.xyz`)"
- "traefik.http.routers.netronome.entrypoints=websecure"
- "traefik.http.routers.netronome.tls=true"
- "traefik.http.routers.netronome.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.netronome-local.rule=Host(`nm.workstation.internal`)"
- "traefik.http.routers.netronome-local.entrypoints=websecure"
@@ -41,7 +41,7 @@ services:
volumes:
- data:/var/lib/postgresql/data
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U netronome" ]
test: ["CMD-SHELL", "pg_isready -U netronome"]
interval: 5s
timeout: 5s
retries: 5
@@ -51,7 +51,6 @@ services:
volumes:
data:
networks:
proxy:
external: true
+4 -3
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^nm\.forust\.xyz$`)
- match: Host(`nm.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: netronome-service
port: 7575
@@ -24,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^nm\.(workstation|gigaforust)\.internal$`)
- match: Host(`nm.workstation.internal`) || Host(`nm.gigaforust.internal`)
kind: Rule
services:
- name: netronome-service
port: 7575
+2 -2
View File
@@ -5,7 +5,7 @@ services:
restart: unless-stopped
container_name: nextcloud-aio-mastercontainer # Do not change
volumes:
- nextcloud_aio_mastercontainer:/mnt/docker-aio-config # Do not change (backup)
- nextcloud_aio_mastercontainer:/mnt/docker-aio-config # Do not change (backup)
- /var/run/docker.sock:/var/run/docker.sock
- /dev/dri:/dev/dri
ports:
@@ -25,7 +25,7 @@ services:
# - "traefik.http.routers.nextcloud-aio.rule=Host(`naio.forust.xyz`)"
# - "traefik.http.routers.nextcloud-aio.entrypoints=websecure"
# - "traefik.http.routers.nextcloud-aio.middlewares=security-chain@file"
# - "traefik.http.routers.nextcloud-aio.tls=true"
# - "traefik.http.routers.nextcloud-aio.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.nextcloud-aio-local.rule=Host(`naio.workstation.internal`)"
- "traefik.http.routers.nextcloud-aio-local.entrypoints=websecure"
+24 -24
View File
@@ -8,10 +8,11 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^nextcloud\.forust\.xyz$`)
- match: Host(`nextcloud.forust.xyz`)
kind: Rule
middlewares:
- name: nextcloud-chain@file
- name: crowdsec-crowdsec-bouncer@kubernetescrd
services:
- name: nextcloud-apache
port: 11000
@@ -27,34 +28,34 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^nextcloud\.(workstation|gigaforust)\.internal$`)
- match: Host(`nextcloud.workstation.internal`) || Host(`nextcloud.gigaforust.internal`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
- name: nextcloud-chain@file
services:
- name: nextcloud-apache
port: 11000
---
# ---
# Nextcloud AIO
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: naio-prod
namespace: nextcloud
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^naio\.forust\.xyz$`)
kind: Rule
services:
- name: nextcloud-aio
port: 8888
scheme: https
serversTransport: insecure-transport # <-- Ссылка на ваш CRD ресурс вместо @file
tls:
certResolver: letsencrypt
# apiVersion: traefik.io/v1alpha1
# kind: IngressRoute
# metadata:
# name: naio-prod
# namespace: nextcloud
# spec:
# entryPoints:
# - websecure
# routes:
# - match: Host(`naio.forust.xyz`)
# kind: Rule
# services:
# - name: nextcloud-aio
# port: 8888
# scheme: https
# serversTransport: insecure-transport
# tls:
# certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
@@ -65,11 +66,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^naio\.(workstation|gigaforust)\.internal$`)
- match: Host(`naio.workstation.internal`) || Host(`naio.gigaforust.internal`)
kind: Rule
services:
- name: nextcloud-aio
port: 8888
scheme: https
serversTransport: insecure-transport
+6 -6
View File
@@ -107,7 +107,7 @@ services:
# Prod Router
- "traefik.http.routers.penpot.rule=Host(`penpot.forust.xyz`)"
- "traefik.http.routers.penpot.entrypoints=websecure"
- "traefik.http.routers.penpot.tls=true"
- "traefik.http.routers.penpot.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.penpot-local.rule=Host(`penpot.workstation.internal`)"
- "traefik.http.routers.penpot-local.entrypoints=websecure"
@@ -117,7 +117,7 @@ services:
- "traefik.http.routers.penpot-dev.entrypoints=websecure"
- "traefik.http.routers.penpot-dev.tls=true"
environment:
<<: [ *penpot-flags, *penpot-http-body-size ]
<<: [*penpot-flags, *penpot-http-body-size]
penpot-backend:
image: "penpotapp/backend:${PENPOT_VERSION:-latest}"
restart: always
@@ -137,7 +137,7 @@ services:
## Configuration envronment variables for the backend container.
environment:
<<: [ *penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key ]
<<: [*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
@@ -200,7 +200,7 @@ services:
- penpot
environment:
<<: [ *penpot-secret-key ]
<<: [*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
@@ -214,7 +214,7 @@ services:
stop_signal: SIGINT
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U penpot" ]
test: ["CMD-SHELL", "pg_isready -U penpot"]
interval: 2s
timeout: 10s
retries: 5
@@ -237,7 +237,7 @@ services:
restart: always
healthcheck:
test: [ "CMD-SHELL", "valkey-cli ping | grep PONG" ]
test: ["CMD-SHELL", "valkey-cli ping | grep PONG"]
interval: 1s
timeout: 3s
retries: 5
+1 -1
View File
@@ -16,7 +16,7 @@ services:
# Prod Router
- "traefik.http.routers.portainer.rule=Host(`portainer.forust.xyz`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls=true"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.portainer-local.rule=Host(`portainer.workstation.internal`)"
- "traefik.http.routers.portainer-local.entrypoints=websecure"
+4 -3
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^portainer\.forust\.xyz$`)
- match: Host(`portainer.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: portainer-service
port: 9000
@@ -24,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^portainer\.(workstation|gigaforust)\.internal$`)
- match: Host(`portainer.workstation.internal`) || Host(`portainer.gigaforust.internal`)
kind: Rule
services:
- name: portainer-service
port: 9000
+2
View File
@@ -0,0 +1,2 @@
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=changeme
+5
View File
@@ -0,0 +1,5 @@
route:
receiver: default
receivers:
- name: default
+66
View File
@@ -0,0 +1,66 @@
services:
grafana:
image: grafana/grafana:11.6.0
container_name: prometheus-grafana
restart: unless-stopped
env_file:
- .env
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?grafana admin password required}
volumes:
- grafana-data:/var/lib/grafana
labels:
- "traefik.enable=true"
- "traefik.http.services.prometheus-grafana.loadbalancer.server.port=3000"
# Prod Router
- "traefik.http.routers.grafana.rule=Host(`grafana.forust.xyz`)"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls=true"
# Local Router
- "traefik.http.routers.grafana-local.rule=Host(`grafana.workstation.internal`)"
- "traefik.http.routers.grafana-local.entrypoints=websecure"
- "traefik.http.routers.grafana-local.tls=true"
# Dev Router
- "traefik.http.routers.grafana-dev.rule=Host(`grafana.gigaforust.internal`)"
- "traefik.http.routers.grafana-dev.entrypoints=websecure"
- "traefik.http.routers.grafana-dev.tls=true"
networks:
- proxy
prometheus:
image: prom/prometheus:v3.2.1
container_name: prometheus-prometheus
restart: unless-stopped
command:
- "--config.file=/etc/prometheus/prometheus.yaml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=90d"
volumes:
- prometheus-data:/prometheus
- ./prometheus.yaml:/etc/prometheus/prometheus.yaml:ro
networks:
- proxy
alertmanager:
image: prom/alertmanager:v0.28.1
container_name: prometheus-alertmanager
restart: unless-stopped
command:
- "--config.file=/etc/alertmanager/alertmanager.yaml"
- "--storage.path=/alertmanager"
volumes:
- alertmanager-data:/alertmanager
- ./alertmanager.yaml:/etc/alertmanager/alertmanager.yaml:ro
networks:
- proxy
volumes:
grafana-data:
prometheus-data:
alertmanager-data:
networks:
proxy:
external: true
+52
View File
@@ -0,0 +1,52 @@
grafana:
grafana.ini:
server:
domain: grafana.forust.xyz
root_url: https://grafana.forust.xyz
auth.anonymous:
enabled: false
users:
allow_sign_up: false
admin:
existingSecret: grafana-admin
userKey: admin-user
passwordKey: admin-password
persistence:
enabled: true
size: 10Gi
ingress:
enabled: false
service:
port: 80
prometheus:
prometheusSpec:
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: "local-path"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 40Gi
resources:
requests:
memory: "700Mi"
cpu: 200m
limits:
memory: "2Gi"
alertmanager:
alertmanagerSpec:
storage:
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
+36
View File
@@ -0,0 +1,36 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: grafana-prod
namespace: prometheus
spec:
entryPoints:
- websecure
routes:
- match: Host(`grafana.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
- name: "security-chain@file"
services:
- name: prometheus-stack-grafana
port: 80
tls:
certResolver: letsencrypt
domains:
- main: grafana.forust.xyz
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: grafana-local
namespace: prometheus
spec:
entryPoints:
- websecure
routes:
- match: Host(`grafana.workstation.internal`) || Host(`grafana.gigaforust.internal`)
kind: Rule
services:
- name: prometheus-stack-grafana
port: 80
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: prometheus
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: grafana-admin
namespace: prometheus
type: Opaque
stringData:
admin-user: "admin"
admin-password: ""
+8
View File
@@ -0,0 +1,8 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
+11
View File
@@ -0,0 +1,11 @@
target-version = "py311"
line-length = 120
[lint]
select = ["E", "F", "W", "I", "N", "UP", "S", "B", "A", "C4", "SIM", "ARG"]
ignore = ["E501", "S101"]
[format]
quote-style = "single"
indent-style = "space"
line-ending = "lf"
+3 -3
View File
@@ -6,15 +6,15 @@ services:
image: docker.io/searxng/searxng:${SEARXNG_VERSION:-latest}
restart: unless-stopped
# ports:
# - ${SEARXNG_PORT:-8080}
# - ${SEARXNG_PORT:-8080}
env_file: .env
labels:
- "traefik.enable=true"
- "traefik.http.services.searxng.loadbalancer.server.port=8080"
# Prod Router
- "traefik.http.routers.searxng.rule=Host(`s.forust.xyz` || `searxng.forust.xyz`)"
- "traefik.http.routers.searxng.rule=Host(`s.forust.xyz` || `search.forust.xyz`)"
- "traefik.http.routers.searxng.entrypoints=websecure"
- "traefik.http.routers.searxng.tls=true"
- "traefik.http.routers.searxng.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.searxng-local.rule=Host(`s.workstation.internal` || `searxng.workstation.internal`)"
- "traefik.http.routers.searxng-local.entrypoints=websecure"
+4 -3
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(s|searxng)\.forust\.xyz$`)
- match: Host(`s.forust.xyz`) || Host(`search.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: searxng-service
port: 8080
@@ -24,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(s|searxng)\.(workstation|gigaforust)\.internal$`)
- match: Host(`s.workstation.internal`) || Host(`searxng.workstation.internal`) || Host(`s.gigaforust.internal`) || Host(`searxng.gigaforust.internal`)
kind: Rule
services:
- name: searxng-service
port: 8080
+2 -2
View File
@@ -16,7 +16,7 @@ services:
# Prod Router
- "traefik.http.routers.termix.rule=Host(`termix.forust.xyz`)"
- "traefik.http.routers.termix.entrypoints=websecure"
- "traefik.http.routers.termix.tls=true"
- "traefik.http.routers.termix.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.termix-local.rule=Host(`termix.workstation.internal`)"
- "traefik.http.routers.termix-local.entrypoints=websecure"
@@ -31,4 +31,4 @@ networks:
proxy:
external: true
volumes:
data:
data:
+4 -3
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^termix\.forust\.xyz$`)
- match: Host(`termix.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: termix-service
port: 8080
@@ -24,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^termix\.(workstation|gigaforust)\.internal$`)
- match: Host(`termix.workstation.internal`) || Host(`termix.gigaforust.internal`)
kind: Rule
services:
- name: termix-service
port: 8080
+2 -2
View File
@@ -32,7 +32,7 @@ services:
# # STAGING
# - "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
# 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.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
@@ -49,7 +49,7 @@ services:
- "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"
- "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.traefik-dashboard-local.rule=Host(`traefik.workstation.internal`)"
- "traefik.http.routers.traefik-dashboard-local.entrypoints=websecure"
+1 -1
View File
@@ -1,4 +1,4 @@
http:
serversTransports:
insecureTransport:
insecureSkipVerify: true
insecureSkipVerify: true
+2 -1
View File
@@ -8,7 +8,8 @@ http:
service: nextcloud
middlewares:
- nextcloud-chain
tls: {}
tls:
certresolver: letsencrypt
# Nextcloud dev
nextcloud-local:
+4 -3
View File
@@ -8,8 +8,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^traefik\.forust\.xyz$`)
- match: Host(`traefik.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: api@internal
kind: TraefikService
@@ -25,9 +27,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^traefik\.(workstation|gigaforust)\.internal$`)
- match: Host(`traefik.workstation.internal`) || Host(`traefik.gigaforust.internal`)
kind: Rule
services:
- name: api@internal
kind: TraefikService
+1 -1
View File
@@ -50,7 +50,7 @@ data:
- security-headers@file
authentik:
forwardAuth:
address: "http://authentik-server:9000/outpost.goauthentik.io/auth/traefik"
address: "http://authentik-server-service.authentik.svc:9000/outpost.goauthentik.io/auth/traefik"
trustForwardHeader: true
maxResponseBodySize: 1048576
authResponseHeaders:
+29 -5
View File
@@ -13,6 +13,8 @@ service:
type: LoadBalancer
annotations:
metallb.io/loadBalancerIPs: "192.168.80.2"
spec:
externalTrafficPolicy: Local
api:
dashboard: true
insecure: true
@@ -75,12 +77,22 @@ ports:
protocol: UDP
expose:
default: true
dot:
port: 853
exposedPort: 853
metrics:
port: 9100
exposedPort: 9100
protocol: TCP
expose:
default: true
default: false
metrics:
prometheus:
enabled: true
entryPoint: metrics
serviceMonitor:
enabled: true
additionalLabels:
release: prometheus-stack
namespace: prometheus
ingressRoute:
dashboard:
@@ -111,14 +123,26 @@ volumes:
- name: traefik-dynamic
mountPath: /etc/traefik/dynamic
type: configMap
- name: crowdsec-bouncer-secrets
mountPath: /etc/traefik/secrets
type: secret
additionalArguments:
- "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true"
- "--providers.kubernetesCRD.allowCrossNamespace=true"
- "--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,192.168.1.1,192.168.1.0/24,192.168.88.0/24,192.168.88.1"
- "--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,192.168.1.1,192.168.1.0/24,192.168.88.0/24,192.168.88.1"
logs:
general:
level: INFO
format: json
access:
enabled: true
format: common
experimental:
plugins:
crowdsec-bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.3.3
+2 -2
View File
@@ -14,7 +14,7 @@ services:
# 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.routers.uptime-kuma.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.uptime-kuma-local.rule=Host(`uptime.workstation.internal`)"
- "traefik.http.routers.uptime-kuma-local.entrypoints=websecure"
@@ -29,4 +29,4 @@ volumes:
data:
networks:
proxy:
external: true
external: true
+4 -3
View File
@@ -7,8 +7,10 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^uptime\.forust\.xyz$`)
- match: Host(`uptime.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
services:
- name: uptime-kuma-service
port: 3001
@@ -24,9 +26,8 @@ spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^uptime\.(workstation|gigaforust)\.internal$`)
- match: Host(`uptime.workstation.internal`) || Host(`uptime.gigaforust.internal`)
kind: Rule
services:
- name: uptime-kuma-service
port: 3001
+1 -7
View File
@@ -2,13 +2,7 @@
"name": "Moon-userbot",
"description": "A Simple, Fast, Customizable, Ai powered Userbot for Telegram with most easiest installation.",
"logo": "https://camo.githubusercontent.com/1efdfa6416b3cd08471d865ca9ebf0fbdd38602ea95425f18a9bec6aeeefe49b/68747470733a2f2f74656c656772612e70682f66696c652f3063333763326662306631393463633163303334342e6a7067",
"keywords": [
"telegram",
"Moon-userbot",
"bot",
"python",
"pyrogram"
],
"keywords": ["telegram", "Moon-userbot", "bot", "python", "pyrogram"],
"env": {
"API_ID": {
"description": "Get it from my.telegram.org",
+3 -3
View File
@@ -3,10 +3,10 @@ from flask import Flask
app = Flask(__name__)
@app.route("/")
@app.route('/')
def hello_world():
return "This is Moon"
return 'This is Moon'
if __name__ == "__main__":
if __name__ == '__main__':
app.run()
+15 -15
View File
@@ -1,17 +1,17 @@
services:
- type: worker
name: Moon-Userbot
runtime: docker
repo: https://github.com/The-MoonTg-project/Moon-Userbot
plan: starter
envVars:
- key: STABILITY_KEY
sync: false
- key: CLARIFAI_PAT
sync: false
- key: .env
sync: false
region: oregon
dockerContext: .
dockerfilePath: ./Dockerfile
- type: worker
name: Moon-Userbot
runtime: docker
repo: https://github.com/The-MoonTg-project/Moon-Userbot
plan: starter
envVars:
- key: STABILITY_KEY
sync: false
- key: CLARIFAI_PAT
sync: false
- key: .env
sync: false
region: oregon
dockerContext: .
dockerfilePath: ./Dockerfile
version: "1"
+19 -9
View File
@@ -1,18 +1,28 @@
FROM python:3.11-slim AS builder
WORKDIR /src
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
git wget ffmpeg mediainfo && \
rm -rf /var/lib/apt/lists/*
git \
mediainfo \
wget \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt /app/
RUN python -m pip install --no-cache-dir --upgrade pip && \
python -m pip install --no-cache-dir -r /app/requirements.txt
RUN wget -qO- https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz | \
tar xJ && \
cp ffmpeg-*-static/ffmpeg /usr/local/bin/ && \
cp ffmpeg-*-static/ffprobe /usr/local/bin/ && \
rm -rf ffmpeg-*
COPY --from=builder /root/.local /usr/local
COPY . /app
WORKDIR /app
ENV PYTHONUNBUFFERED=1
CMD ["python", "-u", "main.py"]
CMD ["python", "-u", "main.py"]
+36 -41
View File
@@ -1,83 +1,80 @@
import re
import requests
import random
import re
from io import BytesIO
import requests
from bs4 import BeautifulSoup as bs
from pyrogram import Client, filters
from pyrogram.types import Message, InputMediaPhoto
from pyrogram.errors import RPCError
from pyrogram.types import InputMediaPhoto, Message
from utils.misc import modules_help, prefix
@Client.on_message(filters.command("icon", prefix) & filters.me)
@Client.on_message(filters.command('icon', prefix) & filters.me)
async def search_icon(_, message: Message):
if not len(message.command) == 2:
return await message.edit_text(
"Please provide some text to search icons from Flaticon.com."
)
return await message.edit_text('Please provide some text to search icons from Flaticon.com.')
query = message.text.split(maxsplit=1)[1]
await message.edit_text("Searching for icons...")
search_query = query.replace(" ", "%20")
url = f"https://www.flaticon.com/search?word={search_query}"
await message.edit_text('Searching for icons...')
search_query = query.replace(' ', '%20')
url = f'https://www.flaticon.com/search?word={search_query}'
try:
html_content = requests.get(url).text
soup = bs(html_content, "html.parser")
soup = bs(html_content, 'html.parser')
results = soup.find_all(
"img",
src=re.compile(r"https://cdn-icons-png.flaticon.com/128/[0-9]+/[0-9]+.png"),
'img',
src=re.compile(r'https://cdn-icons-png.flaticon.com/128/[0-9]+/[0-9]+.png'),
)
if not results:
return await message.edit("No results found.")
return await message.edit('No results found.')
random.shuffle(results)
icons = []
for i in range(5):
icons.append(results[i]["src"].replace("128", "512"))
icons.append(results[i]['src'].replace('128', '512'))
for icon in icons:
await message.reply_document(icon)
return await message.delete()
except Exception as e:
await message.edit(f"An error occurred: {e}")
print(f"Error: {e}")
await message.edit(f'An error occurred: {e}')
print(f'Error: {e}')
@Client.on_message(filters.command("freepik", prefix) & filters.me)
@Client.on_message(filters.command('freepik', prefix) & filters.me)
async def freepik_search(client: Client, message: Message):
parts = message.text.split(" ", 1)
parts = message.text.split(' ', 1)
if len(parts) < 2:
await message.edit_text("Please provide a search query!")
await message.edit_text('Please provide a search query!')
return
query = parts[1]
limit = 5
if " ; " in query:
match, limit_str = query.split(" ; ", 1)
if ' ; ' in query:
match, limit_str = query.split(' ; ', 1)
try:
limit = int(limit_str)
except ValueError:
await message.edit_text("Invalid limit! Using the default value of 5.")
await message.edit_text('Invalid limit! Using the default value of 5.')
else:
match = query
match = match.replace(" ", "%20")
await message.edit_text("Searching Freepik...")
match = match.replace(' ', '%20')
await message.edit_text('Searching Freepik...')
try:
url = f"https://www.freepik.com/api/regular/search?locale=en&term={match}"
url = f'https://www.freepik.com/api/regular/search?locale=en&term={match}'
json_content = requests.get(url).json()
results = []
for i in json_content["items"]:
results.append(i["preview"]["url"])
for i in json_content['items']:
results.append(i['preview']['url'])
if results is None:
return await message.edit_text("No results found.")
return await message.edit_text('No results found.')
random.shuffle(results)
img_urls = results[:limit]
@@ -89,27 +86,25 @@ async def freepik_search(client: Client, message: Message):
media_group.append(InputMediaPhoto(media=BytesIO(icon.content)))
if not media_group:
await message.edit_text("No images could be downloaded.")
await message.edit_text('No images could be downloaded.')
return
try:
await client.send_media_group(chat_id=message.chat.id, media=media_group)
except RPCError:
await message.edit_text(
"Failed to send some images. Retrying individually..."
)
await message.edit_text('Failed to send some images. Retrying individually...')
for media in media_group:
try:
await message.reply_photo(photo=media.media)
except Exception as e:
await message.edit_text(f"Error sending image: {e}")
await message.edit_text(f'Error sending image: {e}')
except Exception as e:
await message.edit_text(f"Failed to fetch data: {e}")
print(f"Error: {e}")
await message.edit_text(f'Failed to fetch data: {e}')
print(f'Error: {e}')
modules_help["icons"] = {
"icon [query]": "Search for icons on Flaticon.",
"freepik [query] [limit]": "Search for images on Freepik. Limit is optional and defaults to 5.",
modules_help['icons'] = {
'icon [query]': 'Search for icons on Flaticon.',
'freepik [query] [limit]': 'Search for images on Freepik. Limit is optional and defaults to 5.',
}
+16 -21
View File
@@ -1,52 +1,47 @@
import os
import base64
import requests
from pyrogram import Client, filters
from pyrogram.types import Message
from utils.misc import modules_help, prefix
@Client.on_message(filters.command(["imgur"], prefix) & filters.me)
@Client.on_message(filters.command(['imgur'], prefix) & filters.me)
async def imgur(_, message: Message):
# Check if a reply exists
msg = await message.edit_text("🎉 Please wait. trying to upload...")
msg = await message.edit_text('🎉 Please wait. trying to upload...')
if message.reply_to_message and message.reply_to_message.photo:
# Download the photo
photo_path = await message.reply_to_message.download()
# Read the photo file and encode as base64
with open(photo_path, "rb") as file:
with open(photo_path, 'rb') as file:
data = file.read()
base64_data = base64.b64encode(data)
# Set API endpoint and headers for image upload
url = "https://api.imgur.com/3/image"
headers = {"Authorization": "Client-ID a10ad04550b0648"}
url = 'https://api.imgur.com/3/image'
headers = {'Authorization': 'Client-ID a10ad04550b0648'}
# Upload image to Imgur and get URL
response = requests.post(url, headers=headers, data={"image": base64_data})
response = requests.post(url, headers=headers, data={'image': base64_data})
result = response.json()
await msg.edit_text(result["data"]["link"])
await msg.edit_text(result['data']['link'])
elif message.reply_to_message and message.reply_to_message.animation:
# Download the animation (GIF)
animation_path = await message.reply_to_message.download()
# Read the animation file and encode as base64
with open(animation_path, "rb") as file:
with open(animation_path, 'rb') as file:
data = file.read()
base64_data = base64.b64encode(data)
# Set API endpoint and headers for animation upload
url = "https://api.imgur.com/3/image"
headers = {"Authorization": "Client-ID a10ad04550b0648"}
url = 'https://api.imgur.com/3/image'
headers = {'Authorization': 'Client-ID a10ad04550b0648'}
# Upload animation to Imgur and get URL
response = requests.post(url, headers=headers, data={"image": base64_data})
response = requests.post(url, headers=headers, data={'image': base64_data})
result = response.json()
await msg.edit_text(result["data"]["link"])
await msg.edit_text(result['data']['link'])
else:
await msg.edit_text(
"Please reply to a photo or animation (GIF) to upload to Imgur."
)
await msg.edit_text('Please reply to a photo or animation (GIF) to upload to Imgur.')
modules_help["imgur"] = {
"imgur [img]*": "upload a photo or animation (GIF) to imgur",
modules_help['imgur'] = {
'imgur [img]*': 'upload a photo or animation (GIF) to imgur',
}
+18 -26
View File
@@ -1,55 +1,47 @@
from pyrogram import Client, filters
from pyrogram.types import Message
from utils.misc import prefix, modules_help
from pyrogram import Client, filters
from pyrogram.types import Message
from pyrogram.errors import MessageNotModified
import os
import pygments
from pygments.formatters import ImageFormatter
from pygments.lexers import Python3Lexer
from pyrogram import Client, filters
from pyrogram.errors import MessageNotModified
from pyrogram.types import Message
from utils.misc import modules_help, prefix
@Client.on_message(filters.command("ncode", prefix) & filters.me)
@Client.on_message(filters.command('ncode', prefix) & filters.me)
async def coder_print(client, message: Message):
if message.reply_to_message:
reply_message = message.reply_to_message
if reply_message.media:
download_path = await client.download_media(reply_message)
with open(download_path, "r") as file:
with open(download_path) as file:
code = file.read()
if os.path.exists(download_path):
os.remove(download_path)
pygments.highlight(
f"{code}",
f'{code}',
Python3Lexer(),
ImageFormatter(font_name="DejaVu Sans Mono", line_numbers=True),
"result.png",
ImageFormatter(font_name='DejaVu Sans Mono', line_numbers=True),
'result.png',
)
try:
sent_message = await message.edit_text(
"Pasting this code on my page..."
)
sent_message = await message.edit_text('Pasting this code on my page...')
await client.send_document(
chat_id=message.chat.id,
document="result.png",
caption="Code highlighted by Pygments",
document='result.png',
caption='Code highlighted by Pygments',
reply_to_message_id=message.id,
)
except MessageNotModified:
pass
await sent_message.delete()
if os.path.exists("result.png"):
os.remove("result.png")
if os.path.exists('result.png'):
os.remove('result.png')
else:
return await message.reply_text("Please reply to a text or a file.")
return await message.reply_text('Please reply to a text or a file.')
else:
return await message.reply_text("Please reply to a text or a file.")
return await message.reply_text('Please reply to a text or a file.')
modules_help["ncode"] = {
"ncode": "Highlight the code using Pygments and send it as an image."
}
modules_help['ncode'] = {'ncode': 'Highlight the code using Pygments and send it as an image.'}
+24 -35
View File
@@ -1,13 +1,14 @@
from pyrogram import Client, filters, enums
from pyrogram.types import Message, InputMediaPhoto
from io import BytesIO
from PIL import Image
import requests
import asyncio
from io import BytesIO
import requests
from PIL import Image
from pyrogram import Client, enums, filters
from pyrogram.types import InputMediaPhoto, Message
from utils.misc import modules_help, prefix
# Pinterest API URL
API_URL = "https://bk9.fun/pinterest/search?q="
API_URL = 'https://bk9.fun/pinterest/search?q='
def resize_image(image_bytes):
@@ -17,13 +18,13 @@ def resize_image(image_bytes):
if img.size > max_size:
img.thumbnail(max_size)
output = BytesIO()
img.save(output, format="JPEG")
img.save(output, format='JPEG')
output.seek(0)
return output
image_bytes.seek(0) # Reset pointer if not resized
return image_bytes
except Exception as e:
print(f"Error resizing image: {e}")
print(f'Error resizing image: {e}')
return image_bytes
@@ -34,69 +35,57 @@ async def download_image(url):
img_bytes = BytesIO(response.content)
return resize_image(img_bytes)
except Exception as e:
print(f"Error downloading image: {e}")
print(f'Error downloading image: {e}')
return None
@Client.on_message(filters.command("pinterest", prefix) & filters.me)
@Client.on_message(filters.command('pinterest', prefix) & filters.me)
async def pinterest_search(client: Client, message: Message):
if len(message.command) < 2:
await message.edit(
"Usage: `pinterest [number] <query>`", parse_mode=enums.ParseMode.MARKDOWN
)
await message.edit('Usage: `pinterest [number] <query>`', parse_mode=enums.ParseMode.MARKDOWN)
return
num_pics = int(message.command[1]) if message.command[1].isdigit() else 10
query = " ".join(message.command[2:])
query = ' '.join(message.command[2:])
# Update status
status_message = await message.edit(
"Searching for images...", parse_mode=enums.ParseMode.MARKDOWN
)
status_message = await message.edit('Searching for images...', parse_mode=enums.ParseMode.MARKDOWN)
url = f"{API_URL}{query}"
url = f'{API_URL}{query}'
response = requests.get(url)
if response.status_code == 200:
data = response.json()
if data.get("status"):
urls = [item["images_url"] for item in data.get("BK9", [])[:num_pics]]
if data.get('status'):
urls = [item['images_url'] for item in data.get('BK9', [])[:num_pics]]
images = [download_image(img_url) for img_url in urls]
# Download images
downloaded_images = await asyncio.gather(*images)
media = [
InputMediaPhoto(media=img_bytes)
for img_bytes in downloaded_images
if img_bytes
]
media = [InputMediaPhoto(media=img_bytes) for img_bytes in downloaded_images if img_bytes]
if media:
await status_message.edit(
"Uploading pictures...", parse_mode=enums.ParseMode.MARKDOWN
)
await status_message.edit('Uploading pictures...', parse_mode=enums.ParseMode.MARKDOWN)
while media:
batch = media[:10]
media = media[10:]
await message.reply_media_group(batch)
await status_message.delete() # Delete status message after uploading
else:
await status_message.edit(
"No valid images found.", parse_mode=enums.ParseMode.MARKDOWN
)
await status_message.edit('No valid images found.', parse_mode=enums.ParseMode.MARKDOWN)
else:
await status_message.edit(
"No images found for the given query.",
'No images found for the given query.',
parse_mode=enums.ParseMode.MARKDOWN,
)
else:
await status_message.edit(
"An error occurred, please try again later.",
'An error occurred, please try again later.',
parse_mode=enums.ParseMode.MARKDOWN,
)
modules_help["pinterest"] = {
"pinterest [number]* [query]": "Get images from Pinterest. Default number of images is 10",
modules_help['pinterest'] = {
'pinterest [number]* [query]': 'Get images from Pinterest. Default number of images is 10',
}
+27 -32
View File
@@ -1,26 +1,27 @@
from utils.misc import modules_help, prefix
import os
import requests
from modules.url import generate_screenshot
from pyrogram import Client, filters
from pyrogram.types import Message
from modules.url import generate_screenshot
import os
from utils.misc import modules_help, prefix
# API endpoints for reverse image search engines
SEARCH_ENGINES = {
"lens": "https://lens.google.com/uploadbyurl?url={image}",
"reverse": "https://www.google.com/searchbyimage?sbisrc=4chanx&image_url={image}&safe=off",
"tineye": "https://www.tineye.com/search?url={image}",
"bing": "https://www.bing.com/images/search?view=detailv2&iss=sbi&form=SBIVSP&sbisrc=UrlPaste&q=imgurl:{image}",
"yandex": "https://yandex.com/images/search?source=collections&&url={image}&rpt=imageview",
"saucenao": "https://saucenao.com/search.php?db=999&url={image}",
'lens': 'https://lens.google.com/uploadbyurl?url={image}',
'reverse': 'https://www.google.com/searchbyimage?sbisrc=4chanx&image_url={image}&safe=off',
'tineye': 'https://www.tineye.com/search?url={image}',
'bing': 'https://www.bing.com/images/search?view=detailv2&iss=sbi&form=SBIVSP&sbisrc=UrlPaste&q=imgurl:{image}',
'yandex': 'https://yandex.com/images/search?source=collections&&url={image}&rpt=imageview',
'saucenao': 'https://saucenao.com/search.php?db=999&url={image}',
}
@Client.on_message(filters.command("risearch", prefix) & filters.reply)
@Client.on_message(filters.command('risearch', prefix) & filters.reply)
async def reverse_image_search(client: Client, message: Message):
if not message.reply_to_message or not message.reply_to_message.photo:
await message.reply_text(
f"Please reply to an image with <code>{prefix}risearch [engine]</code> or <code>{prefix}risearch</code>."
f'Please reply to an image with <code>{prefix}risearch [engine]</code> or <code>{prefix}risearch</code>.'
)
return
@@ -31,16 +32,14 @@ async def reverse_image_search(client: Client, message: Message):
else list(SEARCH_ENGINES.keys())
)
invalid_engines = [
engine for engine in engines_to_use if engine not in SEARCH_ENGINES
]
invalid_engines = [engine for engine in engines_to_use if engine not in SEARCH_ENGINES]
if invalid_engines:
await message.reply_text(
f"Invalid engine(s): {', '.join(invalid_engines)}. Available: {', '.join(SEARCH_ENGINES.keys())}"
f'Invalid engine(s): {", ".join(invalid_engines)}. Available: {", ".join(SEARCH_ENGINES.keys())}'
)
return
processing_message = await message.edit_text("Processing the image...")
processing_message = await message.edit_text('Processing the image...')
try:
# Download and upload the image
@@ -48,7 +47,7 @@ async def reverse_image_search(client: Client, message: Message):
img_url = upload_image(photo_path)
print(img_url)
if not img_url:
await processing_message.edit("Error: Could not upload the image.")
await processing_message.edit('Error: Could not upload the image.')
return
# Perform searches for the selected engines
@@ -56,7 +55,7 @@ async def reverse_image_search(client: Client, message: Message):
search_url = SEARCH_ENGINES[engine].format(image=img_url)
await send_screenshot(client, message, search_url, engine)
except Exception as e:
await processing_message.edit(f"An error occurred: {e}")
await processing_message.edit(f'An error occurred: {e}')
finally:
if photo_path and os.path.exists(photo_path):
os.remove(photo_path)
@@ -65,15 +64,13 @@ async def reverse_image_search(client: Client, message: Message):
def upload_image(photo_path):
"""Uploads an image to tmpfiles.org and returns the direct download URL."""
try:
with open(photo_path, "rb") as image_file:
response = requests.post(
"https://tmpfiles.org/api/v1/upload", files={"file": image_file}
)
with open(photo_path, 'rb') as image_file:
response = requests.post('https://tmpfiles.org/api/v1/upload', files={'file': image_file})
if response.status_code == 200:
data = response.json()
url = data["data"]["url"]
pic_url = url.split("/")[-2] + "/" + url.split("/")[-1]
direct_download_url = url.replace(f"/{pic_url}", f"/dl/{pic_url}")
url = data['data']['url']
pic_url = url.split('/')[-2] + '/' + url.split('/')[-1]
direct_download_url = url.replace(f'/{pic_url}', f'/dl/{pic_url}')
print(direct_download_url)
return direct_download_url
else:
@@ -89,17 +86,15 @@ async def send_screenshot(client, message, url, engine_name):
await client.send_photo(
message.chat.id,
screenshot_data,
caption=f"<b>{engine_name.capitalize()} Result</b>\nURL: <code>{url}</code>",
caption=f'<b>{engine_name.capitalize()} Result</b>\nURL: <code>{url}</code>',
reply_to_message_id=message.id,
)
else:
await message.reply(
f"Failed to take screenshot for {engine_name.capitalize()}."
)
await message.reply(f'Failed to take screenshot for {engine_name.capitalize()}.')
# Add module details to help
modules_help["risearch"] = {
"risearch": f"Reply to a photo with `{prefix}risearch [engine]` (e.g., `{prefix}risearch lens`, `{prefix}risearch bing`) "
f"\nor use `{prefix}risearch` to analyze the image with all engines.",
modules_help['risearch'] = {
'risearch': f'Reply to a photo with `{prefix}risearch [engine]` (e.g., `{prefix}risearch lens`, `{prefix}risearch bing`) '
f'\nor use `{prefix}risearch` to analyze the image with all engines.',
}
+36 -44
View File
@@ -1,64 +1,60 @@
import asyncio
import json
import os
import shutil
import aiohttp
import requests
from pyrogram import Client, enums, filters
from pyrogram.types import Message
import requests
from utils.misc import modules_help, prefix
class AioHttp:
async def get_json(self, link):
headers = {
"accept": "*/*",
"accept-language": "en-US",
"cache-control": "no-cache",
"client-geo-region": "global",
"dnt": "1",
"pragma": "no-cache",
"priority": "u=1, i",
"sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0",
'accept': '*/*',
'accept-language': 'en-US',
'cache-control': 'no-cache',
'client-geo-region': 'global',
'dnt': '1',
'pragma': 'no-cache',
'priority': 'u=1, i',
'sec-ch-ua': '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0',
}
async with aiohttp.ClientSession() as session:
async with session.get(link, headers=headers) as resp:
return await resp.json()
async with aiohttp.ClientSession() as session, session.get(link, headers=headers) as resp:
return await resp.json()
@Client.on_message(filters.command("unsplash", prefix) & filters.me)
@Client.on_message(filters.command('unsplash', prefix) & filters.me)
async def unsplash(client: Client, message: Message):
if len(message.command) > 1 and isinstance(message.command[1], str):
keyword = message.command[1]
unsplash_dir = "downloads/unsplash/"
unsplash_dir = 'downloads/unsplash/'
if not os.path.exists(unsplash_dir):
os.makedirs(unsplash_dir)
if len(message.command) > 2 and 2 <= int(message.command[2]) <= 10:
await message.edit(
"<b>Getting Pictures</b>", parse_mode=enums.ParseMode.HTML
)
await message.edit('<b>Getting Pictures</b>', parse_mode=enums.ParseMode.HTML)
count = int(message.command[2])
images = []
data = await AioHttp().get_json(
f"https://unsplash.com/napi/search/photos?page=1&per_page={count}&query={keyword}"
f'https://unsplash.com/napi/search/photos?page=1&per_page={count}&query={keyword}'
)
while len(images) < count:
for ia in range(len(images), count):
img = data["results"][ia]["urls"]["raw"]
if img.startswith("https://images.unsplash.com/photo"):
img = data['results'][ia]['urls']['raw']
if img.startswith('https://images.unsplash.com/photo'):
image_content = requests.get(img).content
with open(f"{unsplash_dir}/unsplash_{ia}.jpg", "wb") as f:
with open(f'{unsplash_dir}/unsplash_{ia}.jpg', 'wb') as f:
f.write(image_content)
imgr = f"{unsplash_dir}/unsplash_{ia}.jpg"
imgr = f'{unsplash_dir}/unsplash_{ia}.jpg'
images.append(imgr)
else:
images.append(img)
@@ -72,23 +68,19 @@ async def unsplash(client: Client, message: Message):
shutil.rmtree(unsplash_dir)
return
else:
await message.edit(
"<b>Getting Picture</b>", parse_mode=enums.ParseMode.HTML
)
await message.edit('<b>Getting Picture</b>', parse_mode=enums.ParseMode.HTML)
data = await AioHttp().get_json(
f"https://unsplash.com/napi/search/photos?page=1&per_page=1&query={keyword}"
)
img = data["results"][0]["urls"]["raw"]
await asyncio.gather(
message.delete(), client.send_document(message.chat.id, str(img))
f'https://unsplash.com/napi/search/photos?page=1&per_page=1&query={keyword}'
)
img = data['results'][0]['urls']['raw']
await asyncio.gather(message.delete(), client.send_document(message.chat.id, str(img)))
modules_help["unsplash"] = {
"unsplash": f"[keyword]*",
"unsplash": f"[keyword]* [number of results you want]*\n"
"Makes a request to <code>unsplash.com</code> and sends the image with the keyword you provided.\n\n"
"<b>Note:</b>\n1. The number of results you can get is limited to 10.\n"
"2. Keyword is required and should be of one word only!.\n"
"3. Images are sent as document to maintain quality.",
modules_help['unsplash'] = {
'unsplash': '[keyword]*',
'unsplash': '[keyword]* [number of results you want]*\n'
'Makes a request to <code>unsplash.com</code> and sends the image with the keyword you provided.\n\n'
'<b>Note:</b>\n1. The number of results you can get is limited to 10.\n'
'2. Keyword is required and should be of one word only!.\n'
'3. Images are sent as document to maintain quality.',
}
+25 -36
View File
@@ -1,12 +1,13 @@
from pyrogram import Client, filters, enums
from pyrogram.types import Message, InputMediaPhoto
from io import BytesIO
from PIL import Image
import requests
import asyncio
from io import BytesIO
import requests
from PIL import Image
from pyrogram import Client, enums, filters
from pyrogram.types import InputMediaPhoto, Message
from utils.misc import modules_help, prefix
API_URL = "https://bk9.fun/search/unsplash?q="
API_URL = 'https://bk9.fun/search/unsplash?q='
def resize_image(image_bytes):
@@ -16,13 +17,13 @@ def resize_image(image_bytes):
if img.size > max_size:
img.thumbnail(max_size)
output = BytesIO()
img.save(output, format="JPEG")
img.save(output, format='JPEG')
output.seek(0)
return output
image_bytes.seek(0) # Reset pointer if not resized
return image_bytes
except Exception as e:
print(f"Error resizing image: {e}")
print(f'Error resizing image: {e}')
return image_bytes
@@ -34,70 +35,58 @@ async def download_image(url):
resized_img_bytes = resize_image(img_bytes)
return resized_img_bytes
except Exception as e:
print(f"Error downloading image: {e}")
print(f'Error downloading image: {e}')
return None
@Client.on_message(filters.command(["unsplash2", "usp2"], prefix) & filters.me)
@Client.on_message(filters.command(['unsplash2', 'usp2'], prefix) & filters.me)
async def imgsearch(client: Client, message: Message):
if len(message.command) < 2:
await message.edit(
"Usage: `img [number] <query>`", parse_mode=enums.ParseMode.MARKDOWN
)
await message.edit('Usage: `img [number] <query>`', parse_mode=enums.ParseMode.MARKDOWN)
return
num_pics = int(message.command[1]) if message.command[1].isdigit() else 10
query = " ".join(message.command[2:])
query = ' '.join(message.command[2:])
# Update status
status_message = await message.edit(
"Searching for images...", parse_mode=enums.ParseMode.MARKDOWN
)
status_message = await message.edit('Searching for images...', parse_mode=enums.ParseMode.MARKDOWN)
url = f"{API_URL}{query}"
url = f'{API_URL}{query}'
response = requests.get(url)
if response.status_code == 200:
data = response.json()
if data.get("status"):
urls = data.get("BK9", [])[:num_pics]
if data.get('status'):
urls = data.get('BK9', [])[:num_pics]
images = [download_image(img_url) for img_url in urls]
# Download images
downloaded_images = await asyncio.gather(*images)
media = [
InputMediaPhoto(media=img_bytes)
for img_bytes in downloaded_images
if img_bytes
]
media = [InputMediaPhoto(media=img_bytes) for img_bytes in downloaded_images if img_bytes]
if media:
await status_message.edit(
"Uploading pictures...", parse_mode=enums.ParseMode.MARKDOWN
)
await status_message.edit('Uploading pictures...', parse_mode=enums.ParseMode.MARKDOWN)
while media:
batch = media[:10]
media = media[10:]
await message.reply_media_group(batch)
await status_message.delete() # Delete status message after uploading
else:
await status_message.edit(
"No valid images found.", parse_mode=enums.ParseMode.MARKDOWN
)
await status_message.edit('No valid images found.', parse_mode=enums.ParseMode.MARKDOWN)
else:
await status_message.edit(
"No images found for the given query.",
'No images found for the given query.',
parse_mode=enums.ParseMode.MARKDOWN,
)
else:
await status_message.edit(
"An error occurred, please try again later.",
'An error occurred, please try again later.',
parse_mode=enums.ParseMode.MARKDOWN,
)
modules_help["unsplash2"] = {
"unsplash2 [number]* [query]": "Get HD images. Default number of images is 10",
"usp2 [number]* [query]": "Get HD images. Default number of images is 10",
modules_help['unsplash2'] = {
'unsplash2 [number]* [query]': 'Get HD images. Default number of images is 10',
'usp2 [number]* [query]': 'Get HD images. Default number of images is 10',
}
+24 -24
View File
@@ -1,23 +1,23 @@
from datetime import datetime
import sys
from pyrogram import Client
from datetime import datetime
from pyrogram import Client
from utils import config
common_params = {
"api_id": config.api_id,
"api_hash": config.api_hash,
"hide_password": True,
"test_mode": config.test_server,
'api_id': config.api_id,
'api_hash': config.api_hash,
'hide_password': True,
'test_mode': config.test_server,
}
if __name__ == "__main__":
if __name__ == '__main__':
if config.STRINGSESSION:
common_params["session_string"] = config.STRINGSESSION
common_params['session_string'] = config.STRINGSESSION
app = Client("my_account", **common_params)
app = Client('my_account', **common_params)
if config.db_type in ["mongo", "mongodb"]:
if config.db_type in ['mongo', 'mongodb']:
from pymongo import MongoClient, errors
db = MongoClient(config.db_url)
@@ -26,27 +26,27 @@ if __name__ == "__main__":
except errors.ConnectionFailure as e:
raise RuntimeError(
"MongoDB server isn't available! "
f"Provided url: {config.db_url}. "
"Enter valid URL and restart installation"
f'Provided url: {config.db_url}. '
'Enter valid URL and restart installation'
) from e
install_type = sys.argv[1] if len(sys.argv) > 1 else "3"
if install_type == "1":
restart = "pm2 restart Moon"
elif install_type == "2":
restart = "sudo systemctl restart Moon"
install_type = sys.argv[1] if len(sys.argv) > 1 else '3'
if install_type == '1':
restart = 'pm2 restart Moon'
elif install_type == '2':
restart = 'sudo systemctl restart Moon'
else:
restart = "cd Moon-Userbot/ && python main.py"
restart = 'cd Moon-Userbot/ && python main.py'
app.start()
try:
app.send_message(
"me",
f"<b>[{datetime.now()}] Userbot launched! \n"
"Custom modules: @moonub_modules\n"
f"For restart, enter:</b>\n"
f"<code>{restart}</code>",
'me',
f'<b>[{datetime.now()}] Userbot launched! \n'
'Custom modules: @moonub_modules\n'
f'For restart, enter:</b>\n'
f'<code>{restart}</code>',
)
except Exception as e:
print(f"[ERROR]: Sending Message to me failed! {e}")
print(f'[ERROR]: Sending Message to me failed! {e}')
app.stop()
+51 -60
View File
@@ -39,92 +39,88 @@
# "pySmartDL",
# ]
# ///
import os
import logging
import sqlite3
import os
import platform
import sqlite3
import subprocess
from pyrogram import Client, idle, errors
from pyrogram.enums.parse_mode import ParseMode
from pyrogram.raw.functions.account import GetAuthorizations, DeleteAccount
import requests
from pyrogram import Client, errors, idle
from pyrogram.enums.parse_mode import ParseMode
from pyrogram.raw.functions.account import DeleteAccount, GetAuthorizations
from utils import config
from utils.db import db
from utils.misc import gitrepo, userbot_version
from utils.scripts import restart
from utils.rentry import rentry_cleanup_job
from utils.misc import userbot_version
from utils.module import ModuleManager
from utils.rentry import rentry_cleanup_job
from utils.scripts import restart
SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__))
if SCRIPT_PATH != os.getcwd():
if os.getcwd() != SCRIPT_PATH:
os.chdir(SCRIPT_PATH)
common_params = {
"api_id": config.api_id,
"api_hash": config.api_hash,
"hide_password": True,
"workdir": SCRIPT_PATH,
"app_version": userbot_version,
"device_model": f"mUserbot",
"system_version": platform.version() + " " + platform.machine(),
"sleep_threshold": 30,
"test_mode": config.test_server,
"parse_mode": ParseMode.HTML,
'api_id': config.api_id,
'api_hash': config.api_hash,
'hide_password': True,
'workdir': SCRIPT_PATH,
'app_version': userbot_version,
'device_model': 'mUserbot',
'system_version': platform.version() + ' ' + platform.machine(),
'sleep_threshold': 30,
'test_mode': config.test_server,
'parse_mode': ParseMode.HTML,
}
if config.STRINGSESSION:
common_params["session_string"] = config.STRINGSESSION
common_params['session_string'] = config.STRINGSESSION
app = Client("my_account", **common_params)
app = Client('my_account', **common_params)
def load_missing_modules():
all_modules = db.get("custom.modules", "allModules", [])
all_modules = db.get('custom.modules', 'allModules', [])
if not all_modules:
return
custom_modules_path = f"{SCRIPT_PATH}/modules/custom_modules"
custom_modules_path = f'{SCRIPT_PATH}/modules/custom_modules'
os.makedirs(custom_modules_path, exist_ok=True)
try:
resp = requests.get(
"https://raw.githubusercontent.com/The-MoonTg-project/custom_modules/main/full.txt",
'https://raw.githubusercontent.com/The-MoonTg-project/custom_modules/main/full.txt',
timeout=10,
)
if not resp.ok:
logging.error(
"Failed to fetch custom modules list: HTTP %s",
'Failed to fetch custom modules list: HTTP %s',
resp.status_code,
)
return
f = resp.text
except Exception as e:
logging.error("Failed to fetch custom modules list: %s", e)
logging.error('Failed to fetch custom modules list: %s', e)
return
modules_dict = {
line.split("/")[-1].split()[0]: line.strip() for line in f.splitlines()
}
modules_dict = {line.split('/')[-1].split()[0]: line.strip() for line in f.splitlines()}
for module_name in all_modules:
module_path = f"{custom_modules_path}/{module_name}.py"
module_path = f'{custom_modules_path}/{module_name}.py'
if not os.path.exists(module_path) and module_name in modules_dict:
url = f"https://raw.githubusercontent.com/The-MoonTg-project/custom_modules/main/{modules_dict[module_name]}.py"
url = f'https://raw.githubusercontent.com/The-MoonTg-project/custom_modules/main/{modules_dict[module_name]}.py'
resp = requests.get(url)
if resp.ok:
with open(module_path, "wb") as f:
with open(module_path, 'wb') as f:
f.write(resp.content)
logging.info("Loaded missing module: %s", module_name)
logging.info('Loaded missing module: %s', module_name)
else:
logging.warning("Failed to load module: %s", module_name)
logging.warning('Failed to load module: %s', module_name)
async def main():
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.FileHandler("moonlogs.txt"), logging.StreamHandler()],
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.FileHandler('moonlogs.txt'), logging.StreamHandler()],
level=logging.INFO,
)
DeleteAccount.__new__ = None
@@ -132,49 +128,44 @@ async def main():
try:
await app.start()
except sqlite3.OperationalError as e:
if str(e) == "database is locked" and os.name == "posix":
logging.warning(
"Session file is locked. Trying to kill blocking process..."
)
subprocess.run(["fuser", "-k", "my_account.session"], check=True)
if str(e) == 'database is locked' and os.name == 'posix':
logging.warning('Session file is locked. Trying to kill blocking process...')
subprocess.run(['fuser', '-k', 'my_account.session'], check=True)
restart()
raise
except (errors.NotAcceptable, errors.Unauthorized) as e:
logging.error(
"%s: %s\nMoving session file to my_account.session-old...",
'%s: %s\nMoving session file to my_account.session-old...',
e.__class__.__name__,
e,
)
os.rename("./my_account.session", "./my_account.session-old")
os.rename('./my_account.session', './my_account.session-old')
restart()
load_missing_modules()
module_manager = ModuleManager.get_instance()
await module_manager.load_modules(app)
if info := db.get("core.updater", "restart_info"):
if info := db.get('core.updater', 'restart_info'):
text = {
"restart": "<b>Restart completed!</b>",
"update": "<b>Update process completed!</b>",
}[info["type"]]
'restart': '<b>Restart completed!</b>',
'update': '<b>Update process completed!</b>',
}[info['type']]
try:
await app.edit_message_text(info["chat_id"], info["message_id"], text)
await app.edit_message_text(info['chat_id'], info['message_id'], text)
except errors.RPCError:
pass
db.remove("core.updater", "restart_info")
db.remove('core.updater', 'restart_info')
# required for sessionkiller module
if db.get("core.sessionkiller", "enabled", False):
if db.get('core.sessionkiller', 'enabled', False):
db.set(
"core.sessionkiller",
"auths_hashes",
[
auth.hash
for auth in (await app.invoke(GetAuthorizations())).authorizations
],
'core.sessionkiller',
'auths_hashes',
[auth.hash for auth in (await app.invoke(GetAuthorizations())).authorizations],
)
logging.info("Moon-Userbot started!")
logging.info('Moon-Userbot started!')
app.loop.create_task(rentry_cleanup_job())
@@ -183,5 +174,5 @@ async def main():
await app.stop()
if __name__ == "__main__":
if __name__ == '__main__':
app.run(main())
+5 -11
View File
@@ -2,20 +2,16 @@ from asyncio import sleep
from pyrogram import Client, filters
from pyrogram.types import Message
from utils.misc import modules_help, prefix
digits = {
str(i): el
for i, el in enumerate(["0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣"])
}
digits = {str(i): el for i, el in enumerate(['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣'])}
def prettify(val: int) -> str:
return "".join(digits[i] for i in str(val))
return ''.join(digits[i] for i in str(val))
@Client.on_message(filters.command("ghoul", prefix) & filters.me)
@Client.on_message(filters.command('ghoul', prefix) & filters.me)
async def ghoul_counter(_, message: Message):
await message.delete()
@@ -33,9 +29,7 @@ async def ghoul_counter(_, message: Message):
await msg.edit(prettify(counter))
await sleep(1)
await msg.edit("<b>🤡 GHOUL 🤡</b>")
await msg.edit('<b>🤡 GHOUL 🤡</b>')
modules_help["1000-7"] = {
"ghoul [count_from]": "counting from 1000 (or given [count_from] to 0 as a ghoul"
}
modules_help['1000-7'] = {'ghoul [count_from]': 'counting from 1000 (or given [count_from] to 0 as a ghoul'}
+93 -104
View File
@@ -16,60 +16,57 @@
from contextlib import suppress
from pyrogram.enums import ChatType
from pyrogram import Client, ContinuePropagation, filters
from pyrogram.enums import ChatType
from pyrogram.errors import (
UserAdminInvalid,
ChatAdminRequired,
RPCError,
UserAdminInvalid,
)
from pyrogram.raw import functions
from pyrogram.types import Message, ChatPermissions
from pyrogram.types import ChatPermissions, Message
from utils.db import db
from utils.scripts import format_exc, with_reply
from utils.misc import modules_help, prefix
from utils.handlers import (
BanHandler,
UnbanHandler,
KickHandler,
KickDeletedAccountsHandler,
TimeMuteHandler,
TimeUnmuteHandler,
TimeMuteUsersHandler,
UnmuteHandler,
MuteHandler,
DemoteHandler,
PromoteHandler,
AntiChannelsHandler,
DeleteHistoryHandler,
AntiRaidHandler,
BanHandler,
DeleteHistoryHandler,
DemoteHandler,
KickDeletedAccountsHandler,
KickHandler,
MuteHandler,
PromoteHandler,
TimeMuteHandler,
TimeMuteUsersHandler,
TimeUnmuteHandler,
UnbanHandler,
UnmuteHandler,
)
from utils.misc import modules_help, prefix
from utils.scripts import format_exc, with_reply
db_cache: dict = db.get_collection("core.ats")
db_cache: dict = db.get_collection('core.ats')
def update_cache():
db_cache.clear()
db_cache.update(db.get_collection("core.ats"))
db_cache.update(db.get_collection('core.ats'))
@Client.on_message(filters.group & ~filters.me)
async def admintool_handler(_, message: Message):
if message.sender_chat and (
message.sender_chat.type == "supergroup"
or message.sender_chat.id == db_cache.get(f"linked{message.chat.id}", 0)
message.sender_chat.type == 'supergroup'
or message.sender_chat.id == db_cache.get(f'linked{message.chat.id}', 0)
):
raise ContinuePropagation
if message.sender_chat and db_cache.get(f"antich{message.chat.id}", False):
if message.sender_chat and db_cache.get(f'antich{message.chat.id}', False):
with suppress(RPCError):
await message.delete()
await message.chat.ban_member(message.sender_chat.id)
tmuted_users = db_cache.get(f"c{message.chat.id}", [])
tmuted_users = db_cache.get(f'c{message.chat.id}', [])
if (
message.from_user
and message.from_user.id in tmuted_users
@@ -79,7 +76,7 @@ async def admintool_handler(_, message: Message):
with suppress(RPCError):
await message.delete()
if db_cache.get(f"antiraid{message.chat.id}", False):
if db_cache.get(f'antiraid{message.chat.id}', False):
with suppress(RPCError):
await message.delete()
if message.from_user:
@@ -87,11 +84,9 @@ async def admintool_handler(_, message: Message):
elif message.sender_chat:
await message.chat.ban_member(message.sender_chat.id)
if message.new_chat_members and db_cache.get(
f"welcome_enabled{message.chat.id}", False
):
if message.new_chat_members and db_cache.get(f'welcome_enabled{message.chat.id}', False):
await message.reply(
db_cache.get(f"welcome_text{message.chat.id}"),
db_cache.get(f'welcome_text{message.chat.id}'),
disable_web_page_preview=True,
)
@@ -111,88 +106,88 @@ async def get_user_and_name(message):
)
@Client.on_message(filters.command(["ban"], prefix) & filters.me)
@Client.on_message(filters.command(['ban'], prefix) & filters.me)
async def ban_command(client: Client, message: Message):
handler = BanHandler(client, message)
await handler.handle_ban()
@Client.on_message(filters.command(["unban"], prefix) & filters.me)
@Client.on_message(filters.command(['unban'], prefix) & filters.me)
async def unban_command(client: Client, message: Message):
handler = UnbanHandler(client, message)
await handler.handle_unban()
@Client.on_message(filters.command(["kick"], prefix) & filters.me)
@Client.on_message(filters.command(['kick'], prefix) & filters.me)
async def kick_command(client: Client, message: Message):
handler = KickHandler(client, message)
await handler.handle_kick()
@Client.on_message(filters.command(["kickdel"], prefix) & filters.me)
@Client.on_message(filters.command(['kickdel'], prefix) & filters.me)
async def kickdel_cmd(client: Client, message: Message):
handler = KickDeletedAccountsHandler(client, message)
await handler.kick_deleted_accounts()
@Client.on_message(filters.command(["tmute"], prefix) & filters.me)
@Client.on_message(filters.command(['tmute'], prefix) & filters.me)
async def tmute_command(client: Client, message: Message):
handler = TimeMuteHandler(client, message)
await handler.handle_tmute()
update_cache()
@Client.on_message(filters.command(["tunmute"], prefix) & filters.me)
@Client.on_message(filters.command(['tunmute'], prefix) & filters.me)
async def tunmute_command(client: Client, message: Message):
handler = TimeUnmuteHandler(client, message)
await handler.handle_tunmute()
update_cache()
@Client.on_message(filters.command(["tmute_users"], prefix) & filters.me)
@Client.on_message(filters.command(['tmute_users'], prefix) & filters.me)
async def tunmute_users_command(client: Client, message: Message):
handler = TimeMuteUsersHandler(client, message)
await handler.list_tmuted_users()
@Client.on_message(filters.command(["unmute"], prefix) & filters.me)
@Client.on_message(filters.command(['unmute'], prefix) & filters.me)
async def unmute_command(client: Client, message: Message):
handler = UnmuteHandler(client, message)
await handler.handle_unmute()
@Client.on_message(filters.command(["mute"], prefix) & filters.me)
@Client.on_message(filters.command(['mute'], prefix) & filters.me)
async def mute_command(client: Client, message: Message):
handler = MuteHandler(client, message)
await handler.handle_mute()
@Client.on_message(filters.command(["demote"], prefix) & filters.me)
@Client.on_message(filters.command(['demote'], prefix) & filters.me)
async def demote_command(client: Client, message: Message):
handler = DemoteHandler(client, message)
await handler.handle_demote()
@Client.on_message(filters.command(["promote"], prefix) & filters.me)
@Client.on_message(filters.command(['promote'], prefix) & filters.me)
async def promote_command(client: Client, message: Message):
handler = PromoteHandler(client, message)
await handler.handle_promote()
@Client.on_message(filters.command(["antich"], prefix))
@Client.on_message(filters.command(['antich'], prefix))
async def anti_channels(client: Client, message: Message):
handler = AntiChannelsHandler(client, message)
await handler.handle_anti_channels()
update_cache()
@Client.on_message(filters.command(["delete_history", "dh"], prefix))
@Client.on_message(filters.command(['delete_history', 'dh'], prefix))
async def delete_history(client: Client, message: Message):
handler = DeleteHistoryHandler(client, message)
await handler.handle_delete_history()
@Client.on_message(filters.command(["report_spam", "rs"], prefix))
@Client.on_message(filters.command(['report_spam', 'rs'], prefix))
@with_reply
async def report_spam(client: Client, message: Message):
try:
@@ -210,33 +205,33 @@ async def report_spam(client: Client, message: Message):
except Exception as e:
await message.edit(format_exc(e))
else:
await message.edit(f"<b>Message</a> from {name} was reported</b>")
await message.edit(f'<b>Message</a> from {name} was reported</b>')
@Client.on_message(filters.command("pin", prefix) & filters.me)
@Client.on_message(filters.command('pin', prefix) & filters.me)
@with_reply
async def pin(_, message: Message):
try:
await message.reply_to_message.pin()
await message.edit("<b>Pinned!</b>")
await message.edit('<b>Pinned!</b>')
except Exception as e:
await message.edit(format_exc(e))
@Client.on_message(filters.command("unpin", prefix) & filters.me)
@Client.on_message(filters.command('unpin', prefix) & filters.me)
@with_reply
async def unpin(_, message: Message):
try:
await message.reply_to_message.unpin()
await message.edit("<b>Unpinned!</b>")
await message.edit('<b>Unpinned!</b>')
except Exception as e:
await message.edit(format_exc(e))
@Client.on_message(filters.command("ro", prefix) & filters.me)
@Client.on_message(filters.command('ro', prefix) & filters.me)
async def ro(client: Client, message: Message):
if message.chat.type not in [ChatType.GROUP, ChatType.SUPERGROUP]:
await message.edit("<b>Invalid chat type</b>")
await message.edit('<b>Invalid chat type</b>')
return
try:
@@ -250,42 +245,39 @@ async def ro(client: Client, message: Message):
perms.can_invite_users,
perms.can_pin_messages,
]
db.set("core.ats", f"ro{message.chat.id}", perms_list)
db.set('core.ats', f'ro{message.chat.id}', perms_list)
try:
await client.set_chat_permissions(message.chat.id, ChatPermissions())
except (UserAdminInvalid, ChatAdminRequired):
await message.edit("<b>No rights</b>")
await message.edit('<b>No rights</b>')
else:
await message.edit(
"<b>Read-only mode activated!\n"
f"Turn off with:</b><code>{prefix}unro</code>"
)
await message.edit(f'<b>Read-only mode activated!\nTurn off with:</b><code>{prefix}unro</code>')
except Exception as e:
await message.edit(format_exc(e))
@Client.on_message(filters.command("unro", prefix) & filters.me)
@Client.on_message(filters.command('unro', prefix) & filters.me)
async def unro(client: Client, message: Message):
if message.chat.type not in [ChatType.GROUP, ChatType.SUPERGROUP]:
await message.edit("<b>Invalid chat type</b>")
await message.edit('<b>Invalid chat type</b>')
return
try:
perms_list = db.get(
"core.ats",
f"ro{message.chat.id}",
'core.ats',
f'ro{message.chat.id}',
[True, True, False, False, False, False, False],
)
common_perms = {
"can_send_messages": perms_list[0],
"can_send_media_messages": perms_list[1],
"can_send_polls": perms_list[2],
"can_add_web_page_previews": perms_list[3],
"can_change_info": perms_list[4],
"can_invite_users": perms_list[5],
"can_pin_messages": perms_list[6],
'can_send_messages': perms_list[0],
'can_send_media_messages': perms_list[1],
'can_send_polls': perms_list[2],
'can_add_web_page_previews': perms_list[3],
'can_change_info': perms_list[4],
'can_invite_users': perms_list[5],
'can_pin_messages': perms_list[6],
}
perms = ChatPermissions(**common_perms)
@@ -293,61 +285,58 @@ async def unro(client: Client, message: Message):
try:
await client.set_chat_permissions(message.chat.id, perms)
except (UserAdminInvalid, ChatAdminRequired):
await message.edit("<b>No rights</b>")
await message.edit('<b>No rights</b>')
else:
await message.edit("<b>Read-only mode disabled!</b>")
await message.edit('<b>Read-only mode disabled!</b>')
except Exception as e:
await message.edit(format_exc(e))
@Client.on_message(filters.command("antiraid", prefix) & filters.me)
@Client.on_message(filters.command('antiraid', prefix) & filters.me)
async def antiraid(client: Client, message: Message):
handler = AntiRaidHandler(client, message)
await handler.handle_antiraid()
update_cache()
@Client.on_message(filters.command(["welcome", "wc"], prefix) & filters.me)
@Client.on_message(filters.command(['welcome', 'wc'], prefix) & filters.me)
async def welcome(_, message: Message):
if message.chat.type not in [ChatType.GROUP, ChatType.SUPERGROUP]:
return await message.edit("<b>Unsupported chat type</b>")
return await message.edit('<b>Unsupported chat type</b>')
if len(message.command) > 1:
text = message.text.split(maxsplit=1)[1]
db.set("core.ats", f"welcome_enabled{message.chat.id}", True)
db.set("core.ats", f"welcome_text{message.chat.id}", text)
db.set('core.ats', f'welcome_enabled{message.chat.id}', True)
db.set('core.ats', f'welcome_text{message.chat.id}', text)
await message.edit(
f"<b>Welcome enabled in this chat\nText:</b> <code>{text}</code>"
)
await message.edit(f'<b>Welcome enabled in this chat\nText:</b> <code>{text}</code>')
else:
db.set("core.ats", f"welcome_enabled{message.chat.id}", False)
await message.edit("<b>Welcome disabled in this chat</b>")
db.set('core.ats', f'welcome_enabled{message.chat.id}', False)
await message.edit('<b>Welcome disabled in this chat</b>')
update_cache()
modules_help["admintool"] = {
"ban [reply]/[username/id]* [reason] [report_spam] [delete_history]": "ban user in chat",
"unban [reply]/[username/id]* [reason]": "unban user in chat",
"kick [reply]/[userid]* [reason] [report_spam] [delete_history]": "kick user out of chat",
"mute [reply]/[userid]* [reason] [1m]/[1h]/[1d]/[1w]": "mute user in chat",
"unmute [reply]/[userid]* [reason]": "unmute user in chat",
"promote [reply]/[userid]* [prefix]": "promote user in chat",
"demote [reply]/[userid]* [reason]": "demote user in chat",
"tmute [reply]/[username/id]* [reason]": "delete all new messages from user in chat",
"tunmute [reply]/[username/id]* [reason]": "stop deleting all messages from user in chat",
"tmute_users": "list of tmuted (.tmute) users",
"antich [enable/disable]": "turn on/off blocking channels in this chat",
"delete_history [reply]/[username/id]* [reason]": "delete history from member in chat",
"report_spam [reply]*": "report spam message in chat",
"pin [reply]*": "Pin replied message",
"unpin [reply]*": "Unpin replied message",
"ro": "enable read-only mode",
"unro": "disable read-only mode",
"antiraid [on|off]": "when enabled, anyone who writes message will be blocked. Useful in raids. "
"Running without arguments equals to toggling state",
"welcome [text]*": "enable auto-welcome to new users in groups. "
"Running without text equals to disable",
"kickdel": "Kick all deleted accounts",
modules_help['admintool'] = {
'ban [reply]/[username/id]* [reason] [report_spam] [delete_history]': 'ban user in chat',
'unban [reply]/[username/id]* [reason]': 'unban user in chat',
'kick [reply]/[userid]* [reason] [report_spam] [delete_history]': 'kick user out of chat',
'mute [reply]/[userid]* [reason] [1m]/[1h]/[1d]/[1w]': 'mute user in chat',
'unmute [reply]/[userid]* [reason]': 'unmute user in chat',
'promote [reply]/[userid]* [prefix]': 'promote user in chat',
'demote [reply]/[userid]* [reason]': 'demote user in chat',
'tmute [reply]/[username/id]* [reason]': 'delete all new messages from user in chat',
'tunmute [reply]/[username/id]* [reason]': 'stop deleting all messages from user in chat',
'tmute_users': 'list of tmuted (.tmute) users',
'antich [enable/disable]': 'turn on/off blocking channels in this chat',
'delete_history [reply]/[username/id]* [reason]': 'delete history from member in chat',
'report_spam [reply]*': 'report spam message in chat',
'pin [reply]*': 'Pin replied message',
'unpin [reply]*': 'Unpin replied message',
'ro': 'enable read-only mode',
'unro': 'disable read-only mode',
'antiraid [on|off]': 'when enabled, anyone who writes message will be blocked. Useful in raids. '
'Running without arguments equals to toggling state',
'welcome [text]*': 'enable auto-welcome to new users in groups. Running without text equals to disable',
'kickdel': 'Kick all deleted accounts',
}
+84 -104
View File
@@ -14,12 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from collections.abc import AsyncGenerator
from time import perf_counter
from typing import AsyncGenerator, List, Optional, Union
from typing import Optional
from pyrogram import Client, enums, filters, raw, types, utils
from pyrogram.types.object import Object
from utils.misc import modules_help, prefix
from utils.scripts import format_exc
@@ -28,7 +28,7 @@ class Chat(Object):
def __init__(
self,
*,
client: "Client" = None,
client: 'Client' = None,
id: id,
type: type,
is_verified: bool = None,
@@ -41,7 +41,7 @@ class Chat(Object):
username: str = None,
first_name: str = None,
last_name: str = None,
photo: "types.ChatPhoto" = None,
photo: 'types.ChatPhoto' = None,
bio: str = None,
description: str = None,
dc_id: int = None,
@@ -51,12 +51,12 @@ class Chat(Object):
sticker_set_name: str = None,
can_set_sticker_set: bool = None,
members_count: int = None,
restrictions: List["types.Restriction"] = None,
permissions: "types.ChatPermissions" = None,
restrictions: list['types.Restriction'] = None,
permissions: 'types.ChatPermissions' = None,
distance: int = None,
linked_chat: "types.Chat" = None,
send_as_chat: "types.Chat" = None,
available_reactions: Optional["types.ChatReactions"] = None,
linked_chat: 'types.Chat' = None,
send_as_chat: 'types.Chat' = None,
available_reactions: Optional['types.ChatReactions'] = None,
is_admin: bool = False,
deactivated: bool = False,
):
@@ -94,98 +94,82 @@ class Chat(Object):
self.deactivated = deactivated
@staticmethod
def _parse_user_chat(client, user: raw.types.User) -> "Chat":
def _parse_user_chat(client, user: raw.types.User) -> 'Chat':
peer_id = user.id
return Chat(
id=peer_id,
type=enums.ChatType.BOT if user.bot else enums.ChatType.PRIVATE,
is_verified=getattr(user, "verified", None),
is_restricted=getattr(user, "restricted", None),
is_scam=getattr(user, "scam", None),
is_fake=getattr(user, "fake", None),
is_support=getattr(user, "support", None),
is_verified=getattr(user, 'verified', None),
is_restricted=getattr(user, 'restricted', None),
is_scam=getattr(user, 'scam', None),
is_fake=getattr(user, 'fake', None),
is_support=getattr(user, 'support', None),
username=user.username,
first_name=user.first_name,
last_name=user.last_name,
photo=types.ChatPhoto._parse(client, user.photo, peer_id, user.access_hash),
restrictions=types.List(
[types.Restriction._parse(r) for r in user.restriction_reason]
)
or None,
dc_id=getattr(getattr(user, "photo", None), "dc_id", None),
restrictions=types.List([types.Restriction._parse(r) for r in user.restriction_reason]) or None,
dc_id=getattr(getattr(user, 'photo', None), 'dc_id', None),
client=client,
)
@staticmethod
def _parse_chat_chat(client, chat: raw.types.Chat) -> "Chat":
def _parse_chat_chat(client, chat: raw.types.Chat) -> 'Chat':
peer_id = -chat.id
return Chat(
id=peer_id,
type=enums.ChatType.GROUP,
title=chat.title,
is_creator=getattr(chat, "creator", None),
photo=types.ChatPhoto._parse(
client, getattr(chat, "photo", None), peer_id, 0
),
permissions=types.ChatPermissions._parse(
getattr(chat, "default_banned_rights", None)
),
members_count=getattr(chat, "participants_count", None),
dc_id=getattr(getattr(chat, "photo", None), "dc_id", None),
has_protected_content=getattr(chat, "noforwards", None),
is_creator=getattr(chat, 'creator', None),
photo=types.ChatPhoto._parse(client, getattr(chat, 'photo', None), peer_id, 0),
permissions=types.ChatPermissions._parse(getattr(chat, 'default_banned_rights', None)),
members_count=getattr(chat, 'participants_count', None),
dc_id=getattr(getattr(chat, 'photo', None), 'dc_id', None),
has_protected_content=getattr(chat, 'noforwards', None),
client=client,
is_admin=bool(getattr(chat, "admin_rights", False)),
is_admin=bool(getattr(chat, 'admin_rights', False)),
deactivated=chat.deactivated,
)
@staticmethod
def _parse_channel_chat(client, channel: raw.types.Channel) -> "Chat":
def _parse_channel_chat(client, channel: raw.types.Channel) -> 'Chat':
peer_id = utils.get_channel_id(channel.id)
restriction_reason = getattr(channel, "restriction_reason", [])
restriction_reason = getattr(channel, 'restriction_reason', [])
return Chat(
id=peer_id,
type=(
enums.ChatType.SUPERGROUP
if getattr(channel, "megagroup", None)
else enums.ChatType.CHANNEL
),
is_verified=getattr(channel, "verified", None),
is_restricted=getattr(channel, "restricted", None),
is_creator=getattr(channel, "creator", None),
is_scam=getattr(channel, "scam", None),
is_fake=getattr(channel, "fake", None),
type=(enums.ChatType.SUPERGROUP if getattr(channel, 'megagroup', None) else enums.ChatType.CHANNEL),
is_verified=getattr(channel, 'verified', None),
is_restricted=getattr(channel, 'restricted', None),
is_creator=getattr(channel, 'creator', None),
is_scam=getattr(channel, 'scam', None),
is_fake=getattr(channel, 'fake', None),
title=channel.title,
username=getattr(channel, "username", None),
username=getattr(channel, 'username', None),
photo=types.ChatPhoto._parse(
client,
getattr(channel, "photo", None),
getattr(channel, 'photo', None),
peer_id,
getattr(channel, "access_hash", 0),
getattr(channel, 'access_hash', 0),
),
restrictions=types.List(
[types.Restriction._parse(r) for r in restriction_reason]
)
or None,
permissions=types.ChatPermissions._parse(
getattr(channel, "default_banned_rights", None)
),
members_count=getattr(channel, "participants_count", None),
dc_id=getattr(getattr(channel, "photo", None), "dc_id", None),
has_protected_content=getattr(channel, "noforwards", None),
is_admin=bool(getattr(channel, "admin_rights", False)),
restrictions=types.List([types.Restriction._parse(r) for r in restriction_reason]) or None,
permissions=types.ChatPermissions._parse(getattr(channel, 'default_banned_rights', None)),
members_count=getattr(channel, 'participants_count', None),
dc_id=getattr(getattr(channel, 'photo', None), 'dc_id', None),
has_protected_content=getattr(channel, 'noforwards', None),
is_admin=bool(getattr(channel, 'admin_rights', False)),
client=client,
)
@staticmethod
def _parse(
client,
message: Union[raw.types.Message, raw.types.MessageService],
message: raw.types.Message | raw.types.MessageService,
users: dict,
chats: dict,
is_chat: bool,
) -> "Chat":
) -> 'Chat':
from_id = utils.get_raw_peer_id(message.from_id)
peer_id = utils.get_raw_peer_id(message.peer_id)
chat_id = (peer_id or from_id) if is_chat else (from_id or peer_id)
@@ -211,9 +195,9 @@ class Dialog(Object):
def __init__(
self,
*,
client: "Client" = None,
chat: "types.Chat",
top_message: "types.Message",
client: 'Client' = None,
chat: 'types.Chat',
top_message: 'types.Message',
unread_messages_count: int,
unread_mentions_count: int,
unread_mark: bool,
@@ -229,7 +213,7 @@ class Dialog(Object):
self.is_pinned = is_pinned
@staticmethod
def _parse(client, dialog: "raw.types.Dialog", messages, users, chats) -> "Dialog":
def _parse(client, dialog: 'raw.types.Dialog', messages, users, chats) -> 'Dialog':
return Dialog(
chat=Chat._parse_dialog(client, dialog.peer, users, chats),
top_message=messages.get(utils.get_peer_id(dialog.peer)),
@@ -241,9 +225,7 @@ class Dialog(Object):
)
async def get_dialogs(
self: "Client", limit: int = 0
) -> Optional[AsyncGenerator["types.Dialog", None]]:
async def get_dialogs(self: 'Client', limit: int = 0) -> AsyncGenerator['types.Dialog', None] | None:
current = 0
total = limit or (1 << 31) - 1
limit = min(100, total)
@@ -287,7 +269,7 @@ async def get_dialogs(
return
@Client.on_message(filters.command("admlist", prefix) & filters.me)
@Client.on_message(filters.command('admlist', prefix) & filters.me)
async def admlist(client: Client, message: types.Message):
await message.edit("<b>Retrieving information... (it'll take some time)</b>")
@@ -298,48 +280,46 @@ async def admlist(client: Client, message: types.Message):
owned_usernamed_chats = []
async for dialog in get_dialogs(client):
chat = dialog.chat
if getattr(chat, "deactivated", False):
if getattr(chat, 'deactivated', False):
continue
if getattr(chat, "is_creator", False) and getattr(chat, "username", None):
if getattr(chat, 'is_creator', False) and getattr(chat, 'username', None):
owned_usernamed_chats.append(chat)
elif getattr(chat, "is_creator", False):
elif getattr(chat, 'is_creator', False):
owned_chats.append(chat)
elif getattr(chat, "is_admin", False):
elif getattr(chat, 'is_admin', False):
adminned_chats.append(chat)
text = "<b>Adminned chats:</b>\n"
text = '<b>Adminned chats:</b>\n'
for index, chat in enumerate(adminned_chats):
cid = str(chat.id).replace("-100", "")
text += f"{index + 1}. <a href=https://t.me/c/{cid}/1>{chat.title}</a>\n"
cid = str(chat.id).replace('-100', '')
text += f'{index + 1}. <a href=https://t.me/c/{cid}/1>{chat.title}</a>\n'
text += "\n<b>Owned chats:</b>\n"
text += '\n<b>Owned chats:</b>\n'
for index, chat in enumerate(owned_chats):
cid = str(chat.id).replace("-100", "")
text += f"{index + 1}. <a href=https://t.me/c/{cid}/1>{chat.title}</a>\n"
cid = str(chat.id).replace('-100', '')
text += f'{index + 1}. <a href=https://t.me/c/{cid}/1>{chat.title}</a>\n'
text += "\n<b>Owned chats with username:</b>\n"
text += '\n<b>Owned chats with username:</b>\n'
for index, chat in enumerate(owned_usernamed_chats):
cid = str(chat.id).replace("-100", "")
text += f"{index + 1}. <a href=https://t.me/c/{cid}/1>{chat.title}</a>\n"
cid = str(chat.id).replace('-100', '')
text += f'{index + 1}. <a href=https://t.me/c/{cid}/1>{chat.title}</a>\n'
stop = perf_counter()
total_count = (
len(adminned_chats) + len(owned_chats) + len(owned_usernamed_chats)
)
total_count = len(adminned_chats) + len(owned_chats) + len(owned_usernamed_chats)
await message.edit(
text + "\n"
f"<b><u>Total:</u></b> {total_count}"
f"\n<b><u>Adminned chats:</u></b> {len(adminned_chats)}\n"
f"<b><u>Owned chats:</u></b> {len(owned_chats)}\n"
f"<b><u>Owned chats with username:</u></b> {len(owned_usernamed_chats)}\n\n"
f"Done in {round(stop - start, 3)} seconds.",
text + '\n'
f'<b><u>Total:</u></b> {total_count}'
f'\n<b><u>Adminned chats:</u></b> {len(adminned_chats)}\n'
f'<b><u>Owned chats:</u></b> {len(owned_chats)}\n'
f'<b><u>Owned chats with username:</u></b> {len(owned_usernamed_chats)}\n\n'
f'Done in {round(stop - start, 3)} seconds.',
)
except Exception as e:
await message.edit(format_exc(e))
return
@Client.on_message(filters.command("admcount", prefix) & filters.me)
@Client.on_message(filters.command('admcount', prefix) & filters.me)
async def admcount(client: Client, message: types.Message):
await message.edit("<b>Retrieving information... (it'll take some time)</b>")
@@ -350,31 +330,31 @@ async def admcount(client: Client, message: types.Message):
owned_usernamed_chats = 0
async for dialog in get_dialogs(client):
chat = dialog.chat
if getattr(chat, "deactivated", False):
if getattr(chat, 'deactivated', False):
continue
if getattr(chat, "is_creator", False) and getattr(chat, "username", None):
if getattr(chat, 'is_creator', False) and getattr(chat, 'username', None):
owned_usernamed_chats += 1
elif getattr(chat, "is_creator", False):
elif getattr(chat, 'is_creator', False):
owned_chats += 1
elif getattr(chat, "is_admin", False):
elif getattr(chat, 'is_admin', False):
adminned_chats += 1
stop = perf_counter()
total_count = adminned_chats + owned_chats + owned_usernamed_chats
await message.edit(
f"<b><u>Total:</u></b> {total_count}"
f"\n<b><u>Adminned chats:</u></b> {adminned_chats}\n"
f"<b><u>Owned chats:</u></b> {owned_chats}\n"
f"<b><u>Owned chats with username:</u></b> {owned_usernamed_chats}\n\n"
f"Done in {round(stop - start, 3)} seconds.\n\n"
f"<b>Get full list: </b><code>{prefix}admlist</code>",
f'<b><u>Total:</u></b> {total_count}'
f'\n<b><u>Adminned chats:</u></b> {adminned_chats}\n'
f'<b><u>Owned chats:</u></b> {owned_chats}\n'
f'<b><u>Owned chats with username:</u></b> {owned_usernamed_chats}\n\n'
f'Done in {round(stop - start, 3)} seconds.\n\n'
f'<b>Get full list: </b><code>{prefix}admlist</code>',
)
except Exception as e:
await message.edit(format_exc(e))
return
modules_help["admlist"] = {
"admcount": "Get count of adminned and owned chats",
"admlist": "Get list of adminned and owned chats",
modules_help['admlist'] = {
'admcount': 'Get count of adminned and owned chats',
'admlist': 'Get list of adminned and owned chats',
}

Some files were not shown because too many files have changed in this diff Show More