Merge pull request #3 from lyx0/rewrite

This commit is contained in:
lyx0 2023-10-10 19:34:10 +02:00 committed by GitHub
commit 77d22580b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 2285 additions and 1386 deletions

View file

@ -2,24 +2,25 @@ name: Go
on: on:
push: push:
branches: [ master ] branches: [ "master", "rewrite" ]
pull_request: pull_request:
branches: [ master ] branches: [ "master", "rewrite" ]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v4
with: with:
go-version: 1.19 go-version: '1.20'
- name: Build - name: Build
run: go build -v ./... run: go build -v ./...
- name: Test - name: Test
run: go test -v ./... run: go test -v ./...

View file

@ -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

30
LICENSE
View file

@ -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 Permission is hereby granted, free of charge, to any person obtaining a copy
purpose with or without fee is hereby granted, provided that the above of this software and associated documentation files (the "Software"), to deal
copyright notice and this permission notice appear in all copies. 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 The above copyright notice and this permission notice shall be included in all
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF copies or substantial portions of the Software.
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 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.

View file

@ -1,21 +1,28 @@
BINARY_NAME=Nourybot.out BINARY_NAME=Nourybot.out
BINARY_NAME_API=NourybotApi.out BINARY_NAME_API=NourybotApi.out
cup:
sudo docker compose up
xd: 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: 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: 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: 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"

View file

@ -1,7 +1,2 @@
# nourybot # nourybot
Lidl Twitch bot
Near future abandoned project in development.
### Make:
Development:
make jq

View file

@ -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{}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -4,83 +4,70 @@ import (
"fmt" "fmt"
"github.com/gempir/go-twitch-irc/v4" "github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/commands/decapi" "github.com/lyx0/nourybot/internal/ivr"
"github.com/lyx0/nourybot/internal/common"
"github.com/lyx0/nourybot/internal/data"
) )
// AddChannel takes in a channel name, then calls GetIdByLogin for the // AddChannel takes in a channel name, then calls GetIdByLogin for the
// channels ID and inserts both the name and id value into the database. // 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. // If there is no error thrown the TwitchClient joins the channel afterwards.
func (app *Application) AddChannel(login string, message twitch.PrivateMessage) { func (app *application) AddChannel(login string, message twitch.PrivateMessage) {
userId, err := decapi.GetIdByLogin(login) userID := ivr.IDByUsername(login)
if err != nil {
app.Logger.Error(err)
return
}
// Initialize a new channel struct holding the values that will be err := app.Models.Channels.Insert(login, userID)
// passed into the app.Models.Channels.Insert() method.
channel := &data.Channel{
Login: login,
TwitchID: userId,
}
err = app.Models.Channels.Insert(channel)
if err != nil { if err != nil {
reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err) reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err)
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return return
} else { } else {
app.TwitchClient.Join(login) app.TwitchClient.Join(login)
reply := fmt.Sprintf("Added channel %s", login) reply := fmt.Sprintf("Added channel %s", login)
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return return
} }
} }
// GetAllChannels() queries the database and lists all channels. // GetAllChannels() queries the database and lists all channels.
// Only used for debug/information purposes. // Only used for debug/information purposes.
func (app *Application) GetAllChannels() { func (app *application) GetAllChannels() {
channel, err := app.Models.Channels.GetAll() channel, err := app.Models.Channels.GetAll()
if err != nil { if err != nil {
app.Logger.Error(err) app.Log.Error(err)
return return
} }
app.Logger.Infow("All channels:", app.Log.Infow("All channels:",
"channel", channel) "channel", channel)
} }
// DeleteChannel queries the database for a channel name and if it exists // DeleteChannel queries the database for a channel name and if it exists
// deletes the channel and makes the bot depart said channel. // 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) err := app.Models.Channels.Delete(login)
if err != nil { if err != nil {
common.Send(message.Channel, "Something went wrong FeelsBadMan", app.TwitchClient) app.Send(message.Channel, "Something went wrong FeelsBadMan", message)
app.Logger.Error(err) app.Log.Error(err)
return return
} }
app.TwitchClient.Depart(login) app.TwitchClient.Depart(login)
reply := fmt.Sprintf("Deleted channel %s", 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 // InitialJoin is called on startup and queries the database for a list of
// channels which the TwitchClient then joins. // channels which the TwitchClient then joins.
func (app *Application) InitialJoin() { func (app *application) InitialJoin() {
// GetJoinable returns a slice of channel names. // GetJoinable returns a slice of channel names.
channel, err := app.Models.Channels.GetJoinable() channel, err := app.Models.Channels.GetJoinable()
if err != nil { if err != nil {
app.Logger.Error(err) app.Log.Error(err)
return return
} }
// Iterate over the slice of channels and join each. // Iterate over the slice of channels and join each.
for _, v := range channel { for _, v := range channel {
app.TwitchClient.Join(v) app.TwitchClient.Join(v)
app.Logger.Infow("Joining channel", app.Log.Infow("Joining channel",
"channel", v) "channel", v)
} }
} }

View file

@ -5,18 +5,18 @@ import (
"strconv" "strconv"
"github.com/gempir/go-twitch-irc/v4" "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/lyx0/nourybot/internal/data"
) )
// AddCommand splits a message into two parts and passes on the // AddCommand splits a message into two parts and passes on the
// name and text to the database handler. // 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`. // snipLength is the length we need to "snip" off of the start of `message`.
// `()addcommand` = +12 // `()add command` = +12
// trailing space = +1 // trailing space = +1
// zero-based = +1 // zero-based = +1
// = 14 // = 15
snipLength := 14 snipLength := 14
// Split the twitch message at `snipLength` plus length of the name of the // 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, Level: 0,
Help: "", Help: "",
} }
app.Log.Info(command)
err := app.Models.Commands.Insert(command) err := app.Models.Commands.Insert(command)
if err != nil { if err != nil {
reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err) reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err)
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return return
} else { } else {
reply := fmt.Sprintf("Successfully added command: %s", name) reply := fmt.Sprintf("Successfully added command: %s", name)
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return 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 // 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 // user who sent the message. If the users level is equal or higher
// the command.Text field is returned. // 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. // 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 { if err != nil {
// It probably did not exist // It probably did not exist
return "", err 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 { if command.Level == 0 {
return command.Text, nil return command.Text, nil
} else { } else if userLevel >= command.Level {
// Get the user from the database to check if the userlevel is equal if command.Category == "ascii" {
// or higher than the command.Level. // Cannot use app.Send() here since the command is a ascii pasta and will be
user, err := app.Models.Users.Get(username) // timed out, thus not passing the banphrase check app.Send() does before actually
if err != nil { // sending the message.
return "", err app.SendNoBanphrase(target, command.Text)
}
if user.Level >= command.Level { return "", nil
} else {
// Userlevel is sufficient so return the command.Text // Userlevel is sufficient so return the command.Text
return command.Text, nil 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. // Userlevel was not enough so return an empty string and error.
return "", ErrUserInsufficientLevel 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 // 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 // user who sent the message. If the users level is equal or higher
// the command.Text field is returned. // 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. // Fetch the command from the database if it exists.
command, err := app.Models.Commands.Get(name) command, err := app.Models.Commands.Get(name)
if err != nil { 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 // EditCommandLevel takes in a name and level string and updates the entry with name
// to the supplied level value. // 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) level, err := strconv.Atoi(lvl)
if err != nil { if err != nil {
app.Logger.Error(err) app.Log.Error(err)
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrCommandLevelNotInteger), app.TwitchClient) app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrCommandLevelNotInteger), message)
return return
} }
err = app.Models.Commands.SetLevel(name, level) err = app.Models.Commands.SetLevel(name, level)
if err != nil { if err != nil {
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient) app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message)
app.Logger.Error(err) app.Log.Error(err)
return return
} else { } else {
reply := fmt.Sprintf("Updated command %s to level %v", name, level) 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 return
} }
} }
// EditCommandCategory takes in a name and category string and updates the command // EditCommandCategory takes in a name and category string and updates the command
// in the databse with the passed in new category. // 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) err := app.Models.Commands.SetCategory(name, category)
if err != nil { if err != nil {
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient) app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message)
app.Logger.Error(err) app.Log.Error(err)
return return
} else { } else {
reply := fmt.Sprintf("Updated command %s to category %v", name, category) 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 return
} }
} }
// DebugCommand checks if a command with the provided name exists in the database // DebugCommand checks if a command with the provided name exists in the database
// and outputs information about it in the chat. // 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 // Query the database for a command with the provided name
cmd, err := app.Models.Commands.Get(name) cmd, err := app.Models.Commands.Get(name)
if err != nil { if err != nil {
reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err) reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err)
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
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)
return return
} else { } 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.ID,
cmd.Name, cmd.Name,
cmd.Level, cmd.Level,
@ -189,16 +178,24 @@ func (app *Application) DebugCommand(name string, message twitch.PrivateMessage)
cmd.Help, 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 return
} }
} }
// SetCommandHelp updates the `help` column of a given commands name in the // SetCommandHelp updates the `help` column of a given commands name in the
// database to the provided new help text. // 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`. // snipLength is the length we need to "snip" off of the start of `message`.
// `()editcommand` = +13 // `()edit command` = +13
// trailing space = +1 // trailing space = +1
// zero-based = +1 // zero-based = +1
// `help` = +4 // `help` = +4
@ -216,25 +213,36 @@ func (app *Application) EditCommandHelp(name string, message twitch.PrivateMessa
err := app.Models.Commands.SetHelp(name, text) err := app.Models.Commands.SetHelp(name, text)
if err != nil { if err != nil {
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient) app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message)
app.Logger.Error(err) app.Log.Error(err)
return return
} else { } else {
reply := fmt.Sprintf("Updated help text for command %s to: %v", name, text) 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 return
} }
} }
// DeleteCommand takes in a name value and deletes the command from the database if it exists. // 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) err := app.Models.Commands.Delete(name)
if err != nil { if err != nil {
common.Send(message.Channel, "Something went wrong FeelsBadMan", app.TwitchClient) app.Send(message.Channel, "Something went wrong FeelsBadMan", message)
app.Logger.Error(err) app.Log.Error(err)
return return
} }
reply := fmt.Sprintf("Deleted command %s", name) 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)
} }

309
cmd/nourybot/commands.go Normal file
View file

@ -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 <emote name>"
} else {
reply = commands.Bttv(cmdParams[1])
}
// Coinflip
case "coin":
reply = commands.Coinflip()
case "coinflip":
reply = commands.Coinflip()
case "cf":
reply = commands.Coinflip()
// ()currency <amount> <input currency> to <output currency>
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 <text to translate>"
} else {
reply, _ = commands.Phonetic(message.Message[11:len(message.Message)])
}
case "ping":
reply = commands.Ping()
// ()bttv <emote name>
// ()weather <location>
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 <emote name>",
"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 <emote name>",
"followage": "Returns how long a given user has been following a channel. Example usage: ()followage <channel> <username>",
"firstline": "Returns the first message a user has sent in a given channel. Aliases: firstline, fl. Example usage: ()firstline <channel> <username>",
"fl": "Returns the first message a user has sent in a given channel. Aliases: firstline, fl. Example usage: ()fl <channel> <username>",
"help": "Returns more information about a command and its usage. 4Head Example usage: ()help <command name>",
"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 <channel>",
"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 <channel>",
"tweet": "Returns the latest tweet for a provided user. Example usage: ()tweet <username>",
"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)
}

217
cmd/nourybot/download.go Normal file
View file

@ -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)
}

34
cmd/nourybot/email.go Normal file
View file

@ -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)
}
}

View file

@ -9,4 +9,5 @@ var (
ErrCommandLevelNotInteger = errors.New("command level must be a number") ErrCommandLevelNotInteger = errors.New("command level must be a number")
ErrRecordNotFound = errors.New("user not found in the database") ErrRecordNotFound = errors.New("user not found in the database")
ErrUserInsufficientLevel = errors.New("user has insufficient level") ErrUserInsufficientLevel = errors.New("user has insufficient level")
ErrDuringPasteUpload = errors.New("could not upload paste")
) )

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"database/sql" "database/sql"
"flag" "flag"
"log"
"os" "os"
"time" "time"
@ -14,8 +13,9 @@ import (
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/lyx0/nourybot/internal/common" "github.com/lyx0/nourybot/internal/common"
"github.com/lyx0/nourybot/internal/data" "github.com/lyx0/nourybot/internal/data"
"github.com/nicklaw5/helix" "github.com/nicklaw5/helix/v2"
"github.com/redis/go-redis/v9" "github.com/rs/zerolog/log"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -24,6 +24,7 @@ type config struct {
twitchOauth string twitchOauth string
twitchClientId string twitchClientId string
twitchClientSecret string twitchClientSecret string
twitchID string
commandPrefix string commandPrefix string
db struct { db struct {
dsn string dsn string
@ -33,36 +34,39 @@ type config struct {
} }
} }
type Application struct { type application struct {
TwitchClient *twitch.Client TwitchClient *twitch.Client
HelixClient *helix.Client HelixClient *helix.Client
Logger *zap.SugaredLogger Log *zap.SugaredLogger
Db *sql.DB Db *sql.DB
Models data.Models Models data.Models
Scheduler *cron.Cron Scheduler *cron.Cron
Rdb *redis.Client // Rdb *redis.Client
} }
var envFlag string var envFlag string
var ctx = context.Background()
func init() { func init() {
flag.StringVar(&envFlag, "env", "dev", "database connection to use: (dev/prod)") flag.StringVar(&envFlag, "env", "dev", "database connection to use: (dev/prod)")
flag.Parse() flag.Parse()
} }
func main() { func main() {
var cfg config var cfg config
// Initialize a new sugared logger that we'll pass on // Initialize a new sugared logger that we'll pass on
// down through the application. // down through the application.
logger := zap.NewExample() 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() sugar := logger.Sugar()
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
log.Fatal("Error loading .env file") sugar.Fatal("Error loading .env")
} }
// Twitch config variables // Twitch config variables
@ -71,15 +75,15 @@ func main() {
cfg.twitchClientId = os.Getenv("TWITCH_CLIENT_ID") cfg.twitchClientId = os.Getenv("TWITCH_CLIENT_ID")
cfg.twitchClientSecret = os.Getenv("TWITCH_CLIENT_SECRET") cfg.twitchClientSecret = os.Getenv("TWITCH_CLIENT_SECRET")
cfg.commandPrefix = os.Getenv("TWITCH_COMMAND_PREFIX") cfg.commandPrefix = os.Getenv("TWITCH_COMMAND_PREFIX")
cfg.twitchID = os.Getenv("TWITCH_ID")
tc := twitch.NewClient(cfg.twitchUsername, cfg.twitchOauth) tc := twitch.NewClient(cfg.twitchUsername, cfg.twitchOauth)
switch envFlag { switch envFlag {
case "dev": case "dev":
cfg.db.dsn = os.Getenv("LOCAL_DSN") cfg.db.dsn = os.Getenv("LOCAL_DSN")
case "prod": case "prod":
cfg.db.dsn = os.Getenv("SUPABASE_DSN") cfg.db.dsn = os.Getenv("REMOTE_DSN")
} }
// Database config variables // Database config variables
cfg.db.maxOpenConns = 25 cfg.db.maxOpenConns = 25
cfg.db.maxIdleConns = 25 cfg.db.maxIdleConns = 25
@ -105,9 +109,6 @@ func main() {
"err", err, "err", err,
) )
} }
sugar.Infow("Got new helix AppAccessToken",
"helixClient", helixResp,
)
// Set the access token on the client // Set the access token on the client
helixClient.SetAppAccessToken(helixResp.Data.AccessToken) helixClient.SetAppAccessToken(helixResp.Data.AccessToken)
@ -115,54 +116,37 @@ func main() {
// Establish database connection // Establish database connection
db, err := openDB(cfg) db, err := openDB(cfg)
if err != nil { if err != nil {
sugar.Fatal(err) sugar.Fatalw("could not establish database connection",
"err", err,
)
} }
app := &application{
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{
TwitchClient: tc, TwitchClient: tc,
HelixClient: helixClient, HelixClient: helixClient,
Logger: sugar, Log: sugar,
Db: db, Db: db,
Models: data.NewModels(db), Models: data.NewModels(db),
Scheduler: cron.New(), Scheduler: cron.New(),
Rdb: rdb,
} }
app.Log.Infow("db.Stats",
"db.Stats", db.Stats(),
)
// Received a PrivateMessage (normal chat message). // Received a PrivateMessage (normal chat message).
app.TwitchClient.OnPrivateMessage(func(message twitch.PrivateMessage) { app.TwitchClient.OnPrivateMessage(func(message twitch.PrivateMessage) {
sugar.Infow("New Twitch PrivateMessage",
// app.Logger.Infow("Message received", "message.Channel", message.Channel,
// "message", message, "message.User.DisplayName", message.User.DisplayName,
// "message.User.DisplayName", message.User.DisplayName, "message.User.ID", message.User.ID,
// "message.Message", message.Message, "message.Message", message.Message,
// ) )
// roomId is the Twitch UserID of the channel the message originated from. // roomId is the Twitch UserID of the channel the message originated from.
// If there is no roomId something went really wrong. // If there is no roomId something went really wrong.
roomId := message.Tags["room-id"] roomId := message.Tags["room-id"]
if roomId == "" { if roomId == "" {
app.Logger.Errorw("Missing room-id in message tag", log.Error().Msgf("Missing room-id in message tag: %s", roomId)
"roomId", roomId,
)
return return
} }
@ -172,32 +156,27 @@ func main() {
// Check if the first 2 characters of the mesage were our prefix. // Check if the first 2 characters of the mesage were our prefix.
// if they were forward the message to the command handler. // if they were forward the message to the command handler.
if message.Message[:2] == cfg.commandPrefix { if message.Message[:2] == cfg.commandPrefix {
app.InitUser(message.User.Name, message.User.ID, message) go app.handleCommand(message)
app.handleCommand(message)
return return
} }
// Special rule for #pajlada. // Special rule for #pajlada.
if message.Message == "!nourybot" { 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.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, "Bot username", cfg.twitchUsername,
"Environment", envFlag, "Environment", envFlag,
"Database Open Conns", cfg.db.maxOpenConns, "Database Open Conns", cfg.db.maxOpenConns,
@ -207,31 +186,19 @@ func main() {
"Helix", helixResp, "Helix", helixResp,
) )
// Start time
common.StartTime()
app.loadCommandHelp()
// Join the channels in the database.
app.InitialJoin() app.InitialJoin()
// Load the initial timers from the database. // Load the initial timers from the database.
app.InitialTimers() app.InitialTimers()
// Start the timers. // Start the timers.
app.Scheduler.Start() 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. // Actually connect to chat.
err = app.TwitchClient.Connect() err = app.TwitchClient.Connect()
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
// openDB returns the sql.DB connection pool. // openDB returns the sql.DB connection pool.

59
cmd/nourybot/paste.go Normal file
View file

@ -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
}

261
cmd/nourybot/send.go Normal file
View file

@ -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
}
}

View file

@ -6,41 +6,50 @@ import (
"strings" "strings"
"github.com/gempir/go-twitch-irc/v4" "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/lyx0/nourybot/internal/data"
"github.com/redis/go-redis/v9"
) )
// AddTimer slices the message into relevant parts, adding the values onto a // 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. // 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) cmdParams := strings.SplitN(message.Message, " ", 500)
// snipLength is the length of `()addcommand` plus +2 (for the space and zero based) // prefixLength is the length of `()add timer` plus +2 (for the space and zero based)
snipLength := 12 prefix := "()add timer"
repeat := cmdParams[2] 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. // 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 -------------> | // 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 // validateTimeFormat will be true if the repeat parameter is in
// the format of either 30m, 10h, or 10h30m. // 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) 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 { if err != nil {
app.Logger.Errorw("Received malformed time format in timer", app.Log.Errorw("Received malformed time format in timer",
"repeat", repeat, "repeat", repeat,
"error", err, "error", err,
) )
return return
} }
id := uuid.NewString()
timer := &data.Timer{ timer := &data.Timer{
Name: name, Name: name,
Text: text, Text: text,
Channel: message.Channel, Identifier: id,
Repeat: repeat, Channel: message.Channel,
Repeat: repeat,
} }
// Check if the time string we got is valid, this is important // 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. // time format string is supplied.
if validateTimeFormat { if validateTimeFormat {
timer := &data.Timer{ timer := &data.Timer{
Name: name, Name: name,
Text: text, Text: text,
Channel: message.Channel, Identifier: id,
Repeat: repeat, Channel: message.Channel,
Repeat: repeat,
} }
err = app.Models.Timers.Insert(timer) err = app.Models.Timers.Insert(timer)
if err != nil { if err != nil {
app.Logger.Errorw("Error inserting new timer into database", app.Log.Errorw("Error inserting new timer into database",
"timer", timer, "timer", timer,
"error", err, "error", err,
) )
reply := fmt.Sprintln("Something went wrong FeelsBadMan") reply := fmt.Sprintln("Something went wrong FeelsBadMan")
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return return
} else { } else {
// cronName is the internal, unique tag/name for the timer. // cronName is the internal, unique tag/name for the timer.
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor" // 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.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, id)
app.Logger.Infow("Added new timer", app.Log.Infow("Added new timer",
"timer", 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) 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 return
} }
} else { } else {
app.Logger.Errorw("Received malformed time format in timer", app.Log.Errorw("Received malformed time format in timer",
"timer", timer, "timer", timer,
"error", err, "error", err,
) )
reply := "Something went wrong FeelsBadMan received wrong time format. Allowed formats: 30m, 10h, 10h30m" 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 return
} }
} }
// EditTimer just contains the logic for deleting a timer, and then adding a new one // 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. // 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. // Check if a timer with that name is in the database.
app.Log.Info(name)
old, err := app.Models.Timers.Get(name) old, err := app.Models.Timers.Get(name)
if err != nil { if err != nil {
app.Logger.Errorw("Could not get timer", app.Log.Errorw("Could not get timer",
"timer", old, "timer", old,
"error", err, "error", err,
) )
reply := "Something went wrong FeelsBadMan" reply := "Something went wrong FeelsBadMan"
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return return
} }
// -----------------------
// Delete the old timer // 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) err = app.Models.Timers.Delete(name)
if err != nil { if err != nil {
app.Logger.Errorw("Error deleting timer from database", app.Log.Errorw("Error deleting timer from database",
"name", name, "name", name,
"cronName", cronName, "identifier", identifier,
"error", err, "error", err,
) )
reply := fmt.Sprintln("Something went wrong FeelsBadMan") reply := fmt.Sprintln("Something went wrong FeelsBadMan")
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return return
} }
// -----------------------
// Add the new timer // Add the new timer
cmdParams := strings.SplitN(message.Message, " ", 500) // -----------------------
// snipLength is the length of `()editcommand` plus +2 (for the space and zero based) //cmdParams := strings.SplitN(message.Message, " ", 500)
snipLength := 13 // prefixLength is the length of `()editcommand` plus +2 (for the space and zero based)
repeat := cmdParams[2] 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. // Split the message into the parts we need.
// //
// message: ()addtimer sponsor 20m hecking love my madmonq pills BatChest // message: ()addtimer sponsor 20m hecking love my madmonq pills BatChest
// parts: | prefix | |name | |repeat | <----------- text -------------> | // 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 // validateTimeFormat will be true if the repeat parameter is in
// the format of either 30m, 10h, or 10h30m. // 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) 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 { if err != nil {
app.Logger.Errorw("Received malformed time format in timer", app.Log.Errorw("Received malformed time format in timer",
"repeat", repeat, "repeat", repeat,
"error", err, "error", err,
) )
return return
} }
id := uuid.NewString()
timer := &data.Timer{ timer := &data.Timer{
Name: name, Name: name,
Text: text, Text: text,
Channel: message.Channel, Identifier: id,
Repeat: repeat, Channel: message.Channel,
Repeat: repeat,
} }
// Check if the time string we got is valid, this is important // 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. // time format string is supplied.
if validateTimeFormat { if validateTimeFormat {
timer := &data.Timer{ timer := &data.Timer{
Name: name, Name: name,
Text: text, Text: text,
Channel: message.Channel, Identifier: id,
Repeat: repeat, Channel: message.Channel,
Repeat: repeat,
} }
err = app.Models.Timers.Insert(timer) err = app.Models.Timers.Insert(timer)
if err != nil { if err != nil {
app.Logger.Errorw("Error inserting new timer into database", app.Log.Errorw("Error inserting new timer into database",
"timer", timer, "timer", timer,
"error", err, "error", err,
) )
reply := fmt.Sprintln("Something went wrong FeelsBadMan") reply := fmt.Sprintln("Something went wrong FeelsBadMan")
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return return
} else { // this is a bit scuffed. The else here is the end of a successful call. } 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. // cronName is the internal, unique tag/name for the timer.
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor" // 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.Log.Infow("Updated a timer",
app.Logger.Infow("Updated a timer",
"Name", name, "Name", name,
"Channel", message.Channel, "Channel", message.Channel,
"Old timer", old, "Old timer", old,
"New timer", timer, "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) reply := fmt.Sprintf("Successfully updated timer %s", name)
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return return
} }
} else { } else {
app.Logger.Errorw("Received malformed time format in timer", app.Log.Errorw("Received malformed time format in timer",
"timer", timer, "timer", timer,
"error", err, "error", err,
) )
reply := "Something went wrong FeelsBadMan received wrong time format. Allowed formats: 30s, 30m, 10h, 10h30m" 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 return
} }
} }
// InitialTimers is called on startup and queries the database for a list of // InitialTimers is called on startup and queries the database for a list of
// timers and then adds each onto the scheduler. // timers and then adds each onto the scheduler.
func (app *Application) InitialTimers() { func (app *application) ListTimers() string {
timer, err := app.Models.Timers.GetAll() timer, err := app.Models.Timers.GetAll()
if err != nil { 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 return
} }
@ -253,73 +295,61 @@ func (app *Application) InitialTimers() {
// cronName is the internal, unique tag/name for the timer. // cronName is the internal, unique tag/name for the timer.
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor" // 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. // Repeating is at what times the timer should repeat.
// 2 minute timer is @every 2m // 2 minute timer is @every 2m
repeating := fmt.Sprintf("@every %s", v.Repeat) 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 // Add new value to the slice
ts = append(ts, v) ts = append(ts, v)
app.Scheduler.AddFunc(repeating, func() { app.newPrivateMessageTimer(v.Channel, v.Text) }, v.Identifier)
} }
// var model1 rdbVal app.Log.Infow("Initial timers",
// if err := app.Rdb.HGetAll(ctx, cronName).Scan(&model1); err != nil {
// app.Logger.Panic(err)
// }
app.Logger.Infow("Initial timers",
"timer", ts, "timer", ts,
) )
} }
// newPrivateMessageTimer is a helper function to set timers // newPrivateMessageTimer is a helper function to set timers
// which trigger into sending a twitch PrivateMessage. // which trigger into sending a twitch PrivateMessage.
func (app *Application) newPrivateMessageTimer(channel, text string) { func (app *application) newPrivateMessageTimer(channel, text string) {
common.Send(channel, text, app.TwitchClient) app.SendNoContext(channel, text)
} }
// DeleteTimer takes in the name of a timer and tries to delete the timer from the database. // 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) { func (app *application) DeleteTimer(name string, message twitch.PrivateMessage) {
cronName := fmt.Sprintf("%s-%s", message.Channel, name)
app.Scheduler.RemoveJob(cronName)
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, "name", name,
"identifier", identifier,
"message.Channel", message.Channel, "message.Channel", message.Channel,
"cronName", cronName,
) )
_ = app.Rdb.Del(ctx, cronName) err = app.Models.Timers.Delete(identifier)
err := app.Models.Timers.Delete(name)
if err != nil { if err != nil {
app.Logger.Errorw("Error deleting timer from database", app.Log.Errorw("Error deleting timer from database",
"name", name, "name", name,
"cronName", cronName, "identifier", identifier,
"error", err, "error", err,
) )
reply := fmt.Sprintln("Something went wrong FeelsBadMan") reply := fmt.Sprintln("Something went wrong FeelsBadMan")
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
return return
} }
reply := fmt.Sprintf("Deleted timer with name %s", name) reply := fmt.Sprintf("Deleted timer with name %s", name)
common.Send(message.Channel, reply, app.TwitchClient) app.Send(message.Channel, reply, message)
} }

368
cmd/nourybot/upload.go Normal file
View file

@ -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)
}

183
cmd/nourybot/user.go Normal file
View file

@ -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 <location> to register. Otherwise use ()weather <location> 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 <username> to register. Otherwise use ()lastfm <username> without registering."
return reply
}
reply := commands.LastFmUserRecent(target, lastfmUser)
return reply
}

View file

@ -1,7 +0,0 @@
version: "3.9"
services:
nourybot:
build:
dockerfile: Dockerfile
context: .
# restart: unless-stopped

15
env.example Normal file
View file

@ -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

92
go.sum
View file

@ -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/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 h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 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 h1:kCvm3G3u+eTRbjfLPyfsfznJtraYEfZer/UvQ6CaQhI=
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5/go.mod h1:6DM2KNNK69jRu0lAHmYK9LYxmqpNjYHOaNp/ZxttD4U= 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.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M= 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/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/nicklaw5/helix/v2 v2.25.1 h1:hccFfWf1kdPKeC/Zp8jNbOvqV0f6ya12hdeNHuQa5wg=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= 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 h1:cgqwZtnR+IQfUYDLJ3Kiy4aE+O/wExTzEIg8xwC4Qfs=
github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0/go.mod h1:n3nudMl178cEvD44PaopxH9jhJaQzthSxUzLO5iKMy4= 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/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/wader/goutubedl v0.0.0-20230924165737-427b7fa536e6 h1:KHJV3fnnKsdWdGu5IKrDAA0Oa5RzGwrJpfx+bvVAjLA=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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.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 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.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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=

View file

@ -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"}]}

View file

@ -1,3 +0,0 @@
# Insomnia
Api collection import/export for [insomnia](https://insomnia.rest/)

View file

@ -2,13 +2,10 @@ package commands
import ( import (
"fmt" "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) reply := fmt.Sprintf("https://betterttv.com/emotes/shared/search?query=%s", query)
common.Send(target, reply, tc) return reply
} }

View file

@ -1,23 +1,21 @@
package commands package commands
import ( import (
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common" "github.com/lyx0/nourybot/internal/common"
) )
func Coinflip(target string, tc *twitch.Client) { func Coinflip() string {
flip := common.GenerateRandomNumber(2) flip := common.GenerateRandomNumber(2)
var reply string
switch flip { switch flip {
case 0: case 0:
common.Send(target, "Heads!", tc) reply = "Heads!"
return
case 1: case 1:
common.Send(target, "Tails!", tc) reply = "Tails!"
return
default: default:
common.Send(target, "Heads!", tc) reply = "Heads!"
return
} }
return reply
} }

View file

@ -1,21 +1,30 @@
package commands package commands
import ( import (
"github.com/gempir/go-twitch-irc/v4" "fmt"
"github.com/lyx0/nourybot/internal/commands/decapi" "io"
"github.com/lyx0/nourybot/internal/common" "net/http"
"go.uber.org/zap"
) )
// ()currency 10 USD to EUR func Currency(currAmount, currFrom, currTo string) (string, error) {
func Currency(target, currAmount, currFrom, currTo string, tc *twitch.Client) { basePath := "https://decapi.me/misc/currency/"
sugar := zap.NewExample().Sugar() from := fmt.Sprintf("?from=%s", currFrom)
defer sugar.Sync() 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 { 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
} }

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -1,6 +0,0 @@
package decapi
var (
twitterUserNotFoundError = "[Error] - [34] Sorry, that page does not exist."
followageUserCannotFollowOwn = "A user cannot follow themself."
)

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,8 @@
package commands
import "errors"
var (
ErrInternalServerError = errors.New("internal server error")
ErrWeatherLocationNotFound = errors.New("location not found")
)

View file

@ -1,14 +1,10 @@
package commands package commands
import ( import "fmt"
"fmt"
"github.com/gempir/go-twitch-irc/v4" "github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common" "github.com/lyx0/nourybot/internal/common"
) )
func Ffz(target, query string, tc *twitch.Client) { return reply
reply := fmt.Sprintf("https://www.frankerfacez.com/emoticons/?q=%s", query)
common.Send(target, reply, tc)
} }

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -4,44 +4,12 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/gempir/go-twitch-irc/v4"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/lyx0/nourybot/internal/common"
"github.com/shkh/lastfm-go/lastfm" "github.com/shkh/lastfm-go/lastfm"
"go.uber.org/zap" "go.uber.org/zap"
) )
func LastFmArtistTop(target string, message twitch.PrivateMessage, tc *twitch.Client) { func LastFmUserRecent(target, user string) string {
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) {
sugar := zap.NewExample().Sugar() sugar := zap.NewExample().Sugar()
defer sugar.Sync() 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) 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
} }

13
internal/commands/osrs.go Normal file
View file

@ -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
}

View file

@ -2,9 +2,6 @@ package commands
import ( import (
"fmt" "fmt"
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common"
) )
var cm = map[string]string{ var cm = map[string]string{
@ -49,6 +46,7 @@ var cm = map[string]string{
"b": "б", "b": "б",
"n": "н", "n": "н",
"m": "м", "m": "м",
"Q": "Я", "Q": "Я",
"W": "Ш", "W": "Ш",
"E": "Е", "E": "Е",
@ -77,7 +75,7 @@ var cm = map[string]string{
"M": "М", "M": "М",
} }
func Phonetic(target, message string, tc *twitch.Client) { func Phonetic(message string) (string, error) {
var ts string var ts string
for _, c := range message { for _, c := range message {
@ -87,8 +85,7 @@ func Phonetic(target, message string, tc *twitch.Client) {
ts = ts + string(c) ts = ts + string(c)
} }
//ts = append(ts, cm[string(c)])
} }
common.Send(target, fmt.Sprint(ts), tc) return fmt.Sprint(ts), nil
} }

View file

@ -3,15 +3,14 @@ package commands
import ( import (
"fmt" "fmt"
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common" "github.com/lyx0/nourybot/internal/common"
"github.com/lyx0/nourybot/internal/humanize" "github.com/lyx0/nourybot/internal/humanize"
) )
func Ping(target string, tc *twitch.Client) { func Ping() string {
botUptime := humanize.Time(common.GetUptime()) botUptime := humanize.Time(common.GetUptime())
commandsUsed := common.GetCommandsUsed() commandsUsed := common.GetCommandsUsed()
reply := fmt.Sprintf("Pong! :) Commands used: %v, Last restart: %v", commandsUsed, botUptime) reply := fmt.Sprintf("Pong! :) Commands used: %v, Last restart: %v", commandsUsed, botUptime)
common.Send(target, reply, tc) return reply
} }

View file

@ -3,14 +3,13 @@ package commands
import ( import (
"fmt" "fmt"
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common" "github.com/lyx0/nourybot/internal/common"
) )
func Preview(target, channel string, tc *twitch.Client) { func Preview(channel string) string {
imageHeight := common.GenerateRandomNumberRange(1040, 1080) imageHeight := common.GenerateRandomNumberRange(1040, 1080)
imageWidth := common.GenerateRandomNumberRange(1890, 1920) imageWidth := common.GenerateRandomNumberRange(1890, 1920)
reply := fmt.Sprintf("https://static-cdn.jtvnw.net/previews-ttv/live_user_%v-%vx%v.jpg", channel, imageWidth, imageHeight) 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
} }

View file

@ -1,14 +1,10 @@
package commands package commands
import ( import "fmt"
"fmt"
"github.com/gempir/go-twitch-irc/v4" "github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common" "github.com/lyx0/nourybot/internal/common"
) )
func Seventv(target, emote string, tc *twitch.Client) { return reply
reply := fmt.Sprintf("https://7tv.app/emotes?query=%s", emote)
common.Send(target, reply, tc)
} }

View file

@ -5,35 +5,31 @@ import (
"os" "os"
owm "github.com/briandowns/openweathermap" owm "github.com/briandowns/openweathermap"
"github.com/gempir/go-twitch-irc/v4"
"github.com/joho/godotenv" "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 // Weather queries the OpenWeatherMap Api for the given location and sends the
// current weather response to the target twitch chat. // current weather response to the target twitch chat.
func Weather(target, location string, tc *twitch.Client) { func Weather(location string) (string, error) {
sugar := zap.NewExample().Sugar()
defer sugar.Sync()
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
sugar.Error("Error loading OpenWeatherMap API key from .env file") return "", ErrInternalServerError
} }
owmKey := os.Getenv("OWM_KEY") owmKey := os.Getenv("OWM_KEY")
w, err := owm.NewCurrent("C", "en", owmKey) w, err := owm.NewCurrent("C", "en", owmKey)
if err != nil { 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 // Longitude and Latitude are returned as 0 when the supplied location couldn't be
// assigned to a OpenWeatherMap location. // assigned to a OpenWeatherMap location.
if w.GeoPos.Longitude == 0 && w.GeoPos.Latitude == 0 { if w.GeoPos.Longitude == 0 && w.GeoPos.Latitude == 0 {
reply := "Location not found FeelsBadMan" return "", ErrWeatherLocationNotFound
common.Send(target, reply, tc)
} else { } 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. // 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.", 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.Main.Humidity,
w.Wind.Speed, w.Wind.Speed,
) )
common.Send(target, reply, tc) return reply, nil
} }
} }

View file

@ -6,9 +6,7 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common" "github.com/lyx0/nourybot/internal/common"
"go.uber.org/zap"
) )
type xkcdResponse struct { type xkcdResponse struct {
@ -17,44 +15,42 @@ type xkcdResponse struct {
Img string `json:"img"` Img string `json:"img"`
} }
func Xkcd(target string, tc *twitch.Client) { func Xkcd() (string, error) {
sugar := zap.NewExample().Sugar()
defer sugar.Sync()
response, err := http.Get("https://xkcd.com/info.0.json") response, err := http.Get("https://xkcd.com/info.0.json")
if err != nil { if err != nil {
sugar.Error(err) return "", ErrInternalServerError
} }
responseData, err := io.ReadAll(response.Body) responseData, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
sugar.Error(err) return "", ErrInternalServerError
} }
var responseObject xkcdResponse 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) 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) { func RandomXkcd() (string, error) {
sugar := zap.NewExample().Sugar() comicNum := fmt.Sprint(common.GenerateRandomNumber(2772))
defer sugar.Sync()
comicNum := fmt.Sprint(common.GenerateRandomNumber(2655))
response, err := http.Get(fmt.Sprint("http://xkcd.com/" + comicNum + "/info.0.json")) response, err := http.Get(fmt.Sprint("http://xkcd.com/" + comicNum + "/info.0.json"))
if err != nil { if err != nil {
sugar.Error(err) return "", ErrInternalServerError
} }
responseData, err := io.ReadAll(response.Body) responseData, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
sugar.Error(err) return "", ErrInternalServerError
} }
var responseObject xkcdResponse 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) reply := fmt.Sprint("Random Xkcd #", responseObject.Num, " Title: ", responseObject.SafeTitle, " ", responseObject.Img)
common.Send(target, reply, tc) return reply, nil
} }

View file

@ -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
}

View file

@ -14,3 +14,4 @@ func CommandUsed() {
func GetCommandsUsed() int { func GetCommandsUsed() int {
return tempCommands return tempCommands
} }

View file

@ -49,7 +49,7 @@ func (c ChannelModel) Get(login string) (*Channel, error) {
} }
// Insert takes in a channel struct and inserts it into the database. // 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 := ` query := `
INSERT INTO channels(login, twitchid) INSERT INTO channels(login, twitchid)
VALUES ($1, $2) VALUES ($1, $2)
@ -58,7 +58,7 @@ func (c ChannelModel) Insert(channel *Channel) error {
RETURNING id, added_at; RETURNING id, added_at;
` `
args := []interface{}{channel.Login, channel.TwitchID} args := []interface{}{login, id}
// Execute the query returning the number of affected rows. // Execute the query returning the number of affected rows.
result, err := c.DB.Exec(query, args...) result, err := c.DB.Exec(query, args...)

View file

@ -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
}
}

View file

@ -18,7 +18,7 @@ var (
// as app.models.Channels.Get(login) // as app.models.Channels.Get(login)
type Models struct { type Models struct {
Channels interface { Channels interface {
Insert(channel *Channel) error Insert(login, id string) error
Get(login string) (*Channel, error) Get(login string) (*Channel, error)
GetAll() ([]*Channel, error) GetAll() ([]*Channel, error)
GetJoinable() ([]string, error) GetJoinable() ([]string, error)
@ -47,18 +47,32 @@ type Models struct {
} }
Timers interface { Timers interface {
Get(name string) (*Timer, error) Get(name string) (*Timer, error)
GetIdentifier(name string) (string, error)
Insert(timer *Timer) error Insert(timer *Timer) error
Update(timer *Timer) error Update(timer *Timer) error
GetAll() ([]*Timer, error) GetAll() ([]*Timer, error)
Delete(name string) 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 { func NewModels(db *sql.DB) Models {
return Models{ return Models{
Channels: ChannelModel{DB: db}, Channels: ChannelModel{DB: db},
Users: UserModel{DB: db}, Users: UserModel{DB: db},
Commands: CommandModel{DB: db}, Commands: CommandModel{DB: db},
Timers: TimerModel{DB: db}, Timers: TimerModel{DB: db},
Uploads: UploadModel{DB: db},
CommandsLogs: CommandsLogModel{DB: db},
SentMessagesLogs: SentMessagesLogModel{DB: db},
} }
} }

View file

@ -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
}
}

View file

@ -8,12 +8,12 @@ import (
) )
type Timer struct { type Timer struct {
ID int `json:"id" redis:"timer-id"` ID int `json:"id" redis:"timer-id"`
Name string `json:"name" redis:"timer-name"` Name string `json:"name" redis:"timer-name"`
CronName string `redis:"timer-cronname"` Identifier string `json:"identifier"`
Text string `json:"text" redis:"timer-text"` Text string `json:"text" redis:"timer-text"`
Channel string `json:"channel" redis:"timer-channel"` Channel string `json:"channel" redis:"timer-channel"`
Repeat string `json:"repeat" redis:"timer-repeat"` Repeat string `json:"repeat" redis:"timer-repeat"`
} }
type TimerModel struct { type TimerModel struct {
@ -22,7 +22,7 @@ type TimerModel struct {
func (t TimerModel) Get(name string) (*Timer, error) { func (t TimerModel) Get(name string) (*Timer, error) {
query := ` query := `
SELECT id, name, text, channel, repeat SELECT id, name, identifier, text, channel, repeat
FROM timers FROM timers
WHERE name = $1 WHERE name = $1
` `
@ -32,6 +32,7 @@ func (t TimerModel) Get(name string) (*Timer, error) {
err := t.DB.QueryRow(query, name).Scan( err := t.DB.QueryRow(query, name).Scan(
&timer.ID, &timer.ID,
&timer.Name, &timer.Name,
&timer.Identifier,
&timer.Text, &timer.Text,
&timer.Channel, &timer.Channel,
&timer.Repeat, &timer.Repeat,
@ -48,15 +49,44 @@ func (t TimerModel) Get(name string) (*Timer, error) {
return &timer, nil 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. // Insert adds a command into the database.
func (t TimerModel) Insert(timer *Timer) error { func (t TimerModel) Insert(timer *Timer) error {
query := ` query := `
INSERT into timers(name, text, channel, repeat) INSERT into timers(name, identifier, text, channel, repeat)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
RETURNING id; 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...) result, err := t.DB.Exec(query, args...)
if err != nil { 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. // GetAll() returns a pointer to a slice of all channels (`[]*Channel`) in the database.
func (t TimerModel) GetAll() ([]*Timer, error) { func (t TimerModel) GetAll() ([]*Timer, error) {
query := ` query := `
SELECT id, name, text, channel, repeat SELECT id, name, identifier, text, channel, repeat
FROM timers FROM timers
ORDER BY id` ORDER BY id`
@ -110,6 +140,7 @@ func (t TimerModel) GetAll() ([]*Timer, error) {
err := rows.Scan( err := rows.Scan(
&timer.ID, &timer.ID,
&timer.Name, &timer.Name,
&timer.Identifier,
&timer.Text, &timer.Text,
&timer.Channel, &timer.Channel,
&timer.Repeat, &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 // Delete takes in a command name and queries the database for an entry with
// the same name and tries to delete that entry. // 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. // Prepare the statement.
query := ` query := `
DELETE FROM timers DELETE FROM timers
WHERE name = $1` WHERE identifier = $1`
// Execute the query returning the number of affected rows. // 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 { if err != nil {
return err return err
} }

84
internal/data/uploads.go Normal file
View file

@ -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
}
}
}

View file

@ -23,14 +23,14 @@ type UserModel struct {
// Insert inserts a user model into the database. // Insert inserts a user model into the database.
func (u UserModel) Insert(login, twitchId string) error { func (u UserModel) Insert(login, twitchId string) error {
query := ` query := `
INSERT INTO users(login, twitchid) INSERT INTO users(login, twitchid, level, location, lastfm_username)
VALUES ($1, $2) VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (login) ON CONFLICT (login)
DO NOTHING DO NOTHING
RETURNING id, added_at; RETURNING id, added_at;
` `
args := []interface{}{login, twitchId} args := []interface{}{login, twitchId, "0", "", ""}
// Execute the query returning the number of affected rows. // Execute the query returning the number of affected rows.
result, err := u.DB.Exec(query, args...) 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. // Get searches the database for a login name and returns the user struct on success.
func (u UserModel) Get(login string) (*User, error) { func (u UserModel) Get(login string) (*User, error) {
query := ` query := `
SELECT id, added_at, login, twitchid, level, location SELECT id, added_at, login, twitchid, level, location, lastfm_username
FROM users FROM users
WHERE login = $1` WHERE login = $1`
@ -227,6 +227,7 @@ func (u UserModel) Get(login string) (*User, error) {
&user.TwitchID, &user.TwitchID,
&user.Level, &user.Level,
&user.Location, &user.Location,
&user.LastFMUsername,
) )
if err != nil { if err != nil {

View file

@ -3,7 +3,7 @@ package humanize
import ( import (
"time" "time"
"github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
) )
func Time(t time.Time) string { func Time(t time.Time) string {

27
internal/ivr/user.go Normal file
View file

@ -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
}

View file

@ -8,12 +8,9 @@ CREATE TABLE IF NOT EXISTS users (
lastfm_username text lastfm_username text
); );
INSERT INTO users (added_at,login,twitchid,"level") VALUES INSERT INTO users (added_at,login,twitchid,"level",location,lastfm_username) VALUES
(NOW(),'nourylul','31437432',1000), (NOW(),'nourylul','31437432',1000,'vilnius','nouryqt'),
(NOW(),'nourybot','596581605',1000), (NOW(),'nourybot','596581605',1000,'',''),
(NOW(),'uudelleenkytkeytynyt','465178364',1000), (NOW(),'uudelleenkytkeytynyt','465178364',1000,'',''),
(NOW(),'xnoury','197780373',500), (NOW(),'xnoury','197780373',500,'',''),
(NOW(),'noemience','135447564',500); (NOW(),'noemience','135447564',500,'','');
UPDATE users SET location = 'vilnius' WHERE login = 'nourylul';
UPDATE users SET lastfm_username = 'nouryqt' WHERE login = 'nourylul';

View file

@ -1,16 +1,16 @@
CREATE TABLE IF NOT EXISTS timers ( CREATE TABLE IF NOT EXISTS timers (
id bigserial PRIMARY KEY, id bigserial PRIMARY KEY,
name text NOT NULL, name text NOT NULL,
identifier text NOT NULL,
text text NOT NULL, text text NOT NULL,
channel text NOT NULL, channel text NOT NULL,
repeat text NOT NULL repeat text NOT NULL
); );
INSERT INTO timers (name,"text",channel,repeat) VALUES INSERT INTO timers (name,identifier,"text",channel,repeat) VALUES
('nourylul-60m','timer every 60 minutes :)','nourylul','60m'), ('nourylul-60m','678efbe2-fa2f-4849-8dbc-9ec32e6ffd3b','timer every 60 minutes :)','nourylul','60m'),
('nourybot-60m','timer every 60 minutes :)','nourybot','60m'), ('nourylul-2m','63142f10-1672-4353-8b03-e72f5a4dd566','timer every 2 minutes :)','nourylul','2m'),
('nourybot-1h','timer every 1 hour :)','nourybot','1h'), ('nourybot-60m','2ad01f96-05d3-444e-9dd6-524d397caa96','timer every 60 minutes :)','nourybot','60m'),
('xnoury-60m','timer every 420 minutes :)','xnoury','420m'), ('nourybot-1h','2353fd22-fef9-4cbd-b01e-bc8804992f4c', 'timer every 1 hour :)','nourybot','1h'),
('xnoury-1h','timer every 1 hour :)','xnoury','1h'), ('xnoury-15m','6e178e14-36c2-45e1-af59-b5dea4903fee','180 minutes timer :)','xnoury','180m');
('xnoury-15m','180 minutes timer :)','xnoury','180m');

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS uploads;

View file

@ -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');

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS commands_logs;

View file

@ -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');

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS sent_messages_logs;

View file

@ -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');