From 6ed7c4b09775636435e2af35fffd92b773aebe0f Mon Sep 17 00:00:00 2001 From: Nolan Jacobson <50815660+nolanjacobson@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:44:59 -0400 Subject: [PATCH] feat: telegram API integration, message scraping, sentiment analysis (#456) * feat: authentication for telegram * chore: latest * authentication is working * chore: telegram fetchChannelMessages * feat: sentiment analysis for telegram is working * chore: fix discord docs * updates to docs * chore: updates to swagger docs * stash * chore: final touches to Telegram PR * chore: saved file * chore: update vyper * chore: fixed merge conflicts with test * chore: update teelgram messages handler * chore: fix context issues * chore: minor fixes * chore: fixes * chore: latest with ctx still closing * new context for each call * chore: context still failing * Refactor Telegram integration and handle background connection Removed the usage of the Run() method which automatically closes connections Refactor Telegram initialization, updated logging to use logrus. Added context handling in Telegram authentication functions. Implemented TelegramStop function in configuration to manage the background connection shutdown when the system exits. * chore: pr ready for merge * chore: remove hardcoded values --------- Co-authored-by: Bob Stevens <35038919+restevens402@users.noreply.github.com> --- cmd/masa-node/main.go | 9 +- docs/docs.go | 195 ++++++++++++++++++++ docs/oracle-node/twitter-data.md | 2 +- go.mod | 43 +++-- go.sum | 90 +++++---- pkg/api/handlers_data.go | 153 +++++++++++++-- pkg/api/handlers_node.go | 27 +-- pkg/api/routes.go | 49 +++++ pkg/api/templates/index.html | 16 ++ pkg/config/app.go | 5 + pkg/config/constants.go | 1 + pkg/config/welcome.go | 3 +- pkg/llmbridge/sentiment.go | 85 +++++++++ pkg/oracle_node.go | 85 +++++---- pkg/pubsub/node_data.go | 26 ++- pkg/pubsub/node_event_tracker.go | 4 + pkg/scrapers/telegram/getchannelmessages.go | 86 +++++++++ pkg/scrapers/telegram/telegram_client.go | 159 ++++++++++++++++ pkg/workers/handler.go | 12 +- pkg/workers/workers.go | 69 +++---- 20 files changed, 961 insertions(+), 158 deletions(-) create mode 100644 pkg/scrapers/telegram/getchannelmessages.go create mode 100644 pkg/scrapers/telegram/telegram_client.go diff --git a/cmd/masa-node/main.go b/cmd/masa-node/main.go index cf4ea293..d4ff7088 100644 --- a/cmd/masa-node/main.go +++ b/cmd/masa-node/main.go @@ -113,6 +113,13 @@ func main() { nodeData.Left() } cancel() + // Call the global StopFunc to stop the Telegram background connection + cfg := config.GetInstance() + if cfg.TelegramStop != nil { + if err := cfg.TelegramStop(); err != nil { + logrus.Errorf("Error stopping the background connection: %v", err) + } + } }() router := api.SetupRoutes(node) @@ -127,7 +134,7 @@ func main() { multiAddr := node.GetMultiAddrs().String() // Get the multiaddress ipAddr := node.Host.Addrs()[0].String() // Get the IP address // Display the welcome message with the multiaddress and IP address - config.DisplayWelcomeMessage(multiAddr, ipAddr, keyManager.EthAddress, isStaked, isValidator, cfg.TwitterScraper, cfg.DiscordScraper, cfg.WebScraper, cfg.Version) + config.DisplayWelcomeMessage(multiAddr, ipAddr, keyManager.EthAddress, isStaked, isValidator, cfg.TwitterScraper, cfg.TelegramScraper, cfg.DiscordScraper, cfg.WebScraper, config.Version) <-ctx.Done() } diff --git a/docs/docs.go b/docs/docs.go index 24d49473..f4f8fde6 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -474,6 +474,53 @@ const docTemplate = `{ ] } }, + "/data/telegram/channel/messages": { + "post": { + "description": "Retrieves messages from a specified Telegram channel.", + "tags": ["Telegram"], + "summary": "Get Telegram Channel Messages", + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Request body", + "required": true, + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "Telegram Username" + } + }, + "required": ["username"] + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved messages", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Message" + } + } + }, + "400": { + "description": "Invalid username or error fetching messages", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/data/web": { "post": { "description": "Retrieves data from the web", @@ -848,6 +895,45 @@ const docTemplate = `{ } } }, + "/sentiment/telegram": { + "post": { + "description": "Searches for Telegram messages and analyzes their sentiment", + "tags": ["Sentiment"], + "summary": "Analyze Sentiment of Telegram Messages", + "consumes": ["application/json"], + "produces": ["application/json"], + "parameters": [ + { + "name": "query", + "in": "body", + "description": "Search Query", + "required": true, + "schema": { + "type": "object", + "properties": { + "query": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Successfully analyzed sentiment of Telegram messages", + "schema": { + "$ref": "#/definitions/SentimentAnalysisResponse" + } + }, + "400": { + "description": "Error analyzing sentiment of Telegram messages", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, "/sentiment/discord": { "post": { "description": "Searches for Discord messages and analyzes their sentiment", @@ -932,6 +1018,115 @@ const docTemplate = `{ } } }, + "/auth/telegram/start": { + "post": { + "description": "Initiates the authentication process with Telegram by sending a code to the provided phone number.", + "tags": ["Authentication"], + "summary": "Start Telegram Authentication", + "consumes": ["application/json"], + "produces": ["application/json"], + "parameters": [ + { + "name": "phone_number", + "in": "body", + "description": "Phone Number", + "required": true, + "schema": { + "type": "object", + "properties": { + "phone_number": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Successfully sent authentication code", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Failed to initialize Telegram client or to start authentication", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/auth/telegram/complete": { + "post": { + "description": "Completes the authentication process with Telegram using the code sent to the phone number.", + "tags": ["Authentication"], + "summary": "Complete Telegram Authentication", + "consumes": ["application/json"], + "produces": ["application/json"], + "parameters": [ + { + "name": "phone_number", + "in": "body", + "description": "Phone Number", + "required": true, + "schema": { + "type": "object", + "properties": { + "phone_number": { + "type": "string" + }, + "code": { + "type": "string" + }, + "phone_code_hash": { + "type": "string" + } + }, + "required": ["phone_number", "code", "phone_code_hash"] + } + } + ], + "responses": { + "200": { + "description": "Successfully authenticated", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "401": { + "description": "Two-factor authentication is required", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Failed to initialize Telegram client or to complete authentication", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, "/sentiment/web": { "post": { "description": "Searches for web content and analyzes its sentiment", diff --git a/docs/oracle-node/twitter-data.md b/docs/oracle-node/twitter-data.md index 9dc52b86..4eba986d 100644 --- a/docs/oracle-node/twitter-data.md +++ b/docs/oracle-node/twitter-data.md @@ -468,4 +468,4 @@ Upon collecting the tweets, CryptoSentimentAI processes and analyzes the text co ### Conclusion -The `/data/tweets` endpoint in the Masa Oracle Node API provides a rich foundation for developing decentralized applications that can interact with social media data in real-time. By leveraging this endpoint, developers are empowered to create innovative AI agents capable of analyzing social media trends and sentiments. These agents can uncover deep insights from the vast stream of social media conversations, offering valuable intelligence for a wide range of applications and decision-making processes. +The `/data/tweets` endpoint in the Masa Oracle Node API provides a rich foundation for developing decentralized applications that can interact with social media data in real-time. By leveraging this endpoint, developers are empowered to create innovative AI agents capable of analyzing social media trends and sentiments. These agents can uncover deep insights from the vast stream of social media conversations, offering valuable intelligence for a wide range of applications and decision-making processes. \ No newline at end of file diff --git a/go.mod b/go.mod index 7b89333a..63c733cb 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,8 @@ require ( github.com/gocolly/colly/v2 v2.1.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.6.0 + github.com/gotd/contrib v0.20.0 + github.com/gotd/td v0.105.0 github.com/ipfs/go-cid v0.4.1 github.com/ipfs/go-datastore v0.6.0 github.com/ipfs/go-ds-leveldb v0.5.0 @@ -47,9 +49,9 @@ require ( github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/PuerkitoBio/goquery v1.5.1 // indirect + github.com/PuerkitoBio/goquery v1.9.1 // indirect github.com/Workiva/go-datastructures v1.1.3 // indirect - github.com/andybalholm/cascadia v1.2.0 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect github.com/antchfx/htmlquery v1.2.3 // indirect github.com/antchfx/xmlquery v1.3.1 // indirect github.com/antchfx/xpath v1.1.10 // indirect @@ -88,7 +90,10 @@ require ( github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect + github.com/go-faster/jx v1.1.0 // indirect + github.com/go-faster/xor v1.0.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -110,6 +115,8 @@ require ( github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect github.com/gorilla/websocket v1.5.1 // indirect + github.com/gotd/ige v0.2.2 // indirect + github.com/gotd/neo v0.1.5 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect @@ -128,7 +135,7 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kennygrant/sanitize v1.2.4 // indirect - github.com/klauspost/compress v1.17.8 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/koron/go-ssdp v0.0.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -209,6 +216,7 @@ require ( github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect + github.com/segmentio/asm v1.2.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/smartystreets/assertions v1.13.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -230,27 +238,28 @@ require ( github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.44.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.24.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.17.1 // indirect go.uber.org/fx v1.21.1 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.23.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect gonum.org/v1/gonum v0.15.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect @@ -258,5 +267,7 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect + nhooyr.io/websocket v1.8.11 // indirect + rsc.io/qr v0.2.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index d9404356..a35134b1 100644 --- a/go.sum +++ b/go.sum @@ -18,15 +18,17 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= +github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= github.com/Workiva/go-datastructures v1.1.3 h1:LRdRrug9tEuKk7TGfz/sct5gjVj44G9pfqDt4qm7ghw= github.com/Workiva/go-datastructures v1.1.3/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE= github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antchfx/htmlquery v1.2.3 h1:sP3NFDneHx2stfNXCKbhHFo8XgNjCACnU/4AO5gWz6M= github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0= @@ -195,9 +197,16 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg= +github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg= +github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ= +github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38= +github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -305,6 +314,14 @@ github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f h1:KMlcu9X58lhTA github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gotd/contrib v0.20.0 h1:1Wc4+HMQiIKYQuGHVwVksIx152HFTP6B5n88dDe0ZYw= +github.com/gotd/contrib v0.20.0/go.mod h1:P6o8W4niqhDPHLA0U+SA/L7l3BQHYLULpeHfRSePn9o= +github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk= +github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0= +github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ= +github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ= +github.com/gotd/td v0.105.0 h1:FjU9pgmL5Qt10+cosPCz4agvQT/hMBz6QMi1fFH7ekY= +github.com/gotd/td v0.105.0/go.mod h1:aVe5/LP/nNIyAqaW3CwB0Ckum+MkcfvazwMOLHV0bqQ= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -379,8 +396,8 @@ github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 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.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -639,8 +656,8 @@ github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= @@ -656,6 +673,8 @@ github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxT github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/sashabaranov/go-openai v1.27.0 h1:L3hO6650YUbKrbGUC6yCjsUluhKZ9h1/jcgbTItI8Mo= github.com/sashabaranov/go-openai v1.27.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= @@ -784,20 +803,22 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel/exporters/prometheus v0.44.0 h1:08qeJgaPC0YEBu2PQMbqU3rogTlyzpjhCI2b58Yn00w= go.opentelemetry.io/otel/exporters/prometheus v0.44.0/go.mod h1:ERL2uIeBtg4TxZdojHUwzZfIFlUIjZtxubT5p4h1Gjg= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= -go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= +go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.21.1 h1:RqBh3cYdzZS0uqwVeEjOX2p73dddLpym315myy/Bpb0= @@ -840,8 +861,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= @@ -858,8 +879,8 @@ 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -896,8 +917,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -910,6 +931,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ 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-20201207232520-09787c993a3a/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -959,8 +981,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -972,8 +994,8 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -987,8 +1009,8 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -1014,8 +1036,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 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= @@ -1098,8 +1120,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= +nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= diff --git a/pkg/api/handlers_data.go b/pkg/api/handlers_data.go index 96cdf90c..9c4e5e22 100644 --- a/pkg/api/handlers_data.go +++ b/pkg/api/handlers_data.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "context" "encoding/base64" "encoding/hex" "encoding/json" @@ -9,25 +10,21 @@ import ( "io" "net/http" "os" - "sync" - - "github.com/masa-finance/masa-oracle/pkg/chain" - "github.com/masa-finance/masa-oracle/pkg/workers" - "github.com/multiformats/go-multiaddr" - "github.com/sirupsen/logrus" - "strings" - + "sync" "time" - "github.com/google/uuid" - "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/multiformats/go-multiaddr" + "github.com/sirupsen/logrus" - pubsub2 "github.com/masa-finance/masa-oracle/pkg/pubsub" - + "github.com/masa-finance/masa-oracle/pkg/chain" "github.com/masa-finance/masa-oracle/pkg/config" + pubsub2 "github.com/masa-finance/masa-oracle/pkg/pubsub" "github.com/masa-finance/masa-oracle/pkg/scrapers/discord" + "github.com/masa-finance/masa-oracle/pkg/scrapers/telegram" + "github.com/masa-finance/masa-oracle/pkg/workers" ) type LLMChat struct { @@ -248,6 +245,56 @@ func (api *API) SearchDiscordMessagesAndAnalyzeSentiment() gin.HandlerFunc { } } +// SearchTelegramMessagesAndAnalyzeSentiment processes a request to search Telegram messages and analyze sentiment. +func (api *API) SearchTelegramMessagesAndAnalyzeSentiment() gin.HandlerFunc { + return func(c *gin.Context) { + if !api.Node.IsStaked { + c.JSON(http.StatusBadRequest, gin.H{"error": "Node has not staked and cannot participate"}) + return + } + + var reqBody struct { + Username string `json:"username"` // Telegram usernames are used instead of channel IDs + Prompt string `json:"prompt"` + Model string `json:"model"` + } + if err := c.ShouldBindJSON(&reqBody); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + if reqBody.Username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username parameter is missing"}) + return + } + + if reqBody.Prompt == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Prompt parameter is missing"}) + return + } + + if reqBody.Model == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Model parameter is missing"}) + return + } + + bodyBytes, wErr := json.Marshal(reqBody) + if wErr != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": wErr.Error()}) + return + } + requestID := uuid.New().String() + responseCh := pubsub2.GetResponseChannelMap().CreateChannel(requestID) + defer pubsub2.GetResponseChannelMap().Delete(requestID) + wErr = publishWorkRequest(api, requestID, workers.WORKER.TelegramSentiment, bodyBytes) + if wErr != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": wErr.Error()}) + return + } + handleWorkResponse(c, responseCh) + } +} + // SearchWebAndAnalyzeSentiment returns a gin.HandlerFunc that processes web search requests and performs sentiment analysis. // It first validates the request body for required fields such as URL, Depth, and Model. If the Model is set to "all", // it iterates through all available models to perform sentiment analysis on the web content fetched from the specified URL. @@ -716,6 +763,88 @@ func (api *API) WebData() gin.HandlerFunc { } } +// StartAuth starts the authentication process with Telegram. +func (api *API) StartAuth() gin.HandlerFunc { + return func(c *gin.Context) { + var reqBody struct { + PhoneNumber string `json:"phone_number"` + } + if err := c.ShouldBindJSON(&reqBody); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + phoneCodeHash, err := telegram.StartAuthentication(context.Background(), reqBody.PhoneNumber) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start authentication"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Code sent to Telegram app", "phone_code_hash": phoneCodeHash}) + } +} + +// CompleteAuth completes the authentication process with Telegram. +func (api *API) CompleteAuth() gin.HandlerFunc { + return func(c *gin.Context) { + var reqBody struct { + PhoneNumber string `json:"phone_number"` + Code string `json:"code"` + PhoneCodeHash string `json:"phone_code_hash"` + } + if err := c.ShouldBindJSON(&reqBody); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + auth, err := telegram.CompleteAuthentication(context.Background(), reqBody.PhoneNumber, reqBody.Code, reqBody.PhoneCodeHash) + if err != nil { + // Check if 2FA is required + if err.Error() == "2FA required" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Two-factor authentication is required"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to complete authentication", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Authentication successful", "auth": auth}) + } +} + +func (api *API) GetChannelMessagesHandler() gin.HandlerFunc { + return func(c *gin.Context) { + var reqBody struct { + Username string `json:"username"` // Telegram usernames are used instead of channel IDs + } + + // Bind the JSON body to the struct + if err := c.ShouldBindJSON(&reqBody); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + if reqBody.Username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username parameter is missing"}) + return + } + + // worker handler implementation + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + requestID := uuid.New().String() + responseCh := pubsub2.GetResponseChannelMap().CreateChannel(requestID) + defer pubsub2.GetResponseChannelMap().Delete(requestID) + err = publishWorkRequest(api, requestID, workers.WORKER.TelegramChannelMessages, bodyBytes) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + handleWorkResponse(c, responseCh) + } +} + // LocalLlmChat handles requests for chatting with AI models hosted by ollama. // It expects a JSON request body with a structure formatted for the model. For example for Ollama: // diff --git a/pkg/api/handlers_node.go b/pkg/api/handlers_node.go index fdfcce2c..0d91b312 100644 --- a/pkg/api/handlers_node.go +++ b/pkg/api/handlers_node.go @@ -389,18 +389,20 @@ func (api *API) NodeStatusPageHandler() gin.HandlerFunc { return func(c *gin.Context) { // Initialize default values for the template data templateData := gin.H{ - "TotalPeers": 0, - "Name": "Masa Status Page", - "PeerID": api.Node.Host.ID().String(), - "IsStaked": false, - "IsTwitterScraper": false, - "IsDiscordScraper": false, - "IsWebScraper": false, - "FirstJoined": api.Node.FromUnixTime(time.Now().Unix()), - "LastJoined": api.Node.FromUnixTime(time.Now().Unix()), - "CurrentUptime": "0", - "TotalUptime": "0", - "BytesScraped": "0 MB", + "TotalPeers": 0, + "Name": "Masa Status Page", + "PeerID": api.Node.Host.ID().String(), + "IsStaked": false, + "IsTwitterScraper": false, + "IsDiscordScraper": false, + "IsTelegramScraper": false, + "IsWebScraper": false, + "FirstJoined": api.Node.FromUnixTime(time.Now().Unix()), + "LastJoined": api.Node.FromUnixTime(time.Now().Unix()), + "CurrentUptime": "0", + "TotalUptime": "0", + "Rewards": "Coming Soon!", + "BytesScraped": "0 MB", } if api.Node != nil && api.Node.Host != nil { @@ -412,6 +414,7 @@ func (api *API) NodeStatusPageHandler() gin.HandlerFunc { templateData["IsStaked"] = nd.IsStaked templateData["IsTwitterScraper"] = nd.IsTwitterScraper templateData["IsDiscordScraper"] = nd.IsDiscordScraper + templateData["IsTelegramScraper"] = nd.IsTelegramScraper templateData["IsWebScraper"] = nd.IsWebScraper templateData["FirstJoined"] = api.Node.FromUnixTime(nd.FirstJoinedUnix) templateData["LastJoined"] = api.Node.FromUnixTime(nd.LastJoinedUnix) diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 2d73ecbb..141b9eaa 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -215,6 +215,44 @@ func SetupRoutes(node *masa.OracleNode) *gin.Engine { // @Router /discord/profile/{userID} [get] v1.GET("/data/discord/profile/:userID", API.SearchDiscordProfile()) + // @Summary Start Telegram Authentication + // @Description Initiates the authentication process with Telegram by sending a code to the provided phone number. + // @Tags Authentication + // @Accept json + // @Produce json + // @Param phone_number body string true "Phone Number" + // @Success 200 {object} map[string]interface{} "Successfully sent authentication code" + // @Failure 400 {object} ErrorResponse "Invalid request body" + // @Failure 500 {object} ErrorResponse "Failed to initialize Telegram client or to start authentication" + // @Router /auth/telegram/start [post] + v1.POST("/auth/telegram/start", API.StartAuth()) + + // @Summary Complete Telegram Authentication + // @Description Completes the authentication process with Telegram using the code sent to the phone number. + // @Tags Authentication + // @Accept json + // @Produce json + // @Param phone_number body string true "Phone Number" + // @Param code body string true "Authentication Code" + // @Param phone_code_hash body string true "Phone Code Hash" + // @Success 200 {object} map[string]interface{} "Successfully authenticated" + // @Failure 400 {object} ErrorResponse "Invalid request body" + // @Failure 401 {object} ErrorResponse "Two-factor authentication is required" + // @Failure 500 {object} ErrorResponse "Failed to initialize Telegram client or to complete authentication" + // @Router /auth/telegram/complete [post] + v1.POST("/auth/telegram/complete", API.CompleteAuth()) + + // @Summary Get Telegram Channel Messages + // @Description Retrieves messages from a specified Telegram channel. + // @Tags Telegram + // @Accept json + // @Produce json + // @Success 200 {object} map[string][]Message "Successfully retrieved messages" + // @Failure 400 {object} ErrorResponse "Username must be provided" + // @Failure 500 {object} ErrorResponse "Failed to fetch channel messages" + // @Router /telegram/channel/{username}/messages [get] + v1.POST("/data/telegram/channel/messages", API.GetChannelMessagesHandler()) + // oauth tests // v1.GET("/data/discord/exchangetoken/:code", API.ExchangeDiscordTokenHandler()) @@ -377,6 +415,17 @@ func SetupRoutes(node *masa.OracleNode) *gin.Engine { // @Router /sentiment/tweets [post] v1.POST("/sentiment/discord", API.SearchDiscordMessagesAndAnalyzeSentiment()) + // @Summary Analyze Sentiment of Telegram Messages + // @Description Searches for Telegram messages and analyzes their sentiment + // @Tags Sentiment + // @Accept json + // @Produce json + // @Param query body string true "Search Query" + // @Success 200 {object} SentimentAnalysisResponse "Successfully analyzed sentiment of Telegram messages" + // @Failure 400 {object} ErrorResponse "Error analyzing sentiment of Telegram messages" + // @Router /sentiment/telegram [post] + v1.POST("/sentiment/telegram", API.SearchTelegramMessagesAndAnalyzeSentiment()) + // @Summary Analyze Sentiment of Web Content // @Description Searches for web content and analyzes its sentiment // @Tags Sentiment diff --git a/pkg/api/templates/index.html b/pkg/api/templates/index.html index 24030ed6..d689fce1 100644 --- a/pkg/api/templates/index.html +++ b/pkg/api/templates/index.html @@ -70,6 +70,22 @@
Node Information
{{end}} + + Telegram Scraper + {{if .IsTelegramScraper}} + + {{.IsTelegramScraper}} + + {{else}} + + {{.IsTelegramScraper}} + + {{end}} + Web Scraper {{if .IsWebScraper}} diff --git a/pkg/config/app.go b/pkg/config/app.go index 80973673..bba00661 100644 --- a/pkg/config/app.go +++ b/pkg/config/app.go @@ -8,6 +8,7 @@ import ( "strings" "sync" + "github.com/gotd/contrib/bg" "github.com/joho/godotenv" "github.com/sirupsen/logrus" "github.com/spf13/pflag" @@ -85,10 +86,13 @@ type AppConfig struct { GPTApiKey string `mapstructure:"gptApiKey"` TwitterScraper bool `mapstructure:"twitterScraper"` DiscordScraper bool `mapstructure:"discordScraper"` + TelegramScraper bool `mapstructure:"telegramScraper"` WebScraper bool `mapstructure:"webScraper"` LlmServer bool `mapstructure:"llmServer"` LLMChatUrl string `mapstructure:"llmChatUrl"` LLMCfUrl string `mapstructure:"llmCfUrl"` + + TelegramStop bg.StopFunc } // GetInstance returns the singleton instance of AppConfig. @@ -214,6 +218,7 @@ func (c *AppConfig) setCommandLineConfig() error { pflag.StringVar(&c.LLMCfUrl, "llmCfUrl", viper.GetString(LlmCfUrl), "URL for support LLM Cloudflare calls") pflag.BoolVar(&c.TwitterScraper, "twitterScraper", viper.GetBool(TwitterScraper), "TwitterScraper") pflag.BoolVar(&c.DiscordScraper, "discordScraper", viper.GetBool(DiscordScraper), "DiscordScraper") + pflag.BoolVar(&c.TelegramScraper, "telegramScraper", viper.GetBool(TelegramScraper), "TelegramScraper") pflag.BoolVar(&c.WebScraper, "webScraper", viper.GetBool(WebScraper), "WebScraper") pflag.BoolVar(&c.LlmServer, "llmServer", viper.GetBool(LlmServer), "Can service LLM requests") pflag.BoolVar(&c.Faucet, "faucet", viper.GetBool(Faucet), "Faucet") diff --git a/pkg/config/constants.go b/pkg/config/constants.go index e18639e1..f1e49b5f 100644 --- a/pkg/config/constants.go +++ b/pkg/config/constants.go @@ -118,6 +118,7 @@ const ( GPTApiKey = "OPENAI_API_KEY" TwitterScraper = "TWITTER_SCRAPER" DiscordScraper = "DISCORD_SCRAPER" + TelegramScraper = "TELEGRAM_SCRAPER" WebScraper = "WEB_SCRAPER" LlmServer = "LLM_SERVER" LlmChatUrl = "LLM_CHAT_URL" diff --git a/pkg/config/welcome.go b/pkg/config/welcome.go index b26d3010..968edd0e 100644 --- a/pkg/config/welcome.go +++ b/pkg/config/welcome.go @@ -4,7 +4,7 @@ import ( "fmt" ) -func DisplayWelcomeMessage(multiAddr, ipAddr, publicKeyHex string, isStaked bool, isValidator bool, isTwitterScraper bool, isDiscordScraper bool, isWebScraper bool, version string) { +func DisplayWelcomeMessage(multiAddr, ipAddr, publicKeyHex string, isStaked bool, isValidator bool, isTwitterScraper bool, isTelegramScraper bool, isDiscordScraper bool, isWebScraper bool, version string) { // ANSI escape code for yellow text yellow := "\033[33m" blue := "\033[34m" @@ -31,6 +31,7 @@ func DisplayWelcomeMessage(multiAddr, ipAddr, publicKeyHex string, isStaked bool fmt.Printf(blue+"%-20s %t\n"+reset, "Is Validator:", isValidator) fmt.Printf(blue+"%-20s %t\n"+reset, "Is TwitterScraper:", isTwitterScraper) fmt.Printf(blue+"%-20s %t\n"+reset, "Is DiscordScraper:", isDiscordScraper) + fmt.Printf(blue+"%-20s %t\n"+reset, "Is TelegramScraper:", isTelegramScraper) fmt.Printf(blue+"%-20s %t\n"+reset, "Is WebScraper:", isWebScraper) fmt.Println("") } diff --git a/pkg/llmbridge/sentiment.go b/pkg/llmbridge/sentiment.go index 69653c1c..504015f1 100644 --- a/pkg/llmbridge/sentiment.go +++ b/pkg/llmbridge/sentiment.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + "github.com/gotd/td/tg" "github.com/masa-finance/masa-oracle/pkg/config" twitterscraper "github.com/masa-finance/masa-twitter-scraper" "github.com/ollama/ollama/api" @@ -306,3 +307,87 @@ func AnalyzeSentimentDiscord(messages []string, model string, prompt string) (st return messagesContent, SanitizeResponse(sentimentSummary), nil } } + +// AnalyzeSentimentTelegram analyzes the sentiment of the provided Telegram messages by sending them to the sentiment analysis API. +func AnalyzeSentimentTelegram(messages []*tg.Message, model string, prompt string) (string, string, error) { + // Concatenate messages with a newline character + var messageTexts []string + for _, msg := range messages { + if msg != nil { + messageTexts = append(messageTexts, msg.Message) + } + } + messagesContent := strings.Join(messageTexts, "\n") + + // The rest of the code follows the same pattern as AnalyzeSentimentDiscord + if strings.Contains(model, "claude-") { + client := NewClaudeClient() // Adjusted to call without arguments + payloadBytes, err := CreatePayload(messagesContent, model, prompt) + if err != nil { + logrus.Errorf("Error creating payload: %v", err) + return "", "", err + } + resp, err := client.SendRequest(payloadBytes) + if err != nil { + logrus.Errorf("Error sending request to Claude API: %v", err) + return "", "", err + } + defer resp.Body.Close() + sentimentSummary, err := ParseResponse(resp) + if err != nil { + logrus.Errorf("Error parsing response from Claude: %v", err) + return "", "", err + } + return messagesContent, sentimentSummary, nil + + } else { + stream := false + + genReq := api.ChatRequest{ + Model: model, + Messages: []api.Message{ + {Role: "user", Content: messagesContent}, + {Role: "assistant", Content: prompt}, + }, + Stream: &stream, + Options: map[string]interface{}{ + "temperature": 0.0, + "seed": 42, + "num_ctx": 4096, + }, + } + + requestJSON, err := json.Marshal(genReq) + if err != nil { + logrus.Errorf("Error marshaling request JSON: %v", err) + return "", "", err + } + uri := config.GetInstance().LLMChatUrl + if uri == "" { + errMsg := "ollama api url not set" + logrus.Errorf(errMsg) + return "", "", errors.New(errMsg) + } + resp, err := http.Post(uri, "application/json", bytes.NewReader(requestJSON)) + if err != nil { + logrus.Errorf("Error sending request to API: %v", err) + return "", "", err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + logrus.Errorf("Error reading response body: %v", err) + return "", "", err + } + + var payload api.ChatResponse + err = json.Unmarshal(body, &payload) + if err != nil { + logrus.Errorf("Error unmarshaling response JSON: %v", err) + return "", "", err + } + + sentimentSummary := payload.Message.Content + return messagesContent, SanitizeResponse(sentimentSummary), nil + } +} diff --git a/pkg/oracle_node.go b/pkg/oracle_node.go index 3283639a..37c0658a 100644 --- a/pkg/oracle_node.go +++ b/pkg/oracle_node.go @@ -44,29 +44,30 @@ import ( ) type OracleNode struct { - Host host.Host - PrivKey *ecdsa.PrivateKey - Protocol protocol.ID - priorityAddrs multiaddr.Multiaddr - multiAddrs []multiaddr.Multiaddr - DHT *dht.IpfsDHT - Context context.Context - PeerChan chan myNetwork.PeerEvent - NodeTracker *pubsub2.NodeEventTracker - PubSubManager *pubsub2.Manager - Signature string - IsStaked bool - IsValidator bool - IsTwitterScraper bool - IsDiscordScraper bool - IsWebScraper bool - IsLlmServer bool - StartTime time.Time - WorkerTracker *pubsub2.WorkerEventTracker - BlockTracker *BlockEventTracker - ActorEngine *actor.RootContext - ActorRemote *remote.Remote - Blockchain *chain.Chain + Host host.Host + PrivKey *ecdsa.PrivateKey + Protocol protocol.ID + priorityAddrs multiaddr.Multiaddr + multiAddrs []multiaddr.Multiaddr + DHT *dht.IpfsDHT + Context context.Context + PeerChan chan myNetwork.PeerEvent + NodeTracker *pubsub2.NodeEventTracker + PubSubManager *pubsub2.Manager + Signature string + IsStaked bool + IsValidator bool + IsTwitterScraper bool + IsDiscordScraper bool + IsTelegramScraper bool + IsWebScraper bool + IsLlmServer bool + StartTime time.Time + WorkerTracker *pubsub2.WorkerEventTracker + BlockTracker *BlockEventTracker + ActorEngine *actor.RootContext + ActorRemote *remote.Remote + Blockchain *chain.Chain } // GetMultiAddrs returns the priority multiaddr for this node. @@ -170,23 +171,24 @@ func NewOracleNode(ctx context.Context, isStaked bool) (*OracleNode, error) { go r.Start() return &OracleNode{ - Host: hst, - PrivKey: masacrypto.KeyManagerInstance().EcdsaPrivKey, - Protocol: config.ProtocolWithVersion(config.OracleProtocol), - multiAddrs: myNetwork.GetMultiAddressesForHostQuiet(hst), - Context: ctx, - PeerChan: make(chan myNetwork.PeerEvent), - NodeTracker: pubsub2.NewNodeEventTracker(config.GetInstance().Version, cfg.Environment), - PubSubManager: subscriptionManager, - IsStaked: isStaked, - IsValidator: cfg.Validator, - IsTwitterScraper: cfg.TwitterScraper, - IsDiscordScraper: cfg.DiscordScraper, - IsWebScraper: cfg.WebScraper, - IsLlmServer: cfg.LlmServer, - ActorEngine: engine, - ActorRemote: r, - Blockchain: &chain.Chain{}, + Host: hst, + PrivKey: masacrypto.KeyManagerInstance().EcdsaPrivKey, + Protocol: config.ProtocolWithVersion(config.OracleProtocol), + multiAddrs: myNetwork.GetMultiAddressesForHostQuiet(hst), + Context: ctx, + PeerChan: make(chan myNetwork.PeerEvent), + NodeTracker: pubsub2.NewNodeEventTracker(config.Version, cfg.Environment), + PubSubManager: subscriptionManager, + IsStaked: isStaked, + IsValidator: cfg.Validator, + IsTwitterScraper: cfg.TwitterScraper, + IsDiscordScraper: cfg.DiscordScraper, + IsTelegramScraper: cfg.TelegramScraper, + IsWebScraper: cfg.WebScraper, + IsLlmServer: cfg.LlmServer, + ActorEngine: engine, + ActorRemote: r, + Blockchain: &chain.Chain{}, }, nil } @@ -231,6 +233,7 @@ func (node *OracleNode) Start() (err error) { nodeData.IsStaked = node.IsStaked nodeData.SelfIdentified = true nodeData.IsDiscordScraper = node.IsDiscordScraper + nodeData.IsTelegramScraper = node.IsTelegramScraper nodeData.IsTwitterScraper = node.IsTwitterScraper nodeData.IsWebScraper = node.IsWebScraper nodeData.IsValidator = node.IsValidator @@ -313,7 +316,7 @@ func (node *OracleNode) handleStream(stream network.Stream) { func (node *OracleNode) IsWorker() bool { // need to get this by node data cfg := config.GetInstance() - if cfg.TwitterScraper || cfg.DiscordScraper || cfg.WebScraper { + if cfg.TwitterScraper || cfg.DiscordScraper || cfg.TelegramScraper || cfg.WebScraper { return true } return false diff --git a/pkg/pubsub/node_data.go b/pkg/pubsub/node_data.go index 98053b33..f12e8a6c 100644 --- a/pkg/pubsub/node_data.go +++ b/pkg/pubsub/node_data.go @@ -62,6 +62,7 @@ type NodeData struct { IsValidator bool `json:"isValidator"` IsTwitterScraper bool `json:"isTwitterScraper"` IsDiscordScraper bool `json:"isDiscordScraper"` + IsTelegramScraper bool `json:"isTelegramScraper"` IsWebScraper bool `json:"isWebScraper"` Records any `json:"records,omitempty"` Version string `json:"version"` @@ -105,6 +106,12 @@ func (n *NodeData) DiscordScraper() bool { return n.IsDiscordScraper } +// TelegramScraper checks if the current node is configured as a Discord scraper. +// It retrieves the configuration instance and returns the value of the TelegramScraper field. +func (n *NodeData) TelegramScraper() bool { + return n.IsTelegramScraper +} + // WebScraper checks if the current node is configured as a Web scraper. // It retrieves the configuration instance and returns the value of the WebScraper field. func (n *NodeData) WebScraper() bool { @@ -208,15 +215,16 @@ func (n *NodeData) UpdateAccumulatedUptime() { func GetSelfNodeDataJson(host host.Host, isStaked bool) []byte { // Create and populate NodeData nodeData := NodeData{ - PeerId: host.ID(), - IsStaked: isStaked, - EthAddress: masacrypto.KeyManagerInstance().EthAddress, - IsTwitterScraper: config.GetInstance().TwitterScraper, - IsDiscordScraper: config.GetInstance().DiscordScraper, - IsWebScraper: config.GetInstance().WebScraper, - IsValidator: config.GetInstance().Validator, - IsActive: true, - Version: config.GetInstance().Version, + PeerId: host.ID(), + IsStaked: isStaked, + EthAddress: masacrypto.KeyManagerInstance().EthAddress, + IsTwitterScraper: config.GetInstance().TwitterScraper, + IsDiscordScraper: config.GetInstance().DiscordScraper, + IsTelegramScraper: config.GetInstance().TelegramScraper, + IsWebScraper: config.GetInstance().WebScraper, + IsValidator: config.GetInstance().Validator, + IsActive: true, + Version: config.Version, } // Convert NodeData to JSON diff --git a/pkg/pubsub/node_event_tracker.go b/pkg/pubsub/node_event_tracker.go index f8634d58..5a6ca099 100644 --- a/pkg/pubsub/node_event_tracker.go +++ b/pkg/pubsub/node_event_tracker.go @@ -302,6 +302,10 @@ func (net *NodeEventTracker) AddOrUpdateNodeData(nodeData *NodeData, forceGossip } dataChanged = true nd.IsStaked = nodeData.IsStaked + nd.IsDiscordScraper = nodeData.IsDiscordScraper + nd.IsTelegramScraper = nodeData.IsTelegramScraper + nd.IsTwitterScraper = nodeData.IsTwitterScraper + nd.IsWebScraper = nodeData.IsWebScraper nd.Records = nodeData.Records nd.Multiaddrs = nodeData.Multiaddrs nd.EthAddress = nodeData.EthAddress diff --git a/pkg/scrapers/telegram/getchannelmessages.go b/pkg/scrapers/telegram/getchannelmessages.go new file mode 100644 index 00000000..6e17eb99 --- /dev/null +++ b/pkg/scrapers/telegram/getchannelmessages.go @@ -0,0 +1,86 @@ +package telegram + +import ( + "context" + "fmt" + "log" + + "github.com/gotd/td/tg" + + "github.com/masa-finance/masa-oracle/pkg/llmbridge" +) + +// FetchChannelMessages Fetch messages from a group +func FetchChannelMessages(ctx context.Context, username string) ([]*tg.Message, error) { + // Initialize the Telegram client (if not already initialized) + client, err := GetClient() + if err != nil { + log.Printf("Failed to initialize Telegram client: %v", err) + return nil, err // Edit: Added nil as the first return value + } + + var messagesSlice []*tg.Message // Define a slice to hold the messages + + err = client.Run(ctx, func(ctx context.Context) error { + resolved, err := client.API().ContactsResolveUsername(ctx, username) + if err != nil { + return err + } + + channel := &tg.InputChannel{ + ChannelID: resolved.Chats[0].GetID(), + AccessHash: resolved.Chats[0].(*tg.Channel).AccessHash, + } + + fmt.Printf("Channel ID: %d, Access Hash: %d\n", channel.ChannelID, channel.AccessHash) + + inputPeer := &tg.InputPeerChannel{ // Use InputPeerChannel instead of InputChannel + ChannelID: channel.ChannelID, + AccessHash: channel.AccessHash, + } + result, err := client.API().MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{ + Peer: inputPeer, // Pass inputPeer here + Limit: 100, // Adjust the number of messages to fetch + }) + if err != nil { + return err + } + + // Type assert the result to *tg.MessagesChannelMessages to access Messages field + messages, ok := result.(*tg.MessagesChannelMessages) + if !ok { + return fmt.Errorf("unexpected type %T", result) + } + + // Process the messages + for _, m := range messages.Messages { + message, ok := m.(*tg.Message) // Type assert to *tg.Message + if !ok { + // Handle the case where the message is not a regular message (e.g., service message) + continue + } + messagesSlice = append(messagesSlice, message) // Append the message to the slice + } + + return nil + }) + + return messagesSlice, err // Return the slice of messages and any error +} + +// ScrapeTelegramMessagesForSentiment scrapes messages from a Telegram channel and analyzes their sentiment. +func ScrapeTelegramMessagesForSentiment(ctx context.Context, username string, model string, prompt string) (string, string, error) { + // Fetch messages from the Telegram channel + messages, err := FetchChannelMessages(ctx, username) + if err != nil { + return "", "", fmt.Errorf("error fetching messages from Telegram channel: %v", err) + } + + // Analyze the sentiment of the fetched messages + // Note: Ensure that llmbridge.AnalyzeSentimentTelegram is implemented and can handle the analysis + analysisPrompt, sentiment, err := llmbridge.AnalyzeSentimentTelegram(messages, model, prompt) + if err != nil { + return "", "", fmt.Errorf("error analyzing sentiment of Telegram messages: %v", err) + } + return analysisPrompt, sentiment, nil +} diff --git a/pkg/scrapers/telegram/telegram_client.go b/pkg/scrapers/telegram/telegram_client.go new file mode 100644 index 00000000..8bc42c8a --- /dev/null +++ b/pkg/scrapers/telegram/telegram_client.go @@ -0,0 +1,159 @@ +package telegram + +import ( + "context" + "crypto/rand" + "errors" + "log" + "os" + "path/filepath" + "strconv" + "sync" + + "github.com/gotd/contrib/bg" + "github.com/gotd/td/session" + "github.com/gotd/td/telegram" + "github.com/gotd/td/telegram/auth" + "github.com/gotd/td/tg" + "github.com/sirupsen/logrus" +) + +var ( + client *telegram.Client + once sync.Once + appID int // Your actual app ID + appHash string // Your actual app hash + sessionDir = filepath.Join(os.Getenv("HOME"), ".telegram-sessions") +) + +func GetClient() (*telegram.Client, error) { + var err error + + appID, err = strconv.Atoi(os.Getenv("TELEGRAM_APP_ID")) + if err != nil { + logrus.Fatalf("Invalid TELEGRAM_APP_ID: %v", err) + } + appHash = os.Getenv("TELEGRAM_APP_HASH") + if appHash == "" { + logrus.Fatal("TELEGRAM_APP_HASH must be set") + } + + // Ensure the session directory exists + if err = os.MkdirAll(sessionDir, 0700); err != nil { + logrus.Error(err) + return nil, err // Added return statement to handle the error + + } + + // Create a session storage + storage := &session.FileStorage{ + Path: filepath.Join(sessionDir, "session.json"), + } + + // Create a random seed for the client + seed := make([]byte, 32) + if _, err = rand.Read(seed); err != nil { + logrus.Error(err) + return nil, err // Added return statement to handle the error + + } + + // Initialize the Telegram client + client = telegram.NewClient(appID, appHash, telegram.Options{ + SessionStorage: storage, + }) + + return client, err +} + +// StartAuthentication sends the phone number to Telegram and requests a code. +func StartAuthentication(ctx context.Context, phoneNumber string) (string, error) { + // Initialize the Telegram client (if not already initialized) + client, err := GetClient() + if err != nil { + logrus.Errorf("Failed to initialize Telegram client: %v", err) + return "", err + } + + var phoneCodeHash string + + // Use client.Run to start the client and execute the SendCode method + err = client.Run(ctx, func(ctx context.Context) error { + // Call the SendCode method of the client to send the code to the user's Telegram app + sentCode, err := client.Auth().SendCode(ctx, phoneNumber, auth.SendCodeOptions{ + AllowFlashCall: true, + CurrentNumber: true, + }) + if err != nil { + log.Printf("Error sending code: %v", err) + return err + } + + log.Printf("Code sent successfully to: %s", phoneNumber) + + // Extract the phoneCodeHash from the sentCode object + switch code := sentCode.(type) { + case *tg.AuthSentCode: + phoneCodeHash = code.PhoneCodeHash + default: + return errors.New("unexpected type of AuthSentCode") + } + + return nil + }) + + if err != nil { + log.Printf("Failed to run client or send code: %v", err) + return "", err + } + + // Return the phoneCodeHash to be used in the next step + log.Printf("Authentication process started successfully for: %s", phoneNumber) + return phoneCodeHash, nil +} + +// CompleteAuthentication uses the provided code to authenticate with Telegram. +func CompleteAuthentication(ctx context.Context, phoneNumber, code, phoneCodeHash string) (*tg.AuthAuthorization, error) { + // Initialize the Telegram client (if not already initialized) + client, err := GetClient() + if err != nil { + logrus.Printf("Failed to initialize Telegram client: %v", err) + return nil, err // Edit: Added nil as the first return value + } + + // Define a variable to hold the authentication result + var authResult *tg.AuthAuthorization + // Use client.Run to start the client and execute the SignIn method + err = client.Run(ctx, func(ctx context.Context) error { + // Use the provided code and phoneCodeHash to authenticate + auth, err := client.Auth().SignIn(ctx, phoneNumber, code, phoneCodeHash) + if err != nil { + log.Printf("Error during SignIn: %v", err) + return err + } + + // At this point, authentication was successful, and you have the user's Telegram auth data. + authResult = auth + stop, err := bg.Connect(client) + if err != nil { + return err + } + defer func() { _ = stop() }() + + // Now you can use client. + if _, err := client.Auth().Status(ctx); err != nil { + return err + } + + return nil + }) + + if err != nil { + log.Printf("Failed to run client or sign in: %v", err) + return nil, err + } + + // You can now create a session for the user or perform other post-authentication tasks. + log.Printf("Authentication successful for: %s", phoneNumber) + return authResult, nil +} diff --git a/pkg/workers/handler.go b/pkg/workers/handler.go index 867a54c0..3c208c91 100644 --- a/pkg/workers/handler.go +++ b/pkg/workers/handler.go @@ -1,6 +1,7 @@ package workers import ( + "context" "encoding/json" "fmt" "strings" @@ -10,6 +11,7 @@ import ( masa "github.com/masa-finance/masa-oracle/pkg" "github.com/masa-finance/masa-oracle/pkg/config" "github.com/masa-finance/masa-oracle/pkg/scrapers/discord" + "github.com/masa-finance/masa-oracle/pkg/scrapers/telegram" "github.com/masa-finance/masa-oracle/pkg/scrapers/twitter" "github.com/masa-finance/masa-oracle/pkg/scrapers/web" "github.com/masa-finance/masa-oracle/pkg/workers/messages" @@ -98,6 +100,14 @@ func (a *Worker) HandleWork(ctx actor.Context, m *messages.Work, node *masa.Orac logrus.Infof("[+] Discord Channel Messages %s %s", m.Data, m.Sender) channelID := bodyData["channelID"].(string) _, resp, err = discord.ScrapeDiscordMessagesForSentiment(channelID, bodyData["model"].(string), bodyData["prompt"].(string)) + case string(WORKER.TelegramChannelMessages): + logrus.Infof("[+] Telegram Channel Messages %s %s", m.Data, m.Sender) + username := bodyData["username"].(string) + resp, err = telegram.FetchChannelMessages(context.Background(), username) // Removed the underscore placeholder + case string(WORKER.TelegramSentiment): + logrus.Infof("[+] Telegram Channel Messages %s %s", m.Data, m.Sender) + username := bodyData["username"].(string) + _, resp, err = telegram.ScrapeTelegramMessagesForSentiment(context.Background(), username, bodyData["model"].(string), bodyData["prompt"].(string)) case string(WORKER.DiscordGuildChannels): guildID := bodyData["guildID"].(string) resp, err = discord.GetGuildChannels(guildID) @@ -177,7 +187,7 @@ func (a *Worker) HandleWork(ctx actor.Context, m *messages.Work, node *masa.Orac return } cfg := config.GetInstance() - if cfg.TwitterScraper || cfg.DiscordScraper || cfg.WebScraper { + if cfg.TwitterScraper || cfg.DiscordScraper || cfg.TwitterScraper || cfg.WebScraper { ctx.Respond(&messages.Response{RequestId: workData["request_id"], Value: string(jsn)}) } for _, pid := range getPeers(node) { diff --git a/pkg/workers/workers.go b/pkg/workers/workers.go index bada06cf..d7ac48cc 100644 --- a/pkg/workers/workers.go +++ b/pkg/workers/workers.go @@ -29,41 +29,45 @@ import ( type WorkerType string const ( - Discord WorkerType = "discord" - DiscordProfile WorkerType = "discord-profile" - DiscordChannelMessages WorkerType = "discord-channel-messages" - DiscordSentiment WorkerType = "discord-sentiment" - DiscordGuildChannels WorkerType = "discord-guild-channels" - DiscordUserGuilds WorkerType = "discord-user-guilds" - LLMChat WorkerType = "llm-chat" - Twitter WorkerType = "twitter" - TwitterFollowers WorkerType = "twitter-followers" - TwitterProfile WorkerType = "twitter-profile" - TwitterSentiment WorkerType = "twitter-sentiment" - TwitterTrends WorkerType = "twitter-trends" - Web WorkerType = "web" - WebSentiment WorkerType = "web-sentiment" - Test WorkerType = "test" + Discord WorkerType = "discord" + DiscordProfile WorkerType = "discord-profile" + DiscordChannelMessages WorkerType = "discord-channel-messages" + DiscordSentiment WorkerType = "discord-sentiment" + TelegramSentiment WorkerType = "telegram-sentiment" + TelegramChannelMessages WorkerType = "telegram-channel-messages" + DiscordGuildChannels WorkerType = "discord-guild-channels" + DiscordUserGuilds WorkerType = "discord-user-guilds" + LLMChat WorkerType = "llm-chat" + Twitter WorkerType = "twitter" + TwitterFollowers WorkerType = "twitter-followers" + TwitterProfile WorkerType = "twitter-profile" + TwitterSentiment WorkerType = "twitter-sentiment" + TwitterTrends WorkerType = "twitter-trends" + Web WorkerType = "web" + WebSentiment WorkerType = "web-sentiment" + Test WorkerType = "test" ) var WORKER = struct { - Discord, DiscordProfile, DiscordChannelMessages, DiscordSentiment, DiscordGuildChannels, DiscordUserGuilds, LLMChat, Twitter, TwitterFollowers, TwitterProfile, TwitterSentiment, TwitterTrends, Web, WebSentiment, Test WorkerType + Discord, DiscordProfile, DiscordChannelMessages, DiscordSentiment, TelegramSentiment, TelegramChannelMessages, DiscordGuildChannels, DiscordUserGuilds, LLMChat, Twitter, TwitterFollowers, TwitterProfile, TwitterSentiment, TwitterTrends, Web, WebSentiment, Test WorkerType }{ - Discord: Discord, - DiscordProfile: DiscordProfile, - DiscordChannelMessages: DiscordChannelMessages, - DiscordSentiment: DiscordSentiment, - DiscordGuildChannels: DiscordGuildChannels, - DiscordUserGuilds: DiscordUserGuilds, - LLMChat: LLMChat, - Twitter: Twitter, - TwitterFollowers: TwitterFollowers, - TwitterProfile: TwitterProfile, - TwitterSentiment: TwitterSentiment, - TwitterTrends: TwitterTrends, - Web: Web, - WebSentiment: WebSentiment, - Test: Test, + Discord: Discord, + DiscordProfile: DiscordProfile, + DiscordChannelMessages: DiscordChannelMessages, + DiscordSentiment: DiscordSentiment, + TelegramSentiment: TelegramSentiment, + TelegramChannelMessages: TelegramChannelMessages, + DiscordGuildChannels: DiscordGuildChannels, + DiscordUserGuilds: DiscordUserGuilds, + LLMChat: LLMChat, + Twitter: Twitter, + TwitterFollowers: TwitterFollowers, + TwitterProfile: TwitterProfile, + TwitterSentiment: TwitterSentiment, + TwitterTrends: TwitterTrends, + Web: Web, + WebSentiment: WebSentiment, + Test: Test, } var ( @@ -235,7 +239,8 @@ func SendWork(node *masa.OracleNode, m *pubsub2.Message) { for _, p := range peers { for _, addr := range p.Multiaddrs { ipAddr, _ := addr.ValueForProtocol(multiaddr.P_IP4) - if p.IsStaked && (p.IsTwitterScraper || p.IsWebScraper || p.IsDiscordScraper) { + if p.IsStaked && (p.IsTwitterScraper || p.IsWebScraper || p.IsDiscordScraper || p.IsTelegramScraper) { + logrus.Infof("[+] Worker Address: %s", ipAddr) wg.Add(1) go func(p pubsub.NodeData) { defer wg.Done()