diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..5fe6a11 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,16 @@ +name: Golang CI +on: [push, pull_request] +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: 1.19 + - name: Build + run: make + - name: Test + run: make test + diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..bdfa495 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,55 @@ +name: golangci-lint +on: + push: + branches: + - master + - dev + pull_request: + +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + # pull-requests: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.20' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Require: The version of golangci-lint to use. + # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. + # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. + version: v1.53 + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # + # Note: By default, the `.golangci.yml` file should be at the root of the repository. + # The location of the configuration file can be changed by using `--config=` + # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true, then all caching functionality will be completely disabled, + # takes precedence over all other caching options. + # skip-cache: true + + # Optional: if set to true, then the action won't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. + # skip-build-cache: true + + # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. + # install-mode: "goinstall" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..02146f0 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,31 @@ +name: Deploy +on: + push: + branches: ['dev'] + +env: + IMAGE_NAME: 'codeinu/shibesbot' + +jobs: + build-and-push-images: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.16 + - name: Log in to the Container registry + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: ${{ env.IMAGE_NAME }}:dev diff --git a/.gitignore b/.gitignore index a1338d6..5b54145 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +.vscode +bin/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..47bfe13 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.21 AS build +WORKDIR $GOPATH/src/github.com/codeinuit/shibesbot +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o app ./cmd/shibesbot + +FROM alpine:3.19 +WORKDIR /root/ +COPY --from=build /go/src/github.com/codeinuit/shibesbot/app . +ENTRYPOINT ["./app"] diff --git a/Makefile b/Makefile index 01a9def..73db258 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,15 @@ BINARY = shibes -SRC_DIRECTORY = src/ -BIN_DIRECTORY = . -SRC = $(SRC_DIRECTORY)shibes.go \ - $(SRC_DIRECTORY)discord.go \ - $(SRC_DIRECTORY)dogequests.go \ +BIN_DIRECTORY = bin all: $(BINARY) $(BINARY): clean - go build -o $(BIN_DIRECTORY)/$(BINARY) $(SRC) + go build -o $(BIN_DIRECTORY)/$(BINARY) cmd/shibesbot/*.go + +test: + go test ./... -v clean: rm -f bin/$(BINARY) diff --git a/cmd/shibesbot/discord.go b/cmd/shibesbot/discord.go new file mode 100644 index 0000000..b7a3fc2 --- /dev/null +++ b/cmd/shibesbot/discord.go @@ -0,0 +1,136 @@ +package main + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/bwmarrin/discordgo" +) + +var ( + commands = []*discordgo.ApplicationCommand{ + { + Name: "shibes", + Description: "Returns an image of a Shiba", + Options: []*discordgo.ApplicationCommandOption{ + + { + Type: discordgo.ApplicationCommandOptionString, + Name: "count", + Description: "Ask for more pictures in one request", + Required: false, + }, + }, + }, + { + Name: "shelp", + Description: "Returns helper", + }, + } +) + +func (sb *Shibesbot) initDiscord() error { + var err error + + sb.session, err = discordgo.New("Bot " + sb.apiConfigurations.discordToken) + if err != nil { + return err + } + + sb.session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { sb.commandPicker(s, i) }) + sb.session.AddHandlerOnce(func(s *discordgo.Session, i *discordgo.Ready) { + t := time.Now() + key := fmt.Sprintf("usage:%d%d%d", t.Day(), t.Month(), t.Year()) + sb.dailyKey = key + + count, err := sb.cache.Get(context.Background(), sb.dailyKey) + if err != nil { + sb.log.Warn("could not get daily counter from cache : ", err.Error()) + return + } + + countString, ok := count.(string) + if !ok { + sb.log.Warn("could not get daily counter from cache : conversion error") + return + } + countInt, err := strconv.Atoi(countString) + if err != nil { + sb.log.Warn("could not get daily counter from cache : ", err.Error()) + return + } + + sb.setDailyCounter(int64(countInt)) + }) + + if err = sb.session.Open(); err != nil { + return err + } + + sb.log.Debug("updating Discord command list") + for _, cmd := range commands { + sb.log.Debugf("adding command %s", cmd.Name) + if _, err := sb.session.ApplicationCommandCreate(sb.session.State.User.ID, "", cmd); err != nil { + return err + } + } + + return nil +} + +func (sb *Shibesbot) commandPicker(s *discordgo.Session, i *discordgo.InteractionCreate) { + sb.log.Info("command received: " + i.ApplicationCommandData().Name) + var response string + switch i.ApplicationCommandData().Name { + case "shibes": + response = sb.getShibes() + case "shelp": + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{getHelp()}, + }, + }) + if err != nil { + sb.log.Error("could not answer to user help command: ", err.Error()) + } + } + + if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: response, + }, + }); err != nil { + sb.log.Error("could not answer to user help command: ", err.Error()) + return + } + + sb.updateDailyCounter() +} + +func (sb *Shibesbot) updateDailyCounter() { + sb.mtx.RLock() + defer sb.mtx.RUnlock() + count, err := sb.cache.Incr(context.Background(), sb.dailyKey) + if err != nil { + sb.log.Warn("could not get daily counter from cache : ", err.Error()) + return + } + countInt, ok := count.(int64) + if !ok { + sb.log.Warn("could not get daily counter from cache") + return + } + + sb.setDailyCounter(countInt) +} + +func (sb *Shibesbot) setDailyCounter(count int64) { + err := sb.session.UpdateGameStatus(0, fmt.Sprintf("used %d times today", count)) + if err != nil { + sb.log.Warn("could not update daily counter: ", err.Error()) + } +} diff --git a/cmd/shibesbot/dogequests.go b/cmd/shibesbot/dogequests.go new file mode 100644 index 0000000..e20f0ba --- /dev/null +++ b/cmd/shibesbot/dogequests.go @@ -0,0 +1,65 @@ +package main + +import ( + "net/http" + + "encoding/json" + + "github.com/bwmarrin/discordgo" +) + +var ( + Shibes ShibesData +) + +type ShibesPictures struct { + Shibes []string + Total int + Cursor int +} + +type ShibesData struct { + Images ShibesPictures +} + +func (sb *Shibesbot) getShibes() string { + if Shibes.Images.Cursor >= Shibes.Images.Total { + Shibes.Images.Cursor = 0 + Shibes.Images.Total = 10 + resp, err := http.Get("http://shibe.online/api/shibes?count=10") + if err != nil { + sb.log.Warn("could not get images from shibes.online: ", err.Error()) + return "" + } + defer resp.Body.Close() + err = json.NewDecoder(resp.Body).Decode(&Shibes.Images.Shibes) + if err != nil { + sb.log.Warn("could not get images from shibes.online: ", err.Error()) + return "" + } + sb.log.Info("Updated ", Shibes.Images.Total, " pictures from shibes.online") + } + Shibes.Images.Cursor++ + return Shibes.Images.Shibes[Shibes.Images.Cursor-1] +} + +func getHelp() *discordgo.MessageEmbed { + test := &discordgo.MessageEmbed{ + Thumbnail: &discordgo.MessageEmbedThumbnail{ + URL: "http://img.over-blog-kiwi.com/1/47/73/14/20160709/ob_bcc896_chiot-shiba-inu-a-vendre-2016.jpg", + }, + Description: "Thanks for using Shibesbot on your Discord server !\n\n" + + "Our purpose is to distribute many **shibes** on your server, using http://shibe.online/ as puppy distributor.\n\n" + + "Are you enjoying this bot ? You can help us spread the doge ! https://github.com/P147x/discord-shibesbot", + Fields: []*discordgo.MessageEmbedField{ + { + Name: "Available commands", + Value: "- *shibes* to get a random shibe !\n" + + "- *shelp* to get help", + Inline: false, + }, + }, + Title: "Hello shibes !", + } + return test +} diff --git a/cmd/shibesbot/main.go b/cmd/shibesbot/main.go new file mode 100644 index 0000000..04963ed --- /dev/null +++ b/cmd/shibesbot/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/codeinuit/shibesbot/cmd/shibesbot/monitoring" + "github.com/codeinuit/shibesbot/pkg/cache" + "github.com/codeinuit/shibesbot/pkg/cache/localstorage" + "github.com/codeinuit/shibesbot/pkg/cache/redis" + "github.com/codeinuit/shibesbot/pkg/logger" + "github.com/codeinuit/shibesbot/pkg/logger/logrus" + + "github.com/bwmarrin/discordgo" + "github.com/robfig/cron/v3" +) + +// ENV variables +const ( + // Token configuration + DISCORD_TOKEN = "SHIBESBOT_TOKEN" + SHIBESONLINE_TOKEN = "SHIBESONLINE_TOKEN" + + // Flags + ENV_CACHE = "CACHE" + + // Redis configuration + REDIS_ADDR = "REDIS_ADDR" + REDIS_PORT = "REDIS_PORT" + REDIS_PASS = "REDIS_PASS" + REDIS_DB = "REDIS_DB" +) + +type ApiConfigurations struct { + discordToken string + shibesolineToken string +} + +type Shibesbot struct { + session *discordgo.Session + + dailyKey string + mtx sync.RWMutex + + apiConfigurations ApiConfigurations + log logger.Logger + cache cache.Cache +} + +func NewShibesbot() (*Shibesbot, error) { + var cache cache.Cache = localstorage.NewLocalStorageCache() + var log logger.Logger = logrus.NewLogrusLogger() + var err error + + // check if Redis is enabled; otherwise fallback to local storage + if c := os.Getenv(ENV_CACHE); strings.ToUpper(c) == "REDIS" { + var port int + + log.Info("Redis enabled") + + address := os.Getenv(REDIS_ADDR) + if port, err = strconv.Atoi(os.Getenv(REDIS_PORT)); err != nil { + log.Warnf("environnement variable %s is undefined; using default value", REDIS_PORT) + port = 6379 + } + log.Infof("using Redis on %s with port %d", address, port) + + cache, err = redis.NewRedisCache(redis.RedisOptions{ + Address: address, + Port: int32(port), + Password: os.Getenv(REDIS_PASS), + }) + } + + return &Shibesbot{ + cache: cache, + log: log, + apiConfigurations: ApiConfigurations{ + discordToken: os.Getenv(DISCORD_TOKEN), + shibesolineToken: os.Getenv(SHIBESONLINE_TOKEN), + }, + }, err +} + +func (sb *Shibesbot) setDailyKey(t time.Time) { + sb.mtx.Lock() + defer sb.mtx.Unlock() + + key := fmt.Sprintf("usage:%d%d%d", t.Day(), t.Month(), t.Year()) + + isUnset, err := sb.cache.SetNX(context.Background(), key, 0) + if err != nil { + sb.log.Warn("could not update and retrieve usage count: ", err.Error()) + return + } + + if isUnset { + sb.dailyKey = key + + return + } +} + +func main() { + sb, err := NewShibesbot() + if err != nil { + fmt.Printf("could not initialize bot : %s", err.Error()) + os.Exit(1) + } + + sb.log.Info("starting bot") + c := cron.New() + monitor := monitoring.NewHTTPMonitorServer(sb.log) + + if len(sb.apiConfigurations.discordToken) <= 0 { + sb.log.Errorf("environnement variable %s is not provided", SHIBESONLINE_TOKEN) + return + } + + if err := sb.initDiscord(); err != nil { + sb.log.Error("connexion error: ", err.Error()) + os.Exit(1) + } + defer func() { + if err := sb.session.Close(); err != nil { + sb.log.Error("discord session could not close properly:", err.Error()) + os.Exit(1) + } + + sb.log.Info("discord session closed successfully") + }() + + _, err = c.AddFunc("0 0 * * *", func() { + sb.log.Info("updating usage count status") + sb.setDailyKey(time.Now()) + sb.setDailyCounter(0) + }) + + if err != nil { + sb.log.Error("could not create cronjob: ", err.Error()) + os.Exit(1) + } + + c.Start() + go monitor.Run() + defer func() { + monitor.Stop() + c.Stop() + }() + + sb.log.Info("shibesbot OK, ready to nicely bork on people") + + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + <-sc + sb.log.Info("stop signal has been received, stopping Shibesbot..") +} diff --git a/cmd/shibesbot/main_test.go b/cmd/shibesbot/main_test.go new file mode 100644 index 0000000..48a9603 --- /dev/null +++ b/cmd/shibesbot/main_test.go @@ -0,0 +1,16 @@ +package main + +// func TestResetDailyCounterKey(t *testing.T) { +// bot := &Shibesbot{ +// cache: localstorage.NewLocalStorageCache(), +// log: logrus.NewLogrusLogger(), +// } +// +// date := time.Unix(1690212965, 0) +// bot.setDailyKey(date) +// assert.Equal(t, "usage:2472023", bot.dailyKey) +// +// date = time.Unix(1690236000, 0) +// bot.setDailyKey(date) +// assert.Equal(t, "usage:2572023", bot.dailyKey) +// } diff --git a/cmd/shibesbot/monitoring/monitoring.go b/cmd/shibesbot/monitoring/monitoring.go new file mode 100644 index 0000000..8c3f40b --- /dev/null +++ b/cmd/shibesbot/monitoring/monitoring.go @@ -0,0 +1,42 @@ +package monitoring + +import ( + "context" + "net/http" + + "github.com/codeinuit/shibesbot/pkg/logger" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type Monitoring interface { + Run() + Stop() +} + +type HttpMonitor struct { + log logger.Logger + srv *http.Server +} + +func NewHTTPMonitorServer(l logger.Logger) *HttpMonitor { + http.Handle("/metrics", promhttp.Handler()) + return &HttpMonitor{ + log: l, + srv: &http.Server{Addr: ":8080"}, + } +} + +func (hm *HttpMonitor) Run() { + err := hm.srv.ListenAndServe() + if err != http.ErrServerClosed { + hm.log.Error(err.Error()) + } +} + +func (hm *HttpMonitor) Stop() { + if hm.srv != nil { + if err := hm.srv.Shutdown(context.Background()); err != nil { + hm.log.Error(err.Error()) + } + } +} diff --git a/deployments/k8s-staging.yaml b/deployments/k8s-staging.yaml new file mode 100644 index 0000000..a7bb860 --- /dev/null +++ b/deployments/k8s-staging.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: shibesbot +spec: + replicas: 1 + selector: + matchLabels: + app: shibesbot + template: + metadata: + labels: + app: shibesbot + spec: + containers: + - name: shibesbot + image: codeinu/shibesbot:latest + env: + - name: SHIBESBOT_TOKEN + valueFrom: + secretKeyRef: + name: shibesbot-token + key: password +--- +apiVersion: v1 +kind: Service +metadata: + name: shibesbot-entrypoint + namespace: default +spec: + type: NodePort + selector: + app: shibesbot + ports: + - port: 80 + targetPort: 80 + nodePort: 30001 \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b779d8f --- /dev/null +++ b/go.mod @@ -0,0 +1,37 @@ +module github.com/codeinuit/shibesbot + +go 1.21 + +require ( + github.com/bwmarrin/discordgo v0.27.1 + github.com/ivolo/go-giphy v0.0.0-20150927080050-d31b832022e4 + github.com/redis/go-redis/v9 v9.0.5 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/robfig/cron/v3 v3.0.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/gorilla/websocket v1.5.0 // indirect + go.uber.org/zap v1.24.0 + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/sys v0.15.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c58b319 --- /dev/null +++ b/go.sum @@ -0,0 +1,74 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= +github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/ivolo/go-giphy v0.0.0-20150927080050-d31b832022e4 h1:Rq1bBuOq3A4P/41LjhqHV9nqWAfFTiaPe83WdkI+MYc= +github.com/ivolo/go-giphy v0.0.0-20150927080050-d31b832022e4/go.mod h1:9KUT8qK3+k9SRKuDCiqe3BS2GZ74e5HtIVSGNorAbHk= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= +github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= +github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +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= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.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/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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 0000000..77fa84a --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,22 @@ +package cache + +import "context" + +// Cache is used as the main implementation for caching libraries +type Cache interface { + // Get returns the assigned value for a given key + Get(context.Context, string) (any, error) + + // Set registers a value, regardless if the key as been + // already set or not. + Set(context.Context, string, any) (any, error) + + // Incr increments an value for a given key. + // If the key does not exists, Incr assumes the default + // value is 0 and will return 1. + Incr(context.Context, string) (any, error) + + // SetNX check if the key is already defined, and return + // the result as a boolean value. + SetNX(context.Context, string, any) (bool, error) +} diff --git a/pkg/cache/localstorage/localstorage.go b/pkg/cache/localstorage/localstorage.go new file mode 100644 index 0000000..ea3b085 --- /dev/null +++ b/pkg/cache/localstorage/localstorage.go @@ -0,0 +1,69 @@ +package localstorage + +import ( + "context" + "errors" + "sync" +) + +type LocalStorage struct { + array map[string]any + rwm sync.RWMutex +} + +func NewLocalStorageCache() *LocalStorage { + return &LocalStorage{ + array: make(map[string]any), + } +} + +func (ls *LocalStorage) Get(ctx context.Context, k string) (any, error) { + ls.rwm.RLock() + defer ls.rwm.RUnlock() + + value, ok := ls.array[k] + if !ok { + return nil, errors.New("could not get value") + } + + return value, nil +} + +func (ls *LocalStorage) Set(ctx context.Context, k string, v any) (any, error) { + ls.rwm.Lock() + defer ls.rwm.Unlock() + + ls.array[k] = v + + return v, nil +} + +func (ls *LocalStorage) Incr(ctx context.Context, k string) (any, error) { + ls.rwm.Lock() + defer ls.rwm.Unlock() + + if ls.array[k] == nil { + ls.array[k] = 1 + return 1, nil + } + convertedValue, ok := ls.array[k].(int) + if !ok { + return nil, errors.New("could not increment value") + } + + ls.array[k] = convertedValue + 1 + return ls.array[k], nil +} + +func (ls *LocalStorage) SetNX(ctx context.Context, k string, v any) (bool, error) { + ls.rwm.Lock() + defer ls.rwm.Unlock() + + if ls.array[k] != nil { + ls.array[k] = v + + return false, nil + } + + return true, nil +} diff --git a/pkg/cache/localstorage_test.go b/pkg/cache/localstorage_test.go new file mode 100644 index 0000000..614d87c --- /dev/null +++ b/pkg/cache/localstorage_test.go @@ -0,0 +1,48 @@ +package cache_test + +import ( + "context" + "testing" + + "github.com/codeinuit/shibesbot/pkg/cache" + "github.com/codeinuit/shibesbot/pkg/cache/localstorage" + "github.com/stretchr/testify/assert" +) + +func TestLocalStorage(t *testing.T) { + var cache cache.Cache + + const key string = "test" + const value string = "test" + + cache = localstorage.NewLocalStorageCache() + setValue, error := cache.Set(context.Background(), key, value) + assert.Nil(t, error) + assert.Equal(t, value, setValue) + + awaitedValue, error := cache.Get(context.Background(), key) + + assert.Equal(t, nil, error) + assert.Equal(t, awaitedValue, value) +} + +func TestLocalStorageIncr(t *testing.T) { + var cache cache.Cache + + const key string = "test" + const value int = 41 + const newAwaitedValue int = 42 + + cache = localstorage.NewLocalStorageCache() + + setValue, error := cache.Set(context.Background(), key, value) + assert.Nil(t, error) + assert.Equal(t, value, setValue) + + awaitedValue, error := cache.Incr(context.Background(), key) + convertedAwaitedValue, ok := awaitedValue.(int) + + assert.True(t, ok) + assert.Equal(t, nil, error) + assert.Equal(t, newAwaitedValue, convertedAwaitedValue) +} diff --git a/pkg/cache/redis/redis.go b/pkg/cache/redis/redis.go new file mode 100644 index 0000000..255b639 --- /dev/null +++ b/pkg/cache/redis/redis.go @@ -0,0 +1,55 @@ +package redis + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +type RedisDB struct { + client *redis.Client +} + +type RedisOptions struct { + Port int32 + Address string + Password string + Database string +} + +// NewRedisCache returns a Redis implemenation based on Cache interface +func NewRedisCache(opt RedisOptions) (*RedisDB, error) { + client := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%d", opt.Address, opt.Port), + Password: opt.Password, + DB: 0, + }) + + ctx := context.TODO() + if err := client.Ping(ctx).Err(); err != nil { + return &RedisDB{}, err + } + + return &RedisDB{client: client}, nil +} + +// Get is the implementation of Get Redis function +func (r *RedisDB) Get(ctx context.Context, k string) (any, error) { + return r.client.Get(ctx, k).Result() +} + +// Set is the implementation of Set Redis function +func (r *RedisDB) Set(ctx context.Context, k string, v any) (any, error) { + return r.client.Set(ctx, k, v, 0).Result() +} + +// Incr is the implementation of Inch Redis function +func (r *RedisDB) Incr(ctx context.Context, k string) (any, error) { + return r.client.Incr(ctx, k).Result() +} + +// SetNX is the implementation of SetNX Redis function +func (r *RedisDB) SetNX(ctx context.Context, k string, v any) (bool, error) { + return r.client.SetNX(ctx, k, v, 0).Result() +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..530c2cd --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,13 @@ +package logger + +// Logger interface implementation +type Logger interface { + Debug(...any) + Debugf(string, ...any) + Info(...any) + Infof(string, ...any) + Warn(...any) + Warnf(string, ...any) + Error(...any) + Errorf(string, ...any) +} diff --git a/pkg/logger/logrus/logrus.go b/pkg/logger/logrus/logrus.go new file mode 100644 index 0000000..3f24290 --- /dev/null +++ b/pkg/logger/logrus/logrus.go @@ -0,0 +1,46 @@ +package logrus + +import "github.com/sirupsen/logrus" + +type LogrusLogger struct { + logger *logrus.Logger +} + +func NewLogrusLogger() *LogrusLogger { + logger := logrus.New() + return &LogrusLogger{ + logger: logger, + } +} + +func (l LogrusLogger) Debug(v ...any) { + l.logger.Debug(v...) +} + +func (l LogrusLogger) Debugf(format string, v ...any) { + l.logger.Debugf(format, v...) +} + +func (l LogrusLogger) Info(v ...any) { + l.logger.Info(v...) +} + +func (l LogrusLogger) Infof(format string, v ...any) { + l.logger.Infof(format, v...) +} + +func (l LogrusLogger) Error(v ...any) { + l.logger.Error(v...) +} + +func (l LogrusLogger) Errorf(format string, v ...any) { + l.logger.Errorf(format, v...) +} + +func (l LogrusLogger) Warn(v ...any) { + l.logger.Warn(v...) +} + +func (l LogrusLogger) Warnf(format string, v ...any) { + l.logger.Warnf(format, v...) +} diff --git a/pkg/logger/zap/zap.go b/pkg/logger/zap/zap.go new file mode 100644 index 0000000..ff72dee --- /dev/null +++ b/pkg/logger/zap/zap.go @@ -0,0 +1,46 @@ +package zap + +import "go.uber.org/zap" + +type ZapSugarLogger struct { + logger *zap.SugaredLogger +} + +func NewSugarLogger() ZapSugarLogger { + logger, _ := zap.NewProduction() + return ZapSugarLogger{ + logger: logger.Sugar(), + } +} + +func (z ZapSugarLogger) Debug(v ...any) { + z.logger.Debug(v) +} + +func (z ZapSugarLogger) Debugf(format string, v ...any) { + z.logger.Debugf(format, v...) +} + +func (z ZapSugarLogger) Info(v ...any) { + z.logger.Info(v) +} + +func (z ZapSugarLogger) Infof(format string, v ...any) { + z.logger.Infof(format, v...) +} + +func (z ZapSugarLogger) Warn(v ...any) { + z.logger.Warn(v) +} + +func (z ZapSugarLogger) Warnf(format string, v ...any) { + z.logger.Warnf(format, v...) +} + +func (z ZapSugarLogger) Error(v ...any) { + z.logger.Error(v) +} + +func (z ZapSugarLogger) Errorf(format string, v ...any) { + z.logger.Errorf(format, v...) +} diff --git a/scripts/deploy-staging.sh b/scripts/deploy-staging.sh new file mode 100644 index 0000000..8ab124b --- /dev/null +++ b/scripts/deploy-staging.sh @@ -0,0 +1,2 @@ +#!/bin/sh +kubectl apply -f deployments/k8s-staging.yaml diff --git a/scripts/remove-staging.sh b/scripts/remove-staging.sh new file mode 100644 index 0000000..b11a16b --- /dev/null +++ b/scripts/remove-staging.sh @@ -0,0 +1,2 @@ +#!/bin/sh +kubectl delete -f deployments/k8s-staging.yaml diff --git a/src/discord.go b/src/discord.go deleted file mode 100644 index ebb87fa..0000000 --- a/src/discord.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/signal" - "strconv" - "strings" - "syscall" - "time" - - "github.com/bwmarrin/discordgo" -) - -var ( - Users int - Time int -) - -func initDiscord(t string) { - dg, err := discordgo.New("Bot " + Token) - if err != nil { - fmt.Println("error creating Discord session,", err) - return - } - - dg.AddHandler(shibesHandler) - err = dg.Open() - if err != nil { - fmt.Println("error opening connection,", err) - return - } - - fmt.Println("Shibes ready for duty. Press CTRL+C for no shibes.") - sc := make(chan os.Signal, 1) - signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) - <-sc - dg.Close() -} - -func commandPicker(s *discordgo.Session, m *discordgo.MessageCreate) error { - var err error - if strings.HasPrefix(m.Content, "s") { - switch m.Content { - case "shibes": - _, err = s.ChannelMessageSend(m.ChannelID, getShibes()) - presenceUpdate(s) - break - case "sgifs": - _, err = s.ChannelMessageSend(m.ChannelID, getShibesGifs()) - break - case "shelp": - _, err = s.ChannelMessageSendEmbed(m.ChannelID, getHelp()) - break - case "swalls": - _, err = s.ChannelMessageSend(m.ChannelID, getShibesWallpaper()) - break - } - } - return err -} - -func presenceUpdate(s *discordgo.Session) { - if Time != int(time.Now().Day()) { - Time = int(time.Now().Day()) - Users = 0 - } - Users++ - s.UpdateStatus(Users, "shelp for help || "+ - strconv.Itoa(Users)+" usages this day.") -} - -func shibesHandler(s *discordgo.Session, m *discordgo.MessageCreate) { - if m.Author.ID == s.State.User.ID { - return - } - if commandPicker(s, m) != nil { - s.ChannelMessageSend(m.ChannelID, "Oops, something wrong happened. :<") - } -} diff --git a/src/dogequests.go b/src/dogequests.go deleted file mode 100644 index 4f731b8..0000000 --- a/src/dogequests.go +++ /dev/null @@ -1,123 +0,0 @@ -package main - -import ( - "encoding/json" - "io/ioutil" - "math/rand" - "net/http" - - "github.com/bwmarrin/discordgo" - "github.com/ivolo/go-giphy" -) - -var ( - Shibes ShibesData -) - -type WallpaperData struct { - Id int - Width int - Height int - Url_Image string -} - -type AlphacodersData struct { - Success bool - Wallpapers []WallpaperData - Total_Match int -} - -type ShibesPictures struct { - Shibes []string - Total int - Cursor int -} - -type ShibesGifs struct { - Shibes []giphy.Gif - Total int - Cursor int -} - -type ShibesWallpapers struct { - Shibes []WallpaperData - Total int - Cursor int -} - -type ShibesData struct { - Images ShibesPictures - Gifs ShibesGifs - Wallpapers ShibesWallpapers -} - -func init() { - req, err := http.NewRequest("GET", "https://wall.alphacoders.com/api2.0/get.php", nil) - if err != nil { - } - q := req.URL.Query() - q.Add("auth", "c8b66cee6ef7022a615da5cbba315f3c") - q.Add("method", "search") - q.Add("term", "Shiba") - req.URL.RawQuery = q.Encode() - resp, _ := http.Get(req.URL.String()) - defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) - - var res AlphacodersData - json.Unmarshal(body, &res) - Shibes.Wallpapers.Shibes = make([]WallpaperData, len(res.Wallpapers)) - Shibes.Wallpapers.Shibes = res.Wallpapers - Shibes.Wallpapers.Total = len(res.Wallpapers) - - gp := giphy.New("PcVZFoFsmh2vhFHqSKjhvbnwq74N7JSi") - Shibes.Gifs.Shibes, _ = gp.Search("shiba") - Shibes.Gifs.Total = len(Shibes.Gifs.Shibes) - Shibes.Gifs.Cursor = 0 -} - -func getShibes() string { - if Shibes.Images.Cursor >= Shibes.Images.Total { - Shibes.Images.Cursor = 0 - Shibes.Images.Total = 10 - resp, err := http.Get("http://shibe.online/api/shibes?count=10") - if err == nil { - defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) - json.Unmarshal(body, &Shibes.Images.Shibes) - } - } - Shibes.Images.Cursor++ - return Shibes.Images.Shibes[Shibes.Images.Cursor-1] -} - -func getHelp() *discordgo.MessageEmbed { - test := &discordgo.MessageEmbed{ - Thumbnail: &discordgo.MessageEmbedThumbnail{ - URL: "http://img.over-blog-kiwi.com/1/47/73/14/20160709/ob_bcc896_chiot-shiba-inu-a-vendre-2016.jpg", - }, - Description: "Thanks for using Shibesbot on your Discord server !\n\n" + - "Our purpose is to distribute many **shibes** on your server, using http://shibe.online/ as puppy distributor.\n\n" + - "Are you enjoying this bot ? You can help us spread the doge ! https://github.com/P147x/discord-shibesbot", - Fields: []*discordgo.MessageEmbedField{ - { - Name: "Available commands", - Value: "- *shibes* to get a random shibe !\n" + - "- *sgifs* to get a random gif of shiba !\n" + - "- *shelp* to get help\n" + - "- *swalls* to get an amazing shibe wallpaper", - Inline: false, - }, - }, - Title: "Hello shibes !", - } - return test -} - -func getShibesGifs() string { - return Shibes.Gifs.Shibes[rand.Int()%Shibes.Gifs.Total].URL -} - -func getShibesWallpaper() string { - return string(Shibes.Wallpapers.Shibes[rand.Int()%Shibes.Wallpapers.Total].Url_Image) -} diff --git a/src/shibes.go b/src/shibes.go deleted file mode 100644 index f246a57..0000000 --- a/src/shibes.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "flag" -) - -var ( - Token string -) - -func init() { - - flag.StringVar(&Token, "t", "", "Bot Token") - flag.Parse() -} - -func main() { - initDiscord(Token) -}