diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 46b6270..425fb85 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,24 +2,25 @@ name: Go on: push: - branches: [ master ] + branches: [ "master", "rewrite" ] pull_request: - branches: [ master ] + branches: [ "master", "rewrite" ] jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: '1.20' - name: Build run: go build -v ./... - name: Test run: go test -v ./... + diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8c48392..0000000 --- a/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -# Download latest golang image -FROM golang:latest -# Create a directory for the app -RUN mkdir /app -# Copy all files from current directory to working directory -COPY . /app -# Set working directory -WORKDIR /app - -RUN make xdprod \ No newline at end of file diff --git a/LICENSE b/LICENSE index 5095c51..c428445 100644 --- a/LICENSE +++ b/LICENSE @@ -1,15 +1,21 @@ -ISC License +MIT License -Copyright (c) 2023, lyx0 +Copyright (c) 2023 lyx0 -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 748bedd..59ff8d5 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,28 @@ BINARY_NAME=Nourybot.out BINARY_NAME_API=NourybotApi.out -cup: - sudo docker compose up - xd: - cd cmd/bot && go build -o ${BINARY_NAME} && ./${BINARY_NAME} -env="dev" + cd cmd/nourybot && go build -o ${BINARY_NAME} + mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME} + ./bin/${BINARY_NAME} -env="dev" xdprod: - cd cmd/bot && go build -o ${BINARY_NAME} && ./${BINARY_NAME} -env="prod" + cd cmd/nourybot && go build -o ${BINARY_NAME} + mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME} + ./bin/${BINARY_NAME} -env="prod" jq: - cd cmd/bot && go build -o ${BINARY_NAME} && ./${BINARY_NAME} -env="dev" | jq + cd cmd/nourybot && go build -o ${BINARY_NAME} + mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME} + ./bin/${BINARY_NAME} -env="dev" | jq jqprod: - cd cmd/bot && go build -o ${BINARY_NAME} && ./${BINARY_NAME} -env="prod" | jq + cd cmd/nourybot && go build -o ${BINARY_NAME} + mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME} + ./bin/${BINARY_NAME} -env="prod" | jq -jqapi: - go build -o ${BINARY_NAME_API} cmd/api && ./${BINARY_NAME} | jq +prod: + cd cmd/nourybot && go build -o ${BINARY_NAME} + mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME} + ./bin/${BINARY_NAME} -env="prod" diff --git a/README.md b/README.md index b5ed411..a4b7341 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,2 @@ # nourybot - -Near future abandoned project in development. - -### Make: - Development: - make jq +Lidl Twitch bot diff --git a/cmd/api/command.go b/cmd/api/command.go deleted file mode 100644 index 0f67c61..0000000 --- a/cmd/api/command.go +++ /dev/null @@ -1,181 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "net/http" - - "github.com/lyx0/nourybot/internal/data" -) - -func (app *application) showCommandHandler(w http.ResponseWriter, r *http.Request) { - name, err := app.readCommandNameParam(r) - if err != nil { - app.logError(r, err) - return - } - - // Get the data for a specific movie from our helper method, - // then check if an error was returned, and which. - command, err := app.Models.Commands.Get(name) - if err != nil { - switch { - case errors.Is(err, data.ErrRecordNotFound): - app.logError(r, err) - return - default: - app.serverErrorResponse(w, r, err) - return - } - } - - app.Logger.Infow("GET Command", - "Command", command, - ) - - w.Header().Set("Access-Control-Allow-Origin", "*") - err = app.writeJSON(w, http.StatusOK, envelope{"command": command}, nil) - if err != nil { - app.serverErrorResponse(w, r, err) - } -} - -func (app *application) createCommandHandler(w http.ResponseWriter, r *http.Request) { - var input struct { - Name string `json:"name"` - Text string `json:"text"` - Category string `json:"category"` - Level int `json:"level"` - } - - err := app.readJSON(w, r, &input) - if err != nil { - app.badRequestResponse(w, r, err) - return - } - - command := &data.Command{ - Name: input.Name, - Text: input.Text, - Category: input.Category, - Level: input.Level, - } - - err = app.Models.Commands.Insert(command) - if err != nil { - app.serverErrorResponse(w, r, err) - return - } - - headers := make(http.Header) - headers.Set("Location", fmt.Sprintf("/v1/commands/%s", command.Name)) - - app.Logger.Infow("PUT Command", - "Command", command, - ) - - err = app.writeJSON(w, http.StatusCreated, envelope{"command": command}, headers) - if err != nil { - app.serverErrorResponse(w, r, err) - } -} - -func (app *application) updateCommandHandler(w http.ResponseWriter, r *http.Request) { - name, err := app.readCommandNameParam(r) - if err != nil { - app.notFoundResponse(w, r) - return - } - - command, err := app.Models.Commands.Get(name) - if err != nil { - switch { - case errors.Is(err, data.ErrRecordNotFound): - app.notFoundResponse(w, r) - default: - app.serverErrorResponse(w, r, err) - } - return - } - - var input struct { - Name *string `json:"name"` - Text *string `json:"text"` - Category *string `json:"category"` - Level *int `json:"level"` - } - - err = app.readJSON(w, r, &input) - if err != nil { - app.badRequestResponse(w, r, err) - return - } - - // There is a name since we successfully queried the database for - // a command, so no need to check != nil. - command.Name = *input.Name - - if input.Text != nil { - command.Text = *input.Text - } - - if input.Category != nil { - command.Category = *input.Category - } - - if input.Level != nil { - command.Level = *input.Level - } - - err = app.Models.Commands.Update(command) - if err != nil { - switch { - case errors.Is(err, data.ErrEditConflict): - app.editConflictResponse(w, r) - return - default: - app.serverErrorResponse(w, r, err) - return - } - } - - app.Logger.Infow("PATCH Command", - "Command", command, - ) - - err = app.writeJSON(w, http.StatusOK, envelope{"command": command}, nil) - if err != nil { - app.serverErrorResponse(w, r, err) - } - -} - -func (app *application) deleteCommandHandler(w http.ResponseWriter, r *http.Request) { - name, err := app.readCommandNameParam(r) - if err != nil { - app.notFoundResponse(w, r) - return - } - - err = app.Models.Commands.Delete(name) - if err != nil { - switch { - case errors.Is(err, data.ErrRecordNotFound): - app.notFoundResponse(w, r) - default: - app.serverErrorResponse(w, r, err) - } - return - } - - app.Logger.Infow("DELETE Command", - "Name", name, - ) - - err = app.writeJSON(w, http.StatusOK, envelope{"message": fmt.Sprintf("command %s deleted", name)}, nil) - if err != nil { - app.serverErrorResponse(w, r, err) - } -} - -type envelope map[string]interface{} diff --git a/cmd/api/errors.go b/cmd/api/errors.go deleted file mode 100644 index 5dd46c0..0000000 --- a/cmd/api/errors.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "fmt" - "net/http" -) - -func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) { - app.logError(r, err) - - message := "the server encountered a problem and could not process your request" - app.errorResponse(w, r, http.StatusInternalServerError, message) -} - -// The logError() method is a generic helper for logging an error message. -func (app *application) logError(r *http.Request, err error) { - app.Logger.Errorw("Error", - "Request URI", r.RequestURI, - "error", err, - ) -} - -func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) { - - // Write the response using the writeJSON() helper. If this happens to return an - // error then log it, and fall back to sending the client an empty response with a - // 500 Internal Server Error status code. - fmt.Fprintf(w, "Error: %d", status) -} - -func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) { - app.errorResponse(w, r, http.StatusBadRequest, err.Error()) -} - -func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) { - message := "the requested resource could not be found" - app.errorResponse(w, r, http.StatusNotFound, message) -} - -func (app *application) editConflictResponse(w http.ResponseWriter, r *http.Request) { - message := "unable to update the record due to an edit conflict, please try again" - app.errorResponse(w, r, http.StatusConflict, message) -} - -func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) { - message := fmt.Sprintf("the %s method is not supported for this resource", r.Method) - app.errorResponse(w, r, http.StatusMethodNotAllowed, message) -} diff --git a/cmd/api/helpers.go b/cmd/api/helpers.go deleted file mode 100644 index 00032cf..0000000 --- a/cmd/api/helpers.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - - "github.com/julienschmidt/httprouter" -) - -// func (app *application) readIDParam(r *http.Request) (int64, error) { -// params := httprouter.ParamsFromContext(r.Context()) -// -// // Use `ByName()` function to get the value of the "id" parameter from the slice. -// // The value returned by `ByName()` is always a string so we try to convert it to -// // base64 with a bit size of 64. -// id, err := strconv.ParseInt(params.ByName("id"), 10, 64) -// if err != nil || id < 1 { -// return 0, errors.New("invalid id parameter") -// } -// -// return id, nil -// } - -func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error { - // Limit the size of the requst body to 1MB. - maxBytes := 1_048_576 - r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes)) - - dec := json.NewDecoder(r.Body) - dec.DisallowUnknownFields() - - err := dec.Decode(dst) - if err != nil { - var syntaxError *json.SyntaxError - var unmarshalTypeError *json.UnmarshalTypeError - var invalidUnmarshalError *json.InvalidUnmarshalError - - switch { - case errors.As(err, &syntaxError): - return fmt.Errorf("body contains malformed JSON (at character %d)", syntaxError.Offset) - - case errors.Is(err, io.ErrUnexpectedEOF): - return errors.New("body contains invalid JSON") - - case errors.As(err, &unmarshalTypeError): - if unmarshalTypeError.Field != "" { - return fmt.Errorf("body contains invalid JSON type for field %q", unmarshalTypeError.Field) - } - return fmt.Errorf("body contains invalid JSON type (at character %d)", unmarshalTypeError.Offset) - - case errors.Is(err, io.EOF): - return errors.New("body contains no data") - - case strings.HasPrefix(err.Error(), "json: unknown field "): - fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") - return fmt.Errorf("body contains unknown key %s", fieldName) - - case err.Error() == "htto: request body too large": - return fmt.Errorf("body must not be larger than %d bytes", maxBytes) - - case errors.As(err, &invalidUnmarshalError): - app.Logger.Panic(err) - default: - return err - } - } - - err = dec.Decode(&struct{}{}) - if err != io.EOF { - return errors.New("body must only contain a single JSON value") - } - return nil -} - -func (app *application) readCommandNameParam(r *http.Request) (string, error) { - params := httprouter.ParamsFromContext(r.Context()) - - // Use `ByName()` function to get the value of the "id" parameter from the slice. - // The value returned by `ByName()` is always a string so we try to convert it to - // base64 with a bit size of 64. - name := params.ByName("name") - - return name, nil -} - -func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error { - // Encode the data into JSON and return any errors if there were any. - // Use MarshalIndent instead of normal Marshal so it looks prettier on terminals. - js, err := json.MarshalIndent(data, "", "\t") - if err != nil { - return err - } - - // Append a newline to make it prettier on terminals. - js = append(js, '\n') - - // Iterate over the header map and add each header to the - // http.ResponseWriter header map. - for key, value := range headers { - w.Header()[key] = value - } - - // Set `Content-Type` to `application/json` because go - // defaults to `text-plain; charset=utf8`. - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - w.Write(js) - - return nil -} diff --git a/cmd/api/main.go b/cmd/api/main.go deleted file mode 100644 index e073aad..0000000 --- a/cmd/api/main.go +++ /dev/null @@ -1,110 +0,0 @@ -package main - -import ( - "context" - "database/sql" - "fmt" - "log" - "net/http" - "os" - "time" - - "github.com/joho/godotenv" - _ "github.com/lib/pq" - "github.com/lyx0/nourybot/internal/data" - "go.uber.org/zap" -) - -type config struct { - port int - db struct { - dsn string - maxOpenConns int - maxIdleConns int - maxIdleTime string - } -} - -type application struct { - Logger *zap.SugaredLogger - Db *sql.DB - Models data.Models -} - -func main() { - var cfg config - - // Initialize a new sugared logger that we'll pass on - // down through the application. - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - - err := godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } - - // Database - cfg.db.dsn = os.Getenv("DB_DSN") - cfg.port = 3000 - cfg.db.maxOpenConns = 25 - cfg.db.maxIdleConns = 25 - cfg.db.maxIdleTime = "15m" - - // Establish database connection - db, err := openDB(cfg) - if err != nil { - sugar.Fatal(err) - } - - // Initialize Application - app := &application{ - Logger: sugar, - Db: db, - Models: data.NewModels(db), - } - - srv := &http.Server{ - Addr: fmt.Sprintf(":%d", cfg.port), - Handler: app.routes(), - IdleTimeout: time.Minute, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - } - - err = srv.ListenAndServe() - sugar.Fatal(err) -} - -// openDB returns the sql.DB connection pool. -func openDB(cfg config) (*sql.DB, error) { - // sql.Open() creates an empty connection pool with the provided DSN - db, err := sql.Open("postgres", cfg.db.dsn) - if err != nil { - return nil, err - } - - // Set database restraints. - db.SetMaxOpenConns(cfg.db.maxOpenConns) - db.SetMaxIdleConns(cfg.db.maxIdleConns) - - // Parse the maxIdleTime string into an actual duration and set it. - duration, err := time.ParseDuration(cfg.db.maxIdleTime) - if err != nil { - return nil, err - } - db.SetConnMaxIdleTime(duration) - - // Create a new context with a 5 second timeout. - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // db.PingContext() is needed to actually check if the - // connection to the database was successful. - err = db.PingContext(ctx) - if err != nil { - return nil, err - } - - return db, nil -} diff --git a/cmd/api/routes.go b/cmd/api/routes.go deleted file mode 100644 index 1fdf1c3..0000000 --- a/cmd/api/routes.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/julienschmidt/httprouter" -) - -func (app *application) routes() *httprouter.Router { - router := httprouter.New() - - router.NotFound = http.HandlerFunc(app.notFoundResponse) - router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) - - // cors.Default().Handler(router) - router.HandlerFunc(http.MethodGet, "/v1/commands/:name", app.showCommandHandler) - router.HandlerFunc(http.MethodPost, "/v1/commands", app.createCommandHandler) - router.HandlerFunc(http.MethodPatch, "/v1/commands/:name", app.updateCommandHandler) - router.HandlerFunc(http.MethodDelete, "/v1/commands/:name", app.deleteCommandHandler) - - return router -} - -func MiddleCORS(next httprouter.Handle) httprouter.Handle { - return func(w http.ResponseWriter, - r *http.Request, ps httprouter.Params) { - w.Header().Set("Access-Control-Allow-Origin", "*") - next(w, r, ps) - } -} diff --git a/cmd/bot/.env.example b/cmd/bot/.env.example deleted file mode 100644 index 5689f5c..0000000 --- a/cmd/bot/.env.example +++ /dev/null @@ -1,15 +0,0 @@ -TWITCH_USERNAME=nourybot -TWITCH_OAUTH=oauth:mycooloauth42060 -TWITCH_COMMAND_PREFIX=() - -TWITCH_CLIENT_ID=mycooltwitchclientid -TWITCH_CLIENT_SECRET=mycooltwitchclientsecret - -LOCAL_DSN=postgres://user:password@localhost/database-name?sslmode=disable -SUPABASE_DSN=postgres://user:password@db.XXXXXXXXX.supabase.co/postgres - -OWM_KEY=mycoolopenweathermapapikey - -LAST_FM_APPLICATION_NAME=mycoolapplicationname -LAST_FM_API_KEY=mycoollastfmapikey -LAST_FM_SECRET=mycoollastfmsecret diff --git a/cmd/bot/channel.go b/cmd/nourybot/channel.go similarity index 56% rename from cmd/bot/channel.go rename to cmd/nourybot/channel.go index b942981..96e517e 100644 --- a/cmd/bot/channel.go +++ b/cmd/nourybot/channel.go @@ -4,83 +4,70 @@ import ( "fmt" "github.com/gempir/go-twitch-irc/v4" - "github.com/lyx0/nourybot/internal/commands/decapi" - "github.com/lyx0/nourybot/internal/common" - "github.com/lyx0/nourybot/internal/data" + "github.com/lyx0/nourybot/internal/ivr" ) // AddChannel takes in a channel name, then calls GetIdByLogin for the // channels ID and inserts both the name and id value into the database. // If there is no error thrown the TwitchClient joins the channel afterwards. -func (app *Application) AddChannel(login string, message twitch.PrivateMessage) { - userId, err := decapi.GetIdByLogin(login) - if err != nil { - app.Logger.Error(err) - return - } +func (app *application) AddChannel(login string, message twitch.PrivateMessage) { + userID := ivr.IDByUsername(login) - // Initialize a new channel struct holding the values that will be - // passed into the app.Models.Channels.Insert() method. - channel := &data.Channel{ - Login: login, - TwitchID: userId, - } - - err = app.Models.Channels.Insert(channel) + err := app.Models.Channels.Insert(login, userID) if err != nil { reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } else { app.TwitchClient.Join(login) reply := fmt.Sprintf("Added channel %s", login) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } } // GetAllChannels() queries the database and lists all channels. // Only used for debug/information purposes. -func (app *Application) GetAllChannels() { +func (app *application) GetAllChannels() { channel, err := app.Models.Channels.GetAll() if err != nil { - app.Logger.Error(err) + app.Log.Error(err) return } - app.Logger.Infow("All channels:", + app.Log.Infow("All channels:", "channel", channel) } // DeleteChannel queries the database for a channel name and if it exists // deletes the channel and makes the bot depart said channel. -func (app *Application) DeleteChannel(login string, message twitch.PrivateMessage) { +func (app *application) DeleteChannel(login string, message twitch.PrivateMessage) { err := app.Models.Channels.Delete(login) if err != nil { - common.Send(message.Channel, "Something went wrong FeelsBadMan", app.TwitchClient) - app.Logger.Error(err) + app.Send(message.Channel, "Something went wrong FeelsBadMan", message) + app.Log.Error(err) return } app.TwitchClient.Depart(login) reply := fmt.Sprintf("Deleted channel %s", login) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) } // InitialJoin is called on startup and queries the database for a list of // channels which the TwitchClient then joins. -func (app *Application) InitialJoin() { +func (app *application) InitialJoin() { // GetJoinable returns a slice of channel names. channel, err := app.Models.Channels.GetJoinable() if err != nil { - app.Logger.Error(err) + app.Log.Error(err) return } // Iterate over the slice of channels and join each. for _, v := range channel { app.TwitchClient.Join(v) - app.Logger.Infow("Joining channel", + app.Log.Infow("Joining channel", "channel", v) } } diff --git a/cmd/bot/command.go b/cmd/nourybot/command.go similarity index 65% rename from cmd/bot/command.go rename to cmd/nourybot/command.go index 8e4db79..58fbb7e 100644 --- a/cmd/bot/command.go +++ b/cmd/nourybot/command.go @@ -5,18 +5,18 @@ import ( "strconv" "github.com/gempir/go-twitch-irc/v4" - "github.com/lyx0/nourybot/internal/common" + "github.com/google/uuid" "github.com/lyx0/nourybot/internal/data" ) // AddCommand splits a message into two parts and passes on the // name and text to the database handler. -func (app *Application) AddCommand(name string, message twitch.PrivateMessage) { +func (app *application) AddCommand(name string, message twitch.PrivateMessage) { // snipLength is the length we need to "snip" off of the start of `message`. - // `()addcommand` = +12 - // trailing space = +1 - // zero-based = +1 - // = 14 + // `()add command` = +12 + // trailing space = +1 + // zero-based = +1 + // = 15 snipLength := 14 // Split the twitch message at `snipLength` plus length of the name of the @@ -35,15 +35,16 @@ func (app *Application) AddCommand(name string, message twitch.PrivateMessage) { Level: 0, Help: "", } + app.Log.Info(command) err := app.Models.Commands.Insert(command) if err != nil { reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } else { reply := fmt.Sprintf("Successfully added command: %s", name) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } } @@ -54,31 +55,32 @@ func (app *Application) AddCommand(name string, message twitch.PrivateMessage) { // If the Command.Level is not 0 it queries the database for the level of the // user who sent the message. If the users level is equal or higher // the command.Text field is returned. -func (app *Application) GetCommand(name, username string) (string, error) { +func (app *application) GetCommand(target, commandName string, userLevel int) (string, error) { // Fetch the command from the database if it exists. - command, err := app.Models.Commands.Get(name) + command, err := app.Models.Commands.Get(commandName) if err != nil { // It probably did not exist return "", err } - // If the command has no level set just return the text. - // Otherwise check if the level is high enough. if command.Level == 0 { return command.Text, nil - } else { - // Get the user from the database to check if the userlevel is equal - // or higher than the command.Level. - user, err := app.Models.Users.Get(username) - if err != nil { - return "", err - } - if user.Level >= command.Level { + } else if userLevel >= command.Level { + if command.Category == "ascii" { + // Cannot use app.Send() here since the command is a ascii pasta and will be + // timed out, thus not passing the banphrase check app.Send() does before actually + // sending the message. + app.SendNoBanphrase(target, command.Text) + + return "", nil + } else { // Userlevel is sufficient so return the command.Text return command.Text, nil } - } + // If the command has no level set just return the text. + // Otherwise check if the level is high enough. + } // Userlevel was not enough so return an empty string and error. return "", ErrUserInsufficientLevel } @@ -89,7 +91,7 @@ func (app *Application) GetCommand(name, username string) (string, error) { // If the Command.Level is not 0 it queries the database for the level of the // user who sent the message. If the users level is equal or higher // the command.Text field is returned. -func (app *Application) GetCommandHelp(name, username string) (string, error) { +func (app *application) GetCommandHelp(name, username string) (string, error) { // Fetch the command from the database if it exists. command, err := app.Models.Commands.Get(name) if err != nil { @@ -120,67 +122,54 @@ func (app *Application) GetCommandHelp(name, username string) (string, error) { // EditCommandLevel takes in a name and level string and updates the entry with name // to the supplied level value. -func (app *Application) EditCommandLevel(name, lvl string, message twitch.PrivateMessage) { +func (app *application) EditCommandLevel(name, lvl string, message twitch.PrivateMessage) { level, err := strconv.Atoi(lvl) if err != nil { - app.Logger.Error(err) - common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrCommandLevelNotInteger), app.TwitchClient) + app.Log.Error(err) + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrCommandLevelNotInteger), message) return } err = app.Models.Commands.SetLevel(name, level) if err != nil { - common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient) - app.Logger.Error(err) + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message) + app.Log.Error(err) return } else { reply := fmt.Sprintf("Updated command %s to level %v", name, level) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } } // EditCommandCategory takes in a name and category string and updates the command // in the databse with the passed in new category. -func (app *Application) EditCommandCategory(name, category string, message twitch.PrivateMessage) { +func (app *application) EditCommandCategory(name, category string, message twitch.PrivateMessage) { err := app.Models.Commands.SetCategory(name, category) if err != nil { - common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient) - app.Logger.Error(err) + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message) + app.Log.Error(err) return } else { reply := fmt.Sprintf("Updated command %s to category %v", name, category) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } } // DebugCommand checks if a command with the provided name exists in the database // and outputs information about it in the chat. -func (app *Application) DebugCommand(name string, message twitch.PrivateMessage) { +func (app *application) DebugCommand(name string, message twitch.PrivateMessage) { // Query the database for a command with the provided name cmd, err := app.Models.Commands.Get(name) if err != nil { reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err) - common.Send(message.Channel, reply, app.TwitchClient) - return - } else if cmd.Category == "ascii" { - // If the command is in the ascii category don't post the Text field - // otherwise it becomes too spammy and won't fit in the max message length. - reply := fmt.Sprintf("ID %v: Name %v, Level: %v, Category: %v Help: %v", - cmd.ID, - cmd.Name, - cmd.Level, - cmd.Category, - cmd.Help, - ) - - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } else { - reply := fmt.Sprintf("ID %v: Name %v, Level: %v, Category: %v, Text: %v, Help: %v", + reply := fmt.Sprintf("id=%v\nname=%v\nlevel=%v\ncategory=%v\ntext=%v\nhelp=%v\n", cmd.ID, cmd.Name, cmd.Level, @@ -189,16 +178,24 @@ func (app *Application) DebugCommand(name string, message twitch.PrivateMessage) cmd.Help, ) - common.Send(message.Channel, reply, app.TwitchClient) + //app.Send(message.Channel, reply) + resp, err := app.uploadPaste(reply) + if err != nil { + app.Log.Errorln("Could not upload paste:", err) + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %v", ErrDuringPasteUpload), message) + return + } + app.Send(message.Channel, resp, message) + //app.SendEmail(fmt.Sprintf("DEBUG for command %s", name), reply) return } } // SetCommandHelp updates the `help` column of a given commands name in the // database to the provided new help text. -func (app *Application) EditCommandHelp(name string, message twitch.PrivateMessage) { +func (app *application) EditCommandHelp(name string, message twitch.PrivateMessage) { // snipLength is the length we need to "snip" off of the start of `message`. - // `()editcommand` = +13 + // `()edit command` = +13 // trailing space = +1 // zero-based = +1 // `help` = +4 @@ -216,25 +213,36 @@ func (app *Application) EditCommandHelp(name string, message twitch.PrivateMessa err := app.Models.Commands.SetHelp(name, text) if err != nil { - common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient) - app.Logger.Error(err) + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message) + app.Log.Error(err) return } else { reply := fmt.Sprintf("Updated help text for command %s to: %v", name, text) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } } // DeleteCommand takes in a name value and deletes the command from the database if it exists. -func (app *Application) DeleteCommand(name string, message twitch.PrivateMessage) { +func (app *application) DeleteCommand(name string, message twitch.PrivateMessage) { err := app.Models.Commands.Delete(name) if err != nil { - common.Send(message.Channel, "Something went wrong FeelsBadMan", app.TwitchClient) - app.Logger.Error(err) + app.Send(message.Channel, "Something went wrong FeelsBadMan", message) + app.Log.Error(err) return } reply := fmt.Sprintf("Deleted command %s", name) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) +} + +func (app *application) LogCommand(msg twitch.PrivateMessage, commandName string, userLevel int) { + twitchLogin := msg.User.Name + twitchID := msg.User.ID + twitchMessage := msg.Message + twitchChannel := msg.Channel + identifier := uuid.NewString() + rawMsg := msg.Raw + + go app.Models.CommandsLogs.Insert(twitchLogin, twitchID, twitchChannel, twitchMessage, commandName, userLevel, identifier, rawMsg) } diff --git a/cmd/nourybot/commands.go b/cmd/nourybot/commands.go new file mode 100644 index 0000000..026eb12 --- /dev/null +++ b/cmd/nourybot/commands.go @@ -0,0 +1,309 @@ +package main + +import ( + "strings" + + "github.com/gempir/go-twitch-irc/v4" + "github.com/lyx0/nourybot/internal/commands" + "github.com/lyx0/nourybot/internal/common" + "github.com/lyx0/nourybot/internal/ivr" +) + +// handleCommand takes in a twitch.PrivateMessage and then routes the message to +// the function that is responsible for each command and knows how to deal with it accordingly. +func (app *application) handleCommand(message twitch.PrivateMessage) { + var reply string + + // Increments the counter how many commands have been used, called in the ping command. + go common.CommandUsed() + + go app.InitUser(message.User.Name, message.User.ID) + + // commandName is the actual name of the command without the prefix. + // e.g. `()ping` would be `ping`. + commandName := strings.ToLower(strings.SplitN(message.Message, " ", 3)[0][2:]) + + // cmdParams are additional command parameters. + // e.g. `()weather san antonio` + // cmdParam[0] is `san` and cmdParam[1] = `antonio`. + // + // Since Twitch messages are at most 500 characters I use a + // maximum count of 500+10 just to be safe. + // https://discuss.dev.twitch.tv/t/missing-client-side-message-length-check/21316 + cmdParams := strings.SplitN(message.Message, " ", 500) + + // msgLen is the amount of words in a message without the prefix. + // Useful to check if enough cmdParams are provided. + msgLen := len(strings.SplitN(message.Message, " ", -2)) + + userLevel := app.GetUserLevel(message.User.ID) + // target is the channelname the message originated from and + // where the TwitchClient should send the response + target := message.Channel + app.Log.Infow("Command received", + // "message", message, // Pretty taxing + "message.Message", message.Message, + "message.Channel", target, + "commandName", commandName, + "cmdParams", cmdParams, + "msgLen", msgLen, + "userLevel", userLevel, + ) + + go app.LogCommand(message, commandName, userLevel) + // A `commandName` is every message starting with `()`. + // Hardcoded commands have a priority over database commands. + // Switch over the commandName and see if there is a hardcoded case for it. + // If there was no switch case satisfied, query the database if there is + // a data.CommandModel.Name equal to the `commandName` + // If there is return the data.CommandModel.Text entry. + // Otherwise we ignore the message. + switch commandName { + case "": + if msgLen == 1 { + reply = "xd" + } + + case "bttv": + if msgLen < 2 { + reply = "Not enough arguments provided. Usage: ()bttv " + } else { + reply = commands.Bttv(cmdParams[1]) + } + + // Coinflip + case "coin": + reply = commands.Coinflip() + case "coinflip": + reply = commands.Coinflip() + case "cf": + reply = commands.Coinflip() + + // ()currency to + case "currency": + if msgLen < 4 { + reply = "Not enough arguments provided. Usage: ()currency 10 USD to EUR" + } else { + reply, _ = commands.Currency(cmdParams[1], cmdParams[2], cmdParams[4]) + } + + case "catbox": + go app.NewDownload("catbox", target, cmdParams[1], message) + + case "kappa": + go app.NewDownload("kappa", target, cmdParams[1], message) + + case "yaf": + go app.NewDownload("yaf", target, cmdParams[1], message) + + case "gofile": + go app.NewDownload("gofile", target, cmdParams[1], message) + + case "osrs": + reply = commands.OSRS(message.Message[7:len(message.Message)]) + + case "preview": + reply = commands.Preview(cmdParams[1]) + + case "thumbnail": + reply = commands.Preview(cmdParams[1]) + + case "ffz": + reply = commands.Ffz(cmdParams[1]) + + case "ddg": + reply = commands.DuckDuckGo(message.Message[6:len(message.Message)]) + + case "youtube": + reply = commands.Youtube(message.Message[10:len(message.Message)]) + + case "godocs": + reply = commands.Godocs(message.Message[9:len(message.Message)]) + + case "google": + reply = commands.Google(message.Message[9:len(message.Message)]) + + case "duckduckgo": + reply = commands.DuckDuckGo(message.Message[13:len(message.Message)]) + + case "seventv": + reply = commands.SevenTV(cmdParams[1]) + + case "7tv": + reply = commands.SevenTV(cmdParams[1]) + + case "mail": + app.SendEmail("Test command used!", "This is an email test") + + case "lastfm": + if msgLen == 1 { + reply = app.UserCheckLastFM(message) + } else { + // Default to first argument supplied being the name + // of the user to look up recently played. + reply = commands.LastFmUserRecent(target, cmdParams[1]) + } + + case "help": + if msgLen > 1 { + app.commandHelp(target, cmdParams[1], message.User.Name, message) + } + + case "nourybot": + reply = "Lidl Twitch bot made by @nourylul. Prefix: ()" + + case "phonetic": + if msgLen == 1 { + reply = "Not enough arguments provided. Usage: ()phonetic " + } else { + reply, _ = commands.Phonetic(message.Message[11:len(message.Message)]) + } + case "ping": + reply = commands.Ping() + // ()bttv + + // ()weather + case "weather": + if msgLen == 1 { + app.UserCheckWeather(message) + } else if msgLen < 2 { + reply = "Not enough arguments provided." + } else { + reply, _ = commands.Weather(message.Message[10:len(message.Message)]) + } + + // Xkcd + // Random Xkcd + case "rxkcd": + reply, _ = commands.RandomXkcd() + case "randomxkcd": + reply, _ = commands.RandomXkcd() + // Latest Xkcd + case "xkcd": + reply, _ = commands.Xkcd() + + case "timer": + switch cmdParams[1] { + case "add": + app.AddTimer(cmdParams[2], cmdParams[3], message) + case "edit": + app.EditTimer(cmdParams[2], cmdParams[3], message) + case "delete": + app.DeleteTimer(cmdParams[2], message) + case "list": + reply = app.ListTimers() + } + + case "debug": + switch cmdParams[1] { + case "user": + if userLevel >= 250 { + app.DebugUser(cmdParams[2], message) + } + case "command": + if userLevel >= 250 { + app.DebugCommand(cmdParams[2], message) + } + } + + case "command": + switch cmdParams[1] { + case "add": + app.AddCommand(cmdParams[2], message) + case "delete": + app.DeleteCommand(cmdParams[2], message) + case "edit": + switch cmdParams[2] { + case "level": + app.EditCommandLevel(cmdParams[3], cmdParams[4], message) + case "category": + app.EditCommandCategory(cmdParams[3], cmdParams[4], message) + } + } + + case "set": + switch cmdParams[1] { + case "lastfm": + app.SetUserLastFM(cmdParams[2], message) + case "location": + app.SetUserLocation(message) + } + + case "user": + switch cmdParams[1] { + case "edit": + switch cmdParams[2] { + case "level": + app.EditUserLevel(cmdParams[3], cmdParams[4], message) + } + } + + case "join": + go app.AddChannel(cmdParams[1], message) + + case "part": + go app.DeleteChannel(cmdParams[1], message) + + case "uid": + reply = ivr.IDByUsername(cmdParams[1]) + + default: + r, err := app.GetCommand(target, commandName, userLevel) + if err != nil { + return + } + reply = r + } + if reply != "" { + go app.Send(target, reply, message) + return + } +} + +// Map of known commands with their help texts. +var helpText = map[string]string{ + "bttv": "Returns the search URL for a given BTTV emote. Example usage: ()bttv ", + "coin": "Flips a coin! Aliases: coinflip, coin, cf", + "cf": "Flips a coin! Aliases: coinflip, coin, cf", + "coinflip": "Flips a coin! Aliases: coinflip, coin, cf", + "currency": "Returns the exchange rate for two currencies. Only three letter abbreviations are supported ( List of supported currencies: https://decapi.me/misc/currency?list ). Example usage: ()currency 10 USD to EUR", + "ffz": "Returns the search URL for a given FFZ emote. Example usage: ()ffz ", + "followage": "Returns how long a given user has been following a channel. Example usage: ()followage ", + "firstline": "Returns the first message a user has sent in a given channel. Aliases: firstline, fl. Example usage: ()firstline ", + "fl": "Returns the first message a user has sent in a given channel. Aliases: firstline, fl. Example usage: ()fl ", + "help": "Returns more information about a command and its usage. 4Head Example usage: ()help ", + "ping": "Hopefully returns a Pong! monkaS", + "preview": "Returns a link to an (almost) live screenshot of a live channel. Alias: preview, thumbnail. Example usage: ()preview ", + "phonetic": "Translates the input to the text equivalent on a phonetic russian keyboard layout. Layout and general functionality is the same as https://russian.typeit.org/", + "ph": "Translates the input to the text equivalent on a phonetic russian keyboard layout. Layout and general functionality is the same as https://russian.typeit.org/", + "thumbnail": "Returns a link to an (almost) live screenshot of a live channel. Alias: preview, thumbnail. Example usage: ()thumbnail ", + "tweet": "Returns the latest tweet for a provided user. Example usage: ()tweet ", + "seventv": "Returns the search URL for a given SevenTV emote. Aliases: seventv, 7tv. Example usage: ()seventv FeelsDankMan", + "7tv": "Returns the search URL for a given SevenTV emote. Aliases: seventv, 7tv. Example usage: ()7tv FeelsDankMan", + "weather": "Returns the weather for a given location. Example usage: ()weather Vilnius", + "randomxkcd": "Returns a link to a random xkcd comic. Alises: randomxkcd, rxkcd. Example usage: ()randomxkcd", + "rxkcd": "Returns a link to a random xkcd comic. Alises: randomxkcd, rxkcd. Example usage: ()rxkcd", + "xkcd": "Returns a link to the latest xkcd comic. Example usage: ()xkcd", +} + +// Help checks if a help text for a given command exists and replies with it. +func (app *application) commandHelp(target, name, username string, message twitch.PrivateMessage) { + // Check if the `helpText` map has an entry for `name`. If it does return it's value entry + // and send that as a reply. + i, ok := helpText[name] + if !ok { + // If it doesn't check the database for a command with that `name`. If there is one + // reply with that commands `help` entry. + c, err := app.GetCommandHelp(name, username) + if err != nil { + app.Log.Infow("commandHelp: no such command found", + "err", err) + return + } + + app.Send(target, c, message) + return + } + + app.Send(target, i, message) +} diff --git a/cmd/nourybot/download.go b/cmd/nourybot/download.go new file mode 100644 index 0000000..092174f --- /dev/null +++ b/cmd/nourybot/download.go @@ -0,0 +1,217 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/gempir/go-twitch-irc/v4" + "github.com/google/uuid" + "github.com/wader/goutubedl" +) + +func (app *application) NewDownload(destination, target, link string, msg twitch.PrivateMessage) { + identifier := uuid.NewString() + go app.Models.Uploads.Insert( + msg.User.Name, + msg.User.ID, + msg.Channel, + msg.Message, + destination, + link, + identifier, + ) + app.Send(target, "xd", msg) + + switch destination { + case "catbox": + app.CatboxDownload(target, link, identifier, msg) + case "yaf": + app.YafDownload(target, link, identifier, msg) + case "kappa": + app.KappaDownload(target, link, identifier, msg) + case "gofile": + app.GofileDownload(target, link, identifier, msg) + } +} + +func (app *application) YafDownload(target, link, identifier string, msg twitch.PrivateMessage) { + goutubedl.Path = "yt-dlp" + + app.Send(target, "Downloading... dankCircle", msg) + result, err := goutubedl.New(context.Background(), link, goutubedl.Options{}) + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + rExt := result.Info.Ext + downloadResult, err := result.Download(context.Background(), "best") + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + app.Send(target, "Downloaded.", msg) + fileName := fmt.Sprintf("%s.%s", identifier, rExt) + f, err := os.Create(fileName) + app.Send(target, fmt.Sprintf("Filename: %s", fileName), msg) + + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + defer f.Close() + if _, err = io.Copy(f, downloadResult); err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + + downloadResult.Close() + f.Close() + // duration := 5 * time.Second + // dl.twitchClient.Say(target, "ResidentSleeper ..") + // time.Sleep(duration) + + go app.NewUpload("yaf", fileName, target, identifier, msg) + +} + +func (app *application) KappaDownload(target, link, identifier string, msg twitch.PrivateMessage) { + goutubedl.Path = "yt-dlp" + + app.Send(target, "Downloading... dankCircle", msg) + result, err := goutubedl.New(context.Background(), link, goutubedl.Options{}) + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + rExt := result.Info.Ext + downloadResult, err := result.Download(context.Background(), "best") + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + app.Send(target, "Downloaded.", msg) + fileName := fmt.Sprintf("%s.%s", identifier, rExt) + f, err := os.Create(fileName) + app.Send(target, fmt.Sprintf("Filename: %s", fileName), msg) + + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + defer f.Close() + if _, err = io.Copy(f, downloadResult); err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + + downloadResult.Close() + f.Close() + // duration := 5 * time.Second + // dl.twitchClient.Say(target, "ResidentSleeper ..") + // time.Sleep(duration) + + go app.NewUpload("kappa", fileName, target, identifier, msg) + +} + +func (app *application) GofileDownload(target, link, identifier string, msg twitch.PrivateMessage) { + goutubedl.Path = "yt-dlp" + + app.Send(target, "Downloading... dankCircle", msg) + result, err := goutubedl.New(context.Background(), link, goutubedl.Options{}) + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + safeFilename := fmt.Sprintf("download_%s", result.Info.Title) + rExt := result.Info.Ext + downloadResult, err := result.Download(context.Background(), "best") + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + app.Send(target, "Downloaded.", msg) + fileName := fmt.Sprintf("%s.%s", safeFilename, rExt) + f, err := os.Create(fileName) + app.Send(target, fmt.Sprintf("Filename: %s", fileName), msg) + + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + defer f.Close() + if _, err = io.Copy(f, downloadResult); err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + + downloadResult.Close() + f.Close() + // duration := 5 * time.Second + // dl.twitchClient.Say(target, "ResidentSleeper ..") + // time.Sleep(duration) + + go app.NewUpload("gofile", fileName, target, identifier, msg) + +} + +func (app *application) CatboxDownload(target, link, identifier string, msg twitch.PrivateMessage) { + goutubedl.Path = "yt-dlp" + var fileName string + + app.Send(target, "Downloading... dankCircle", msg) + result, err := goutubedl.New(context.Background(), link, goutubedl.Options{}) + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + + // I don't know why but I need to set it to mp4, otherwise if + // I use `result.Into.Ext` catbox won't play the video in the + // browser and say this message: + // `No video with supported format and MIME type found.` + rExt := "mp4" + + downloadResult, err := result.Download(context.Background(), "best") + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + app.Send(target, "Downloaded.", msg) + fileName = fmt.Sprintf("%s.%s", identifier, rExt) + f, err := os.Create(fileName) + app.Send(target, fmt.Sprintf("Filename: %s", fileName), msg) + + if err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + defer f.Close() + if _, err = io.Copy(f, downloadResult); err != nil { + app.Log.Errorln(err) + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + + downloadResult.Close() + f.Close() + + go app.NewUpload("catbox", fileName, target, identifier, msg) +} diff --git a/cmd/nourybot/email.go b/cmd/nourybot/email.go new file mode 100644 index 0000000..e22c190 --- /dev/null +++ b/cmd/nourybot/email.go @@ -0,0 +1,34 @@ +package main + +import ( + "crypto/tls" + "os" + + "github.com/joho/godotenv" + "gopkg.in/gomail.v2" +) + +// Thanks to Twitch moving whispers again I just use email now. +func (app *application) SendEmail(subject, body string) { + err := godotenv.Load() + if err != nil { + app.Log.Fatal("Error loading .env") + } + hostname := os.Getenv("EMAIL_HOST") + login := os.Getenv("EMAIL_LOGIN") + password := os.Getenv("EMAIL_PASS") + emailFrom := os.Getenv("EMAIL_FROM") + emailTo := os.Getenv("EMAIL_TO") + d := gomail.NewDialer(hostname, 587, login, password) + + d.TLSConfig = &tls.Config{InsecureSkipVerify: true} + m := gomail.NewMessage() + m.SetHeader("From", emailFrom) + m.SetHeader("To", emailTo) + m.SetHeader("Subject", subject) + m.SetBody("text/plain", body) + + if err := d.DialAndSend(m); err != nil { + panic(err) + } +} diff --git a/cmd/bot/errors.go b/cmd/nourybot/errors.go similarity index 83% rename from cmd/bot/errors.go rename to cmd/nourybot/errors.go index 6aacc49..65e7ec4 100644 --- a/cmd/bot/errors.go +++ b/cmd/nourybot/errors.go @@ -9,4 +9,5 @@ var ( ErrCommandLevelNotInteger = errors.New("command level must be a number") ErrRecordNotFound = errors.New("user not found in the database") ErrUserInsufficientLevel = errors.New("user has insufficient level") + ErrDuringPasteUpload = errors.New("could not upload paste") ) diff --git a/cmd/bot/main.go b/cmd/nourybot/main.go similarity index 71% rename from cmd/bot/main.go rename to cmd/nourybot/main.go index ca48867..a8326b4 100644 --- a/cmd/bot/main.go +++ b/cmd/nourybot/main.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "flag" - "log" "os" "time" @@ -14,8 +13,9 @@ import ( _ "github.com/lib/pq" "github.com/lyx0/nourybot/internal/common" "github.com/lyx0/nourybot/internal/data" - "github.com/nicklaw5/helix" - "github.com/redis/go-redis/v9" + "github.com/nicklaw5/helix/v2" + "github.com/rs/zerolog/log" + "go.uber.org/zap" ) @@ -24,6 +24,7 @@ type config struct { twitchOauth string twitchClientId string twitchClientSecret string + twitchID string commandPrefix string db struct { dsn string @@ -33,36 +34,39 @@ type config struct { } } -type Application struct { +type application struct { TwitchClient *twitch.Client HelixClient *helix.Client - Logger *zap.SugaredLogger + Log *zap.SugaredLogger Db *sql.DB Models data.Models Scheduler *cron.Cron - Rdb *redis.Client + // Rdb *redis.Client } var envFlag string -var ctx = context.Background() func init() { flag.StringVar(&envFlag, "env", "dev", "database connection to use: (dev/prod)") flag.Parse() } - func main() { var cfg config - // Initialize a new sugared logger that we'll pass on // down through the application. logger := zap.NewExample() - defer logger.Sync() + defer func() { + if err := logger.Sync(); err != nil { + logger.Sugar().Fatalw("error syncing logger", + "error", err, + ) + } + }() sugar := logger.Sugar() err := godotenv.Load() if err != nil { - log.Fatal("Error loading .env file") + sugar.Fatal("Error loading .env") } // Twitch config variables @@ -71,15 +75,15 @@ func main() { cfg.twitchClientId = os.Getenv("TWITCH_CLIENT_ID") cfg.twitchClientSecret = os.Getenv("TWITCH_CLIENT_SECRET") cfg.commandPrefix = os.Getenv("TWITCH_COMMAND_PREFIX") + cfg.twitchID = os.Getenv("TWITCH_ID") tc := twitch.NewClient(cfg.twitchUsername, cfg.twitchOauth) switch envFlag { case "dev": cfg.db.dsn = os.Getenv("LOCAL_DSN") case "prod": - cfg.db.dsn = os.Getenv("SUPABASE_DSN") + cfg.db.dsn = os.Getenv("REMOTE_DSN") } - // Database config variables cfg.db.maxOpenConns = 25 cfg.db.maxIdleConns = 25 @@ -105,9 +109,6 @@ func main() { "err", err, ) } - sugar.Infow("Got new helix AppAccessToken", - "helixClient", helixResp, - ) // Set the access token on the client helixClient.SetAppAccessToken(helixResp.Data.AccessToken) @@ -115,54 +116,37 @@ func main() { // Establish database connection db, err := openDB(cfg) if err != nil { - sugar.Fatal(err) + sugar.Fatalw("could not establish database connection", + "err", err, + ) } - - rdb := redis.NewClient(&redis.Options{ - Addr: "127.0.0.1:6379", - Password: "", - DB: 0, - }) - - err = rdb.Set(ctx, "key", "value", 0).Err() - if err != nil { - sugar.Panic(err) - } - val, err := rdb.Get(ctx, "key").Result() - if err != nil { - sugar.Panic(err) - } - sugar.Infow("Redis initialization key", - "key", val, - ) - - // Initialize Application with the new values - app := &Application{ + app := &application{ TwitchClient: tc, HelixClient: helixClient, - Logger: sugar, + Log: sugar, Db: db, Models: data.NewModels(db), Scheduler: cron.New(), - Rdb: rdb, } + app.Log.Infow("db.Stats", + "db.Stats", db.Stats(), + ) + // Received a PrivateMessage (normal chat message). app.TwitchClient.OnPrivateMessage(func(message twitch.PrivateMessage) { - - // app.Logger.Infow("Message received", - // "message", message, - // "message.User.DisplayName", message.User.DisplayName, - // "message.Message", message.Message, - // ) + sugar.Infow("New Twitch PrivateMessage", + "message.Channel", message.Channel, + "message.User.DisplayName", message.User.DisplayName, + "message.User.ID", message.User.ID, + "message.Message", message.Message, + ) // roomId is the Twitch UserID of the channel the message originated from. // If there is no roomId something went really wrong. roomId := message.Tags["room-id"] if roomId == "" { - app.Logger.Errorw("Missing room-id in message tag", - "roomId", roomId, - ) + log.Error().Msgf("Missing room-id in message tag: %s", roomId) return } @@ -172,32 +156,27 @@ func main() { // Check if the first 2 characters of the mesage were our prefix. // if they were forward the message to the command handler. if message.Message[:2] == cfg.commandPrefix { - app.InitUser(message.User.Name, message.User.ID, message) - app.handleCommand(message) + go app.handleCommand(message) return } // Special rule for #pajlada. if message.Message == "!nourybot" { - common.Send(message.Channel, "Lidl Twitch bot made by @nourylul. Prefix: ()", app.TwitchClient) + app.Send(message.Channel, "Lidl Twitch bot made by @nourylul. Prefix: ()", message) } - } }) - // Received a WhisperMessage (Twitch DM). - app.TwitchClient.OnWhisperMessage(func(message twitch.WhisperMessage) { - // Print the whisper message for now. - app.Logger.Infow("Whisper Message received", - "message", message, - "message.User.DisplayName", message.User.DisplayName, - "message.Message", message.Message, - ) - }) - - // Successfully connected to Twitch app.TwitchClient.OnConnect(func() { - app.Logger.Infow("Successfully connected to Twitch Servers", + common.StartTime() + + app.TwitchClient.Join("nourylul") + app.TwitchClient.Join("nourybot") + app.TwitchClient.Say("nourylul", "xD!") + app.TwitchClient.Say("nourybot", "gopherDance") + + // Successfully connected to Twitch + app.Log.Infow("Successfully connected to Twitch Servers", "Bot username", cfg.twitchUsername, "Environment", envFlag, "Database Open Conns", cfg.db.maxOpenConns, @@ -207,31 +186,19 @@ func main() { "Helix", helixResp, ) - // Start time - common.StartTime() - - app.loadCommandHelp() - - // Join the channels in the database. app.InitialJoin() - // Load the initial timers from the database. app.InitialTimers() // Start the timers. app.Scheduler.Start() - - common.Send("nourylul", "dankCircle", app.TwitchClient) - common.Send("nourybot", "gopherDance", app.TwitchClient) - common.Send("xnoury", "pajaDink", app.TwitchClient) - common.Send("uudelleenkytkeytynyt", "PepeS", app.TwitchClient) }) - // Actually connect to chat. err = app.TwitchClient.Connect() if err != nil { panic(err) } + } // openDB returns the sql.DB connection pool. diff --git a/cmd/nourybot/paste.go b/cmd/nourybot/paste.go new file mode 100644 index 0000000..9b37b13 --- /dev/null +++ b/cmd/nourybot/paste.go @@ -0,0 +1,59 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "time" +) + +// uploadPaste uploads a given text to a pastebin site and returns the link +// +// this whole function was pretty much yoinked from here +// https://github.com/zneix/haste-client/blob/master/main.go <3 +func (app *application) uploadPaste(text string) (string, error) { + const hasteURL = "https://haste.noury.cc" + const apiRoute = "/documents" + var httpClient = &http.Client{Timeout: 10 * time.Second} + + type pasteResponse struct { + Key string `json:"key,omitempty"` + } + + req, err := http.NewRequest("POST", hasteURL+apiRoute, bytes.NewBufferString(text)) + if err != nil { + app.Log.Errorln("Could not upload paste:", err) + return "", err + } + + req.Header.Set("User-Agent", "nourybot") + + resp, err := httpClient.Do(req) + if err != nil { + app.Log.Errorln("Error while sending HTTP request:", err) + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusMultipleChoices { + app.Log.Errorln("Failed to upload data, server responded with", resp.StatusCode) + return "", err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + app.Log.Errorln("Error while reading response:", err) + return "", err + } + + jsonResponse := new(pasteResponse) + if err := json.Unmarshal(body, jsonResponse); err != nil { + app.Log.Errorln("Error while unmarshalling JSON response:", err) + return "", err + } + + finalURL := hasteURL + "/" + jsonResponse.Key + + return finalURL, nil +} diff --git a/cmd/nourybot/send.go b/cmd/nourybot/send.go new file mode 100644 index 0000000..744242c --- /dev/null +++ b/cmd/nourybot/send.go @@ -0,0 +1,261 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/gempir/go-twitch-irc/v4" + "github.com/google/uuid" +) + +// banphraseResponse is the data we receive back from +// the banphrase API +type banphraseResponse struct { + Banned bool `json:"banned"` + InputMessage string `json:"input_message"` + BanphraseData banphraseData `json:"banphrase_data"` +} + +// banphraseData contains details about why a message +// was banphrased. +type banphraseData struct { + Id int `json:"id"` + Name string `json:"name"` + Phrase string `json:"phrase"` + Length int `json:"length"` + Permanent bool `json:"permanent"` +} + +var ( + banPhraseUrl = "https://pajlada.pajbot.com/api/v1/banphrases/test" +) + +// CheckMessage checks a given message against the banphrase api. +// returns false, "okay" if a message is allowed +// returns true and a string with the reason if it was banned. +// More information: +// https://gist.github.com/pajlada/57464e519ba8d195a97ddcd0755f9715 +func (app *application) checkMessage(text string) (bool, string) { + // {"message": "AHAHAHAHA LUL"} + reqBody, err := json.Marshal(map[string]string{ + "message": text, + }) + if err != nil { + app.Log.Error(err) + return true, "could not check banphrase api" + } + + resp, err := http.Post(banPhraseUrl, "application/json", bytes.NewBuffer(reqBody)) + if err != nil { + app.Log.Error(err) + return true, "could not check banphrase api" + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + app.Log.Error(err) + } + + var responseObject banphraseResponse + if err := json.Unmarshal(body, &responseObject); err != nil { + app.Log.Error(err) + return true, "could not check banphrase api" + } + + // Bad Message + // + // {"phrase": "No gyazo allowed"} + reason := responseObject.BanphraseData.Name + if responseObject.Banned { + return true, fmt.Sprint(reason) + } else if !responseObject.Banned { + // Good message + return false, "okay" + } + + // Couldn't contact api so assume it was a bad message + return true, "Banphrase API couldn't be reached monkaS" +} + +// Send is used to send twitch replies and contains the necessary +// safeguards and logic for that. +func (app *application) SendNoContext(target, message string) { + // Message we are trying to send is empty. + if len(message) == 0 { + return + } + + identifier := uuid.NewString() + go app.Models.SentMessagesLogs.Insert(target, message, "unavailable", "unavailable", "unavailable", "unavailable", identifier, "unavailable") + + // Since messages starting with `.` or `/` are used for special actions + // (ban, whisper, timeout) and so on, we place an emote infront of it so + // the actions wouldn't execute. `!` and `$` are common bot prefixes so we + // don't allow them either. + if message[0] == '.' || message[0] == '/' || message[0] == '!' || message[0] == '$' { + message = ":tf: " + message + } + + // check the message for bad words before we say it + messageBanned, banReason := app.checkMessage(message) + if !messageBanned { + // In case the message we are trying to send is longer than the + // maximum allowed message length on twitch we split the message in two parts. + // Twitch has a maximum length for messages of 510 characters so to be safe + // we split and check at 500 characters. + // https://discuss.dev.twitch.tv/t/missing-client-side-message-length-check/21316 + // TODO: Make it so it splits at a space instead and not in the middle of a word. + if len(message) > 500 { + firstMessage := message[0:499] + secondMessage := message[499:] + + app.TwitchClient.Say(target, firstMessage) + app.TwitchClient.Say(target, secondMessage) + + return + } else { + // Message was fine. + go app.TwitchClient.Say(target, message) + return + } + } else { + // Bad message, replace message and log it. + app.TwitchClient.Say(target, "[BANPHRASED] monkaS") + app.Log.Infow("banned message detected", + "target channel", target, + "message", message, + "ban reason", banReason, + ) + + return + } +} + +// Send is used to send twitch replies and contains the necessary +// safeguards and logic for that. +func (app *application) Send(target, message string, msgContext twitch.PrivateMessage) { + // Message we are trying to send is empty. + if len(message) == 0 { + return + } + + commandName := strings.ToLower(strings.SplitN(msgContext.Message, " ", 3)[0][2:]) + identifier := uuid.NewString() + go app.Models.SentMessagesLogs.Insert(target, message, commandName, msgContext.User.Name, msgContext.User.ID, msgContext.Message, identifier, msgContext.Raw) + + // Since messages starting with `.` or `/` are used for special actions + // (ban, whisper, timeout) and so on, we place an emote infront of it so + // the actions wouldn't execute. `!` and `$` are common bot prefixes so we + // don't allow them either. + if message[0] == '.' || message[0] == '/' || message[0] == '!' || message[0] == '$' { + message = ":tf: " + message + } + + // check the message for bad words before we say it + messageBanned, banReason := app.checkMessage(message) + if !messageBanned { + // In case the message we are trying to send is longer than the + // maximum allowed message length on twitch we split the message in two parts. + // Twitch has a maximum length for messages of 510 characters so to be safe + // we split and check at 500 characters. + // https://discuss.dev.twitch.tv/t/missing-client-side-message-length-check/21316 + // TODO: Make it so it splits at a space instead and not in the middle of a word. + if len(message) > 500 { + firstMessage := message[0:499] + secondMessage := message[499:] + + app.TwitchClient.Say(target, firstMessage) + app.TwitchClient.Say(target, secondMessage) + + return + } else { + // Message was fine. + go app.TwitchClient.Say(target, message) + return + } + } else { + // Bad message, replace message and log it. + app.TwitchClient.Say(target, "[BANPHRASED] monkaS") + app.Log.Infow("banned message detected", + "target channel", target, + "message", message, + "ban reason", banReason, + ) + + return + } +} + +// Send is used to send twitch replies and contains the necessary +// safeguards and logic for that. +func (app *application) SendNoBanphrase(target, message string) { + // Message we are trying to send is empty. + if len(message) == 0 { + return + } + + identifier := uuid.NewString() + go app.Models.SentMessagesLogs.Insert(target, message, "unavailable", "unavailable", "unavailable", "unavailable", identifier, "unavailable") + + // Since messages starting with `.` or `/` are used for special actions + // (ban, whisper, timeout) and so on, we place an emote infront of it so + // the actions wouldn't execute. `!` and `$` are common bot prefixes so we + // don't allow them either. + if message[0] == '.' || message[0] == '/' || message[0] == '!' || message[0] == '$' { + message = ":tf: " + message + } + + // check the message for bad words before we say it + // Message was fine. + go app.TwitchClient.Say(target, message) +} + +// SendNoLimit does not check for the maximum message size. +// Used in sending commands from the database since the command has to have +// been gotten in there somehow. So it fits. Still checks for banphrases. +func (app *application) SendNoLimit(target, message string) { + // Message we are trying to send is empty. + if len(message) == 0 { + return + } + + // Since messages starting with `.` or `/` are used for special actions + // (ban, whisper, timeout) and so on, we place an emote infront of it so + // the actions wouldn't execute. `!` and `$` are common bot prefixes so we + // don't allow them either. + if message[0] == '.' || message[0] == '/' || message[0] == '!' || message[0] == '$' { + message = ":tf: " + message + } + + // check the message for bad words before we say it + messageBanned, banReason := app.checkMessage(message) + if messageBanned { + // Bad message, replace message and log it. + go app.TwitchClient.Say(target, "[BANPHRASED] monkaS") + app.Log.Infow("banned message detected", + "target channel", target, + "message", message, + "ban reason", banReason, + ) + + return + } else { + // In case the message we are trying to send is longer than the + // maximum allowed message length on twitch we split the message in two parts. + // Twitch has a maximum length for messages of 510 characters so to be safe + // we split and check at 500 characters. + // https://discuss.dev.twitch.tv/t/missing-client-side-message-length-check/21316 + // TODO: Make it so it splits at a space instead and not in the middle of a word. + // Message was fine. + identifier := uuid.NewString() + go app.Models.SentMessagesLogs.Insert(target, message, "unavailable", "unavailable", "unavailable", "unavailable", identifier, "unavailable") + go app.TwitchClient.Say(target, message) + return + } +} diff --git a/cmd/bot/timer.go b/cmd/nourybot/timer.go similarity index 50% rename from cmd/bot/timer.go rename to cmd/nourybot/timer.go index 1862dc5..2d5d610 100644 --- a/cmd/bot/timer.go +++ b/cmd/nourybot/timer.go @@ -6,41 +6,50 @@ import ( "strings" "github.com/gempir/go-twitch-irc/v4" - "github.com/lyx0/nourybot/internal/common" + "github.com/google/uuid" "github.com/lyx0/nourybot/internal/data" - "github.com/redis/go-redis/v9" ) // AddTimer slices the message into relevant parts, adding the values onto a // new data.Timer struct so that the timer can be inserted into the database. -func (app *Application) AddTimer(name string, message twitch.PrivateMessage) { +func (app *application) AddTimer(name, repeat string, message twitch.PrivateMessage) { cmdParams := strings.SplitN(message.Message, " ", 500) - // snipLength is the length of `()addcommand` plus +2 (for the space and zero based) - snipLength := 12 - repeat := cmdParams[2] + // prefixLength is the length of `()add timer` plus +2 (for the space and zero based) + prefix := "()add timer" + prefixLength := len("()add timer") + nameLength := len(name) + repeatLength := len(repeat) + app.Log.Infow("Lengths", + "prefix", prefixLength, + "name", nameLength, + "repeat", repeatLength, + "repeat2", len(cmdParams[2]), + "sum", prefixLength+nameLength+repeatLength, + ) // Split the message into the parts we need. // - // message: ()addtimer sponsor 20m hecking love my madmonq pills BatChest + // message: ()timer add sponsor 20m hecking love my madmonq pills BatChest // parts: | prefix | |name | |repeat | <----------- text -------------> | - text := message.Message[snipLength+len(name)+len(cmdParams[2]) : len(message.Message)] + text := message.Message[3+len(prefix)+len(name)+len(repeat) : len(message.Message)] // validateTimeFormat will be true if the repeat parameter is in // the format of either 30m, 10h, or 10h30m. validateTimeFormat, err := regexp.MatchString(`^(\d{1,2}[h])$|^(\d+[m])$|^(\d+[s])$|((\d{1,2}[h])((([0]?|[1-5]{1})[0-9])[m]))$`, repeat) if err != nil { - app.Logger.Errorw("Received malformed time format in timer", + app.Log.Errorw("Received malformed time format in timer", "repeat", repeat, "error", err, ) return } - + id := uuid.NewString() timer := &data.Timer{ - Name: name, - Text: text, - Channel: message.Channel, - Repeat: repeat, + Name: name, + Text: text, + Identifier: id, + Channel: message.Channel, + Repeat: repeat, } // Check if the time string we got is valid, this is important @@ -48,123 +57,123 @@ func (app *Application) AddTimer(name string, message twitch.PrivateMessage) { // time format string is supplied. if validateTimeFormat { timer := &data.Timer{ - Name: name, - Text: text, - Channel: message.Channel, - Repeat: repeat, + Name: name, + Text: text, + Identifier: id, + Channel: message.Channel, + Repeat: repeat, } err = app.Models.Timers.Insert(timer) if err != nil { - app.Logger.Errorw("Error inserting new timer into database", + app.Log.Errorw("Error inserting new timer into database", "timer", timer, "error", err, ) reply := fmt.Sprintln("Something went wrong FeelsBadMan") - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } else { // cronName is the internal, unique tag/name for the timer. // A timer named "sponsor" in channel "forsen" will be named "forsensponsor" - cronName := fmt.Sprintf("%s-%s", message.Channel, name) - app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, cronName) - app.Logger.Infow("Added new timer", + app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, id) + app.Log.Infow("Added new timer", "timer", timer, ) - if _, err := app.Rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { - rdb.HSet(ctx, cronName, "timer-name", name) - rdb.HSet(ctx, cronName, "timer-cronname", cronName) - rdb.HSet(ctx, cronName, "timer-text", text) - rdb.HSet(ctx, cronName, "timer-channel", message.Channel) - rdb.HSet(ctx, cronName, "timer-repeat", repeat) - return nil - }); err != nil { - app.Logger.Panic(err) - } - app.Logger.Infow("Loaded timer into redis:", - "key", cronName, - "value", app.Rdb.HGetAll(ctx, cronName), - ) - reply := fmt.Sprintf("Successfully added timer %s repeating every %s", name, repeat) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } } else { - app.Logger.Errorw("Received malformed time format in timer", + app.Log.Errorw("Received malformed time format in timer", "timer", timer, "error", err, ) reply := "Something went wrong FeelsBadMan received wrong time format. Allowed formats: 30m, 10h, 10h30m" - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } } // EditTimer just contains the logic for deleting a timer, and then adding a new one // with the same name. It is technically not editing the timer. -func (app *Application) EditTimer(name string, message twitch.PrivateMessage) { +func (app *application) EditTimer(name, repeat string, message twitch.PrivateMessage) { // Check if a timer with that name is in the database. + app.Log.Info(name) + old, err := app.Models.Timers.Get(name) if err != nil { - app.Logger.Errorw("Could not get timer", + app.Log.Errorw("Could not get timer", "timer", old, "error", err, ) reply := "Something went wrong FeelsBadMan" - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } + // ----------------------- // Delete the old timer - cronName := fmt.Sprintf("%s-%s", message.Channel, name) - app.Scheduler.RemoveJob(cronName) + // ----------------------- + identifier := old.Identifier + app.Scheduler.RemoveJob(identifier) - _ = app.Rdb.Del(ctx, cronName) err = app.Models.Timers.Delete(name) if err != nil { - app.Logger.Errorw("Error deleting timer from database", + app.Log.Errorw("Error deleting timer from database", "name", name, - "cronName", cronName, + "identifier", identifier, "error", err, ) reply := fmt.Sprintln("Something went wrong FeelsBadMan") - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } + // ----------------------- // Add the new timer - cmdParams := strings.SplitN(message.Message, " ", 500) - // snipLength is the length of `()editcommand` plus +2 (for the space and zero based) - snipLength := 13 - repeat := cmdParams[2] + // ----------------------- + //cmdParams := strings.SplitN(message.Message, " ", 500) + // prefixLength is the length of `()editcommand` plus +2 (for the space and zero based) + prefix := "()edit timer" + prefixLength := len("()add timer") + nameLength := len(name) + repeatLength := len(repeat) + app.Log.Infow("Lengths", + "prefix", prefixLength, + "name", nameLength, + "repeat", repeatLength, + "sum", prefixLength+nameLength+repeatLength, + ) // Split the message into the parts we need. // // message: ()addtimer sponsor 20m hecking love my madmonq pills BatChest // parts: | prefix | |name | |repeat | <----------- text -------------> | - text := message.Message[snipLength+len(name)+len(cmdParams[2]) : len(message.Message)] + text := message.Message[3+len(prefix)+len(name)+len(repeat) : len(message.Message)] // validateTimeFormat will be true if the repeat parameter is in // the format of either 30m, 10h, or 10h30m. validateTimeFormat, err := regexp.MatchString(`^(\d{1,2}[h])$|^(\d+[m])$|^(\d+[s])$|((\d{1,2}[h])((([0]?|[1-5]{1})[0-9])[m]))$`, repeat) if err != nil { - app.Logger.Errorw("Received malformed time format in timer", + app.Log.Errorw("Received malformed time format in timer", "repeat", repeat, "error", err, ) return } + id := uuid.NewString() timer := &data.Timer{ - Name: name, - Text: text, - Channel: message.Channel, - Repeat: repeat, + Name: name, + Text: text, + Identifier: id, + Channel: message.Channel, + Repeat: repeat, } // Check if the time string we got is valid, this is important @@ -172,71 +181,104 @@ func (app *Application) EditTimer(name string, message twitch.PrivateMessage) { // time format string is supplied. if validateTimeFormat { timer := &data.Timer{ - Name: name, - Text: text, - Channel: message.Channel, - Repeat: repeat, + Name: name, + Text: text, + Identifier: id, + Channel: message.Channel, + Repeat: repeat, } err = app.Models.Timers.Insert(timer) if err != nil { - app.Logger.Errorw("Error inserting new timer into database", + app.Log.Errorw("Error inserting new timer into database", "timer", timer, "error", err, ) reply := fmt.Sprintln("Something went wrong FeelsBadMan") - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } else { // this is a bit scuffed. The else here is the end of a successful call. // cronName is the internal, unique tag/name for the timer. // A timer named "sponsor" in channel "forsen" will be named "forsensponsor" - cronName := fmt.Sprintf("%s-%s", message.Channel, name) + app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, id) - app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, cronName) - app.Logger.Infow("Updated a timer", + app.Log.Infow("Updated a timer", "Name", name, "Channel", message.Channel, "Old timer", old, "New timer", timer, ) - if _, err := app.Rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { - rdb.HSet(ctx, cronName, "timer-name", name) - rdb.HSet(ctx, cronName, "timer-cronname", cronName) - rdb.HSet(ctx, cronName, "timer-text", text) - rdb.HSet(ctx, cronName, "timer-channel", message.Channel) - rdb.HSet(ctx, cronName, "timer-repeat", repeat) - return nil - }); err != nil { - app.Logger.Panic(err) - } - app.Logger.Infow("Loaded timer into redis:", - "key", cronName, - "value", app.Rdb.HGetAll(ctx, cronName), - ) - reply := fmt.Sprintf("Successfully updated timer %s", name) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } } else { - app.Logger.Errorw("Received malformed time format in timer", + app.Log.Errorw("Received malformed time format in timer", "timer", timer, "error", err, ) reply := "Something went wrong FeelsBadMan received wrong time format. Allowed formats: 30s, 30m, 10h, 10h30m" - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } } // InitialTimers is called on startup and queries the database for a list of // timers and then adds each onto the scheduler. -func (app *Application) InitialTimers() { +func (app *application) ListTimers() string { timer, err := app.Models.Timers.GetAll() if err != nil { - app.Logger.Errorw("Error trying to retrieve all timers from database", err) + app.Log.Errorw("Error trying to retrieve all timers from database", err) + return "" + } + + // The slice of timers is only used to log them at + // the start so it looks a bit nicer. + var ts []string + + // Iterate over all timers and then add them onto the scheduler. + for i, v := range timer { + // idk why this works but it does so no touchy touchy. + // https://github.com/robfig/cron/issues/420#issuecomment-940949195 + i, v := i, v + _ = i + + // cronName is the internal, unique tag/name for the timer. + // A timer named "sponsor" in channel "forsen" will be named "forsensponsor" + t := fmt.Sprintf( + "ID: \t\t%v\n"+ + "Name: \t\t%v\n"+ + "Identifier: \t%v\n"+ + "Text: \t\t%v\n"+ + "Channel: \t%v\n"+ + "Repeat: \t%v\n"+ + "\n\n", + v.ID, v.Name, v.Identifier, v.Text, v.Channel, v.Repeat, + ) + + // Add new value to the slice + ts = append(ts, t) + + //app.Scheduler.AddFunc(repeating, func() { app.newPrivateMessageTimer(v.Channel, v.Text) }, cronName) + } + + reply, err := app.uploadPaste(strings.Join(ts, "")) + if err != nil { + app.Log.Errorw("Error trying to retrieve all timers from database", err) + return "" + } + + return reply +} + +// InitialTimers is called on startup and queries the database for a list of +// timers and then adds each onto the scheduler. +func (app *application) InitialTimers() { + timer, err := app.Models.Timers.GetAll() + if err != nil { + app.Log.Errorw("Error trying to retrieve all timers from database", err) return } @@ -253,73 +295,61 @@ func (app *Application) InitialTimers() { // cronName is the internal, unique tag/name for the timer. // A timer named "sponsor" in channel "forsen" will be named "forsensponsor" - cronName := fmt.Sprintf("%s-%s", v.Channel, v.Name) // Repeating is at what times the timer should repeat. // 2 minute timer is @every 2m repeating := fmt.Sprintf("@every %s", v.Repeat) - if _, err := app.Rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { - rdb.HSet(ctx, cronName, "timer-id", v.ID) - rdb.HSet(ctx, cronName, "timer-name", v.Name) - rdb.HSet(ctx, cronName, "timer-text", v.Text) - rdb.HSet(ctx, cronName, "timer-channel", v.Channel) - rdb.HSet(ctx, cronName, "timer-repeat", v.Repeat) - return nil - }); err != nil { - app.Logger.Panic(err) - } - app.Logger.Infow("Loaded timer into redis:", - "key", cronName, - "value", app.Rdb.HGetAll(ctx, v.Channel), - ) - app.Scheduler.AddFunc(repeating, func() { app.newPrivateMessageTimer(v.Channel, v.Text) }, cronName) - // Add new value to the slice ts = append(ts, v) + + app.Scheduler.AddFunc(repeating, func() { app.newPrivateMessageTimer(v.Channel, v.Text) }, v.Identifier) } - // var model1 rdbVal - // if err := app.Rdb.HGetAll(ctx, cronName).Scan(&model1); err != nil { - // app.Logger.Panic(err) - // } - - app.Logger.Infow("Initial timers", + app.Log.Infow("Initial timers", "timer", ts, ) } // newPrivateMessageTimer is a helper function to set timers // which trigger into sending a twitch PrivateMessage. -func (app *Application) newPrivateMessageTimer(channel, text string) { - common.Send(channel, text, app.TwitchClient) +func (app *application) newPrivateMessageTimer(channel, text string) { + app.SendNoContext(channel, text) } // DeleteTimer takes in the name of a timer and tries to delete the timer from the database. -func (app *Application) DeleteTimer(name string, message twitch.PrivateMessage) { - cronName := fmt.Sprintf("%s-%s", message.Channel, name) - app.Scheduler.RemoveJob(cronName) +func (app *application) DeleteTimer(name string, message twitch.PrivateMessage) { - app.Logger.Infow("Deleting timer", + identifier, err := app.Models.Timers.GetIdentifier(name) + if err != nil { + app.Log.Errorw("Error retrieving identifier rom database", + "name", name, + "identifier", identifier, + "error", err, + ) + } + + app.Scheduler.RemoveJob(identifier) + + app.Log.Infow("Deleting timer", "name", name, + "identifier", identifier, "message.Channel", message.Channel, - "cronName", cronName, ) - _ = app.Rdb.Del(ctx, cronName) - err := app.Models.Timers.Delete(name) + err = app.Models.Timers.Delete(identifier) if err != nil { - app.Logger.Errorw("Error deleting timer from database", + app.Log.Errorw("Error deleting timer from database", "name", name, - "cronName", cronName, + "identifier", identifier, "error", err, ) reply := fmt.Sprintln("Something went wrong FeelsBadMan") - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) return } reply := fmt.Sprintf("Deleted timer with name %s", name) - common.Send(message.Channel, reply, app.TwitchClient) + app.Send(message.Channel, reply, message) } diff --git a/cmd/nourybot/upload.go b/cmd/nourybot/upload.go new file mode 100644 index 0000000..edcc890 --- /dev/null +++ b/cmd/nourybot/upload.go @@ -0,0 +1,368 @@ +// The whole catbox upload functionality has been copied from +// here so that I could use it with litterbox: +// https://github.com/wabarc/go-catbox/blob/main/catbox.go <3 +// +// Copyright 2021 Wayback Archiver. All rights reserved. +// Use of this source code is governed by the MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gempir/go-twitch-irc/v4" +) + +const ( + CATBOX_ENDPOINT = "https://litterbox.catbox.moe/resources/internals/api.php" + GOFILE_ENDPOINT = "https://store1.gofile.io/uploadFile" + KAPPA_ENDPOINT = "https://kappa.lol/api/upload" + YAF_ENDPOINT = "https://i.yaf.ee/upload" +) + +func (app *application) NewUpload(destination, fileName, target, identifier string, msg twitch.PrivateMessage) { + + switch destination { + case "catbox": + go app.CatboxUpload(target, fileName, identifier, msg) + case "yaf": + go app.YafUpload(target, fileName, identifier, msg) + case "kappa": + go app.KappaUpload(target, fileName, identifier, msg) + case "gofile": + go app.GofileUpload(target, fileName, identifier, msg) + + } +} + +func (app *application) CatboxUpload(target, fileName, identifier string, msg twitch.PrivateMessage) { + defer os.Remove(fileName) + file, err := os.Open(fileName) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + defer file.Close() + app.Send(target, "Uploading to catbox.moe... dankCircle", msg) + + // if size := helper.FileSize(fileName); size > 209715200 { + // return "", fmt.Errorf("file too large, size: %d MB", size/1024/1024) + // } + + r, w := io.Pipe() + m := multipart.NewWriter(w) + + go func() { + defer w.Close() + defer m.Close() + + err := m.WriteField("reqtype", "fileupload") + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + err = m.WriteField("time", "24h") + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + part, err := m.CreateFormFile("fileToUpload", filepath.Base(file.Name())) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + + if _, err = io.Copy(part, file); err != nil { + return + } + }() + + req, _ := http.NewRequest(http.MethodPost, CATBOX_ENDPOINT, r) + req.Header.Add("Content-Type", m.FormDataContentType()) + + client := &http.Client{ + Timeout: 300 * time.Second, + } + + resp, err := client.Do(req) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + return + } + + reply := string(body) + go app.Models.Uploads.UpdateUploadURL(identifier, reply) + app.Send(target, fmt.Sprintf("Removing file: %s", fileName), msg) + app.Send(target, reply, msg) +} + +func (app *application) GofileUpload(target, path, identifier string, msg twitch.PrivateMessage) { + defer os.Remove(path) + app.Send(target, "Uploading to gofile.io... dankCircle", msg) + pr, pw := io.Pipe() + form := multipart.NewWriter(pw) + + type gofileData struct { + DownloadPage string `json:"downloadPage"` + Code string `json:"code"` + ParentFolder string `json:"parentFolder"` + FileId string `json:"fileId"` + FileName string `json:"fileName"` + Md5 string `json:"md5"` + } + + type gofileResponse struct { + Status string `json:"status"` + Data gofileData + } + + go func() { + defer pw.Close() + + file, err := os.Open(path) // path to image file + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + w, err := form.CreateFormFile("file", path) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + _, err = io.Copy(w, file) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + form.Close() + }() + + req, err := http.NewRequest(http.MethodPost, GOFILE_ENDPOINT, pr) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + req.Header.Set("Content-Type", form.FormDataContentType()) + + httpClient := http.Client{ + Timeout: 300 * time.Second, + } + resp, err := httpClient.Do(req) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + app.Log.Errorln("Error while sending HTTP request:", err) + + return + } + defer resp.Body.Close() + app.Send(target, "Uploaded PogChamp", msg) + + body, err := io.ReadAll(resp.Body) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + app.Log.Errorln("Error while reading response:", err) + return + } + + jsonResponse := new(gofileResponse) + if err := json.Unmarshal(body, jsonResponse); err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + app.Log.Errorln("Error while unmarshalling JSON response:", err) + return + } + + var reply = jsonResponse.Data.DownloadPage + + go app.Models.Uploads.UpdateUploadURL(identifier, reply) + app.Send(target, fmt.Sprintf("Removing file: %s", path), msg) + app.Send(target, reply, msg) +} + +func (app *application) KappaUpload(target, path, identifier string, msg twitch.PrivateMessage) { + defer os.Remove(path) + app.Send(target, "Uploading to kappa.lol... dankCircle", msg) + pr, pw := io.Pipe() + form := multipart.NewWriter(pw) + + type kappaResponse struct { + Link string `json:"link"` + } + + go func() { + defer pw.Close() + + err := form.WriteField("name", "xd") + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + file, err := os.Open(path) // path to image file + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + w, err := form.CreateFormFile("file", path) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + _, err = io.Copy(w, file) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + form.Close() + }() + + req, err := http.NewRequest(http.MethodPost, KAPPA_ENDPOINT, pr) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + req.Header.Set("Content-Type", form.FormDataContentType()) + + httpClient := http.Client{ + Timeout: 300 * time.Second, + } + resp, err := httpClient.Do(req) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + app.Log.Errorln("Error while sending HTTP request:", err) + + return + } + defer resp.Body.Close() + app.Send(target, "Uploaded PogChamp", msg) + + body, err := io.ReadAll(resp.Body) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + app.Log.Errorln("Error while reading response:", err) + return + } + + jsonResponse := new(kappaResponse) + if err := json.Unmarshal(body, jsonResponse); err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + app.Log.Errorln("Error while unmarshalling JSON response:", err) + return + } + + var reply = jsonResponse.Link + + go app.Models.Uploads.UpdateUploadURL(identifier, reply) + app.Send(target, fmt.Sprintf("Removing file: %s", path), msg) + app.Send(target, reply, msg) +} + +func (app *application) YafUpload(target, path, identifier string, msg twitch.PrivateMessage) { + defer os.Remove(path) + app.Send(target, "Uploading to yaf.ee... dankCircle", msg) + pr, pw := io.Pipe() + form := multipart.NewWriter(pw) + + go func() { + defer pw.Close() + + err := form.WriteField("name", "xd") + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + file, err := os.Open(path) // path to image file + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + w, err := form.CreateFormFile("file", path) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + _, err = io.Copy(w, file) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + + form.Close() + }() + + req, err := http.NewRequest(http.MethodPost, YAF_ENDPOINT, pr) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + return + } + req.Header.Set("Content-Type", form.FormDataContentType()) + + httpClient := http.Client{ + Timeout: 300 * time.Second, + } + resp, err := httpClient.Do(req) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + app.Log.Errorln("Error while sending HTTP request:", err) + + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + app.Send(target, fmt.Sprintf("Something went wrong FeelsBadMan: %q", err), msg) + os.Remove(path) + app.Log.Errorln("Error while reading response:", err) + return + } + + var reply = string(body[:]) + + go app.Models.Uploads.UpdateUploadURL(identifier, reply) + app.Send(target, fmt.Sprintf("Removing file: %s", path), msg) + app.Send(target, reply, msg) +} diff --git a/cmd/nourybot/user.go b/cmd/nourybot/user.go new file mode 100644 index 0000000..b26f5ff --- /dev/null +++ b/cmd/nourybot/user.go @@ -0,0 +1,183 @@ +package main + +import ( + "fmt" + "strconv" + + "github.com/gempir/go-twitch-irc/v4" + "github.com/lyx0/nourybot/internal/commands" +) + +// AddUser calls GetIdByLogin to get the twitch id of the login name and then adds +// the login name, twitch id and supplied level to the database. +func (app *application) InitUser(login, twitchId string) { + _, err := app.Models.Users.Check(twitchId) + //app.Log.Error(err) + if err != nil { + go app.Models.Users.Insert(login, twitchId) + + return + } +} + +// DebugUser queries the database for a login name, if that name exists it returns the fields +// and outputs them to twitch chat and a twitch whisper. +func (app *application) DebugUser(login string, message twitch.PrivateMessage) { + user, err := app.Models.Users.Get(login) + + if err != nil { + reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err) + app.Send(message.Channel, reply, message) + return + } else { + // subject := fmt.Sprintf("DEBUG for user %v", login) + body := fmt.Sprintf("id=%v \nlogin=%v \nlevel=%v \nlocation=%v \nlastfm=%v", + user.TwitchID, + user.Login, + user.Level, + user.Location, + user.LastFMUsername, + ) + + resp, err := app.uploadPaste(body) + if err != nil { + app.Log.Errorln("Could not upload paste:", err) + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %v", ErrDuringPasteUpload), message) + return + } + app.Send(message.Channel, resp, message) + // app.SendEmail(subject, body) + return + } +} + +// DeleteUser takes in a login string, queries the database for an entry with +// that login name and tries to delete that entry in the database. +func (app *application) DeleteUser(login string, message twitch.PrivateMessage) { + err := app.Models.Users.Delete(login) + if err != nil { + app.Send(message.Channel, "Something went wrong FeelsBadMan", message) + app.Log.Error(err) + return + } + + reply := fmt.Sprintf("Deleted user %s", login) + app.Send(message.Channel, reply, message) +} + +// EditUserLevel tries to update the database record for the supplied +// login name with the new level. +func (app *application) EditUserLevel(login, lvl string, message twitch.PrivateMessage) { + // Convert the level string to an integer. This is an easy check to see if + // the level supplied was a number only. + level, err := strconv.Atoi(lvl) + if err != nil { + app.Log.Error(err) + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrUserLevelNotInteger), message) + return + } + + err = app.Models.Users.SetLevel(login, level) + if err != nil { + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message) + app.Log.Error(err) + return + } else { + reply := fmt.Sprintf("Updated user %s to level %v", login, level) + app.Send(message.Channel, reply, message) + return + } +} + +// SetUserLocation sets new location for the user +func (app *application) SetUserLocation(message twitch.PrivateMessage) { + // snipLength is the length we need to "snip" off of the start of `message`. + // `()set location` = +13 + // trailing space = +1 + // zero-based = +1 + // = 16 + snipLength := 15 + + // Split the twitch message at `snipLength` plus length of the name of the + // The part of the message we are left over with is then passed on to the database + // handlers as the `location` part of the command. + location := message.Message[snipLength:len(message.Message)] + twitchId := message.User.ID + + err := app.Models.Users.SetLocation(twitchId, location) + if err != nil { + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message) + app.Log.Error(err) + return + } else { + reply := fmt.Sprintf("Successfully set your location to %v", location) + app.Send(message.Channel, reply, message) + return + } +} + +// SetUserLastFM tries to update the database record for the supplied +// login name with the new level. +func (app *application) SetUserLastFM(lastfmUser string, message twitch.PrivateMessage) { + login := message.User.Name + + err := app.Models.Users.SetLastFM(login, lastfmUser) + if err != nil { + app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message) + app.Log.Error(err) + return + } else { + reply := fmt.Sprintf("Successfully set your lastfm username to %v", lastfmUser) + app.Send(message.Channel, reply, message) + return + } +} + +// GetUserLevel takes in a twitchId and queries the database for an entry +// with this twitchId. If there is one it returns the level value as an integer. +// Returns 0 on an error which is the level for unregistered users. +func (app *application) GetUserLevel(twitchId string) int { + userLevel, err := app.Models.Users.GetLevel(twitchId) + if err != nil { + return 0 + } else { + return userLevel + } +} + +func (app *application) UserCheckWeather(message twitch.PrivateMessage) { + target := message.Channel + twitchLogin := message.User.Name + twitchId := message.User.ID + + location, err := app.Models.Users.GetLocation(twitchId) + if err != nil { + app.Log.Errorw("No location data registered for: ", + "twitchLogin:", twitchLogin, + "twitchId:", twitchId, + ) + reply := "No location for your account set in my database. Use ()set location to register. Otherwise use ()weather without registering." + app.Send(message.Channel, reply, message) + return + } + + reply, _ := commands.Weather(location) + app.Send(target, reply, message) +} + +func (app *application) UserCheckLastFM(message twitch.PrivateMessage) string { + twitchLogin := message.User.Name + target := message.Channel + + lastfmUser, err := app.Models.Users.GetLastFM(twitchLogin) + if err != nil { + app.Log.Errorw("No LastFM account registered for: ", + "twitchLogin:", twitchLogin, + ) + reply := "No lastfm account registered in my database. Use ()set lastfm to register. Otherwise use ()lastfm without registering." + return reply + } + + reply := commands.LastFmUserRecent(target, lastfmUser) + return reply +} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 1db322b..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: "3.9" -services: - nourybot: - build: - dockerfile: Dockerfile - context: . - # restart: unless-stopped \ No newline at end of file diff --git a/env.example b/env.example new file mode 100644 index 0000000..30898eb --- /dev/null +++ b/env.example @@ -0,0 +1,15 @@ +TWITCH_USERNAME=cooltwitchusername +TWITCH_OAUTH=oauth:cooltwitchoauthtoken +TWITCH_COMMAND_PREFIX=() + +TWITCH_CLIENT_ID=mycoolclientid +TWITCH_CLIENT_SECRET=mycoolclientsecret + +LOCAL_DSN=postgres://user:password@localhost/database-name?sslmode=disable +REMOTE_DSN=postgresql://user:password@databaseurlfrom.supabase.com:5432/postgres + +OWM_KEY=mycoolopenweatherkey + +LAST_FM_APPLICATION_NAME=goodname +LAST_FM_API_KEY=goodlastfmapikey +LAST_FM_SECRET=goodlastfmsecretkey diff --git a/go.sum b/go.sum index 77251df..a5b0dad 100644 --- a/go.sum +++ b/go.sum @@ -17,71 +17,53 @@ github.com/gempir/go-twitch-irc/v4 v4.0.0 h1:sHVIvbWOv9nHXGEErilclxASv0AaQEr/r/f github.com/gempir/go-twitch-irc/v4 v4.0.0/go.mod h1:QsOMMAk470uxQ7EYD9GJBGAVqM/jDrXBNbuePfTauzg= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5 h1:kCvm3G3u+eTRbjfLPyfsfznJtraYEfZer/UvQ6CaQhI= github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5/go.mod h1:6DM2KNNK69jRu0lAHmYK9LYxmqpNjYHOaNp/ZxttD4U= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -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/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= -github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M= github.com/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/nicklaw5/helix/v2 v2.25.1 h1:hccFfWf1kdPKeC/Zp8jNbOvqV0f6ya12hdeNHuQa5wg= +github.com/nicklaw5/helix/v2 v2.25.1/go.mod h1:zZcKsyyBWDli34x3QleYsVMiiNGMXPAEU5NjsiZDtvY= +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/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k= -github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= +github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 h1:cgqwZtnR+IQfUYDLJ3Kiy4aE+O/wExTzEIg8xwC4Qfs= github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0/go.mod h1:n3nudMl178cEvD44PaopxH9jhJaQzthSxUzLO5iKMy4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/wader/goutubedl v0.0.0-20230924165737-427b7fa536e6 h1:KHJV3fnnKsdWdGu5IKrDAA0Oa5RzGwrJpfx+bvVAjLA= +github.com/wader/goutubedl v0.0.0-20230924165737-427b7fa536e6/go.mod h1:5KXd5tImdbmz4JoVhePtbIokCwAfEhUVVx3WLHmjYuw= +github.com/wader/osleaktest v0.0.0-20191111175233-f643b0fed071 h1:QkrG4Zr5OVFuC9aaMPmFI0ibfhBZlAgtzDYWfu7tqQk= +github.com/wader/osleaktest v0.0.0-20191111175233-f643b0fed071/go.mod h1:XD6emOFPHVzb0+qQpiNOdPL2XZ0SRUM0N5JHuq6OmXo= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= -go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -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/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/insomnia/Insomnia_2022-08-17.json b/insomnia/Insomnia_2022-08-17.json deleted file mode 100644 index 9b85d61..0000000 --- a/insomnia/Insomnia_2022-08-17.json +++ /dev/null @@ -1 +0,0 @@ -{"_type":"export","__export_format":4,"__export_date":"2022-08-17T19:16:53.682Z","__export_source":"insomnia.desktop.app:v2022.5.0","resources":[{"_id":"req_5d958ec4ce374235b6e417cc9be48b89","parentId":"wrk_d4cae4c992a7440290394279400803c6","modified":1660763792428,"created":1660759977370,"url":"localhost:3000/v1/commands/test","name":"command","description":"","method":"PATCH","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"test\",\n\t\"text\": \"test123 changed\",\n\t\"category\": \"testchanged\",\n\t\"level\": 641\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_3fd0c254058a430bb72040eab201c364"}],"authentication":{},"metaSortKey":-1660759977370,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"wrk_d4cae4c992a7440290394279400803c6","parentId":null,"modified":1660748286420,"created":1660748286420,"name":"nourybot-api","description":"","scope":"collection","_type":"workspace"},{"_id":"req_6c8c21fa841d4418a2790e9d91d2d1c8","parentId":"wrk_d4cae4c992a7440290394279400803c6","modified":1660751293669,"created":1660751233597,"url":"localhost:3000/v1/commands/test","name":"command","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1660751233597,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_a77aa307cce4423287d59edbc3719014","parentId":"wrk_d4cae4c992a7440290394279400803c6","modified":1660763777588,"created":1660748340276,"url":"localhost:3000/v1/commands","name":"command","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"test\",\n\t\"text\": \"testing 123\",\n\t\"category\": \"testing\",\n\t\"level\": 666\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_530845ffd47b458aa7a322c12b42695c"}],"authentication":{},"metaSortKey":-1660748340276,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_7ca8659975f04d28a9020e421e9f116d","parentId":"wrk_d4cae4c992a7440290394279400803c6","modified":1660751688968,"created":1660748289073,"url":"localhost:3000/v1/commands/test","name":"command","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1660748289073,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_970e467c8601150ade332a4c172ff5b80bca7d5e","parentId":"wrk_d4cae4c992a7440290394279400803c6","modified":1660748286452,"created":1660748286452,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1660748286452,"_type":"environment"},{"_id":"jar_970e467c8601150ade332a4c172ff5b80bca7d5e","parentId":"wrk_d4cae4c992a7440290394279400803c6","modified":1660748286454,"created":1660748286454,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_e5f1241c4688496995c6a8875182f9ad","parentId":"wrk_d4cae4c992a7440290394279400803c6","modified":1660748286438,"created":1660748286438,"fileName":"nourybot-api","contents":"","contentType":"yaml","_type":"api_spec"}]} \ No newline at end of file diff --git a/insomnia/README.md b/insomnia/README.md deleted file mode 100644 index a6f00f7..0000000 --- a/insomnia/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Insomnia - -Api collection import/export for [insomnia](https://insomnia.rest/) \ No newline at end of file diff --git a/internal/commands/bttv.go b/internal/commands/bttv.go index 143bf3e..813faec 100644 --- a/internal/commands/bttv.go +++ b/internal/commands/bttv.go @@ -2,13 +2,10 @@ package commands import ( "fmt" - - "github.com/gempir/go-twitch-irc/v4" - "github.com/lyx0/nourybot/internal/common" ) -func Bttv(target, query string, tc *twitch.Client) { +func Bttv(query string) string { reply := fmt.Sprintf("https://betterttv.com/emotes/shared/search?query=%s", query) - common.Send(target, reply, tc) + return reply } diff --git a/internal/commands/coinflip.go b/internal/commands/coinflip.go index 8de2c2f..2765120 100644 --- a/internal/commands/coinflip.go +++ b/internal/commands/coinflip.go @@ -1,23 +1,21 @@ package commands import ( - "github.com/gempir/go-twitch-irc/v4" "github.com/lyx0/nourybot/internal/common" ) -func Coinflip(target string, tc *twitch.Client) { +func Coinflip() string { flip := common.GenerateRandomNumber(2) + var reply string switch flip { case 0: - common.Send(target, "Heads!", tc) - return + reply = "Heads!" case 1: - common.Send(target, "Tails!", tc) - return + reply = "Tails!" default: - common.Send(target, "Heads!", tc) - return + reply = "Heads!" } + return reply } diff --git a/internal/commands/currency.go b/internal/commands/currency.go index 6a3b008..f6ca31e 100644 --- a/internal/commands/currency.go +++ b/internal/commands/currency.go @@ -1,21 +1,30 @@ package commands import ( - "github.com/gempir/go-twitch-irc/v4" - "github.com/lyx0/nourybot/internal/commands/decapi" - "github.com/lyx0/nourybot/internal/common" - "go.uber.org/zap" + "fmt" + "io" + "net/http" ) -// ()currency 10 USD to EUR -func Currency(target, currAmount, currFrom, currTo string, tc *twitch.Client) { - sugar := zap.NewExample().Sugar() - defer sugar.Sync() +func Currency(currAmount, currFrom, currTo string) (string, error) { + basePath := "https://decapi.me/misc/currency/" + from := fmt.Sprintf("?from=%s", currFrom) + to := fmt.Sprintf("&to=%s", currTo) + value := fmt.Sprintf("&value=%s", currAmount) - resp, err := decapi.Currency(currAmount, currFrom, currTo) + // https://decapi.me/misc/currency/?from=usd&to=usd&value=10 + resp, err := http.Get(fmt.Sprint(basePath + from + to + value)) if err != nil { - sugar.Error(err) + return "", ErrInternalServerError } - common.Send(target, resp, tc) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", ErrInternalServerError + } + + reply := string(body) + return reply, nil } diff --git a/internal/commands/decapi/bttvemotes.go b/internal/commands/decapi/bttvemotes.go deleted file mode 100644 index dc2ba2d..0000000 --- a/internal/commands/decapi/bttvemotes.go +++ /dev/null @@ -1,35 +0,0 @@ -package decapi - -import ( - "fmt" - "io" - "net/http" - - "go.uber.org/zap" -) - -func Bttvemotes(username string) (string, error) { - var basePath = "https://decapi.me/bttv/emotes/" - - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - - // https://decapi.me/twitter/latest/forsen?url&no_rts - // ?url adds the url at the end and &no_rts ignores retweets. - resp, err := http.Get(fmt.Sprint(basePath + username)) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - reply := string(body) - return reply, nil -} diff --git a/internal/commands/decapi/currency.go b/internal/commands/decapi/currency.go deleted file mode 100644 index 3308bf6..0000000 --- a/internal/commands/decapi/currency.go +++ /dev/null @@ -1,38 +0,0 @@ -package decapi - -import ( - "fmt" - "io" - "net/http" - - "go.uber.org/zap" -) - -// ()currency 10 USD to EUR -func Currency(currAmount, currFrom, currTo string) (string, error) { - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - - basePath := "https://decapi.me/misc/currency/" - from := fmt.Sprintf("?from=%s", currFrom) - to := fmt.Sprintf("&to=%s", currTo) - value := fmt.Sprintf("&value=%s", currAmount) - - // https://decapi.me/misc/currency/?from=usd&to=usd&value=10 - resp, err := http.Get(fmt.Sprint(basePath + from + to + value)) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - reply := string(body) - return reply, nil -} diff --git a/internal/commands/decapi/ffzemotes.go b/internal/commands/decapi/ffzemotes.go deleted file mode 100644 index 579effd..0000000 --- a/internal/commands/decapi/ffzemotes.go +++ /dev/null @@ -1,35 +0,0 @@ -package decapi - -import ( - "fmt" - "io" - "net/http" - - "go.uber.org/zap" -) - -func Ffzemotes(username string) (string, error) { - var basePath = "https://decapi.me/ffz/emotes/" - - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - - // https://decapi.me/twitter/latest/forsen?url&no_rts - // ?url adds the url at the end and &no_rts ignores retweets. - resp, err := http.Get(fmt.Sprint(basePath + username)) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - reply := string(body) - return reply, nil -} diff --git a/internal/commands/decapi/followage.go b/internal/commands/decapi/followage.go deleted file mode 100644 index 4578e47..0000000 --- a/internal/commands/decapi/followage.go +++ /dev/null @@ -1,46 +0,0 @@ -package decapi - -import ( - "fmt" - "io" - "net/http" - - "go.uber.org/zap" -) - -func Followage(channel, username string) (string, error) { - var basePath = "https://decapi.me/twitch/followage/" - - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - - // ?precision is how precise the timestamp should be. - // precision 4 means: 1 2 3 4 - // pajlada has been following forsen for 7 years, 4 months, 4 weeks, 1 day - resp, err := http.Get(fmt.Sprint(basePath + channel + "/" + username + "?precision=4")) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - // User tries to look up how long he follows himself. - if string(body) == followageUserCannotFollowOwn { - return "You cannot follow yourself.", nil - - // Username is not following the requested channel. - } else if string(body) == fmt.Sprintf("%s does not follow %s", username, channel) { - return string(body), nil - } else { - reply := fmt.Sprintf("%s has been following %s for %s", username, channel, string(body)) - return reply, nil - } - -} diff --git a/internal/commands/decapi/tweet.go b/internal/commands/decapi/tweet.go deleted file mode 100644 index 94904cf..0000000 --- a/internal/commands/decapi/tweet.go +++ /dev/null @@ -1,40 +0,0 @@ -package decapi - -import ( - "fmt" - "io" - "net/http" - - "go.uber.org/zap" -) - -func Tweet(username string) (string, error) { - var basePath = "https://decapi.me/twitter/latest/" - - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - - // https://decapi.me/twitter/latest/forsen?url&no_rts - // ?url adds the url at the end and &no_rts ignores retweets. - resp, err := http.Get(fmt.Sprint(basePath + username + "?url" + "&no_rts")) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - // If the response was a known error message return a message with the error. - if string(body) == twitterUserNotFoundError { - return "Something went wrong: Twitter username not found", err - } else { // No known error was found, probably a tweet. - resp := fmt.Sprintf("Latest Tweet from @%s: \"%s \"", username, body) - return resp, nil - } -} diff --git a/internal/commands/decapi/types.go b/internal/commands/decapi/types.go deleted file mode 100644 index 4e8777c..0000000 --- a/internal/commands/decapi/types.go +++ /dev/null @@ -1,6 +0,0 @@ -package decapi - -var ( - twitterUserNotFoundError = "[Error] - [34] Sorry, that page does not exist." - followageUserCannotFollowOwn = "A user cannot follow themself." -) diff --git a/internal/commands/decapi/userid.go b/internal/commands/decapi/userid.go deleted file mode 100644 index c5efba2..0000000 --- a/internal/commands/decapi/userid.go +++ /dev/null @@ -1,34 +0,0 @@ -package decapi - -import ( - "fmt" - "io" - "net/http" - - "go.uber.org/zap" -) - -func GetIdByLogin(login string) (string, error) { - var basePath = "https://decapi.me/twitch/id/" - - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - - resp, err := http.Get(fmt.Sprint(basePath + login)) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - reply := string(body) - return reply, nil - -} diff --git a/internal/commands/duckduckgo.go b/internal/commands/duckduckgo.go new file mode 100644 index 0000000..3ab8d00 --- /dev/null +++ b/internal/commands/duckduckgo.go @@ -0,0 +1,13 @@ +package commands + +import ( + "fmt" + "net/url" +) + +func DuckDuckGo(query string) string { + query = url.QueryEscape(query) + reply := fmt.Sprintf("https://duckduckgo.com/?va=n&hps=1&q=%s", query) + + return reply +} diff --git a/internal/commands/errors.go b/internal/commands/errors.go new file mode 100644 index 0000000..0483496 --- /dev/null +++ b/internal/commands/errors.go @@ -0,0 +1,8 @@ +package commands + +import "errors" + +var ( + ErrInternalServerError = errors.New("internal server error") + ErrWeatherLocationNotFound = errors.New("location not found") +) diff --git a/internal/commands/ffz.go b/internal/commands/ffz.go index f7be09b..011bd43 100644 --- a/internal/commands/ffz.go +++ b/internal/commands/ffz.go @@ -1,14 +1,10 @@ package commands -import ( - "fmt" +import "fmt" "github.com/gempir/go-twitch-irc/v4" "github.com/lyx0/nourybot/internal/common" ) -func Ffz(target, query string, tc *twitch.Client) { - reply := fmt.Sprintf("https://www.frankerfacez.com/emoticons/?q=%s", query) - - common.Send(target, reply, tc) + return reply } diff --git a/internal/commands/godocs.go b/internal/commands/godocs.go new file mode 100644 index 0000000..6cc0b15 --- /dev/null +++ b/internal/commands/godocs.go @@ -0,0 +1,13 @@ +package commands + +import ( + "fmt" + "net/url" +) + +func Godocs(query string) string { + query = url.QueryEscape(query) + reply := fmt.Sprintf("https://godocs.io/?q=%s", query) + + return reply +} diff --git a/internal/commands/google.go b/internal/commands/google.go new file mode 100644 index 0000000..d08d77a --- /dev/null +++ b/internal/commands/google.go @@ -0,0 +1,13 @@ +package commands + +import ( + "fmt" + "net/url" +) + +func Google(query string) string { + query = url.QueryEscape(query) + reply := fmt.Sprintf("https://www.google.com/search?q=%s", query) + + return reply +} diff --git a/internal/commands/ivr/firstline.go b/internal/commands/ivr/firstline.go deleted file mode 100644 index 2d72f6d..0000000 --- a/internal/commands/ivr/firstline.go +++ /dev/null @@ -1,50 +0,0 @@ -package ivr - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "go.uber.org/zap" -) - -type firstLineApiResponse struct { - User string `json:"user"` - Message string `json:"message"` - Time string `json:"time"` - Error string `json:"error"` -} - -// FirstLine returns the first line a given user has sent in a -// given channel. -func FirstLine(channel, username string) (string, error) { - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - - baseUrl := "https://api.ivr.fi/logs/firstmessage" - - resp, err := http.Get(fmt.Sprintf("%s/%s/%s", baseUrl, channel, username)) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - sugar.Error(err) - } - - var responseObject firstLineApiResponse - json.Unmarshal(body, &responseObject) - - // User or channel was not found - if responseObject.Error != "" { - return fmt.Sprintf(responseObject.Error + " FeelsBadMan"), nil - } else { - return fmt.Sprintf(username + ": " + responseObject.Message + " (" + responseObject.Time + " ago)."), nil - } - -} diff --git a/internal/commands/ivr/randomquote.go b/internal/commands/ivr/randomquote.go deleted file mode 100644 index dea2fda..0000000 --- a/internal/commands/ivr/randomquote.go +++ /dev/null @@ -1,49 +0,0 @@ -package ivr - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "go.uber.org/zap" -) - -type randomQuoteApiResponse struct { - User string `json:"user"` - Message string `json:"message"` - Time string `json:"time"` - Error string `json:"error"` -} - -// FirstLine returns the first line a given user has sent in a -// given channel. -func RandomQuote(channel, username string) (string, error) { - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - baseUrl := "https://api.ivr.fi/logs/rq" - - resp, err := http.Get(fmt.Sprintf("%s/%s/%s", baseUrl, channel, username)) - if err != nil { - sugar.Error(err) - return "Something went wrong FeelsBadMan", err - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - sugar.Error(err) - } - - var responseObject randomQuoteApiResponse - json.Unmarshal(body, &responseObject) - - // User or channel was not found - if responseObject.Error != "" { - return fmt.Sprintf(responseObject.Error + " FeelsBadMan"), nil - } else { - return fmt.Sprintf(username + ": " + responseObject.Message + " (" + responseObject.Time + " ago)."), nil - } - -} diff --git a/internal/commands/lastfm.go b/internal/commands/lastfm.go index 272c509..978e2ec 100644 --- a/internal/commands/lastfm.go +++ b/internal/commands/lastfm.go @@ -4,44 +4,12 @@ import ( "fmt" "os" - "github.com/gempir/go-twitch-irc/v4" "github.com/joho/godotenv" - "github.com/lyx0/nourybot/internal/common" "github.com/shkh/lastfm-go/lastfm" "go.uber.org/zap" ) -func LastFmArtistTop(target string, message twitch.PrivateMessage, tc *twitch.Client) { - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - // snipLength is the length we need to "snip" off of the start - // of `message` to only have the artists name left. - // `()lastfm artist top` = +20 - // trailing space = +1 - // zero-based = +1 - // = 22 - snipLength := 20 - - artist := message.Message[snipLength:len(message.Message)] - - err := godotenv.Load() - if err != nil { - sugar.Error("Error loading OpenWeatherMap API key from .env file") - } - apiKey := os.Getenv("LAST_FM_API_KEY") - apiSecret := os.Getenv("LAST_FM_SECRET") - - api := lastfm.New(apiKey, apiSecret) - result, _ := api.Artist.GetTopTracks(lastfm.P{"artist": artist}) //discarding error - for _, track := range result.Tracks { - sugar.Infow("Top tracks: ", - "artist:", artist, - "track", track.Name, - ) - } -} - -func LastFmUserRecent(target, user string, tc *twitch.Client) { +func LastFmUserRecent(target, user string) string { sugar := zap.NewExample().Sugar() defer sugar.Sync() @@ -68,9 +36,8 @@ func LastFmUserRecent(target, user string, tc *twitch.Client) { ) reply = fmt.Sprintf("Most recently played track for user %v: %v - %v", user, track.Artist.Name, track.Name) - common.Send(target, reply, tc) - return } } + return reply } diff --git a/internal/commands/osrs.go b/internal/commands/osrs.go new file mode 100644 index 0000000..1587c76 --- /dev/null +++ b/internal/commands/osrs.go @@ -0,0 +1,13 @@ +package commands + +import ( + "fmt" + "net/url" +) + +func OSRS(query string) string { + query = url.QueryEscape(query) + reply := fmt.Sprintf("https://oldschool.runescape.wiki/?search=%s", query) + + return reply +} diff --git a/internal/commands/phonetic.go b/internal/commands/phonetic.go index 1fef46f..21bf350 100644 --- a/internal/commands/phonetic.go +++ b/internal/commands/phonetic.go @@ -2,9 +2,6 @@ package commands import ( "fmt" - - "github.com/gempir/go-twitch-irc/v4" - "github.com/lyx0/nourybot/internal/common" ) var cm = map[string]string{ @@ -49,6 +46,7 @@ var cm = map[string]string{ "b": "б", "n": "н", "m": "м", + "Q": "Я", "W": "Ш", "E": "Е", @@ -77,7 +75,7 @@ var cm = map[string]string{ "M": "М", } -func Phonetic(target, message string, tc *twitch.Client) { +func Phonetic(message string) (string, error) { var ts string for _, c := range message { @@ -87,8 +85,7 @@ func Phonetic(target, message string, tc *twitch.Client) { ts = ts + string(c) } - //ts = append(ts, cm[string(c)]) } - common.Send(target, fmt.Sprint(ts), tc) + return fmt.Sprint(ts), nil } diff --git a/internal/commands/ping.go b/internal/commands/ping.go index 9799d2c..bf092ac 100644 --- a/internal/commands/ping.go +++ b/internal/commands/ping.go @@ -3,15 +3,14 @@ package commands import ( "fmt" - "github.com/gempir/go-twitch-irc/v4" "github.com/lyx0/nourybot/internal/common" "github.com/lyx0/nourybot/internal/humanize" ) -func Ping(target string, tc *twitch.Client) { +func Ping() string { botUptime := humanize.Time(common.GetUptime()) commandsUsed := common.GetCommandsUsed() reply := fmt.Sprintf("Pong! :) Commands used: %v, Last restart: %v", commandsUsed, botUptime) - common.Send(target, reply, tc) + return reply } diff --git a/internal/commands/preview.go b/internal/commands/preview.go index bb52e77..cc25158 100644 --- a/internal/commands/preview.go +++ b/internal/commands/preview.go @@ -3,14 +3,13 @@ package commands import ( "fmt" - "github.com/gempir/go-twitch-irc/v4" "github.com/lyx0/nourybot/internal/common" ) -func Preview(target, channel string, tc *twitch.Client) { +func Preview(channel string) string { imageHeight := common.GenerateRandomNumberRange(1040, 1080) imageWidth := common.GenerateRandomNumberRange(1890, 1920) reply := fmt.Sprintf("https://static-cdn.jtvnw.net/previews-ttv/live_user_%v-%vx%v.jpg", channel, imageWidth, imageHeight) - common.Send(target, reply, tc) + return reply } diff --git a/internal/commands/seventv.go b/internal/commands/seventv.go index 776508d..011bd43 100644 --- a/internal/commands/seventv.go +++ b/internal/commands/seventv.go @@ -1,14 +1,10 @@ package commands -import ( - "fmt" +import "fmt" "github.com/gempir/go-twitch-irc/v4" "github.com/lyx0/nourybot/internal/common" ) -func Seventv(target, emote string, tc *twitch.Client) { - reply := fmt.Sprintf("https://7tv.app/emotes?query=%s", emote) - - common.Send(target, reply, tc) + return reply } diff --git a/internal/commands/weather.go b/internal/commands/weather.go index 9599881..6f20017 100644 --- a/internal/commands/weather.go +++ b/internal/commands/weather.go @@ -5,35 +5,31 @@ import ( "os" owm "github.com/briandowns/openweathermap" - "github.com/gempir/go-twitch-irc/v4" "github.com/joho/godotenv" - "github.com/lyx0/nourybot/internal/common" - "go.uber.org/zap" ) // Weather queries the OpenWeatherMap Api for the given location and sends the // current weather response to the target twitch chat. -func Weather(target, location string, tc *twitch.Client) { - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - +func Weather(location string) (string, error) { err := godotenv.Load() if err != nil { - sugar.Error("Error loading OpenWeatherMap API key from .env file") + return "", ErrInternalServerError } owmKey := os.Getenv("OWM_KEY") w, err := owm.NewCurrent("C", "en", owmKey) if err != nil { - sugar.Error(err) + return "", ErrInternalServerError + } + + if err := w.CurrentByName(location); err != nil { + return "", ErrInternalServerError } - w.CurrentByName(location) // Longitude and Latitude are returned as 0 when the supplied location couldn't be // assigned to a OpenWeatherMap location. if w.GeoPos.Longitude == 0 && w.GeoPos.Latitude == 0 { - reply := "Location not found FeelsBadMan" - common.Send(target, reply, tc) + return "", ErrWeatherLocationNotFound } else { // Weather for Vilnius, LT: Feels like: 29.67°C. Currently 29.49°C with a high of 29.84°C and a low of 29.49°C, humidity: 45%, wind: 6.17m/s. reply := fmt.Sprintf("Weather for %s, %s: Feels like: %v°C. Currently %v°C with a high of %v°C and a low of %v°C, humidity: %v%%, wind: %vm/s.", @@ -46,6 +42,6 @@ func Weather(target, location string, tc *twitch.Client) { w.Main.Humidity, w.Wind.Speed, ) - common.Send(target, reply, tc) + return reply, nil } } diff --git a/internal/commands/xkcd.go b/internal/commands/xkcd.go index 65bd1e7..0e518f3 100644 --- a/internal/commands/xkcd.go +++ b/internal/commands/xkcd.go @@ -6,9 +6,7 @@ import ( "io" "net/http" - "github.com/gempir/go-twitch-irc/v4" "github.com/lyx0/nourybot/internal/common" - "go.uber.org/zap" ) type xkcdResponse struct { @@ -17,44 +15,42 @@ type xkcdResponse struct { Img string `json:"img"` } -func Xkcd(target string, tc *twitch.Client) { - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - +func Xkcd() (string, error) { response, err := http.Get("https://xkcd.com/info.0.json") if err != nil { - sugar.Error(err) + return "", ErrInternalServerError } responseData, err := io.ReadAll(response.Body) if err != nil { - sugar.Error(err) + return "", ErrInternalServerError } var responseObject xkcdResponse - json.Unmarshal(responseData, &responseObject) + if err = json.Unmarshal(responseData, &responseObject); err != nil { + return "", ErrInternalServerError + } reply := fmt.Sprint("Current Xkcd #", responseObject.Num, " Title: ", responseObject.SafeTitle, " ", responseObject.Img) - common.Send(target, reply, tc) + return reply, nil } -func RandomXkcd(target string, tc *twitch.Client) { - sugar := zap.NewExample().Sugar() - defer sugar.Sync() - - comicNum := fmt.Sprint(common.GenerateRandomNumber(2655)) +func RandomXkcd() (string, error) { + comicNum := fmt.Sprint(common.GenerateRandomNumber(2772)) response, err := http.Get(fmt.Sprint("http://xkcd.com/" + comicNum + "/info.0.json")) if err != nil { - sugar.Error(err) + return "", ErrInternalServerError } responseData, err := io.ReadAll(response.Body) if err != nil { - sugar.Error(err) + return "", ErrInternalServerError } var responseObject xkcdResponse - json.Unmarshal(responseData, &responseObject) + if err = json.Unmarshal(responseData, &responseObject); err != nil { + return "", ErrInternalServerError + } reply := fmt.Sprint("Random Xkcd #", responseObject.Num, " Title: ", responseObject.SafeTitle, " ", responseObject.Img) - common.Send(target, reply, tc) + return reply, nil } diff --git a/internal/commands/youtube.go b/internal/commands/youtube.go new file mode 100644 index 0000000..ea0a9ee --- /dev/null +++ b/internal/commands/youtube.go @@ -0,0 +1,13 @@ +package commands + +import ( + "fmt" + "net/url" +) + +func Youtube(query string) string { + query = url.QueryEscape(query) + reply := fmt.Sprintf("https://www.youtube.com/results?search_query=%s", query) + + return reply +} diff --git a/internal/common/counter.go b/internal/common/counter.go index 28451bc..9a67b09 100644 --- a/internal/common/counter.go +++ b/internal/common/counter.go @@ -14,3 +14,4 @@ func CommandUsed() { func GetCommandsUsed() int { return tempCommands } + diff --git a/internal/data/channel.go b/internal/data/channel.go index 0282552..de5314e 100644 --- a/internal/data/channel.go +++ b/internal/data/channel.go @@ -49,7 +49,7 @@ func (c ChannelModel) Get(login string) (*Channel, error) { } // Insert takes in a channel struct and inserts it into the database. -func (c ChannelModel) Insert(channel *Channel) error { +func (c ChannelModel) Insert(login, id string) error { query := ` INSERT INTO channels(login, twitchid) VALUES ($1, $2) @@ -58,7 +58,7 @@ func (c ChannelModel) Insert(channel *Channel) error { RETURNING id, added_at; ` - args := []interface{}{channel.Login, channel.TwitchID} + args := []interface{}{login, id} // Execute the query returning the number of affected rows. result, err := c.DB.Exec(query, args...) diff --git a/internal/data/commands_logs.go b/internal/data/commands_logs.go new file mode 100644 index 0000000..0eb4b28 --- /dev/null +++ b/internal/data/commands_logs.go @@ -0,0 +1,46 @@ +package data + +import ( + "database/sql" +) + +type CommandsLog struct { + ID int `json:"id"` + TwitchLogin string `json:"twitch_login"` + TwitchID string `json:"twitch_id,omitempty"` + TwitchChannel string `json:"twitch_channel,omitempty"` + TwitchMessage string `json:"twitch_message,omitempty"` + CommandName string `json:"command_name,omitempty"` + UserLevel int `json:"user_level,omitempty"` + Identifier string `json:"identifier,omitempty"` + RawMessage string `json:"raw_message,omitempty"` +} + +type CommandsLogModel struct { + DB *sql.DB +} + +// Get tries to find a command in the database with the provided name. +func (c CommandsLogModel) Insert(twitchLogin, twitchId, twitchChannel, twitchMessage, commandName string, uLvl int, identifier, rawMsg string) { + query := ` + INSERT into commands_logs(twitch_login, twitch_id, twitch_channel, twitch_message, command_name, user_level, identifier, raw_message) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id; + ` + + args := []interface{}{twitchLogin, twitchId, twitchChannel, twitchMessage, commandName, uLvl, identifier, rawMsg} + + result, err := c.DB.Exec(query, args...) + if err != nil { + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return + } + + if rowsAffected == 0 { + return + } +} diff --git a/internal/data/models.go b/internal/data/models.go index f1a6065..73e882c 100644 --- a/internal/data/models.go +++ b/internal/data/models.go @@ -18,7 +18,7 @@ var ( // as app.models.Channels.Get(login) type Models struct { Channels interface { - Insert(channel *Channel) error + Insert(login, id string) error Get(login string) (*Channel, error) GetAll() ([]*Channel, error) GetJoinable() ([]string, error) @@ -47,18 +47,32 @@ type Models struct { } Timers interface { Get(name string) (*Timer, error) + GetIdentifier(name string) (string, error) Insert(timer *Timer) error Update(timer *Timer) error GetAll() ([]*Timer, error) Delete(name string) error } + Uploads interface { + Insert(twitchLogin, twitchID, twitchMessage, twitchChannel, filehoster, downloadURL, identifier string) + UpdateUploadURL(identifier, uploadURL string) + } + CommandsLogs interface { + Insert(twitchLogin, twitchId, twitchChannel, twitchMessage, commandName string, uLvl int, identifier, rawMsg string) + } + SentMessagesLogs interface { + Insert(twitchChannel, twitchMessage, ctxCommandName, ctxUser, ctxUserID, ctxMsg, identifier, ctxRaw string) + } } func NewModels(db *sql.DB) Models { return Models{ - Channels: ChannelModel{DB: db}, - Users: UserModel{DB: db}, - Commands: CommandModel{DB: db}, - Timers: TimerModel{DB: db}, + Channels: ChannelModel{DB: db}, + Users: UserModel{DB: db}, + Commands: CommandModel{DB: db}, + Timers: TimerModel{DB: db}, + Uploads: UploadModel{DB: db}, + CommandsLogs: CommandsLogModel{DB: db}, + SentMessagesLogs: SentMessagesLogModel{DB: db}, } } diff --git a/internal/data/sent_messages_logs.go b/internal/data/sent_messages_logs.go new file mode 100644 index 0000000..a0c0d2c --- /dev/null +++ b/internal/data/sent_messages_logs.go @@ -0,0 +1,46 @@ +package data + +import ( + "database/sql" +) + +type SentMessagesLog struct { + ID int `json:"id"` + TwitchChannel string `json:"twitch_channel,omitempty"` + TwitchMessage string `json:"twitch_message,omitempty"` + ContextCommandName string `json:"context_command_name"` + ContextUsername string `json:"context_user"` + ContextMessage string `json:"context_message"` + ContextUserID string `json:"context_user_id"` + Identifier string `json:"identifier,omitempty"` + ContextRawMsg string `json:"context_raw"` +} + +type SentMessagesLogModel struct { + DB *sql.DB +} + +// Get tries to find a command in the database with the provided name. +func (s SentMessagesLogModel) Insert(twitchChannel, twitchMessage, ctxCommandName, ctxUser, ctxUserID, ctxMsg, identifier, ctxRaw string) { + query := ` + INSERT into sent_messages_logs(twitch_channel, twitch_message, context_command_name, context_username, context_user_id, context_message, identifier, context_raw) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id; + ` + + args := []interface{}{twitchChannel, twitchMessage, ctxCommandName, ctxUser, ctxUserID, ctxMsg, identifier, ctxRaw} + + result, err := s.DB.Exec(query, args...) + if err != nil { + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return + } + + if rowsAffected == 0 { + return + } +} diff --git a/internal/data/timers.go b/internal/data/timers.go index a13a51b..63830ff 100644 --- a/internal/data/timers.go +++ b/internal/data/timers.go @@ -8,12 +8,12 @@ import ( ) type Timer struct { - ID int `json:"id" redis:"timer-id"` - Name string `json:"name" redis:"timer-name"` - CronName string `redis:"timer-cronname"` - Text string `json:"text" redis:"timer-text"` - Channel string `json:"channel" redis:"timer-channel"` - Repeat string `json:"repeat" redis:"timer-repeat"` + ID int `json:"id" redis:"timer-id"` + Name string `json:"name" redis:"timer-name"` + Identifier string `json:"identifier"` + Text string `json:"text" redis:"timer-text"` + Channel string `json:"channel" redis:"timer-channel"` + Repeat string `json:"repeat" redis:"timer-repeat"` } type TimerModel struct { @@ -22,7 +22,7 @@ type TimerModel struct { func (t TimerModel) Get(name string) (*Timer, error) { query := ` - SELECT id, name, text, channel, repeat + SELECT id, name, identifier, text, channel, repeat FROM timers WHERE name = $1 ` @@ -32,6 +32,7 @@ func (t TimerModel) Get(name string) (*Timer, error) { err := t.DB.QueryRow(query, name).Scan( &timer.ID, &timer.Name, + &timer.Identifier, &timer.Text, &timer.Channel, &timer.Repeat, @@ -48,15 +49,44 @@ func (t TimerModel) Get(name string) (*Timer, error) { return &timer, nil } +func (t TimerModel) GetIdentifier(name string) (string, error) { + query := ` + SELECT id, name, identifier, text, channel, repeat + FROM timers + WHERE name = $1 + ` + + var timer Timer + + err := t.DB.QueryRow(query, name).Scan( + &timer.ID, + &timer.Name, + &timer.Identifier, + &timer.Text, + &timer.Channel, + &timer.Repeat, + ) + if err != nil { + switch { + case errors.Is(err, sql.ErrNoRows): + return "", ErrRecordNotFound + default: + return "", err + } + } + + return timer.Identifier, nil +} + // Insert adds a command into the database. func (t TimerModel) Insert(timer *Timer) error { query := ` - INSERT into timers(name, text, channel, repeat) - VALUES ($1, $2, $3, $4) + INSERT into timers(name, identifier, text, channel, repeat) + VALUES ($1, $2, $3, $4, $5) RETURNING id; ` - args := []interface{}{timer.Name, timer.Text, timer.Channel, timer.Repeat} + args := []interface{}{timer.Name, timer.Identifier, timer.Text, timer.Channel, timer.Repeat} result, err := t.DB.Exec(query, args...) if err != nil { @@ -78,7 +108,7 @@ func (t TimerModel) Insert(timer *Timer) error { // GetAll() returns a pointer to a slice of all channels (`[]*Channel`) in the database. func (t TimerModel) GetAll() ([]*Timer, error) { query := ` - SELECT id, name, text, channel, repeat + SELECT id, name, identifier, text, channel, repeat FROM timers ORDER BY id` @@ -110,6 +140,7 @@ func (t TimerModel) GetAll() ([]*Timer, error) { err := rows.Scan( &timer.ID, &timer.Name, + &timer.Identifier, &timer.Text, &timer.Channel, &timer.Repeat, @@ -158,14 +189,14 @@ func (t TimerModel) Update(timer *Timer) error { // Delete takes in a command name and queries the database for an entry with // the same name and tries to delete that entry. -func (t TimerModel) Delete(name string) error { +func (t TimerModel) Delete(identifier string) error { // Prepare the statement. query := ` DELETE FROM timers - WHERE name = $1` + WHERE identifier = $1` // Execute the query returning the number of affected rows. - result, err := t.DB.Exec(query, name) + result, err := t.DB.Exec(query, identifier) if err != nil { return err } diff --git a/internal/data/uploads.go b/internal/data/uploads.go new file mode 100644 index 0000000..5efcae2 --- /dev/null +++ b/internal/data/uploads.go @@ -0,0 +1,84 @@ +package data + +import ( + "database/sql" + "errors" + "time" +) + +type Upload struct { + ID int `json:"id"` + AddedAt time.Time `json:"-"` + TwitchLogin string `json:"twitchlogin"` + TwitchID string `json:"twitchid"` + TwitchChannel string `json:"twitchchannel"` + TwitchMessage string `json:"twitchmessage"` + Filehoster string `json:"filehoster"` + DownloadURL string `json:"downloadurl"` + UploadURL string `json:"uploadurl"` + Identifier string `json:"identifier"` +} + +type UploadModel struct { + DB *sql.DB +} + +// Insert takes in a channel struct and inserts it into the database. +func (u UploadModel) Insert(twitchLogin, twitchID, twitchChannel, twitchMessage, filehoster, downloadURL, identifier string) { + query := ` + INSERT INTO uploads(twitchlogin, twitchid, twitchchannel, twitchmessage, filehoster, downloadurl, uploadurl, identifier) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id, added_at, identifier; + ` + + args := []interface{}{ + twitchLogin, + twitchID, + twitchChannel, + twitchMessage, + filehoster, + downloadURL, + "undefined", + identifier, + } + + // Execute the query returning the number of affected rows. + result, err := u.DB.Exec(query, args...) + if err != nil { + return + } + + // Check how many rows were affected. + rowsAffected, err := result.RowsAffected() + if err != nil { + return + } + + if rowsAffected == 0 { + return + } +} + +func (u UploadModel) UpdateUploadURL(identifier, uploadURL string) { + var id string + query := ` + UPDATE uploads + SET uploadurl = $2 + WHERE identifier = $1 + RETURNING id` + + args := []interface{}{ + identifier, + uploadURL, + } + + err := u.DB.QueryRow(query, args...).Scan(id) + if err != nil { + switch { + case errors.Is(err, sql.ErrNoRows): + return + default: + return + } + } +} diff --git a/internal/data/users.go b/internal/data/user.go similarity index 95% rename from internal/data/users.go rename to internal/data/user.go index 01ed131..a75cae6 100644 --- a/internal/data/users.go +++ b/internal/data/user.go @@ -23,14 +23,14 @@ type UserModel struct { // Insert inserts a user model into the database. func (u UserModel) Insert(login, twitchId string) error { query := ` - INSERT INTO users(login, twitchid) - VALUES ($1, $2) + INSERT INTO users(login, twitchid, level, location, lastfm_username) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT (login) DO NOTHING RETURNING id, added_at; ` - args := []interface{}{login, twitchId} + args := []interface{}{login, twitchId, "0", "", ""} // Execute the query returning the number of affected rows. result, err := u.DB.Exec(query, args...) @@ -214,7 +214,7 @@ func (u UserModel) SetLevel(login string, level int) error { // Get searches the database for a login name and returns the user struct on success. func (u UserModel) Get(login string) (*User, error) { query := ` - SELECT id, added_at, login, twitchid, level, location + SELECT id, added_at, login, twitchid, level, location, lastfm_username FROM users WHERE login = $1` @@ -227,6 +227,7 @@ func (u UserModel) Get(login string) (*User, error) { &user.TwitchID, &user.Level, &user.Location, + &user.LastFMUsername, ) if err != nil { diff --git a/internal/humanize/time.go b/internal/humanize/time.go index 044abd3..2598cd7 100644 --- a/internal/humanize/time.go +++ b/internal/humanize/time.go @@ -3,7 +3,7 @@ package humanize import ( "time" - "github.com/dustin/go-humanize" + humanize "github.com/dustin/go-humanize" ) func Time(t time.Time) string { diff --git a/internal/ivr/user.go b/internal/ivr/user.go new file mode 100644 index 0000000..ea832ae --- /dev/null +++ b/internal/ivr/user.go @@ -0,0 +1,27 @@ +package ivr + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type ivrIDByUsernameResponse struct { + ID string `json:"id"` +} + +func IDByUsername(username string) string { + baseUrl := "https://api.ivr.fi/v2/twitch/user?login=" + + resp, err := http.Get(fmt.Sprintf("%s%s", baseUrl, username)) + if err != nil { + return "xd" + } + + defer resp.Body.Close() + + responseList := make([]ivrIDByUsernameResponse, 0) + _ = json.NewDecoder(resp.Body).Decode(&responseList) + + return responseList[0].ID +} diff --git a/migrations/000002_create_users_table.up.sql b/migrations/000002_create_users_table.up.sql index 4202f2d..84beeb7 100644 --- a/migrations/000002_create_users_table.up.sql +++ b/migrations/000002_create_users_table.up.sql @@ -8,12 +8,9 @@ CREATE TABLE IF NOT EXISTS users ( lastfm_username text ); -INSERT INTO users (added_at,login,twitchid,"level") VALUES - (NOW(),'nourylul','31437432',1000), - (NOW(),'nourybot','596581605',1000), - (NOW(),'uudelleenkytkeytynyt','465178364',1000), - (NOW(),'xnoury','197780373',500), - (NOW(),'noemience','135447564',500); - -UPDATE users SET location = 'vilnius' WHERE login = 'nourylul'; -UPDATE users SET lastfm_username = 'nouryqt' WHERE login = 'nourylul'; +INSERT INTO users (added_at,login,twitchid,"level",location,lastfm_username) VALUES + (NOW(),'nourylul','31437432',1000,'vilnius','nouryqt'), + (NOW(),'nourybot','596581605',1000,'',''), + (NOW(),'uudelleenkytkeytynyt','465178364',1000,'',''), + (NOW(),'xnoury','197780373',500,'',''), + (NOW(),'noemience','135447564',500,'',''); diff --git a/migrations/000004_create_timers_table.up.sql b/migrations/000004_create_timers_table.up.sql index 9d14922..20ad2d2 100644 --- a/migrations/000004_create_timers_table.up.sql +++ b/migrations/000004_create_timers_table.up.sql @@ -1,16 +1,16 @@ CREATE TABLE IF NOT EXISTS timers ( id bigserial PRIMARY KEY, name text NOT NULL, + identifier text NOT NULL, text text NOT NULL, channel text NOT NULL, repeat text NOT NULL ); -INSERT INTO timers (name,"text",channel,repeat) VALUES - ('nourylul-60m','timer every 60 minutes :)','nourylul','60m'), - ('nourybot-60m','timer every 60 minutes :)','nourybot','60m'), - ('nourybot-1h','timer every 1 hour :)','nourybot','1h'), - ('xnoury-60m','timer every 420 minutes :)','xnoury','420m'), - ('xnoury-1h','timer every 1 hour :)','xnoury','1h'), - ('xnoury-15m','180 minutes timer :)','xnoury','180m'); +INSERT INTO timers (name,identifier,"text",channel,repeat) VALUES + ('nourylul-60m','678efbe2-fa2f-4849-8dbc-9ec32e6ffd3b','timer every 60 minutes :)','nourylul','60m'), + ('nourylul-2m','63142f10-1672-4353-8b03-e72f5a4dd566','timer every 2 minutes :)','nourylul','2m'), + ('nourybot-60m','2ad01f96-05d3-444e-9dd6-524d397caa96','timer every 60 minutes :)','nourybot','60m'), + ('nourybot-1h','2353fd22-fef9-4cbd-b01e-bc8804992f4c', 'timer every 1 hour :)','nourybot','1h'), + ('xnoury-15m','6e178e14-36c2-45e1-af59-b5dea4903fee','180 minutes timer :)','xnoury','180m'); diff --git a/migrations/000005_create_uploads_table.down.sql b/migrations/000005_create_uploads_table.down.sql new file mode 100644 index 0000000..281baa8 --- /dev/null +++ b/migrations/000005_create_uploads_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS uploads; diff --git a/migrations/000005_create_uploads_table.up.sql b/migrations/000005_create_uploads_table.up.sql new file mode 100644 index 0000000..5396edc --- /dev/null +++ b/migrations/000005_create_uploads_table.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS uploads ( + id bigserial PRIMARY KEY, + added_at timestamp(0) with time zone NOT NULL DEFAULT NOW(), + twitchlogin text NOT NULL, + twitchid text NOT NULL, + twitchmessage text NOT NULL, + twitchchannel text NOT NULL, + filehoster text NOT NULL, + downloadurl text, + uploadurl text, + identifier text +); + +INSERT INTO uploads (added_at,twitchlogin,twitchid,twitchchannel,twitchmessage,filehoster,downloadurl,uploadurl,identifier) VALUES + (NOW(),'nourylul','31437432','nourylul','()yaf https://www.youtube.com/watch?v=3rBFkwtaQbU','yaf','https://www.youtube.com/watch?v=3rBFkwtaQbU','https://i.yaf.ee/LEFuX.webm','a4af2284-4e13-46fa-9896-393bb1771a9d'), + (NOW(),'uudelleenkytkeytynyt','465178364','nourylul','()gofile https://www.youtube.com/watch?v=st6yupvNkVo','gofile','https://www.youtube.com/watch?v=st6yupvNkVo','https://gofile.io/d/PD1QNr','4ec952cc-42c0-41cd-9b07-637b4ec3c2b3'); + diff --git a/migrations/000006_create_command_logs_table.down.sql b/migrations/000006_create_command_logs_table.down.sql new file mode 100644 index 0000000..dddff0a --- /dev/null +++ b/migrations/000006_create_command_logs_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS commands_logs; diff --git a/migrations/000006_create_command_logs_table.up.sql b/migrations/000006_create_command_logs_table.up.sql new file mode 100644 index 0000000..cfe497a --- /dev/null +++ b/migrations/000006_create_command_logs_table.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS commands_logs ( + id bigserial PRIMARY KEY, + added_at timestamp(0) with time zone NOT NULL DEFAULT NOW(), + twitch_login text NOT NULL, + twitch_id text NOT NULL, + twitch_channel text NOT NULL, + twitch_message text NOT NULL, + command_name text NOT NULL, + user_level integer NOT NULL, + identifier text NOT NULL, + raw_message text NOT NULL +); + +INSERT INTO commands_logs (added_at,twitch_login,twitch_id,twitch_channel,twitch_message,command_name,user_level,identifier,raw_message) VALUES + (NOW(),'nourylul','31437432','nourybot','()weather Vilnius','weather',1000,'8441e97b-f622-4c42-b9b1-9bf22ba0d0bd','@badge-info=;badges=moderator/1,game-developer/1;color=#00F2FB;display-name=nourylul;emotes=;first-msg=0;flags=;id=87d40f5c-8c7c-4105-9f57-b1a953bb42d0;mod=1;returning-chatter=0;room-id=596581605;subscriber=0;tmi-sent-ts=1696945359165;turbo=0;user-id=31437432;user-type=mod :nourylul!nourylul@nourylul.tmi.twitch.tv PRIVMSG #nourybot :()weather Vilnius'); + diff --git a/migrations/000007_create_sent_messages_logs_table.down.sql b/migrations/000007_create_sent_messages_logs_table.down.sql new file mode 100644 index 0000000..e0861b5 --- /dev/null +++ b/migrations/000007_create_sent_messages_logs_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS sent_messages_logs; diff --git a/migrations/000007_create_sent_messages_logs_table.up.sql b/migrations/000007_create_sent_messages_logs_table.up.sql new file mode 100644 index 0000000..d671c7f --- /dev/null +++ b/migrations/000007_create_sent_messages_logs_table.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS sent_messages_logs ( + id bigserial PRIMARY KEY, + added_at timestamp(0) with time zone NOT NULL DEFAULT NOW(), + twitch_channel text NOT NULL, + twitch_message text NOT NULL, + context_command_name text, + context_username text, + context_message text, + context_user_id text, + identifier text, + context_raw text +); + +INSERT INTO sent_messages_logs (added_at,twitch_channel,twitch_message,context_command_name,context_username,context_message,context_user_id,identifier,context_raw) VALUES + (NOW(),'nourybot','Weather for Vilnius, LT: Feels like: 8.07°C. Currently 8.65°C with a high of 9.29°C and a low of 8.49°C, humidity: 66%, wind: 1.54m/s.','weather','nourylul','()weather Vilnius','31437432','654f9761-b2d4-4975-a4fd-84c6ec7f2eb8','@badge-info=;badges=moderator/1,game-developer/1;color=#00F2FB;display-name=nourylul;emotes=;first-msg=0;flags=;id=357d94a4-024e-49ea-ab3d-d97286cd0492;mod=1;returning-chatter=0;room-id=596581605;subscriber=0;tmi-sent-ts=1696952295788;turbo=0;user-id=31437432;user-type=mod :nourylul!nourylul@nourylul.tmi.twitch.tv PRIVMSG #nourybot :()weather Vilnius');