From 4146e35f35293d7dcf204caf066df2cc6327131b Mon Sep 17 00:00:00 2001 From: db123 Date: Tue, 23 Jun 2026 20:22:46 +0330 Subject: [PATCH] Initial scaffold: Go module, SQLite schema, config loader, Telegram bot with webhook/polling modes --- .gitignore | 5 ++ cmd/bot/main.go | 73 ++++++++++++++++++ cmd/bot/webhook.go | 61 +++++++++++++++ db/schema.sql | 26 +++++++ go.mod | 30 ++++++++ go.sum | 83 +++++++++++++++++++++ internal/config/loader.go | 24 ++++++ internal/db/store.go | 152 ++++++++++++++++++++++++++++++++++++++ opencode.json | 146 ++++++++++++++++++++++++++++++++++++ 9 files changed, 600 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/bot/main.go create mode 100644 cmd/bot/webhook.go create mode 100644 db/schema.sql create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/loader.go create mode 100644 internal/db/store.go create mode 100644 opencode.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..63c8fa5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.build/ +*.exe +db.sqlite3 +.env +.DS_Store diff --git a/cmd/bot/main.go b/cmd/bot/main.go new file mode 100644 index 0000000..aac747c --- /dev/null +++ b/cmd/bot/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "log" + "net/http" + "net/url" + "os" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/joho/godotenv" + + "worktimeBot/internal/bot" + "worktimeBot/internal/db" + "worktimeBot/pkg/i18n" +) + +func main() { + // Load .env file if it exists (ignored when running inside Docker with env vars) + _ = godotenv.Load() + + token := os.Getenv("BOT_TOKEN") + proxyURL := os.Getenv("HTTP_PROXY") + dbPath := getEnvDefault("DB_PATH", "db.sqlite3") + + // Build HTTP client with optional proxy + var httpClient *http.Client + if proxyURL != "" { + parsed, err := url.Parse(proxyURL) + if err != nil { + log.Fatal(err) + } + tr := &http.Transport{Proxy: http.ProxyURL(parsed)} + httpClient = &http.Client{Transport: tr} + } else { + httpClient = http.DefaultClient + } + + botAPI, err := tgbotapi.NewBotAPIWithClient(token, "", httpClient) + if err != nil { + log.Fatal(err) + } + + dbStore, err := db.NewStore(dbPath) + if err != nil { + log.Fatal(err) + } + + translator, _ := i18n.NewTranslator("en") + + handler := bot.NewHandler(botAPI, dbStore, translator) + + webhookURL := os.Getenv("BOT_WEBHOOK_URL") + if webhookURL != "" { + startWebhook(botAPI, handler, webhookURL) + } else { + // Fallback to long‑polling + u := tgbotapi.NewUpdate(0) + u.Timeout = 30 + updates := botAPI.GetUpdatesChan(u) + for update := range updates { + if update.Message != nil { + handler.HandleMessage(update) + } + } + } +} + +func getEnvDefault(key, defaultValue string) string { + if val := os.Getenv(key); val != "" { + return val + } + return defaultValue +} diff --git a/cmd/bot/webhook.go b/cmd/bot/webhook.go new file mode 100644 index 0000000..2e0a2b4 --- /dev/null +++ b/cmd/bot/webhook.go @@ -0,0 +1,61 @@ +package main + +import ( + "log" + "net/http" + "os" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + + "worktimeBot/internal/bot" +) + +// startWebhook configures the Telegram webhook and starts the HTTP(S) listener. +func startWebhook(botAPI *tgbotapi.BotAPI, handler *bot.Handler, webhookURL string) { + // Delete any previously set webhook + if _, err := botAPI.Request(tgbotapi.DeleteWebhookConfig{}); err != nil { + log.Fatal("DeleteWebhook:", err) + } + + // Set the new webhook + wh, err := tgbotapi.NewWebhook(webhookURL) + if err != nil { + log.Fatal("NewWebhook:", err) + } + if _, err := botAPI.Request(wh); err != nil { + log.Fatal("SetWebhook:", err) + } + + // Register the webhook endpoint and get the updates channel + updates := botAPI.ListenForWebhook("/webhook") + + // Determine where to listen + listenAddr := getEnvDefault("BOT_LISTEN", ":8080") + tlsCert := os.Getenv("BOT_TLS_CERT") + tlsKey := os.Getenv("BOT_TLS_KEY") + + if tlsCert != "" && tlsKey != "" { + log.Printf("Starting HTTPS webhook on %s", listenAddr) + go func() { + if err := http.ListenAndServeTLS(listenAddr, tlsCert, tlsKey, nil); err != nil { + log.Fatal("HTTPS server:", err) + } + }() + } else { + log.Printf("Starting HTTP webhook on %s (TLS handled by reverse proxy)", listenAddr) + go func() { + if err := http.ListenAndServe(listenAddr, nil); err != nil { + log.Fatal("HTTP server:", err) + } + }() + } + + log.Printf("Webhook registered at %s", webhookURL) + + // Process incoming updates + for update := range updates { + if update.Message != nil { + handler.HandleMessage(update) + } + } +} diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..93884c5 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,26 @@ +CREATE TABLE events ( + id INTEGER PRIMARY KEY, + event_type TEXT NOT NULL + CHECK (event_type IN ('in', 'out')), + occurred_at INTEGER NOT NULL UNIQUE + CHECK (occurred_at > 0), + note TEXT NOT NULL DEFAULT '', + created_at INTEGER NOT NULL + DEFAULT (unixepoch()) +); + +CREATE INDEX idx_events_occurred_at +ON events(occurred_at); + +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL DEFAULT '' +); + +CREATE TABLE days_off ( + date TEXT PRIMARY KEY + CHECK (date = strftime('%Y-%m-%d', date)), + reason TEXT NOT NULL DEFAULT '', + created_at INTEGER NOT NULL + DEFAULT (unixepoch()) +); diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f76f3bf --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module worktimeBot + +go 1.26.4 + +require ( + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + github.com/joho/godotenv v1.5.1 + github.com/xuri/excelize/v2 v2.10.1 + modernc.org/sqlite v1.53.0 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/richardlehane/mscfb v1.0.6 // indirect + github.com/richardlehane/msoleps v1.0.6 // indirect + github.com/tiendc/go-deepcopy v1.7.2 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.34.0 // indirect + modernc.org/libc v1.73.4 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a57f63d --- /dev/null +++ b/go.sum @@ -0,0 +1,83 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= +github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= +github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= +github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= +github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= +github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= +modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= +modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= +modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/config/loader.go b/internal/config/loader.go new file mode 100644 index 0000000..10f9429 --- /dev/null +++ b/internal/config/loader.go @@ -0,0 +1,24 @@ +package config + +import ( + "encoding/json" + "os" +) + +type Config struct { + BotToken string `json:"bot_token"` + ProxyAddr string `json:"proxy_address"` + LogLevel string `json:"log_level"` +} + +func Load(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/internal/db/store.go b/internal/db/store.go new file mode 100644 index 0000000..738100b --- /dev/null +++ b/internal/db/store.go @@ -0,0 +1,152 @@ +package db + +import ( + "database/sql" + "os" + "path/filepath" + + _ "modernc.org/sqlite" +) + +const driverName = "sqlite" + +var schemaPath = filepath.Join("db", "schema.sql") + +// Store wraps the sql.DB connection. +type Store struct { + db *sql.DB +} + +// NewStore opens the SQLite DB and runs the schema file. +func NewStore(path string) (*Store, error) { + db, err := sql.Open(driverName, path) + if err != nil { + return nil, err + } + if err := db.Ping(); err != nil { + return nil, err + } + if err := execSchema(db, schemaPath); err != nil { + return nil, err + } + return &Store{db: db}, nil +} + +func execSchema(db *sql.DB, path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + _, err = db.Exec(string(data)) + return err +} + +// CreateEvent inserts a new event. +func (s *Store) CreateEvent(eventType string, occurredAt int64, note string) error { + query := ` + INSERT INTO events (event_type, occurred_at, note) + VALUES (?, ?, ?) + ` + _, err := s.db.Exec(query, eventType, occurredAt, note) + return err +} + +// GetLastEvent returns the most recent event. +func (s *Store) GetLastEvent() (*Event, error) { + row, err := s.db.Query(` + SELECT id, event_type, occurred_at, note, created_at + FROM events + ORDER BY occurred_at DESC + LIMIT 1 + `) + if err != nil { + return nil, err + } + defer row.Close() + if !row.Next() { + return nil, sql.ErrNoRows + } + var e Event + if err := row.Scan(&e.ID, &e.EventType, &e.OccurredAt, &e.Note, &e.CreatedAt); err != nil { + return nil, err + } + return &e, nil +} + +// GetSetting returns a setting value by key. +func (s *Store) GetSetting(key string) (string, error) { + var value string + err := s.db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value) + if err != nil { + return "", err + } + return value, nil +} + +// SetSetting stores or updates a setting. +func (s *Store) SetSetting(key, value string) error { + query := ` + INSERT INTO settings (key, value) VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value=excluded.value + ` + _, err := s.db.Exec(query, key, value) + return err +} + +// SetDayOff adds or updates a day‑off record. +func (s *Store) SetDayOff(date, reason string) error { + query := ` + INSERT INTO days_off (date, reason) VALUES (?, ?) + ON CONFLICT(date) DO UPDATE SET reason=excluded.reason + ` + _, err := s.db.Exec(query, date, reason) + return err +} + +// RemoveDayOff deletes a day‑off record. +func (s *Store) RemoveDayOff(date string) error { + _, err := s.db.Exec(`DELETE FROM days_off WHERE date = ?`, date) + return err +} + +// IsDayOff returns true if the given date (YYYY-MM-DD) is marked as day off. +func (s *Store) IsDayOff(date string) (bool, error) { + var exists int + err := s.db.QueryRow(`SELECT COUNT(1) FROM days_off WHERE date = ?`, date).Scan(&exists) + if err != nil { + return false, err + } + return exists > 0, nil +} + +// EventsForDay returns all events for a given day (ISO date string). +func (s *Store) EventsForDay(day string) ([]Event, error) { + rows, err := s.db.Query(` + SELECT id, event_type, occurred_at, note, created_at + FROM events + WHERE strftime('%Y-%m-%d', occurred_at, 'unixepoch') = ? + ORDER BY occurred_at ASC + `, day) + if err != nil { + return nil, err + } + defer rows.Close() + var events []Event + for rows.Next() { + var e Event + if err := rows.Scan(&e.ID, &e.EventType, &e.OccurredAt, &e.Note, &e.CreatedAt); err != nil { + return nil, err + } + events = append(events, e) + } + return events, rows.Err() +} + +// Event represents a row from the events table. +type Event struct { + ID int64 `json:"id"` + EventType string `json:"event_type"` + OccurredAt int64 `json:"occurred_at"` + Note string `json:"note"` + CreatedAt int64 `json:"created_at"` +} diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..1c86e7b --- /dev/null +++ b/opencode.json @@ -0,0 +1,146 @@ +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "llama.cpp": { + "npm": "@ai-sdk/openai-compatible", + "name": "llama-server (local)", + "options": { + "baseURL": "http://127.0.0.1:8082/v1" + }, + "models": { + "coder": { + "name": "coder", + "limit": { + "context": 32000, + "output": 65536 + } + } + } + } + }, + "lsp": true, + "formatter": { + "gofmt": { + "command": ["gofmt", "-w", "$FILE"], + "extensions": [".go"] + }, + "prettier": { + "command": ["prettier", "--write", "$FILE"], + "extensions": [ + ".ts", + ".tsx", + ".js", + ".jsx", + ".mjs", + ".cjs", + ".vue", + ".html", + ".htm", + ".css", + ".scss", + ".less", + ".sass", + ".json", + ".jsonc", + ".yaml", + ".yml", + ".md", + ".mdx" + ] + }, + "php-cs-fixer": { + "command": ["php-cs-fixer", "fix", "$FILE"], + "extensions": [".php"] + } + }, + "mcp": { + "playwright": { + "type": "local", + "command": ["npx", "-y", "@playwright/mcp@latest"], + "enabled": false + }, + "postgres": { + "type": "local", + "command": ["uvx", "postgres-mcp"], + "enabled": false, + "environment": { + "DATABASE_URI": "postgresql://user:pass@localhost:5432/mydb" + } + }, + "mariadb": { + "type": "local", + "command": ["npx", "-y", "@oleander/mcp-server-mariadb"], + "enabled": false, + "environment": { + "MARIADB_HOST": "127.0.0.1", + "MARIADB_PORT": "3306", + "MARIADB_USER": "root", + "MARIADB_PASS": "", + "MARIADB_DB": "mydb" + } + }, + "redis": { + "type": "local", + "command": ["uvx", "redis-mcp-server"], + "enabled": false, + "environment": { + "REDIS_URL": "redis://localhost:6379" + } + }, + "sqlite": { + "type": "local", + "command": ["npx", "-y", "mcp-sqlite", "./data/dev.db"], + "enabled": false + }, + "docker": { + "type": "local", + "command": ["uvx", "mcp-server-docker"], + "enabled": false + }, + "gitea": { + "type": "local", + "command": ["uvx", "gitea-mcp"], + "enabled": false, + "environment": { + "GITEA_URL": "https://git.db1232.ir", + "GITEA_TOKEN": "your-personal-access-token" + } + }, + "matrix": { + "type": "local", + "command": ["npx", "-y", "@iflow-mcp/mjknowles-matrix-mcp-server"], + "enabled": false, + "environment": { + "MATRIX_HOMESERVER_URL": "https://tempchat.db123.ir", + "MATRIX_ACCESS_TOKEN": "your-access-token" + } + }, + "fallow": { + "type": "local", + "command": ["npx", "-y", "--package", "fallow", "fallow-mcp"], + "enabled": false + } + }, + "permission": { + "read": "allow", + "glob": "allow", + "grep": "allow", + "list": "allow", + "lsp": "allow", + "webfetch": "allow", + "websearch": "allow", + "question": "allow", + "todowrite": "allow", + "edit": "ask", + "write": "ask", + "task": "ask", + "external_directory": "ask", + "skill": "ask", + "doom_loop": "deny", + "bash": { + "*": "ask", + "git push*": "deny", + "rm -rf*": "deny" + } + } +}