Initial codex telegram bot source

This commit is contained in:
Codex
2026-05-21 08:40:16 +00:00
commit ad61f7eeed
275 changed files with 101972 additions and 0 deletions

516
internal/store/store.go Normal file
View File

@@ -0,0 +1,516 @@
package store
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
_ "modernc.org/sqlite"
)
type Store struct {
db *sql.DB
}
type AllowedUser struct {
TelegramUserID int64
Username string
Notes string
AddedAt string
}
type Workspace struct {
ID int64
Path string
Label string
IsDefault bool
CreatedAt string
}
type Session struct {
TelegramUserID int64
ActiveThreadID int64
ActiveWorkspaceID int64
Model string
ReasoningEffort string
Sandbox string
ActiveTurnID string
SettingsChatID int64
SettingsMessageID int
UpdatedAt string
}
type Thread struct {
ID int64
TelegramUserID int64
CodexThreadID string
WorkspaceID int64
Title string
Archived bool
CreatedAt string
UpdatedAt string
}
type PendingApproval struct {
ID int64
TelegramUserID int64
CodexRequestID string
CodexThreadID string
TurnID string
ItemID string
Kind string
PayloadJSON string
MessageChatID int64
MessageID int
Status string
CreatedAt string
ResolvedAt string
}
func Open(ctx context.Context, path string) (*Store, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, err
}
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
s := &Store{db: db}
if err := s.configure(ctx); err != nil {
_ = db.Close()
return nil, err
}
if err := s.migrate(ctx); err != nil {
_ = db.Close()
return nil, err
}
return s, nil
}
func (s *Store) Close() error {
return s.db.Close()
}
func (s *Store) configure(ctx context.Context) error {
statements := []string{
"PRAGMA foreign_keys = ON",
"PRAGMA journal_mode = WAL",
"PRAGMA busy_timeout = 5000",
}
for _, statement := range statements {
if _, err := s.db.ExecContext(ctx, statement); err != nil {
return err
}
}
return nil
}
func (s *Store) migrate(ctx context.Context) error {
if _, err := s.db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
)`); err != nil {
return err
}
var current int
if err := s.db.QueryRowContext(ctx, "SELECT COALESCE(MAX(version), 0) FROM schema_migrations").Scan(&current); err != nil {
return err
}
for i := current; i < len(migrations); i++ {
version := i + 1
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, migrations[i].SQL); err != nil {
_ = tx.Rollback()
return fmt.Errorf("migration %d %s: %w", version, migrations[i].Name, err)
}
if _, err := tx.ExecContext(ctx, "INSERT INTO schema_migrations (version, name) VALUES (?, ?)", version, migrations[i].Name); err != nil {
_ = tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
return err
}
}
return nil
}
func ValidateWorkspacePath(path string) (string, error) {
if path == "" {
return "", errors.New("workspace path is required")
}
if strings.ContainsRune(path, 0) {
return "", errors.New("workspace path contains a NUL byte")
}
if !filepath.IsAbs(path) {
return "", errors.New("workspace path must be absolute")
}
clean := filepath.Clean(path)
if clean == string(filepath.Separator) {
return "", errors.New("workspace path cannot be filesystem root")
}
return clean, nil
}
func (s *Store) IsAllowed(ctx context.Context, telegramUserID int64) (bool, error) {
var exists int
err := s.db.QueryRowContext(ctx, "SELECT 1 FROM allowed_users WHERE telegram_user_id = ?", telegramUserID).Scan(&exists)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return err == nil, err
}
func (s *Store) AddAllowedUser(ctx context.Context, telegramUserID int64, username, notes string) error {
_, err := s.db.ExecContext(ctx, `
INSERT INTO allowed_users (telegram_user_id, username, notes)
VALUES (?, ?, ?)
ON CONFLICT(telegram_user_id) DO UPDATE SET username = excluded.username, notes = excluded.notes`,
telegramUserID, username, notes)
return err
}
func (s *Store) RemoveAllowedUser(ctx context.Context, telegramUserID int64) error {
_, err := s.db.ExecContext(ctx, "DELETE FROM allowed_users WHERE telegram_user_id = ?", telegramUserID)
return err
}
func (s *Store) ListAllowedUsers(ctx context.Context) ([]AllowedUser, error) {
rows, err := s.db.QueryContext(ctx, "SELECT telegram_user_id, username, notes, added_at FROM allowed_users ORDER BY telegram_user_id")
if err != nil {
return nil, err
}
defer rows.Close()
var users []AllowedUser
for rows.Next() {
var user AllowedUser
if err := rows.Scan(&user.TelegramUserID, &user.Username, &user.Notes, &user.AddedAt); err != nil {
return nil, err
}
users = append(users, user)
}
return users, rows.Err()
}
func (s *Store) AddWorkspace(ctx context.Context, path, label string, isDefault bool) (Workspace, error) {
clean, err := ValidateWorkspacePath(path)
if err != nil {
return Workspace{}, err
}
if label == "" {
label = filepath.Base(clean)
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return Workspace{}, err
}
if isDefault {
if _, err := tx.ExecContext(ctx, "UPDATE workspaces SET is_default = 0"); err != nil {
_ = tx.Rollback()
return Workspace{}, err
}
}
result, err := tx.ExecContext(ctx, `
INSERT INTO workspaces (path, label, is_default)
VALUES (?, ?, ?)
ON CONFLICT(path) DO UPDATE SET label = excluded.label, is_default = excluded.is_default`,
clean, label, boolInt(isDefault))
if err != nil {
_ = tx.Rollback()
return Workspace{}, err
}
if err := tx.Commit(); err != nil {
return Workspace{}, err
}
id, _ := result.LastInsertId()
if id == 0 {
return s.GetWorkspaceByPath(ctx, clean)
}
return s.GetWorkspaceByPath(ctx, clean)
}
func (s *Store) ListWorkspaces(ctx context.Context) ([]Workspace, error) {
rows, err := s.db.QueryContext(ctx, "SELECT id, path, label, is_default, created_at FROM workspaces ORDER BY is_default DESC, label, path")
if err != nil {
return nil, err
}
defer rows.Close()
var workspaces []Workspace
for rows.Next() {
workspace, err := scanWorkspace(rows)
if err != nil {
return nil, err
}
workspaces = append(workspaces, workspace)
}
return workspaces, rows.Err()
}
func (s *Store) GetWorkspaceByID(ctx context.Context, id int64) (Workspace, error) {
row := s.db.QueryRowContext(ctx, "SELECT id, path, label, is_default, created_at FROM workspaces WHERE id = ?", id)
return scanWorkspace(row)
}
func (s *Store) GetWorkspaceByPath(ctx context.Context, path string) (Workspace, error) {
row := s.db.QueryRowContext(ctx, "SELECT id, path, label, is_default, created_at FROM workspaces WHERE path = ?", path)
return scanWorkspace(row)
}
func (s *Store) DefaultWorkspace(ctx context.Context) (Workspace, error) {
row := s.db.QueryRowContext(ctx, "SELECT id, path, label, is_default, created_at FROM workspaces ORDER BY is_default DESC, id ASC LIMIT 1")
return scanWorkspace(row)
}
func (s *Store) GetOrCreateSession(ctx context.Context, telegramUserID int64, defaultModel, defaultSandbox string) (Session, error) {
if defaultSandbox == "" {
defaultSandbox = "workspace-write"
}
_, err := s.db.ExecContext(ctx, `
INSERT INTO sessions (telegram_user_id, model, sandbox)
VALUES (?, ?, ?)
ON CONFLICT(telegram_user_id) DO NOTHING`, telegramUserID, defaultModel, defaultSandbox)
if err != nil {
return Session{}, err
}
return s.GetSession(ctx, telegramUserID)
}
func (s *Store) GetSession(ctx context.Context, telegramUserID int64) (Session, error) {
row := s.db.QueryRowContext(ctx, `
SELECT telegram_user_id, COALESCE(active_thread_id, 0), COALESCE(active_workspace_id, 0), model, COALESCE(reasoning_effort, ''), sandbox, active_turn_id, COALESCE(settings_chat_id, 0), COALESCE(settings_message_id, 0), updated_at
FROM sessions WHERE telegram_user_id = ?`, telegramUserID)
var session Session
err := row.Scan(&session.TelegramUserID, &session.ActiveThreadID, &session.ActiveWorkspaceID, &session.Model, &session.ReasoningEffort, &session.Sandbox, &session.ActiveTurnID, &session.SettingsChatID, &session.SettingsMessageID, &session.UpdatedAt)
return session, err
}
func (s *Store) SetSessionWorkspace(ctx context.Context, telegramUserID, workspaceID int64) error {
_, err := s.db.ExecContext(ctx, `
UPDATE sessions SET active_workspace_id = ?, updated_at = datetime('now')
WHERE telegram_user_id = ?`, workspaceID, telegramUserID)
return err
}
func (s *Store) SetSessionModel(ctx context.Context, telegramUserID int64, model string) error {
_, err := s.db.ExecContext(ctx, "UPDATE sessions SET model = ?, reasoning_effort = '', updated_at = datetime('now') WHERE telegram_user_id = ?", model, telegramUserID)
return err
}
func (s *Store) SetSessionReasoningEffort(ctx context.Context, telegramUserID int64, effort string) error {
_, err := s.db.ExecContext(ctx, "UPDATE sessions SET reasoning_effort = ?, updated_at = datetime('now') WHERE telegram_user_id = ?", effort, telegramUserID)
return err
}
func (s *Store) SetSessionSettingsMessage(ctx context.Context, telegramUserID int64, chatID int64, messageID int) error {
_, err := s.db.ExecContext(ctx, "UPDATE sessions SET settings_chat_id = ?, settings_message_id = ?, updated_at = datetime('now') WHERE telegram_user_id = ?", chatID, messageID, telegramUserID)
return err
}
func (s *Store) SetSessionSandbox(ctx context.Context, telegramUserID int64, sandbox string) error {
_, err := s.db.ExecContext(ctx, "UPDATE sessions SET sandbox = ?, updated_at = datetime('now') WHERE telegram_user_id = ?", sandbox, telegramUserID)
return err
}
func (s *Store) SetActiveThread(ctx context.Context, telegramUserID, threadID int64) error {
_, err := s.db.ExecContext(ctx, `
UPDATE sessions SET active_thread_id = ?, active_turn_id = '', updated_at = datetime('now')
WHERE telegram_user_id = ?`, threadID, telegramUserID)
return err
}
func (s *Store) SetActiveTurn(ctx context.Context, telegramUserID int64, turnID string) error {
_, err := s.db.ExecContext(ctx, "UPDATE sessions SET active_turn_id = ?, updated_at = datetime('now') WHERE telegram_user_id = ?", turnID, telegramUserID)
return err
}
func (s *Store) ClearActiveTurns(ctx context.Context) error {
_, err := s.db.ExecContext(ctx, "UPDATE sessions SET active_turn_id = '', updated_at = datetime('now') WHERE active_turn_id <> ''")
return err
}
func (s *Store) CreateThread(ctx context.Context, telegramUserID int64, codexThreadID string, workspaceID int64, title string) (Thread, error) {
result, err := s.db.ExecContext(ctx, `
INSERT INTO threads (telegram_user_id, codex_thread_id, workspace_id, title)
VALUES (?, ?, ?, ?)`, telegramUserID, codexThreadID, workspaceID, title)
if err != nil {
return Thread{}, err
}
id, err := result.LastInsertId()
if err != nil {
return Thread{}, err
}
return s.GetThreadByID(ctx, telegramUserID, id)
}
func (s *Store) GetThreadByID(ctx context.Context, telegramUserID, id int64) (Thread, error) {
row := s.db.QueryRowContext(ctx, `
SELECT id, telegram_user_id, codex_thread_id, workspace_id, title, archived, created_at, updated_at
FROM threads WHERE telegram_user_id = ? AND id = ?`, telegramUserID, id)
return scanThread(row)
}
func (s *Store) GetThreadByCodexID(ctx context.Context, codexThreadID string) (Thread, error) {
row := s.db.QueryRowContext(ctx, `
SELECT id, telegram_user_id, codex_thread_id, workspace_id, title, archived, created_at, updated_at
FROM threads WHERE codex_thread_id = ?`, codexThreadID)
return scanThread(row)
}
func (s *Store) ListThreads(ctx context.Context, telegramUserID int64, includeArchived bool) ([]Thread, error) {
return s.ListThreadsPage(ctx, telegramUserID, includeArchived, 20, 0)
}
func (s *Store) ListThreadsPage(ctx context.Context, telegramUserID int64, includeArchived bool, limit, offset int) ([]Thread, error) {
if limit <= 0 {
limit = 20
}
if offset < 0 {
offset = 0
}
query := `
SELECT id, telegram_user_id, codex_thread_id, workspace_id, title, archived, created_at, updated_at
FROM threads WHERE telegram_user_id = ?`
args := []any{telegramUserID}
if !includeArchived {
query += " AND archived = 0"
}
query += " ORDER BY updated_at DESC, id DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var threads []Thread
for rows.Next() {
thread, err := scanThread(rows)
if err != nil {
return nil, err
}
threads = append(threads, thread)
}
return threads, rows.Err()
}
func (s *Store) ArchiveThread(ctx context.Context, telegramUserID, id int64) error {
_, err := s.db.ExecContext(ctx, `
UPDATE threads SET archived = 1, updated_at = datetime('now')
WHERE telegram_user_id = ? AND id = ?`, telegramUserID, id)
return err
}
func (s *Store) TouchThread(ctx context.Context, codexThreadID string) error {
_, err := s.db.ExecContext(ctx, "UPDATE threads SET updated_at = datetime('now') WHERE codex_thread_id = ?", codexThreadID)
return err
}
func (s *Store) UpsertPendingApproval(ctx context.Context, approval PendingApproval) (PendingApproval, error) {
_, err := s.db.ExecContext(ctx, `
INSERT INTO pending_approvals (
telegram_user_id, codex_request_id, codex_thread_id, turn_id, item_id, kind, payload_json,
message_chat_id, message_id, status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
ON CONFLICT(telegram_user_id, codex_request_id) DO UPDATE SET
payload_json = excluded.payload_json,
message_chat_id = excluded.message_chat_id,
message_id = excluded.message_id,
status = 'pending',
resolved_at = ''`,
approval.TelegramUserID, approval.CodexRequestID, approval.CodexThreadID, approval.TurnID,
approval.ItemID, approval.Kind, approval.PayloadJSON, approval.MessageChatID, approval.MessageID)
if err != nil {
return PendingApproval{}, err
}
return s.GetPendingApprovalByRequest(ctx, approval.TelegramUserID, approval.CodexRequestID)
}
func (s *Store) UpdatePendingApprovalMessage(ctx context.Context, id int64, chatID int64, messageID int) error {
_, err := s.db.ExecContext(ctx, "UPDATE pending_approvals SET message_chat_id = ?, message_id = ? WHERE id = ?", chatID, messageID, id)
return err
}
func (s *Store) GetPendingApproval(ctx context.Context, telegramUserID, id int64) (PendingApproval, error) {
row := s.db.QueryRowContext(ctx, `
SELECT id, telegram_user_id, codex_request_id, codex_thread_id, turn_id, item_id, kind, payload_json,
message_chat_id, message_id, status, created_at, COALESCE(resolved_at, '')
FROM pending_approvals WHERE telegram_user_id = ? AND id = ?`, telegramUserID, id)
return scanPendingApproval(row)
}
func (s *Store) GetPendingApprovalByRequest(ctx context.Context, telegramUserID int64, requestID string) (PendingApproval, error) {
row := s.db.QueryRowContext(ctx, `
SELECT id, telegram_user_id, codex_request_id, codex_thread_id, turn_id, item_id, kind, payload_json,
message_chat_id, message_id, status, created_at, COALESCE(resolved_at, '')
FROM pending_approvals WHERE telegram_user_id = ? AND codex_request_id = ?`, telegramUserID, requestID)
return scanPendingApproval(row)
}
func (s *Store) ResolvePendingApproval(ctx context.Context, telegramUserID, id int64, status string) error {
_, err := s.db.ExecContext(ctx, `
UPDATE pending_approvals SET status = ?, resolved_at = datetime('now')
WHERE telegram_user_id = ? AND id = ? AND status = 'pending'`, status, telegramUserID, id)
return err
}
func (s *Store) Audit(ctx context.Context, telegramUserID int64, action, details string) error {
_, err := s.db.ExecContext(ctx, "INSERT INTO audit_log (telegram_user_id, action, details) VALUES (?, ?, ?)", telegramUserID, action, details)
return err
}
type scanner interface {
Scan(dest ...any) error
}
func scanWorkspace(row scanner) (Workspace, error) {
var workspace Workspace
var isDefault int
if err := row.Scan(&workspace.ID, &workspace.Path, &workspace.Label, &isDefault, &workspace.CreatedAt); err != nil {
return Workspace{}, err
}
workspace.IsDefault = isDefault != 0
return workspace, nil
}
func scanThread(row scanner) (Thread, error) {
var thread Thread
var archived int
if err := row.Scan(&thread.ID, &thread.TelegramUserID, &thread.CodexThreadID, &thread.WorkspaceID, &thread.Title, &archived, &thread.CreatedAt, &thread.UpdatedAt); err != nil {
return Thread{}, err
}
thread.Archived = archived != 0
return thread, nil
}
func scanPendingApproval(row scanner) (PendingApproval, error) {
var approval PendingApproval
if err := row.Scan(&approval.ID, &approval.TelegramUserID, &approval.CodexRequestID, &approval.CodexThreadID,
&approval.TurnID, &approval.ItemID, &approval.Kind, &approval.PayloadJSON, &approval.MessageChatID,
&approval.MessageID, &approval.Status, &approval.CreatedAt, &approval.ResolvedAt); err != nil {
return PendingApproval{}, err
}
return approval, nil
}
func boolInt(value bool) int {
if value {
return 1
}
return 0
}