Skip to content

Commit 7cc50f5

Browse files
authored
Merge pull request #10 from FreshLabDev/go
Rewrite Quoto in Go; Apache-2.0
2 parents 53ebc34 + 5986096 commit 7cc50f5

261 files changed

Lines changed: 45967 additions & 24273 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.dockerignore‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
.env
2+
.env.*
3+
!.env.example
14
.git
2-
.pytest_cache
3-
.venv
5+
.github
6+
*.md
7+
docs
8+
deploy
49
logs
5-
.env
6-
.collaborator
7-
._*
8-
__pycache__
9-
*/__pycache__
10-
*.pyc
11-
*.code-workspace
10+
scratch
11+
tmp
12+
bin
13+
out

‎.env.example‎

Lines changed: 29 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,64 @@
11
# === BOT ===
22
BOT_TOKEN="123456:ABC-DEF"
3+
# Public username, with or without the @. Used for deep links and the
4+
# OpenRouter attribution header.
35
BOT_USERNAME="quototbot"
46
# Self-hosted Bot API server. Empty means api.telegram.org, which has no
57
# sendRichMessage (Bot API 10.3): the user agreement then renders as plain HTML
68
# instead of a natively formatted Markdown document. The bot says which one it
7-
# got at startup.
9+
# got at startup. When set, BOT_API_FILES_DIR is required: the server's data
10+
# directory as this container sees it.
811
TELEGRAM_BOT_API_BASE_URL=""
12+
BOT_API_FILES_DIR=""
913

1014
# === DATABASE ===
11-
# LOCAL DEV: docker-compose builds the app's DB_URL from these POSTGRES_* vars
12-
# and seeds a copy of the shared `core` schema (deploy/core-init.sql) into the
13-
# bundled Postgres so the FKs into core.person/core.chat resolve.
14-
POSTGRES_USER="quoto"
15-
POSTGRES_PASSWORD="change-me-to-a-strong-password"
16-
POSTGRES_DB="quoto"
17-
# Only needed when running the bot OUTSIDE docker (compose overrides this).
18-
# PRODUCTION points DB_URL at the shared core-postgres as the quoto_core role,
19-
# e.g. postgresql+asyncpg://quoto_core:***@core-postgres:5432/core
20-
# (quoto's tables live in schema `quoto`; identity/language in schema `core`).
21-
DB_URL="postgresql+asyncpg://quoto:change-me-to-a-strong-password@localhost:5432/quoto"
15+
# A postgres:// URL (the Python form postgresql+asyncpg:// is rejected). The
16+
# production compose file builds it from QUOTO_CORE_PASSWORD; the test stack
17+
# sets its own. search_path is set by the bot, not by the URL.
18+
DATABASE_URL="postgres://quoto_core:change-me@localhost:5432/core"
2219

2320
# === AI (Anthropic) ===
2421
# Claude scores the day and describes photos, images and stickers. Set at least
2522
# one of ANTHROPIC_API_KEY and OPENROUTER_API_KEY; with both, OpenRouter is the
26-
# fallback for Claude and the only route for video and audio. An empty model
27-
# skips Claude for that job.
23+
# fallback for Claude and the only route for video and audio. A model set to an
24+
# empty string skips Claude for that job.
2825
ANTHROPIC_API_KEY=""
2926
ANTHROPIC_EVAL_MODEL="claude-opus-5-5"
3027
# low | medium | high | xhigh | max: how much the model thinks before answering.
3128
ANTHROPIC_EVAL_EFFORT="medium"
32-
ANTHROPIC_EVAL_MAX_TOKENS=32000
3329
ANTHROPIC_MEDIA_MODEL="claude-sonnet-5-5"
3430
ANTHROPIC_MEDIA_EFFORT="medium"
35-
ANTHROPIC_MEDIA_MAX_TOKENS=8000
3631

3732
# === AI (OpenRouter) ===
3833
OPENROUTER_API_KEY="sk-or-v1-your-key-here"
3934
# Scores the day when Claude fails or has no key.
4035
OPENROUTER_EVAL_MODEL="openai/gpt-6-luna"
4136
# Used only if the primary eval model errors out after retries.
4237
OPENROUTER_EVAL_FALLBACK_MODEL="google/gemini-3.1-flash-lite"
38+
# none | minimal | low | medium | high | xhigh
4339
OPENROUTER_EVAL_REASONING_EFFORT="low"
44-
OPENROUTER_EVAL_MAX_TOKENS=32000
45-
OPENROUTER_MEDIA_MODEL="google/gemini-2.5-flash-lite"
46-
# Per-kind primary media models. Empty = use OPENROUTER_MEDIA_MODEL.
47-
# Chain per kind: specific -> OPENROUTER_MEDIA_MODEL -> FALLBACK below.
48-
OPENROUTER_MEDIA_IMAGE_MODEL=""
49-
OPENROUTER_MEDIA_VIDEO_MODEL=""
50-
OPENROUTER_MEDIA_AUDIO_MODEL=""
51-
# Used only if the primary media model errors out after retries.
40+
# Describes video and audio, and images when Claude is off or fails. Every model
41+
# must have a zero-data-retention endpoint on OpenRouter: requests carry
42+
# provider.zdr=true and data_collection=deny, which the user agreement promises.
43+
OPENROUTER_MEDIA_MODEL="google/gemini-3.5-flash-lite"
5244
OPENROUTER_MEDIA_FALLBACK_MODEL="google/gemini-3.1-flash-lite"
5345
OPENROUTER_MEDIA_REASONING_EFFORT="medium"
54-
# "deny" keeps chat text and media away from providers that train on it, and
55-
# ZDR narrows that to endpoints that keep nothing: together they are what the
56-
# user agreement promises. A model without such an endpoint (Meta's
57-
# "contributor" tier, for one) fails over to its fallback.
58-
OPENROUTER_DATA_COLLECTION="deny"
59-
OPENROUTER_ZDR=True
60-
OPENROUTER_HTTP_REFERER="https://t.me/quototbot"
61-
OPENROUTER_APP_TITLE="Quoto"
6246

6347
# === MEDIA ===
64-
MEDIA_ANALYSIS_ENABLED=True
65-
MEDIA_PHASH_DISTANCE=5
66-
MEDIA_CACHE_PROMPT_VERSION="v2"
67-
MEDIA_IMAGE_MAX_SIDE=1280
68-
MEDIA_IMAGE_QUALITY=82
69-
MEDIA_VIDEO_MAX_SECONDS=3600
70-
MEDIA_VIDEO_LOW_RES_MAX_SECONDS=10800
71-
MEDIA_VIDEO_MAX_HEIGHT=720
72-
MEDIA_VIDEO_CRF=30
73-
MEDIA_VIDEO_FPS=12
74-
MEDIA_AUDIO_BITRATE="64k"
75-
MEDIA_AUDIO_SAMPLE_RATE=24000
76-
MEDIA_COMMAND_TIMEOUT_SECONDS=300
77-
MEDIA_PENDING_RETRY_INTERVAL_SECONDS=300
78-
MEDIA_PENDING_RETRY_BATCH_SIZE=10
79-
MEDIA_RETRY_MAX_ATTEMPTS=5
80-
MEDIA_RETRY_BASE_DELAY_SECONDS=300
81-
MEDIA_RETRY_MAX_DELAY_SECONDS=3600
82-
MEDIA_PROVIDER_COOLDOWN_SECONDS=900
83-
MEDIA_PENDING_ITEM_TIMEOUT_SECONDS=600
48+
# Cost switch for the whole bot. A group can also turn it off in its settings.
49+
MEDIA_ANALYSIS_ENABLED=true
8450

85-
# === DEVELOPERS ===
86-
ENABLE_DEVELOPERS_NOTIFY=False
51+
# === ACCESS ===
52+
# Telegram ids that get alerts and the developer-only controls. A JSON list or
53+
# a comma list. Empty means no alerts.
8754
DEVELOPER_IDS=[123456789]
88-
89-
# === SCHEDULER ===
90-
QUOTE_HOUR=21
91-
QUOTE_MINUTE=0
92-
TIMEZONE="Europe/Kyiv"
93-
MIN_MESSAGES_FOR_AUTO_REVIEW=10
94-
95-
# === LIMITS ===
96-
# Max messages fed to the AI per group per day (cost safety). Groups above this
97-
# keep only the most recent N and get a "context trimmed" note on the quote.
98-
MAX_MESSAGES_PER_DAILY_EVAL=1500
99-
# Premium groups bypass the message cap entirely. JSON list of chat ids.
55+
# Groups exempt from the daily message cap. Same format.
10056
PREMIUM_CHAT_IDS=[]
10157

102-
# === SCORING ===
103-
# Final score is AI-only; reactions and length are sent/stored as context.
104-
WEIGHT_REACTIONS=0.0
105-
WEIGHT_AI=1.0
106-
WEIGHT_LENGTH=0.0
58+
# === RUNTIME ===
59+
# Holds ai_audit.jsonl (real message text, seven days). Compose sets it.
60+
# AUDIT_LOG_DIR="logs"
61+
# Where /healthz listens.
62+
# HTTP_ADDR=":8080"
63+
# debug | info | warn | error
64+
# LOG_LEVEL="info"

‎.github/dependabot.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# SPDX-License-Identifier: Apache-2.0
12
# Version updates open against dev, like every other change; they reach main
23
# with the next release. One grouped PR per ecosystem keeps the queue short.
34
version: 2
@@ -13,7 +14,7 @@ updates:
1314
groups:
1415
github_actions:
1516
patterns: ["*"]
16-
- package-ecosystem: pip
17+
- package-ecosystem: gomod
1718
directory: "/"
1819
target-branch: dev
1920
schedule:
@@ -22,7 +23,7 @@ updates:
2223
commit-message:
2324
prefix: deps
2425
groups:
25-
pip:
26+
gomod:
2627
patterns: ["*"]
2728
- package-ecosystem: docker
2829
directory: "/"

‎.github/workflows/ci.yml‎

Lines changed: 59 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -19,137 +19,93 @@ concurrency:
1919
group: ci-${{ github.ref }}
2020
cancel-in-progress: true
2121

22-
env:
23-
# tests/__init__.py sets these too, but alembic loads app.models -> app.config,
24-
# which validates settings at import time and exits if they are missing.
25-
BOT_TOKEN: "123456:CITOKEN1234567890"
26-
BOT_USERNAME: quoto_ci_bot
27-
2822
jobs:
2923
test:
30-
name: Test
24+
name: Build, vet, test
3125
runs-on: ubuntu-latest
3226
steps:
3327
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34-
35-
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
28+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
3629
with:
37-
python-version: "3.12"
38-
cache: pip
39-
cache-dependency-path: requirements.txt
40-
41-
- name: Install dependencies
30+
go-version-file: go.mod
31+
cache: true
32+
- name: No local replace in go.mod
33+
# A replace points at a checkout on one machine. It builds there and
34+
# nowhere else, the Docker build included.
4235
run: |
43-
python -m pip install --upgrade pip
44-
pip install -r requirements.txt
45-
46-
- name: Every direct dependency is pinned in requirements.txt
47-
# requirements.in is the intent, requirements.txt is what installs. A
48-
# package added to the first and never compiled into the second is a
49-
# dependency the container will not have.
36+
if grep -nE '^\s*replace\b' go.mod; then
37+
echo "go.mod has a replace directive; release the module it points at and require the tag"
38+
exit 1
39+
fi
40+
- name: Verify modules
5041
run: |
51-
missing=0
52-
while read -r pkg; do
53-
case "$pkg" in ''|'#'*) continue ;; esac
54-
name=$(printf '%s' "$pkg" | sed 's/[<>=!~;[].*//' | tr -d '[:space:]')
55-
[ -z "$name" ] && continue
56-
grep -qiE "^${name}(\[|==)" requirements.txt || {
57-
echo "missing from requirements.txt: ${name}"; missing=1; }
58-
done < requirements.in
59-
exit "$missing"
60-
61-
- name: Compile all sources
62-
run: python -m compileall -q app tests scripts main.py healthcheck.py
63-
42+
go mod verify
43+
go mod tidy
44+
git diff --exit-code -- go.mod go.sum
45+
- name: Format
46+
run: test -z "$(gofmt -l .)"
47+
- name: Build
48+
run: go build ./...
49+
- name: Vet
50+
run: go vet ./...
6451
- name: Test
65-
run: python -m pytest -q tests
52+
run: go test -race ./...
6653

67-
audit:
68-
name: Dependency advisories
54+
database:
55+
name: Database tests
6956
runs-on: ubuntu-latest
70-
# Known advisories are reported on every run but only fail the weekly one:
71-
# a new advisory against a pinned package is not a reason to hold a release
72-
# of unrelated code, and Dependabot opens the upgrade.
73-
continue-on-error: ${{ github.event_name != 'schedule' }}
74-
steps:
75-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
76-
77-
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
78-
with:
79-
python-version: "3.12"
80-
cache: pip
81-
cache-dependency-path: requirements.txt
82-
83-
- name: Audit requirements.txt
84-
run: |
85-
python -m pip install --upgrade pip pip-audit
86-
pip-audit -r requirements.txt --progress-spinner off
87-
88-
migrations:
89-
name: Migrations
90-
runs-on: ubuntu-latest
91-
env:
92-
DB_URL: postgresql+asyncpg://quoto:quoto@localhost:5432/quoto
93-
PGPASSWORD: quoto
9457
services:
9558
postgres:
9659
image: postgres:17-alpine
9760
env:
98-
POSTGRES_USER: quoto
99-
POSTGRES_PASSWORD: quoto
100-
POSTGRES_DB: quoto
101-
options: >-
102-
--health-cmd "pg_isready -U quoto"
103-
--health-interval 5s --health-timeout 5s --health-retries 10
61+
POSTGRES_USER: postgres
62+
POSTGRES_PASSWORD: postgres
63+
POSTGRES_DB: quoto_test
10464
ports: ["5432:5432"]
65+
options: >-
66+
--health-cmd "pg_isready -U postgres -d quoto_test"
67+
--health-interval 5s
68+
--health-timeout 5s
69+
--health-retries 20
10570
steps:
10671
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
107-
108-
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
72+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
10973
with:
110-
python-version: "3.12"
111-
cache: pip
112-
cache-dependency-path: requirements.txt
113-
114-
- name: Install dependencies
115-
run: |
116-
python -m pip install --upgrade pip
117-
pip install -r requirements.txt
118-
119-
- name: Reject a branched migration history
120-
# Two heads mean two migrations claim the same parent. Alembic then
121-
# refuses to upgrade on the host, after the deploy has already started.
122-
run: |
123-
heads=$(alembic heads | grep -c . || true)
124-
if [ "$heads" -ne 1 ]; then
125-
echo "expected exactly one alembic head, found ${heads}:"
126-
alembic heads
127-
exit 1
128-
fi
129-
130-
- name: Seed the shared core schema
131-
# Production applies these from the core repository. deploy/core-init.sql
132-
# is the verbatim copy quoto's foreign keys need to exist first.
133-
run: psql -h localhost -U quoto -d quoto -v ON_ERROR_STOP=1 -f deploy/core-init.sql
74+
go-version-file: go.mod
75+
cache: true
76+
- name: Run database tests
77+
# Every integration test drops and recreates schema quoto, so the
78+
# packages must not run at the same time.
79+
env:
80+
QUOTO_TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/quoto_test?sslmode=disable
81+
run: go test -tags=integration -race -p 1 ./...
13482

135-
- name: Upgrade, step back down, upgrade again
136-
# A migration that cannot be undone is a migration that cannot be rolled
137-
# back on the host either.
83+
govulncheck:
84+
name: Vulnerability scan
85+
runs-on: ubuntu-latest
86+
steps:
87+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
88+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
89+
with:
90+
go-version-file: go.mod
91+
cache: true
92+
- name: Run govulncheck
13893
run: |
139-
alembic upgrade head
140-
alembic downgrade -1
141-
alembic upgrade head
94+
go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
95+
"$(go env GOPATH)/bin/govulncheck" ./...
14296
14397
image:
14498
name: Docker build
14599
runs-on: ubuntu-latest
146100
steps:
147101
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
148-
149102
- name: Build image
150103
run: docker build -t quoto:ci .
151-
152-
- name: Validate compose
104+
- name: Validate Compose
153105
run: |
154106
cp .env.example .env
155-
docker compose config >/dev/null
107+
cp .env.example deploy/ws04/.env
108+
QUOTO_IMAGE=ghcr.io/freshlabdev/quoto:test \
109+
QUOTO_CORE_PASSWORD=ci-validation-only \
110+
docker compose -f deploy/ws04/compose.yaml config >/dev/null
111+
docker compose -f deploy/ws04/compose.test.yaml config >/dev/null

0 commit comments

Comments
 (0)