From ae50b1462c738f7b998e4d9589e19773a2df2daa Mon Sep 17 00:00:00 2001 From: Xevion Date: Tue, 26 Aug 2025 10:45:50 -0500 Subject: [PATCH] feat: implement resty http client, simplified request building, session timer middleware --- cmd/banner/main.go | 23 +- go.mod | 35 ++- go.sum | 99 +++------ internal/api/api.go | 453 ++++++++++++-------------------------- internal/api/session.go | 16 +- internal/config/config.go | 8 +- internal/utils/helpers.go | 56 ++--- 7 files changed, 235 insertions(+), 455 deletions(-) diff --git a/cmd/banner/main.go b/cmd/banner/main.go index ae6bdfa..51a543e 100644 --- a/cmd/banner/main.go +++ b/cmd/banner/main.go @@ -20,7 +20,7 @@ import ( "github.com/rs/zerolog/log" "github.com/rs/zerolog/pkgerrors" "github.com/samber/lo" - "golang.org/x/text/message" + "resty.dev/v3" "banner/internal/api" "banner/internal/bot" @@ -30,7 +30,6 @@ import ( var ( Session *discordgo.Session - p *message.Printer = message.NewPrinter(message.MatchLanguage("en")) ) const ( @@ -175,14 +174,16 @@ func main() { log.Err(err).Msg("Cannot create cookie jar") } - // Create client with timeout, setup session (acquire cookies) - client := &http.Client{ - Jar: cookies, - Timeout: 30 * time.Second, - } - cfg.SetClient(client) - + // Create Resty client with timeout and cookie jar baseURL := os.Getenv("BANNER_BASE_URL") + client := resty.New(). + SetBaseURL(baseURL). + SetTimeout(30*time.Second). + SetCookieJar(cookies). + SetHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"). + AddResponseMiddleware(api.SessionMiddleware) + + cfg.SetClient(client) cfg.SetBaseURL(baseURL) apiInstance := api.New(cfg) @@ -275,9 +276,9 @@ func main() { } }() - // Close session, ensure http client closes idle connections + // Close session, ensure Resty client closes defer session.Close() - defer client.CloseIdleConnections() + defer client.Close() // Setup signal handler channel stop := make(chan os.Signal, 1) diff --git a/go.mod b/go.mod index 1234686..db9638a 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,27 @@ module banner -go 1.21 +go 1.24.0 -require github.com/bwmarrin/discordgo v0.27.1 +toolchain go1.24.2 require ( + github.com/bwmarrin/discordgo v0.29.0 github.com/joho/godotenv v1.5.1 github.com/pkg/errors v0.9.1 - github.com/redis/go-redis/v9 v9.3.1 - github.com/rs/zerolog v1.31.0 - github.com/samber/lo v1.39.0 - golang.org/x/text v0.14.0 + github.com/redis/go-redis/v9 v9.12.1 + github.com/rs/zerolog v1.34.0 + github.com/samber/lo v1.51.0 + resty.dev/v3 v3.0.0-beta.3 ) require ( - github.com/arran4/golang-ical v0.2.3 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect -) - -require ( - github.com/gorilla/websocket v1.5.1 // fndirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index b553d4c..aa0edf1 100644 --- a/go.sum +++ b/go.sum @@ -1,89 +1,52 @@ -github.com/arran4/golang-ical v0.2.3 h1:C4Vj7+BjJBIrAJhHgi6Ku+XUkQVugRq4re5Cqj5QVdE= -github.com/arran4/golang-ical v0.2.3/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ= -github.com/bwmarrin/discordgo v0.27.0 h1:4ZK9KN+rGIxZ0fdGTmgdCcliQeW8Zhu6MnlFI92nf0Q= -github.com/bwmarrin/discordgo v0.27.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= -github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= -github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ= -github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno= +github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= -github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= -github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a h1:45JtCyuNYE+QN9aPuR1ID9++BQU+NMTMudHSuaK0Las= -github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a/go.mod h1:RVHtZuvrpETIepiNUrNlih2OynoFf1eM6DGC6dloXzk= -github.com/juju/persistent-cookiejar v1.0.0 h1:Ag7+QLzqC2m+OYXy2QQnRjb3gTkEBSZagZ6QozwT3EQ= -github.com/juju/persistent-cookiejar v1.0.0/go.mod h1:zrbmo4nBKaiP/Ez3F67ewkMbzGYfXyMvRtbOfuAwG0w= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis v6.15.9+incompatible h1:F+tnlesQSl3h9V8DdmtcYFdvkHLhbb7AgcLW6UJxnC4= -github.com/redis/go-redis v6.15.9+incompatible/go.mod h1:ic6dLmR0d9rkHSzaa0Ab3QVRZcjopJ9hSSPCrecj/+s= -github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds= -github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= -github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= +github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= -gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= -gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs= -gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E= +resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4= diff --git a/internal/api/api.go b/internal/api/api.go index 2676586..523cb18 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -4,13 +4,10 @@ import ( "banner/internal/config" "banner/internal/models" "banner/internal/utils" - "bytes" "context" "encoding/json" "errors" "fmt" - "io" - "net/http" "net/url" "strconv" "strings" @@ -18,9 +15,9 @@ import ( "time" "github.com/redis/go-redis/v9" - "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/samber/lo" + "resty.dev/v3" ) type API struct { @@ -37,13 +34,18 @@ var ( expiryTime = 25 * time.Minute ) -// ResetSessionTimer resets the session timer to the current time. -// This is only used by the DoRequest handler when Banner API calls are detected, which would reset the session timer. -func ResetSessionTimer() { - // Only reset the session time if the session is still valid - if time.Since(sessionTime) <= expiryTime { - sessionTime = time.Now() +// SessionMiddleware creates a Resty middleware that resets the session timer on each Banner API call. +func SessionMiddleware(c *resty.Client, r *resty.Response) error { + // log.Debug().Str("url", r.Request.RawRequest.URL.Path).Msg("Session middleware") + + // Reset session timer on successful requests to Banner API endpoints + if r.IsSuccess() && strings.HasPrefix(r.Request.RawRequest.URL.Path, "StudentRegistrationSsb/ssb/classSearch/") { + // Only reset the session time if the session is still valid + if time.Since(sessionTime) <= expiryTime { + sessionTime = time.Now() + } } + return nil } // GenerateSession generates a new session ID (nonce) for use with the Banner API. @@ -52,64 +54,6 @@ func GenerateSession() string { return utils.RandomString(5) + utils.Nonce() } -// DoRequest performs & logs the request, logging and returning the response -func (a *API) DoRequest(req *http.Request) (*http.Response, error) { - headerSize := 0 - for key, values := range req.Header { - for _, value := range values { - headerSize += len(key) - headerSize += len(value) - } - } - - bodySize := int64(0) - if req.Body != nil { - bodySize, _ = io.Copy(io.Discard, req.Body) - } - - size := zerolog.Dict().Int64("body", bodySize).Int("header", headerSize).Int("url", len(req.URL.String())) - - log.Debug(). - Dict("size", size). - Str("method", strings.TrimRight(req.Method, " ")). - Str("url", req.URL.String()). - Str("query", req.URL.RawQuery). - Str("content-type", req.Header.Get("Content-Type")). - Msg("Request") - - // Create a timeout context for this specific request - ctx, cancel := context.WithTimeout(req.Context(), 15*time.Second) - defer cancel() - - // Clone the request with the timeout context - reqWithTimeout := req.Clone(ctx) - - res, err := a.config.Client.Do(reqWithTimeout) - - if err != nil { - log.Err(err).Stack().Str("method", req.Method).Msg("Request Failed") - } else { - contentLengthHeader := res.Header.Get("Content-Length") - contentLength := int64(-1) - - // If this request was a Banner API request, reset the session timer - if strings.HasPrefix(req.URL.Path, "StudentRegistrationSsb/ssb/classSearch/") { - ResetSessionTimer() - } - - // Get the content length - if contentLengthHeader != "" { - contentLength, err = strconv.ParseInt(contentLengthHeader, 10, 64) - if err != nil { - contentLength = -1 - } - } - - log.Debug().Int("status", res.StatusCode).Int64("content-length", contentLength).Strs("content-type", res.Header["Content-Type"]).Msg("Response") - } - return res, err -} - var terms []BannerTerm var lastTermUpdate time.Time @@ -183,46 +127,25 @@ func (a *API) GetTerms(search string, page int, maxResults int) ([]BannerTerm, e return nil, errors.New("offset must be greater than 0") } - req := utils.BuildRequest(a.config, "GET", "/classSearch/getTerms", map[string]string{ - "searchTerm": search, - // Page vs Offset is not a mistake here, the API uses "offset" as the page number - "offset": strconv.Itoa(page), - "max": strconv.Itoa(maxResults), - "_": utils.Nonce(), - }) + req := a.config.Client.NewRequest(). + SetQueryParam("searchTerm", search). + SetQueryParam("offset", strconv.Itoa(page)). + SetQueryParam("max", strconv.Itoa(maxResults)). + SetQueryParam("_", utils.Nonce()). + SetExpectResponseContentType("application/json"). + SetResult(&[]BannerTerm{}) - if page <= 0 { - return nil, errors.New("Offset must be greater than 0") - } - - res, err := a.DoRequest(req) + res, err := req.Get("/classSearch/getTerms") if err != nil { return nil, fmt.Errorf("failed to get terms: %w", err) } - // Assert that the response is JSON - if contentType := res.Header.Get("Content-Type"); !strings.Contains(contentType, models.JsonContentType) { - return nil, &utils.UnexpectedContentTypeError{ - Expected: models.JsonContentType, - Actual: contentType, - } + terms, ok := res.Result().(*[]BannerTerm) + if !ok { + return nil, fmt.Errorf("terms parsing failed to cast: %v", res.Result()) } - // print the response body - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - terms := make([]BannerTerm, 0, 10) - err = json.Unmarshal(body, &terms) - - if err != nil { - return nil, fmt.Errorf("failed to parse terms: %w", err) - } - - return terms, nil + return *terms, nil } // SelectTerm selects the given term in the Banner system. @@ -237,45 +160,33 @@ func (a *API) SelectTerm(term string, sessionID string) error { "uniqueSessionId": {sessionID}, } - params := map[string]string{ - "mode": "search", + type RedirectResponse struct { + FwdURL string `json:"fwdUrl"` } - req := utils.BuildRequestWithBody(a.config, "POST", "/term/search", params, bytes.NewBufferString(form.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req := a.config.Client.NewRequest(). + SetResult(&RedirectResponse{}). + SetQueryParam("mode", "search"). + SetBody(form.Encode()). + SetExpectResponseContentType("application/json"). + SetHeader("Content-Type", "application/x-www-form-urlencoded") - res, err := a.DoRequest(req) + res, err := req.Post("/term/search") if err != nil { return fmt.Errorf("failed to select term: %w", err) } - // Assert that the response is JSON - if !utils.ContentTypeMatch(res, "application/json") { - return fmt.Errorf("response was not JSON: %s", res.Header.Get("Content-Type")) - } + redirectResponse := res.Result().(*RedirectResponse) - // Acquire fwdUrl - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %w", err) - } - - var redirectResponse struct { - FwdURL string `json:"fwdUrl"` - } - json.Unmarshal(body, &redirectResponse) + // TODO: Mild validation to ensure the redirect is appropriate // Make a GET request to the fwdUrl - req = utils.BuildRequest(a.config, "GET", redirectResponse.FwdURL, nil) - res, err = a.DoRequest(req) - if err != nil { - return fmt.Errorf("failed to follow redirect: %w", err) - } + req = a.config.Client.NewRequest() + res, err = req.Get(redirectResponse.FwdURL) // Assert that the response is OK (200) - if res.StatusCode != 200 { - return fmt.Errorf("redirect response was not 200: %d", res.StatusCode) + if res.StatusCode() != 200 { + return fmt.Errorf("redirect response was not OK: %d", res.StatusCode()) } return nil @@ -289,38 +200,27 @@ func (a *API) GetPartOfTerms(search string, term int, offset int, maxResults int return nil, errors.New("offset must be greater than 0") } - req := utils.BuildRequest(a.config, "GET", "/classSearch/get_partOfTerm", map[string]string{ - "searchTerm": search, - "term": strconv.Itoa(term), - "offset": strconv.Itoa(offset), - "max": strconv.Itoa(maxResults), - "uniqueSessionId": a.EnsureSession(), - "_": utils.Nonce(), - }) + req := a.config.Client.NewRequest(). + SetQueryParam("searchTerm", search). + SetQueryParam("term", strconv.Itoa(term)). + SetQueryParam("offset", strconv.Itoa(offset)). + SetQueryParam("max", strconv.Itoa(maxResults)). + SetQueryParam("uniqueSessionId", a.EnsureSession()). + SetQueryParam("_", utils.Nonce()). + SetExpectResponseContentType("application/json"). + SetResult(&[]BannerTerm{}) - res, err := a.DoRequest(req) + res, err := req.Get("/classSearch/get_partOfTerm") if err != nil { return nil, fmt.Errorf("failed to get part of terms: %w", err) } - // Assert that the response is JSON - if !utils.ContentTypeMatch(res, "application/json") { - log.Panic().Stack().Str("content-type", res.Header.Get("Content-Type")).Msg("Response was not JSON") + terms, ok := res.Result().(*[]BannerTerm) + if !ok { + return nil, fmt.Errorf("term parsing failed to cast: %v", res.Result()) } - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - terms := make([]BannerTerm, 0, 10) - err = json.Unmarshal(body, &terms) - if err != nil { - return nil, fmt.Errorf("failed to parse part of terms: %w", err) - } - - return terms, nil + return *terms, nil } // GetInstructors retrieves and parses the instructor information for a given search term. @@ -333,38 +233,27 @@ func (a *API) GetInstructors(search string, term string, offset int, maxResults return nil, errors.New("offset must be greater than 0") } - req := utils.BuildRequest(a.config, "GET", "/classSearch/get_instructor", map[string]string{ - "searchTerm": search, - "term": term, - "offset": strconv.Itoa(offset), - "max": strconv.Itoa(maxResults), - "uniqueSessionId": a.EnsureSession(), - "_": utils.Nonce(), - }) + req := a.config.Client.NewRequest(). + SetQueryParam("searchTerm", search). + SetQueryParam("term", term). + SetQueryParam("offset", strconv.Itoa(offset)). + SetQueryParam("max", strconv.Itoa(maxResults)). + SetQueryParam("uniqueSessionId", a.EnsureSession()). + SetQueryParam("_", utils.Nonce()). + SetExpectResponseContentType("application/json"). + SetResult(&[]Instructor{}) - res, err := a.DoRequest(req) + res, err := req.Get("/classSearch/get_instructor") if err != nil { return nil, fmt.Errorf("failed to get instructors: %w", err) } - // Assert that the response is JSON - if !utils.ContentTypeMatch(res, "application/json") { - log.Fatal().Stack().Str("content-type", res.Header.Get("Content-Type")).Msg("Response was not JSON") + instructors, ok := res.Result().(*[]Instructor) + if !ok { + return nil, fmt.Errorf("instructor parsing failed to cast: %v", res.Result()) } - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - instructors := make([]Instructor, 0, 10) - err = json.Unmarshal(body, &instructors) - if err != nil { - return nil, fmt.Errorf("failed to parse instructors: %w", err) - } - - return instructors, nil + return *instructors, nil } // ClassDetails represents the details of a course. @@ -372,7 +261,7 @@ func (a *API) GetInstructors(search string, term string, offset int, maxResults type ClassDetails struct { } -func (a *API) GetCourseDetails(term int, crn int) *ClassDetails { +func (a *API) GetCourseDetails(term int, crn int) (*ClassDetails, error) { body, err := json.Marshal(map[string]string{ "term": strconv.Itoa(term), "courseReferenceNumber": strconv.Itoa(crn), @@ -381,19 +270,23 @@ func (a *API) GetCourseDetails(term int, crn int) *ClassDetails { if err != nil { log.Fatal().Stack().Err(err).Msg("Failed to marshal body") } - req := utils.BuildRequestWithBody(a.config, "GET", "/searchResults/getClassDetails", nil, bytes.NewBuffer(body)) - res, err := a.DoRequest(req) + req := a.config.Client.NewRequest(). + SetBody(body). + SetExpectResponseContentType("application/json"). + SetResult(&ClassDetails{}) + + res, err := req.Get("/searchResults/getClassDetails") if err != nil { - return nil + return nil, fmt.Errorf("failed to get course details: %w", err) } - // Assert that the response is JSON - if !utils.ContentTypeMatch(res, "application/json") { - log.Fatal().Stack().Str("content-type", res.Header.Get("Content-Type")).Msg("Response was not JSON") + details, ok := res.Result().(*ClassDetails) + if !ok { + return nil, fmt.Errorf("course details parsing failed to cast: %v", res.Result()) } - return &ClassDetails{} + return details, nil } // Search invokes a search on the Banner system with the given query and returns the results. @@ -411,37 +304,22 @@ func (a *API) Search(term string, query *Query, sort string, sortDescending bool params["startDatepicker"] = "" params["endDatepicker"] = "" - req := utils.BuildRequest(a.config, "GET", "/searchResults/searchResults", params) + req := a.config.Client.NewRequest(). + SetQueryParams(params). + SetExpectResponseContentType("application/json"). + SetResult(&models.SearchResult{}) - res, err := a.DoRequest(req) + res, err := req.Get("/searchResults/searchResults") if err != nil { return nil, fmt.Errorf("failed to search: %w", err) } - if res.StatusCode != 200 { - return nil, fmt.Errorf("search failed with status code: %d", res.StatusCode) + searchResult, ok := res.Result().(*models.SearchResult) + if !ok { + return nil, fmt.Errorf("search result parsing failed to cast: %v", res.Result()) } - // Assert that the response is JSON - if !utils.ContentTypeMatch(res, "application/json") { - // for server 500 errors, parse for the error with '#dialog-message > div.message' - log.Error().Stack().Str("content-type", res.Header.Get("Content-Type")).Msg("Response was not JSON") - } - - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - var result models.SearchResult - err = json.Unmarshal(body, &result) - - if err != nil { - return nil, fmt.Errorf("failed to parse search results: %w", err) - } - - return &result, nil + return searchResult, nil } // GetSubjects retrieves and parses the subject information for a given search term. @@ -453,38 +331,27 @@ func (a *API) GetSubjects(search string, term string, offset int, maxResults int return nil, errors.New("offset must be greater than 0") } - req := utils.BuildRequest(a.config, "GET", "/classSearch/get_subject", map[string]string{ - "searchTerm": search, - "term": term, - "offset": strconv.Itoa(offset), - "max": strconv.Itoa(maxResults), - "uniqueSessionId": a.EnsureSession(), - "_": utils.Nonce(), - }) + req := a.config.Client.NewRequest(). + SetQueryParam("searchTerm", search). + SetQueryParam("term", term). + SetQueryParam("offset", strconv.Itoa(offset)). + SetQueryParam("max", strconv.Itoa(maxResults)). + SetQueryParam("uniqueSessionId", a.EnsureSession()). + SetQueryParam("_", utils.Nonce()). + SetExpectResponseContentType("application/json"). + SetResult(&[]Pair{}) - res, err := a.DoRequest(req) + res, err := req.Get("/classSearch/get_subject") if err != nil { return nil, fmt.Errorf("failed to get subjects: %w", err) } - // Assert that the response is JSON - if !utils.ContentTypeMatch(res, "application/json") { - log.Fatal().Stack().Str("content-type", res.Header.Get("Content-Type")).Msg("Response was not JSON") + subjects, ok := res.Result().(*[]Pair) + if !ok { + return nil, fmt.Errorf("subjects parsing failed to cast: %v", res.Result()) } - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - subjects := make([]Pair, 0, 10) - err = json.Unmarshal(body, &subjects) - if err != nil { - return nil, fmt.Errorf("failed to parse subjects: %w", err) - } - - return subjects, nil + return *subjects, nil } // GetCampuses retrieves and parses the campus information for a given search term. @@ -497,38 +364,27 @@ func (a *API) GetCampuses(search string, term int, offset int, maxResults int) ( return nil, errors.New("offset must be greater than 0") } - req := utils.BuildRequest(a.config, "GET", "/classSearch/get_campus", map[string]string{ - "searchTerm": search, - "term": strconv.Itoa(term), - "offset": strconv.Itoa(offset), - "max": strconv.Itoa(maxResults), - "uniqueSessionId": a.EnsureSession(), - "_": utils.Nonce(), - }) + req := a.config.Client.NewRequest(). + SetQueryParam("searchTerm", search). + SetQueryParam("term", strconv.Itoa(term)). + SetQueryParam("offset", strconv.Itoa(offset)). + SetQueryParam("max", strconv.Itoa(maxResults)). + SetQueryParam("uniqueSessionId", a.EnsureSession()). + SetQueryParam("_", utils.Nonce()). + SetExpectResponseContentType("application/json"). + SetResult(&[]Pair{}) - res, err := a.DoRequest(req) + res, err := req.Get("/classSearch/get_campus") if err != nil { return nil, fmt.Errorf("failed to get campuses: %w", err) } - // Assert that the response is JSON - if !utils.ContentTypeMatch(res, "application/json") { - log.Fatal().Stack().Str("content-type", res.Header.Get("Content-Type")).Msg("Response was not JSON") + campuses, ok := res.Result().(*[]Pair) + if !ok { + return nil, fmt.Errorf("campuses parsing failed to cast: %v", res.Result()) } - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - campuses := make([]Pair, 0, 10) - err = json.Unmarshal(body, &campuses) - if err != nil { - return nil, fmt.Errorf("failed to parse campuses: %w", err) - } - - return campuses, nil + return *campuses, nil } // GetInstructionalMethods retrieves and parses the instructional method information for a given search term. @@ -541,79 +397,56 @@ func (a *API) GetInstructionalMethods(search string, term string, offset int, ma return nil, errors.New("offset must be greater than 0") } - req := utils.BuildRequest(a.config, "GET", "/classSearch/get_instructionalMethod", map[string]string{ - "searchTerm": search, - "term": term, - "offset": strconv.Itoa(offset), - "max": strconv.Itoa(maxResults), - "uniqueSessionId": a.EnsureSession(), - "_": utils.Nonce(), - }) + req := a.config.Client.NewRequest(). + SetQueryParam("searchTerm", search). + SetQueryParam("term", term). + SetQueryParam("offset", strconv.Itoa(offset)). + SetQueryParam("max", strconv.Itoa(maxResults)). + SetQueryParam("uniqueSessionId", a.EnsureSession()). + SetQueryParam("_", utils.Nonce()). + SetExpectResponseContentType("application/json"). + SetResult(&[]Pair{}) - res, err := a.DoRequest(req) + res, err := req.Get("/classSearch/get_instructionalMethod") if err != nil { return nil, fmt.Errorf("failed to get instructional methods: %w", err) } - // Assert that the response is JSON - if !utils.ContentTypeMatch(res, "application/json") { - log.Fatal().Stack().Str("content-type", res.Header.Get("Content-Type")).Msg("Response was not JSON") + methods, ok := res.Result().(*[]Pair) + if !ok { + return nil, fmt.Errorf("instructional methods parsing failed to cast: %v", res.Result()) } - - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - methods := make([]Pair, 0, 10) - err = json.Unmarshal(body, &methods) - if err != nil { - return nil, fmt.Errorf("failed to parse instructional methods: %w", err) - } - - return methods, nil + return *methods, nil } // GetCourseMeetingTime retrieves the meeting time information for a course based on the given term and course reference number (CRN). // It makes an HTTP GET request to the appropriate API endpoint and parses the response to extract the meeting time data. // The function returns a MeetingTimeResponse struct containing the extracted information. func (a *API) GetCourseMeetingTime(term int, crn int) ([]models.MeetingTimeResponse, error) { - req := utils.BuildRequest(a.config, "GET", "/searchResults/getFacultyMeetingTimes", map[string]string{ - "term": strconv.Itoa(term), - "courseReferenceNumber": strconv.Itoa(crn), - }) + req := a.config.Client.NewRequest(). + SetQueryParam("term", strconv.Itoa(term)). + SetQueryParam("courseReferenceNumber", strconv.Itoa(crn)). + SetExpectResponseContentType("application/json"). + SetResult(&[]models.MeetingTimeResponse{}) - res, err := a.DoRequest(req) + res, err := req.Get("/searchResults/getFacultyMeetingTimes") if err != nil { return nil, fmt.Errorf("failed to get meeting time: %w", err) } - // Assert that the response is JSON - if !utils.ContentTypeMatch(res, "application/json") { - log.Fatal().Stack().Str("content-type", res.Header.Get("Content-Type")).Msg("Response was not JSON") + meetingTimes, ok := res.Result().(*[]models.MeetingTimeResponse) + if !ok { + return nil, fmt.Errorf("meeting times parsing failed to cast: %v", res.Result()) } - // Read the response body into JSON - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - // Parse the JSON into a MeetingTimeResponse struct - var meetingTime struct { - Inner []models.MeetingTimeResponse `json:"fmt"` - } - err = json.Unmarshal(body, &meetingTime) - if err != nil { - return nil, fmt.Errorf("failed to parse meeting time: %w", err) - } - - return meetingTime.Inner, nil + return *meetingTimes, nil } // ResetDataForm makes a POST request that needs to be made upon before new search requests can be made. func (a *API) ResetDataForm() { - req := utils.BuildRequest(a.config, "POST", "/classSearch/resetDataForm", nil) - _, err := a.DoRequest(req) + req := a.config.Client.NewRequest() + + _, err := req.Post("/classSearch/resetDataForm") if err != nil { log.Fatal().Stack().Err(err).Msg("Failed to reset data form") } diff --git a/internal/api/session.go b/internal/api/session.go index 8b7586f..7de12f1 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -17,8 +17,18 @@ func (a *API) Setup() { } for _, path := range requestQueue { - req := utils.BuildRequest(a.config, "GET", path, nil) - a.DoRequest(req) + req := a.config.Client.NewRequest(). + SetQueryParam("_", utils.Nonce()). + SetExpectResponseContentType("application/json") + + res, err := req.Get(path) + if err != nil { + log.Fatal().Stack().Str("path", path).Err(err).Msg("Failed to make request") + } + + if res.StatusCode() != 200 { + log.Fatal().Stack().Str("path", path).Int("status", res.StatusCode()).Msg("Failed to make request") + } } // Validate that cookies were set @@ -27,7 +37,7 @@ func (a *API) Setup() { log.Fatal().Stack().Str("baseURL", a.config.BaseURL).Err(err).Msg("Failed to parse baseURL") } - currentCookies := a.config.Client.Jar.Cookies(baseURLParsed) + currentCookies := a.config.Client.CookieJar().Cookies(baseURLParsed) requiredCookies := map[string]bool{ "JSESSIONID": false, "SSB_COOKIE": false, diff --git a/internal/config/config.go b/internal/config/config.go index 084b0fa..291d448 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,17 +2,17 @@ package config import ( "context" - "net/http" "time" "github.com/redis/go-redis/v9" + "resty.dev/v3" ) type Config struct { Ctx context.Context CancelFunc context.CancelFunc KV *redis.Client - Client *http.Client + Client *resty.Client IsDevelopment bool BaseURL string Environment string @@ -50,8 +50,8 @@ func (c *Config) SetEnvironment(env string) { c.IsDevelopment = env == "development" } -// SetClient sets the HTTP client -func (c *Config) SetClient(client *http.Client) { +// SetClient sets the Resty client +func (c *Config) SetClient(client *resty.Client) { c.Client = client } diff --git a/internal/utils/helpers.go b/internal/utils/helpers.go index 1db077f..69c9815 100644 --- a/internal/utils/helpers.go +++ b/internal/utils/helpers.go @@ -16,6 +16,7 @@ import ( "github.com/bwmarrin/discordgo" "github.com/rs/zerolog" log "github.com/rs/zerolog/log" + "resty.dev/v3" "banner/internal/config" ) @@ -40,47 +41,18 @@ func ParseOptions(options []*discordgo.ApplicationCommandInteractionDataOption) return optionMap } -// BuildRequestWithBody builds a request with the given method, path, parameters, and body -func BuildRequestWithBody(cfg *config.Config, method string, path string, params map[string]string, body io.Reader) *http.Request { - // Builds a URL for the given path and parameters - requestUrl := cfg.BaseURL + path - - if params != nil { - takenFirst := false - for key, value := range params { - paramChar := "&" - if !takenFirst { - paramChar = "?" - takenFirst = true - } - - requestUrl += paramChar + url.QueryEscape(key) + "=" + url.QueryEscape(value) - } - } - - request, _ := http.NewRequestWithContext(cfg.Ctx, method, requestUrl, body) - AddUserAgent(request) - return request -} - -// BuildRequest builds a request with the given method, path, and parameters and an empty body -func BuildRequest(cfg *config.Config, method string, path string, params map[string]string) *http.Request { - return BuildRequestWithBody(cfg, method, path, params, nil) -} - // AddUserAgent adds a false but consistent user agent to the request func AddUserAgent(req *http.Request) { req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36") } -// ContentTypeMatch checks if the response has the given content type -func ContentTypeMatch(response *http.Response, search string) bool { - contentType := response.Header.Get("Content-Type") +// ContentTypeMatch checks if the Resty response has the given content type +func ContentTypeMatch(res *resty.Response, expectedContentType string) bool { + contentType := res.Header().Get("Content-Type") if contentType == "" { - return search == "application/octect-stream" + return expectedContentType == "application/octect-stream" } - - return strings.HasPrefix(contentType, search) + return strings.HasPrefix(contentType, expectedContentType) } const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -280,9 +252,9 @@ func GuessExtension(contentType string) string { return ext } -// DumpResponse dumps a response body to a file for debugging purposes -func DumpResponse(res *http.Response) { - contentType := res.Header.Get("Content-Type") +// DumpResponse dumps a Resty response body to a file for debugging purposes +func DumpResponse(res *resty.Response) { + contentType := res.Header().Get("Content-Type") ext := GuessExtension(contentType) // Use current time as filename + /dumps/ prefix @@ -295,9 +267,15 @@ func DumpResponse(res *http.Response) { } defer file.Close() - _, err = io.Copy(file, res.Body) + body, err := io.ReadAll(res.Body) if err != nil { - log.Err(err).Stack().Msg("Error copying response body") + log.Err(err).Stack().Msg("Error reading response body") + return + } + + _, err = file.Write(body) + if err != nil { + log.Err(err).Stack().Msg("Error writing response body") return }