latexgrambot: inline LaTeX bot for Telegram in Go

Renders LaTeX expressions into PNG pictures, PDF documents and native
Telegram rich text messages, entirely locally with TeX Live and poppler.

- Chat mode with /image, /pdf, /rich plus per-user preamble and DPI
- Inline mode delivering picture, PDF and rich text results
- Broad default package set preloaded into a TeX format at image build
- MongoDB persistence: settings, users, groups, render statistics
- Private S3-compatible bucket with short-lived presigned URLs
- OpenTelemetry traces over OTLP
- Kubernetes manifests and Argo CD application, Docker Compose, GitHub
  Actions CI, BSD 3-Clause license
This commit is contained in:
corrado.mulas
2026-10-04 03:38:08 +02:00
commit 98a3614d58
29 changed files with 5140 additions and 0 deletions

48
k8s/configmap.yaml Normal file
View File

@@ -0,0 +1,48 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: latexgrambot
namespace: latexgrambot
labels:
app.kubernetes.io/name: latexgrambot
data:
HEALTH_ADDR: ":8080"
# MongoDB is the only hard dependency: per-chat settings, users, group
# memberships, render statistics and the inline file-id cache live there.
MONGODB_URI: "mongodb://mongo:27017"
MONGODB_DATABASE: "latexgram"
# Private S3-compatible bucket for the inline PNG/PDF assets. Telegram
# receives short-lived presigned GET URLs, so the public endpoint must be
# reachable from the internet. Credentials come from the
# latexgrambot-s3 secret. Leave all S3 values empty to disable the inline
# picture/PDF results (rich text still works).
S3_ENDPOINT_URL: "http://minio:9000"
S3_REGION: "us-east-1"
S3_BUCKET: "latexgrambot"
S3_PREFIX: "content"
S3_PUBLIC_ENDPOINT_URL: "https://s3.example.com"
S3_PRESIGN_TTL: "1h"
# Preloaded TeX format shipped in the image: skips re-loading the default
# package set on every render.
LATEX_FORMAT: "latexgrambot"
RENDER_DPI: "600"
RENDER_TIMEOUT: "30s"
RENDER_MAX_CONCURRENCY: "2"
MAX_EXPRESSION_LEN: "4000"
MAX_PREAMBLE_LEN: "4000"
# Rich message replies (Bot API 10.x). Set to false for older Bot API
# servers; chat and inline fall back to PNG/PDF only.
RICH_TEXT_ENABLED: "true"
# A bare source without delimiters/text commands renders as math (true)
# or as text (false, formulas then need $...$).
RICH_DEFAULT_MATH: "true"
# OpenTelemetry traces (OTLP gRPC). Leave the endpoint empty to disable.
OTEL_EXPORTER_OTLP_ENDPOINT: ""
OTEL_EXPORTER_OTLP_INSECURE: "true"
OTEL_SERVICE_NAME: "latexgrambot"

70
k8s/deployment.yaml Normal file
View File

@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: latexgrambot
namespace: latexgrambot
labels:
app.kubernetes.io/name: latexgrambot
spec:
# Exactly one replica: the bot long-polls Telegram (getUpdates) and
# Telegram allows a single consumer per bot token.
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: latexgrambot
template:
metadata:
labels:
app.kubernetes.io/name: latexgrambot
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: latexgrambot
# Replace with the image built by CI, e.g. your registry plus the
# commit SHA tag.
image: latexgrambot:latest
imagePullPolicy: IfNotPresent
envFrom:
- configMapRef:
name: latexgrambot
- secretRef:
name: latexgrambot-secrets
- secretRef:
name: latexgrambot-s3
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 30
# pdflatex is CPU-bound; the memory limit has to cover a TeX Live
# run (a few hundred MB per concurrent render).
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "2"
memory: 1Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL

31
k8s/secret.example.yaml Normal file
View File

@@ -0,0 +1,31 @@
# Copy to a private file, fill in the values and apply it manually:
# kubectl apply -f latexgrambot-secrets.yaml
#
# Two secrets are used so the S3 credentials can be managed separately from
# the Telegram token.
---
apiVersion: v1
kind: Secret
metadata:
name: latexgrambot-secrets
namespace: latexgrambot
labels:
app.kubernetes.io/name: latexgrambot
type: Opaque
stringData:
# The token must not be long-polled by any other process (Telegram
# delivers updates to a single consumer per token).
TELEGRAM_BOT_TOKEN: "replace-with-bot-token"
---
apiVersion: v1
kind: Secret
metadata:
name: latexgrambot-s3
namespace: latexgrambot
labels:
app.kubernetes.io/name: latexgrambot
type: Opaque
stringData:
# Credentials of an S3-compatible bucket (read/write on S3_BUCKET).
AWS_ACCESS_KEY_ID: "replace-with-s3-access-key"
AWS_SECRET_ACCESS_KEY: "replace-with-s3-secret-key"

15
k8s/service.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: latexgrambot
namespace: latexgrambot
labels:
app.kubernetes.io/name: latexgrambot
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: latexgrambot
ports:
- name: http
port: 8080
targetPort: http