diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5158acf..c2f3da6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,8 +6,11 @@ "Bash(rm:*)", "Bash(make:*)", "Bash(timeout 5s make run)", - "Bash(deadcode:*)" + "Bash(deadcode:*)", + "Bash(sqlc generate)", + "Bash(pg_isready -h localhost -p 5432)", + "Bash(psql -h localhost -p 5432 -U postgres -c \"SELECT 1\")" ], "deny": [] } -} \ No newline at end of file +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 4bb8b86..dd63e39 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -30,7 +30,9 @@ import ( "github.com/eternisai/enchanted-proxy/internal/request_tracking" "github.com/eternisai/enchanted-proxy/internal/search" "github.com/eternisai/enchanted-proxy/internal/storage/pg" + "github.com/eternisai/enchanted-proxy/internal/tasks" "github.com/eternisai/enchanted-proxy/internal/telegram" + "github.com/eternisai/enchanted-proxy/internal/temporal" "github.com/gin-gonic/gin" "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" @@ -87,6 +89,19 @@ func main() { } log.Info("database connection established") + // Initialize Temporal client + log.Info("initializing temporal client", + slog.String("address", config.AppConfig.TemporalAddress), + slog.String("namespace", config.AppConfig.TemporalNamespace), + slog.String("task_queue", config.AppConfig.TemporalTaskQueue), + ) + temporalClient, err := temporal.NewTemporalClientFromConfig(config.AppConfig) + if err != nil { + log.Error("failed to initialize temporal client", slog.String("error", err.Error())) + os.Exit(1) + } + log.Info("temporal client initialized") + tokenValidator, err := NewTokenValidator(config.AppConfig, logger) if err != nil { log.Error("failed to initialize token validator", slog.String("error", err.Error())) @@ -139,6 +154,7 @@ func main() { iapHandler := iap.NewHandler(iapService, logger.WithComponent("iap")) mcpHandler := mcp.NewHandler(mcpService) searchHandler := search.NewHandler(searchService, logger.WithComponent("search")) + tasksHandler := tasks.NewHandler(db.TasksQueries, temporalClient) // Initialize NATS for Telegram var natsClient *nats.Conn @@ -193,6 +209,7 @@ func main() { iapHandler: iapHandler, mcpHandler: mcpHandler, searchHandler: searchHandler, + tasksHandler: tasksHandler, deeprStorage: deeprStorage, }) @@ -294,6 +311,7 @@ type restServerInput struct { iapHandler *iap.Handler mcpHandler *mcp.Handler searchHandler *search.Handler + tasksHandler *tasks.Handler deeprStorage deepr.MessageStorage } @@ -368,6 +386,14 @@ func setupRESTServer(input restServerInput) *gin.Engine { api.POST("/search", input.searchHandler.PostSearchHandler) // POST /api/v1/search (SerpAPI) api.POST("/exa/search", input.searchHandler.PostExaSearchHandler) // POST /api/v1/exa/search (Exa AI) + // Tasks API routes (protected) + tasksGroup := api.Group("/tasks") + { + tasksGroup.POST("", input.tasksHandler.CreateTask) // POST /api/v1/tasks + tasksGroup.GET("", input.tasksHandler.GetTasks) // GET /api/v1/tasks + tasksGroup.DELETE("/:id", input.tasksHandler.DeleteTask) // DELETE /api/v1/tasks/:id + } + // Deep Research WebSocket endpoint (protected) api.GET("/deepresearch/ws", deepr.DeepResearchHandler(input.logger, input.requestTrackingService, input.firebaseClient, input.deeprStorage)) // WebSocket proxy for deep research } diff --git a/go.mod b/go.mod index a0fbd19..7ef6f1f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/eternisai/enchanted-proxy go 1.24.2 require ( + cloud.google.com/go/firestore v1.18.0 firebase.google.com/go/v4 v4.16.1 github.com/99designs/gqlgen v0.17.76 github.com/gin-gonic/gin v1.9.1 @@ -22,7 +23,9 @@ require ( github.com/rs/cors v1.11.1 github.com/sirupsen/logrus v1.9.3 github.com/vektah/gqlparser/v2 v2.5.30 + go.temporal.io/sdk v1.37.0 google.golang.org/api v0.231.0 + google.golang.org/grpc v1.72.0 ) require ( @@ -31,7 +34,6 @@ require ( cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/firestore v1.18.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect @@ -48,9 +50,11 @@ require ( github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -62,11 +66,15 @@ require ( github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-viper/mapstructure/v2 v2.3.0 // indirect github.com/goccy/go-json v0.10.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -84,14 +92,19 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/nexus-rpc/sdk-go v0.3.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron v1.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sosodev/duration v1.3.1 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/urfave/cli/v2 v2.27.7 // indirect @@ -108,6 +121,7 @@ require ( go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.temporal.io/api v1.53.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.39.0 // indirect @@ -123,7 +137,6 @@ require ( google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect - google.golang.org/grpc v1.72.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 51b4f1c..b7665dd 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,8 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -110,11 +112,15 @@ github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+d github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= @@ -134,6 +140,10 @@ github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrk github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= 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/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= @@ -143,6 +153,8 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -192,6 +204,8 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nexus-rpc/sdk-go v0.3.0 h1:Y3B0kLYbMhd4C2u00kcYajvmOrfozEtTV/nHSnV57jA= +github.com/nexus-rpc/sdk-go v0.3.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -206,6 +220,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/richzw/appstore v1.37.0 h1:p18I1lOTtX5pCg1ALc264BTa5T1gHek6VjZ7JxZwy4c= github.com/richzw/appstore v1.37.0/go.mod h1:8SdaqkdMLQ2eyLe9GVa2JmEk3ZHFBAoAUGgyXygURjc= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= @@ -227,6 +243,8 @@ github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GB github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -251,6 +269,9 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= @@ -274,31 +295,51 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.temporal.io/api v1.53.0 h1:6vAFpXaC584AIELa6pONV56MTpkm4Ha7gPWL2acNAjo= +go.temporal.io/api v1.53.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/sdk v1.37.0 h1:RbwCkUQuqY4rfCzdrDZF9lgT7QWG/pHlxfZFq0NPpDQ= +go.temporal.io/sdk v1.37.0/go.mod h1:tOy6vGonfAjrpCl6Bbw/8slTgQMiqvoyegRv2ZHPm5M= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -319,11 +360,16 @@ golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= diff --git a/internal/config/config.go b/internal/config/config.go index cb5857c..395102d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -83,6 +83,12 @@ type Config struct { // Logging LogLevel string LogFormat string + + // Temporal + TemporalAddress string + TemporalNamespace string + TemporalCloudAPIKey string + TemporalTaskQueue string } var AppConfig *Config @@ -192,6 +198,12 @@ func LoadConfig() { // Logging LogLevel: getEnvOrDefault("LOG_LEVEL", "debug"), LogFormat: getEnvOrDefault("LOG_FORMAT", "text"), + + // Temporal + TemporalAddress: getEnvOrDefault("TEMPORAL_ADDRESS", "localhost:7233"), + TemporalNamespace: getEnvOrDefault("TEMPORAL_NAMESPACE", "default"), + TemporalCloudAPIKey: getEnvOrDefault("TEMPORAL_CLOUD_API_KEY", ""), + TemporalTaskQueue: getEnvOrDefault("TEMPORAL_TASK_QUEUE", "tasks-queue"), } // Validate required configs diff --git a/internal/storage/pg/database.go b/internal/storage/pg/database.go index 698896d..5eb3258 100644 --- a/internal/storage/pg/database.go +++ b/internal/storage/pg/database.go @@ -7,12 +7,14 @@ import ( "github.com/eternisai/enchanted-proxy/internal/config" pgdb "github.com/eternisai/enchanted-proxy/internal/storage/pg/sqlc" + tasksdb "github.com/eternisai/enchanted-proxy/internal/storage/pg/queries/tasks" _ "github.com/lib/pq" ) type Database struct { - DB *sql.DB - Queries *pgdb.Queries + DB *sql.DB + Queries *pgdb.Queries + TasksQueries *tasksdb.Queries } // InitDatabase initializes the database connection and runs migrations. @@ -39,9 +41,11 @@ func InitDatabase(databaseURL string) (*Database, error) { // Create queries queries := pgdb.New(db) + tasksQueries := tasksdb.New(db) return &Database{ - DB: db, - Queries: queries, + DB: db, + Queries: queries, + TasksQueries: tasksQueries, }, nil } diff --git a/internal/storage/pg/migrations/007_create_tasks.sql b/internal/storage/pg/migrations/007_create_tasks.sql new file mode 100644 index 0000000..f2a9c3b --- /dev/null +++ b/internal/storage/pg/migrations/007_create_tasks.sql @@ -0,0 +1,21 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS tasks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id TEXT NOT NULL, + chat_id TEXT NOT NULL, + name TEXT NOT NULL, + content TEXT NOT NULL, + cron TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks (user_id); +CREATE INDEX IF NOT EXISTS idx_tasks_chat_id ON tasks (chat_id); +CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks (created_at DESC); + +-- +goose Down +DROP INDEX IF EXISTS idx_tasks_created_at; +DROP INDEX IF EXISTS idx_tasks_chat_id; +DROP INDEX IF EXISTS idx_tasks_user_id; +DROP TABLE IF EXISTS tasks; diff --git a/internal/storage/pg/queries/tasks/db.go b/internal/storage/pg/queries/tasks/db.go new file mode 100644 index 0000000..70ab127 --- /dev/null +++ b/internal/storage/pg/queries/tasks/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package tasks + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/storage/pg/queries/tasks/models.go b/internal/storage/pg/queries/tasks/models.go new file mode 100644 index 0000000..bfe65cb --- /dev/null +++ b/internal/storage/pg/queries/tasks/models.go @@ -0,0 +1,90 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package tasks + +import ( + "database/sql" + "time" +) + +type DeepResearchMessage struct { + ID string `json:"id"` + UserID string `json:"userId"` + ChatID string `json:"chatId"` + SessionID string `json:"sessionId"` + Message string `json:"message"` + MessageType string `json:"messageType"` + Sent bool `json:"sent"` + CreatedAt time.Time `json:"createdAt"` + SentAt sql.NullTime `json:"sentAt"` +} + +type Entitlement struct { + UserID string `json:"userId"` + ProExpiresAt sql.NullTime `json:"proExpiresAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type InviteCode struct { + ID int64 `json:"id"` + Code string `json:"code"` + CodeHash string `json:"codeHash"` + BoundEmail *string `json:"boundEmail"` + CreatedBy int64 `json:"createdBy"` + IsUsed bool `json:"isUsed"` + RedeemedBy *string `json:"redeemedBy"` + RedeemedAt sql.NullTime `json:"redeemedAt"` + ExpiresAt sql.NullTime `json:"expiresAt"` + IsActive bool `json:"isActive"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt sql.NullTime `json:"deletedAt"` +} + +type RequestLog struct { + ID int64 `json:"id"` + UserID string `json:"userId"` + Endpoint string `json:"endpoint"` + Model *string `json:"model"` + Provider string `json:"provider"` + CreatedAt time.Time `json:"createdAt"` + PromptTokens sql.NullInt32 `json:"promptTokens"` + CompletionTokens sql.NullInt32 `json:"completionTokens"` + TotalTokens sql.NullInt32 `json:"totalTokens"` +} + +type Task struct { + ID string `json:"id"` + UserID string `json:"userId"` + ChatID string `json:"chatId"` + Name string `json:"name"` + Content string `json:"content"` + Cron string `json:"cron"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type TelegramChat struct { + ID int64 `json:"id"` + ChatID int64 `json:"chatId"` + ChatUuid string `json:"chatUuid"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type UserRequestCountsDaily struct { + UserID string `json:"userId"` + DayBucket int64 `json:"dayBucket"` + RequestCount int64 `json:"requestCount"` +} + +type UserTokenUsageDaily struct { + UserID string `json:"userId"` + DayBucket int64 `json:"dayBucket"` + RequestCount int64 `json:"requestCount"` + TotalPromptTokens interface{} `json:"totalPromptTokens"` + TotalCompletionTokens interface{} `json:"totalCompletionTokens"` + TotalTokensUsed interface{} `json:"totalTokensUsed"` +} diff --git a/internal/storage/pg/queries/tasks/querier.go b/internal/storage/pg/queries/tasks/querier.go new file mode 100644 index 0000000..cad6505 --- /dev/null +++ b/internal/storage/pg/queries/tasks/querier.go @@ -0,0 +1,22 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package tasks + +import ( + "context" +) + +type Querier interface { + CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error) + DeleteTask(ctx context.Context, id string) error + DeleteTaskByIDAndChatID(ctx context.Context, arg DeleteTaskByIDAndChatIDParams) error + DeleteTaskByIDAndUserID(ctx context.Context, arg DeleteTaskByIDAndUserIDParams) error + GetTaskByID(ctx context.Context, id string) (Task, error) + GetTasksByChatID(ctx context.Context, chatID string) ([]Task, error) + GetTasksByUserID(ctx context.Context, userID string) ([]Task, error) + UpdateTask(ctx context.Context, arg UpdateTaskParams) (Task, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/internal/storage/pg/queries/tasks/tasks.sql b/internal/storage/pg/queries/tasks/tasks.sql new file mode 100644 index 0000000..cf12529 --- /dev/null +++ b/internal/storage/pg/queries/tasks/tasks.sql @@ -0,0 +1,36 @@ +-- name: CreateTask :one +INSERT INTO tasks (user_id, chat_id, name, content, cron, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) +RETURNING *; + +-- name: GetTasksByUserID :many +SELECT * FROM tasks +WHERE user_id = $1 +ORDER BY created_at DESC; + +-- name: GetTasksByChatID :many +SELECT * FROM tasks +WHERE chat_id = $1 +ORDER BY created_at DESC; + +-- name: GetTaskByID :one +SELECT * FROM tasks +WHERE id = $1; + +-- name: DeleteTask :exec +DELETE FROM tasks +WHERE id = $1; + +-- name: DeleteTaskByIDAndChatID :exec +DELETE FROM tasks +WHERE id = $1 AND chat_id = $2; + +-- name: DeleteTaskByIDAndUserID :exec +DELETE FROM tasks +WHERE id = $1 AND user_id = $2; + +-- name: UpdateTask :one +UPDATE tasks +SET name = $2, content = $3, cron = $4, updated_at = NOW() +WHERE id = $1 +RETURNING *; diff --git a/internal/storage/pg/queries/tasks/tasks.sql.go b/internal/storage/pg/queries/tasks/tasks.sql.go new file mode 100644 index 0000000..28ed201 --- /dev/null +++ b/internal/storage/pg/queries/tasks/tasks.sql.go @@ -0,0 +1,218 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: tasks.sql + +package tasks + +import ( + "context" +) + +const createTask = `-- name: CreateTask :one +INSERT INTO tasks (user_id, chat_id, name, content, cron, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) +RETURNING id, user_id, chat_id, name, content, cron, created_at, updated_at +` + +type CreateTaskParams struct { + UserID string `json:"userId"` + ChatID string `json:"chatId"` + Name string `json:"name"` + Content string `json:"content"` + Cron string `json:"cron"` +} + +func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error) { + row := q.db.QueryRowContext(ctx, createTask, + arg.UserID, + arg.ChatID, + arg.Name, + arg.Content, + arg.Cron, + ) + var i Task + err := row.Scan( + &i.ID, + &i.UserID, + &i.ChatID, + &i.Name, + &i.Content, + &i.Cron, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteTask = `-- name: DeleteTask :exec +DELETE FROM tasks +WHERE id = $1 +` + +func (q *Queries) DeleteTask(ctx context.Context, id string) error { + _, err := q.db.ExecContext(ctx, deleteTask, id) + return err +} + +const deleteTaskByIDAndChatID = `-- name: DeleteTaskByIDAndChatID :exec +DELETE FROM tasks +WHERE id = $1 AND chat_id = $2 +` + +type DeleteTaskByIDAndChatIDParams struct { + ID string `json:"id"` + ChatID string `json:"chatId"` +} + +func (q *Queries) DeleteTaskByIDAndChatID(ctx context.Context, arg DeleteTaskByIDAndChatIDParams) error { + _, err := q.db.ExecContext(ctx, deleteTaskByIDAndChatID, arg.ID, arg.ChatID) + return err +} + +const deleteTaskByIDAndUserID = `-- name: DeleteTaskByIDAndUserID :exec +DELETE FROM tasks +WHERE id = $1 AND user_id = $2 +` + +type DeleteTaskByIDAndUserIDParams struct { + ID string `json:"id"` + UserID string `json:"userId"` +} + +func (q *Queries) DeleteTaskByIDAndUserID(ctx context.Context, arg DeleteTaskByIDAndUserIDParams) error { + _, err := q.db.ExecContext(ctx, deleteTaskByIDAndUserID, arg.ID, arg.UserID) + return err +} + +const getTaskByID = `-- name: GetTaskByID :one +SELECT id, user_id, chat_id, name, content, cron, created_at, updated_at FROM tasks +WHERE id = $1 +` + +func (q *Queries) GetTaskByID(ctx context.Context, id string) (Task, error) { + row := q.db.QueryRowContext(ctx, getTaskByID, id) + var i Task + err := row.Scan( + &i.ID, + &i.UserID, + &i.ChatID, + &i.Name, + &i.Content, + &i.Cron, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getTasksByChatID = `-- name: GetTasksByChatID :many +SELECT id, user_id, chat_id, name, content, cron, created_at, updated_at FROM tasks +WHERE chat_id = $1 +ORDER BY created_at DESC +` + +func (q *Queries) GetTasksByChatID(ctx context.Context, chatID string) ([]Task, error) { + rows, err := q.db.QueryContext(ctx, getTasksByChatID, chatID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Task{} + for rows.Next() { + var i Task + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.ChatID, + &i.Name, + &i.Content, + &i.Cron, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTasksByUserID = `-- name: GetTasksByUserID :many +SELECT id, user_id, chat_id, name, content, cron, created_at, updated_at FROM tasks +WHERE user_id = $1 +ORDER BY created_at DESC +` + +func (q *Queries) GetTasksByUserID(ctx context.Context, userID string) ([]Task, error) { + rows, err := q.db.QueryContext(ctx, getTasksByUserID, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Task{} + for rows.Next() { + var i Task + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.ChatID, + &i.Name, + &i.Content, + &i.Cron, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateTask = `-- name: UpdateTask :one +UPDATE tasks +SET name = $2, content = $3, cron = $4, updated_at = NOW() +WHERE id = $1 +RETURNING id, user_id, chat_id, name, content, cron, created_at, updated_at +` + +type UpdateTaskParams struct { + ID string `json:"id"` + Name string `json:"name"` + Content string `json:"content"` + Cron string `json:"cron"` +} + +func (q *Queries) UpdateTask(ctx context.Context, arg UpdateTaskParams) (Task, error) { + row := q.db.QueryRowContext(ctx, updateTask, + arg.ID, + arg.Name, + arg.Content, + arg.Cron, + ) + var i Task + err := row.Scan( + &i.ID, + &i.UserID, + &i.ChatID, + &i.Name, + &i.Content, + &i.Cron, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/internal/storage/pg/sqlc/deep_research_messages.sql.go b/internal/storage/pg/sqlc/deep_research_messages.sql.go new file mode 100644 index 0000000..4a133e2 --- /dev/null +++ b/internal/storage/pg/sqlc/deep_research_messages.sql.go @@ -0,0 +1,182 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: deep_research_messages.sql + +package pgdb + +import ( + "context" + "database/sql" + "time" +) + +const addDeepResearchMessage = `-- name: AddDeepResearchMessage :exec +INSERT INTO deep_research_messages (id, user_id, chat_id, session_id, message, message_type, sent, created_at, sent_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) +` + +type AddDeepResearchMessageParams struct { + ID string `json:"id"` + UserID string `json:"userId"` + ChatID string `json:"chatId"` + SessionID string `json:"sessionId"` + Message string `json:"message"` + MessageType string `json:"messageType"` + Sent bool `json:"sent"` + CreatedAt time.Time `json:"createdAt"` + SentAt sql.NullTime `json:"sentAt"` +} + +func (q *Queries) AddDeepResearchMessage(ctx context.Context, arg AddDeepResearchMessageParams) error { + _, err := q.db.ExecContext(ctx, addDeepResearchMessage, + arg.ID, + arg.UserID, + arg.ChatID, + arg.SessionID, + arg.Message, + arg.MessageType, + arg.Sent, + arg.CreatedAt, + arg.SentAt, + ) + return err +} + +const deleteSessionMessages = `-- name: DeleteSessionMessages :exec +DELETE FROM deep_research_messages +WHERE session_id = $1 +` + +func (q *Queries) DeleteSessionMessages(ctx context.Context, sessionID string) error { + _, err := q.db.ExecContext(ctx, deleteSessionMessages, sessionID) + return err +} + +const getSessionMessageCount = `-- name: GetSessionMessageCount :one +SELECT COUNT(*) as total_messages +FROM deep_research_messages +WHERE session_id = $1 +` + +func (q *Queries) GetSessionMessageCount(ctx context.Context, sessionID string) (int64, error) { + row := q.db.QueryRowContext(ctx, getSessionMessageCount, sessionID) + var total_messages int64 + err := row.Scan(&total_messages) + return total_messages, err +} + +const getSessionMessages = `-- name: GetSessionMessages :many +SELECT id, user_id, chat_id, session_id, message, message_type, sent, created_at, sent_at +FROM deep_research_messages +WHERE session_id = $1 +ORDER BY created_at ASC +` + +func (q *Queries) GetSessionMessages(ctx context.Context, sessionID string) ([]DeepResearchMessage, error) { + rows, err := q.db.QueryContext(ctx, getSessionMessages, sessionID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []DeepResearchMessage{} + for rows.Next() { + var i DeepResearchMessage + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.ChatID, + &i.SessionID, + &i.Message, + &i.MessageType, + &i.Sent, + &i.CreatedAt, + &i.SentAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUnsentMessageCount = `-- name: GetUnsentMessageCount :one +SELECT COUNT(*) as unsent_count +FROM deep_research_messages +WHERE session_id = $1 AND sent = FALSE +` + +func (q *Queries) GetUnsentMessageCount(ctx context.Context, sessionID string) (int64, error) { + row := q.db.QueryRowContext(ctx, getUnsentMessageCount, sessionID) + var unsent_count int64 + err := row.Scan(&unsent_count) + return unsent_count, err +} + +const getUnsentMessages = `-- name: GetUnsentMessages :many +SELECT id, user_id, chat_id, session_id, message, message_type, sent, created_at, sent_at +FROM deep_research_messages +WHERE session_id = $1 AND sent = FALSE +ORDER BY created_at ASC +` + +func (q *Queries) GetUnsentMessages(ctx context.Context, sessionID string) ([]DeepResearchMessage, error) { + rows, err := q.db.QueryContext(ctx, getUnsentMessages, sessionID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []DeepResearchMessage{} + for rows.Next() { + var i DeepResearchMessage + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.ChatID, + &i.SessionID, + &i.Message, + &i.MessageType, + &i.Sent, + &i.CreatedAt, + &i.SentAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const markAllMessagesAsSent = `-- name: MarkAllMessagesAsSent :exec +UPDATE deep_research_messages +SET sent = TRUE, sent_at = NOW() +WHERE session_id = $1 AND sent = FALSE +` + +func (q *Queries) MarkAllMessagesAsSent(ctx context.Context, sessionID string) error { + _, err := q.db.ExecContext(ctx, markAllMessagesAsSent, sessionID) + return err +} + +const markMessageAsSent = `-- name: MarkMessageAsSent :exec +UPDATE deep_research_messages +SET sent = TRUE, sent_at = NOW() +WHERE id = $1 +` + +func (q *Queries) MarkMessageAsSent(ctx context.Context, id string) error { + _, err := q.db.ExecContext(ctx, markMessageAsSent, id) + return err +} diff --git a/internal/storage/pg/sqlc/models.go b/internal/storage/pg/sqlc/models.go index 10d07bd..9c2f7d7 100644 --- a/internal/storage/pg/sqlc/models.go +++ b/internal/storage/pg/sqlc/models.go @@ -7,8 +7,22 @@ package pgdb import ( "database/sql" "time" + + "github.com/google/uuid" ) +type DeepResearchMessage struct { + ID string `json:"id"` + UserID string `json:"userId"` + ChatID string `json:"chatId"` + SessionID string `json:"sessionId"` + Message string `json:"message"` + MessageType string `json:"messageType"` + Sent bool `json:"sent"` + CreatedAt time.Time `json:"createdAt"` + SentAt sql.NullTime `json:"sentAt"` +} + type Entitlement struct { UserID string `json:"userId"` ProExpiresAt sql.NullTime `json:"proExpiresAt"` @@ -43,6 +57,17 @@ type RequestLog struct { TotalTokens sql.NullInt32 `json:"totalTokens"` } +type Task struct { + ID uuid.UUID `json:"id"` + UserID string `json:"userId"` + ChatID string `json:"chatId"` + Name string `json:"name"` + Content string `json:"content"` + Cron string `json:"cron"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + type TelegramChat struct { ID int64 `json:"id"` ChatID int64 `json:"chatId"` diff --git a/internal/storage/pg/sqlc/querier.go b/internal/storage/pg/sqlc/querier.go index 20525b8..bcb32d3 100644 --- a/internal/storage/pg/sqlc/querier.go +++ b/internal/storage/pg/sqlc/querier.go @@ -9,18 +9,24 @@ import ( ) type Querier interface { + AddDeepResearchMessage(ctx context.Context, arg AddDeepResearchMessageParams) error AtomicUseInviteCode(ctx context.Context, arg AtomicUseInviteCodeParams) error CountInviteCodesByRedeemedBy(ctx context.Context, redeemedBy *string) (int64, error) CreateInviteCode(ctx context.Context, arg CreateInviteCodeParams) (InviteCode, error) CreateRequestLog(ctx context.Context, arg CreateRequestLogParams) error CreateTelegramChat(ctx context.Context, arg CreateTelegramChatParams) (TelegramChat, error) + DeleteSessionMessages(ctx context.Context, sessionID string) error DeleteTelegramChat(ctx context.Context, chatID int64) error GetAllInviteCodes(ctx context.Context) ([]InviteCode, error) GetEntitlement(ctx context.Context, userID string) (Entitlement, error) GetInviteCodeByCodeHash(ctx context.Context, codeHash string) (InviteCode, error) GetInviteCodeByID(ctx context.Context, id int64) (InviteCode, error) + GetSessionMessageCount(ctx context.Context, sessionID string) (int64, error) + GetSessionMessages(ctx context.Context, sessionID string) ([]DeepResearchMessage, error) GetTelegramChatByChatID(ctx context.Context, chatID int64) (TelegramChat, error) GetTelegramChatByChatUUID(ctx context.Context, chatUuid string) (TelegramChat, error) + GetUnsentMessageCount(ctx context.Context, sessionID string) (int64, error) + GetUnsentMessages(ctx context.Context, sessionID string) ([]DeepResearchMessage, error) GetUserLifetimeTokenUsage(ctx context.Context, userID string) (int64, error) GetUserRequestCountInLastDay(ctx context.Context, userID string) (int64, error) GetUserRequestCountInTimeWindow(ctx context.Context, arg GetUserRequestCountInTimeWindowParams) (int64, error) @@ -29,6 +35,8 @@ type Querier interface { GetUserTokenUsageInTimeWindow(ctx context.Context, arg GetUserTokenUsageInTimeWindowParams) (int64, error) GetUserTokenUsageToday(ctx context.Context, userID string) (int64, error) ListTelegramChats(ctx context.Context) ([]TelegramChat, error) + MarkAllMessagesAsSent(ctx context.Context, sessionID string) error + MarkMessageAsSent(ctx context.Context, id string) error RefreshUserRequestCountsView(ctx context.Context) error RefreshUserTokenUsageView(ctx context.Context) error ResetInviteCode(ctx context.Context, codeHash string) error diff --git a/internal/tasks/handlers.go b/internal/tasks/handlers.go new file mode 100644 index 0000000..07c786f --- /dev/null +++ b/internal/tasks/handlers.go @@ -0,0 +1,147 @@ +package tasks + +import ( + "context" + "fmt" + "net/http" + + "github.com/eternisai/enchanted-proxy/internal/auth" + tasksdb "github.com/eternisai/enchanted-proxy/internal/storage/pg/queries/tasks" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/client" +) + +type Handler struct { + queries tasksdb.Querier + temporalClient client.Client +} + +func NewHandler(queries tasksdb.Querier, temporalClient client.Client) *Handler { + return &Handler{ + queries: queries, + temporalClient: temporalClient, + } +} + +func (h *Handler) CreateTask(c *gin.Context) { + userID, ok := auth.GetUserUUID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + var req CreateTaskRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + ctx := context.Background() + task, err := h.queries.CreateTask(ctx, tasksdb.CreateTaskParams{ + UserID: userID, + ChatID: userID, + Name: req.Name, + Content: req.Content, + Cron: req.Cron, + }) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + taskId := uuid.New().String() + id := fmt.Sprintf("task-%s", taskId) + opts := client.ScheduleOptions{ + ID: id, + Action: &client.ScheduleWorkflowAction{ + ID: id, + Workflow: "ExecuteTaskWorkflow", + Args: []any{map[string]any{"name": req.Name}}, + TaskQueue: "task-queue", + }, + Overlap: enums.SCHEDULE_OVERLAP_POLICY_SKIP, + Spec: client.ScheduleSpec{ + CronExpressions: []string{req.Cron}, + }, + } + + scheduleHandle, err := h.temporalClient.ScheduleClient().Create(ctx, opts) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"temporal error": err.Error()}) + return + } + + fmt.Println("scheduleHandle", scheduleHandle) + + response := TaskResponse{ + ID: task.ID, + Name: task.Name, + Content: task.Content, + Cron: task.Cron, + CreatedAt: task.CreatedAt, + UpdatedAt: task.UpdatedAt, + } + + c.JSON(http.StatusCreated, response) +} + +func (h *Handler) GetTasks(c *gin.Context) { + userID, ok := auth.GetUserUUID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + ctx := context.Background() + tasks, err := h.queries.GetTasksByChatID(ctx, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + taskResponses := make([]TaskResponse, 0, len(tasks)) + for _, task := range tasks { + taskResponses = append(taskResponses, TaskResponse{ + ID: task.ID, + Name: task.Name, + Content: task.Content, + Cron: task.Cron, + CreatedAt: task.CreatedAt, + UpdatedAt: task.UpdatedAt, + }) + } + + response := TasksResponse{ + Tasks: taskResponses, + } + + c.JSON(http.StatusOK, response) +} + +func (h *Handler) DeleteTask(c *gin.Context) { + userID, ok := auth.GetUserUUID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + taskID := c.Param("id") + if taskID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Task ID is required"}) + return + } + + ctx := context.Background() + err := h.queries.DeleteTaskByIDAndChatID(ctx, tasksdb.DeleteTaskByIDAndChatIDParams{ + ID: taskID, + ChatID: userID, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"}) +} diff --git a/internal/tasks/models.go b/internal/tasks/models.go new file mode 100644 index 0000000..4b8b1af --- /dev/null +++ b/internal/tasks/models.go @@ -0,0 +1,32 @@ +package tasks + +import "time" + +type Task struct { + ID string `json:"id"` + ChatID string `json:"chat_id"` + Name string `json:"name"` + Content string `json:"content"` + Cron string `json:"cron"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateTaskRequest struct { + Name string `json:"name" binding:"required"` + Content string `json:"content" binding:"required"` + Cron string `json:"cron" binding:"required"` +} + +type TaskResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Content string `json:"content"` + Cron string `json:"cron"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TasksResponse struct { + Tasks []TaskResponse `json:"tasks"` +} diff --git a/internal/temporal/temporal.go b/internal/temporal/temporal.go new file mode 100644 index 0000000..2700f50 --- /dev/null +++ b/internal/temporal/temporal.go @@ -0,0 +1,68 @@ +package temporal + +import ( + "context" + "crypto/tls" + "fmt" + + "github.com/eternisai/enchanted-proxy/internal/config" + "go.temporal.io/sdk/client" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +type TemporalClientConfig struct { + Address string + Namespace string + CloudAPIKey string + TaskQueue string +} + +// NewTemporalClientFromConfig creates a new Temporal client from the application config +func NewTemporalClientFromConfig(cfg *config.Config) (client.Client, error) { + clientConfig := TemporalClientConfig{ + Address: cfg.TemporalAddress, + Namespace: cfg.TemporalNamespace, + CloudAPIKey: cfg.TemporalCloudAPIKey, + TaskQueue: cfg.TemporalTaskQueue, + } + return CreateTemporalClient(clientConfig) +} + +func CreateTemporalClient(config TemporalClientConfig) (client.Client, error) { + clientOptions := client.Options{ + HostPort: config.Address, + Namespace: config.Namespace, + } + + // Only configure TLS and credentials if using Temporal Cloud + if config.CloudAPIKey != "" { + clientOptions.ConnectionOptions = client.ConnectionOptions{ + TLS: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + DialOptions: []grpc.DialOption{ + grpc.WithUnaryInterceptor( + func(ctx context.Context, method string, req any, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + return invoker( + metadata.AppendToOutgoingContext(ctx, "temporal-namespace", config.Namespace), + method, + req, + reply, + cc, + opts..., + ) + }, + ), + }, + } + clientOptions.Credentials = client.NewAPIKeyStaticCredentials(config.CloudAPIKey) + } + + c, err := client.Dial(clientOptions) + if err != nil { + return nil, fmt.Errorf("failed to create temporal client: %w", err) + } + + return c, nil +} diff --git a/sqlc.yaml b/sqlc.yaml index 767b5ca..82ad098 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -68,4 +68,36 @@ sql: nullable: true go_type: type: "string" - pointer: true \ No newline at end of file + pointer: true + - engine: "postgresql" + schema: "./internal/storage/pg/migrations" + queries: "./internal/storage/pg/queries/tasks/" + gen: + go: + package: "tasks" + out: "./internal/storage/pg/queries/tasks" + emit_json_tags: true + emit_prepared_queries: false + emit_interface: true + emit_exact_table_names: false + emit_empty_slices: true + emit_exported_queries: false + emit_result_struct_pointers: false + emit_params_struct_pointers: false + emit_methods_with_db_argument: false + emit_pointers_for_null_types: true + emit_enum_valid_method: false + emit_all_enum_values: false + json_tags_case_style: "camel" + overrides: + - column: "*.created_at" + go_type: "time.Time" + - column: "*.updated_at" + go_type: "time.Time" + - db_type: "text" + nullable: true + go_type: + type: "string" + pointer: true + - db_type: "uuid" + go_type: "string" \ No newline at end of file