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:
push:
branches: [ master ]
branches: [ "master", "rewrite" ]
pull_request:
branches: [ master ]
branches: [ "master", "rewrite" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: 1.19
go-version: '1.20'
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

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
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +1,28 @@
BINARY_NAME=Nourybot.out
BINARY_NAME_API=NourybotApi.out
cup:
sudo docker compose up
xd:
cd cmd/bot && go build -o ${BINARY_NAME} && ./${BINARY_NAME} -env="dev"
cd cmd/nourybot && go build -o ${BINARY_NAME}
mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME}
./bin/${BINARY_NAME} -env="dev"
xdprod:
cd cmd/bot && go build -o ${BINARY_NAME} && ./${BINARY_NAME} -env="prod"
cd cmd/nourybot && go build -o ${BINARY_NAME}
mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME}
./bin/${BINARY_NAME} -env="prod"
jq:
cd cmd/bot && go build -o ${BINARY_NAME} && ./${BINARY_NAME} -env="dev" | jq
cd cmd/nourybot && go build -o ${BINARY_NAME}
mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME}
./bin/${BINARY_NAME} -env="dev" | jq
jqprod:
cd cmd/bot && go build -o ${BINARY_NAME} && ./${BINARY_NAME} -env="prod" | jq
cd cmd/nourybot && go build -o ${BINARY_NAME}
mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME}
./bin/${BINARY_NAME} -env="prod" | jq
jqapi:
go build -o ${BINARY_NAME_API} cmd/api && ./${BINARY_NAME} | jq
prod:
cd cmd/nourybot && go build -o ${BINARY_NAME}
mv cmd/nourybot/${BINARY_NAME} ./bin/${BINARY_NAME}
./bin/${BINARY_NAME} -env="prod"

View file

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

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

View file

@ -5,18 +5,18 @@ import (
"strconv"
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common"
"github.com/google/uuid"
"github.com/lyx0/nourybot/internal/data"
)
// AddCommand splits a message into two parts and passes on the
// name and text to the database handler.
func (app *Application) AddCommand(name string, message twitch.PrivateMessage) {
func (app *application) AddCommand(name string, message twitch.PrivateMessage) {
// snipLength is the length we need to "snip" off of the start of `message`.
// `()addcommand` = +12
// trailing space = +1
// zero-based = +1
// = 14
// `()add command` = +12
// trailing space = +1
// zero-based = +1
// = 15
snipLength := 14
// Split the twitch message at `snipLength` plus length of the name of the
@ -35,15 +35,16 @@ func (app *Application) AddCommand(name string, message twitch.PrivateMessage) {
Level: 0,
Help: "",
}
app.Log.Info(command)
err := app.Models.Commands.Insert(command)
if err != nil {
reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
} else {
reply := fmt.Sprintf("Successfully added command: %s", name)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
}
@ -54,31 +55,32 @@ func (app *Application) AddCommand(name string, message twitch.PrivateMessage) {
// If the Command.Level is not 0 it queries the database for the level of the
// user who sent the message. If the users level is equal or higher
// the command.Text field is returned.
func (app *Application) GetCommand(name, username string) (string, error) {
func (app *application) GetCommand(target, commandName string, userLevel int) (string, error) {
// Fetch the command from the database if it exists.
command, err := app.Models.Commands.Get(name)
command, err := app.Models.Commands.Get(commandName)
if err != nil {
// It probably did not exist
return "", err
}
// If the command has no level set just return the text.
// Otherwise check if the level is high enough.
if command.Level == 0 {
return command.Text, nil
} else {
// Get the user from the database to check if the userlevel is equal
// or higher than the command.Level.
user, err := app.Models.Users.Get(username)
if err != nil {
return "", err
}
if user.Level >= command.Level {
} else if userLevel >= command.Level {
if command.Category == "ascii" {
// Cannot use app.Send() here since the command is a ascii pasta and will be
// timed out, thus not passing the banphrase check app.Send() does before actually
// sending the message.
app.SendNoBanphrase(target, command.Text)
return "", nil
} else {
// Userlevel is sufficient so return the command.Text
return command.Text, nil
}
}
// If the command has no level set just return the text.
// Otherwise check if the level is high enough.
}
// Userlevel was not enough so return an empty string and error.
return "", ErrUserInsufficientLevel
}
@ -89,7 +91,7 @@ func (app *Application) GetCommand(name, username string) (string, error) {
// If the Command.Level is not 0 it queries the database for the level of the
// user who sent the message. If the users level is equal or higher
// the command.Text field is returned.
func (app *Application) GetCommandHelp(name, username string) (string, error) {
func (app *application) GetCommandHelp(name, username string) (string, error) {
// Fetch the command from the database if it exists.
command, err := app.Models.Commands.Get(name)
if err != nil {
@ -120,67 +122,54 @@ func (app *Application) GetCommandHelp(name, username string) (string, error) {
// EditCommandLevel takes in a name and level string and updates the entry with name
// to the supplied level value.
func (app *Application) EditCommandLevel(name, lvl string, message twitch.PrivateMessage) {
func (app *application) EditCommandLevel(name, lvl string, message twitch.PrivateMessage) {
level, err := strconv.Atoi(lvl)
if err != nil {
app.Logger.Error(err)
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrCommandLevelNotInteger), app.TwitchClient)
app.Log.Error(err)
app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrCommandLevelNotInteger), message)
return
}
err = app.Models.Commands.SetLevel(name, level)
if err != nil {
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient)
app.Logger.Error(err)
app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message)
app.Log.Error(err)
return
} else {
reply := fmt.Sprintf("Updated command %s to level %v", name, level)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
}
// EditCommandCategory takes in a name and category string and updates the command
// in the databse with the passed in new category.
func (app *Application) EditCommandCategory(name, category string, message twitch.PrivateMessage) {
func (app *application) EditCommandCategory(name, category string, message twitch.PrivateMessage) {
err := app.Models.Commands.SetCategory(name, category)
if err != nil {
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient)
app.Logger.Error(err)
app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message)
app.Log.Error(err)
return
} else {
reply := fmt.Sprintf("Updated command %s to category %v", name, category)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
}
// DebugCommand checks if a command with the provided name exists in the database
// and outputs information about it in the chat.
func (app *Application) DebugCommand(name string, message twitch.PrivateMessage) {
func (app *application) DebugCommand(name string, message twitch.PrivateMessage) {
// Query the database for a command with the provided name
cmd, err := app.Models.Commands.Get(name)
if err != nil {
reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err)
common.Send(message.Channel, reply, app.TwitchClient)
return
} else if cmd.Category == "ascii" {
// If the command is in the ascii category don't post the Text field
// otherwise it becomes too spammy and won't fit in the max message length.
reply := fmt.Sprintf("ID %v: Name %v, Level: %v, Category: %v Help: %v",
cmd.ID,
cmd.Name,
cmd.Level,
cmd.Category,
cmd.Help,
)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
} else {
reply := fmt.Sprintf("ID %v: Name %v, Level: %v, Category: %v, Text: %v, Help: %v",
reply := fmt.Sprintf("id=%v\nname=%v\nlevel=%v\ncategory=%v\ntext=%v\nhelp=%v\n",
cmd.ID,
cmd.Name,
cmd.Level,
@ -189,16 +178,24 @@ func (app *Application) DebugCommand(name string, message twitch.PrivateMessage)
cmd.Help,
)
common.Send(message.Channel, reply, app.TwitchClient)
//app.Send(message.Channel, reply)
resp, err := app.uploadPaste(reply)
if err != nil {
app.Log.Errorln("Could not upload paste:", err)
app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %v", ErrDuringPasteUpload), message)
return
}
app.Send(message.Channel, resp, message)
//app.SendEmail(fmt.Sprintf("DEBUG for command %s", name), reply)
return
}
}
// SetCommandHelp updates the `help` column of a given commands name in the
// database to the provided new help text.
func (app *Application) EditCommandHelp(name string, message twitch.PrivateMessage) {
func (app *application) EditCommandHelp(name string, message twitch.PrivateMessage) {
// snipLength is the length we need to "snip" off of the start of `message`.
// `()editcommand` = +13
// `()edit command` = +13
// trailing space = +1
// zero-based = +1
// `help` = +4
@ -216,25 +213,36 @@ func (app *Application) EditCommandHelp(name string, message twitch.PrivateMessa
err := app.Models.Commands.SetHelp(name, text)
if err != nil {
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient)
app.Logger.Error(err)
app.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), message)
app.Log.Error(err)
return
} else {
reply := fmt.Sprintf("Updated help text for command %s to: %v", name, text)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
}
// DeleteCommand takes in a name value and deletes the command from the database if it exists.
func (app *Application) DeleteCommand(name string, message twitch.PrivateMessage) {
func (app *application) DeleteCommand(name string, message twitch.PrivateMessage) {
err := app.Models.Commands.Delete(name)
if err != nil {
common.Send(message.Channel, "Something went wrong FeelsBadMan", app.TwitchClient)
app.Logger.Error(err)
app.Send(message.Channel, "Something went wrong FeelsBadMan", message)
app.Log.Error(err)
return
}
reply := fmt.Sprintf("Deleted command %s", name)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
}
func (app *application) LogCommand(msg twitch.PrivateMessage, commandName string, userLevel int) {
twitchLogin := msg.User.Name
twitchID := msg.User.ID
twitchMessage := msg.Message
twitchChannel := msg.Channel
identifier := uuid.NewString()
rawMsg := msg.Raw
go app.Models.CommandsLogs.Insert(twitchLogin, twitchID, twitchChannel, twitchMessage, commandName, userLevel, identifier, rawMsg)
}

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")
ErrRecordNotFound = errors.New("user not found in the database")
ErrUserInsufficientLevel = errors.New("user has insufficient level")
ErrDuringPasteUpload = errors.New("could not upload paste")
)

View file

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

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"
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common"
"github.com/google/uuid"
"github.com/lyx0/nourybot/internal/data"
"github.com/redis/go-redis/v9"
)
// AddTimer slices the message into relevant parts, adding the values onto a
// new data.Timer struct so that the timer can be inserted into the database.
func (app *Application) AddTimer(name string, message twitch.PrivateMessage) {
func (app *application) AddTimer(name, repeat string, message twitch.PrivateMessage) {
cmdParams := strings.SplitN(message.Message, " ", 500)
// snipLength is the length of `()addcommand` plus +2 (for the space and zero based)
snipLength := 12
repeat := cmdParams[2]
// prefixLength is the length of `()add timer` plus +2 (for the space and zero based)
prefix := "()add timer"
prefixLength := len("()add timer")
nameLength := len(name)
repeatLength := len(repeat)
app.Log.Infow("Lengths",
"prefix", prefixLength,
"name", nameLength,
"repeat", repeatLength,
"repeat2", len(cmdParams[2]),
"sum", prefixLength+nameLength+repeatLength,
)
// Split the message into the parts we need.
//
// message: ()addtimer sponsor 20m hecking love my madmonq pills BatChest
// message: ()timer add sponsor 20m hecking love my madmonq pills BatChest
// parts: | prefix | |name | |repeat | <----------- text -------------> |
text := message.Message[snipLength+len(name)+len(cmdParams[2]) : len(message.Message)]
text := message.Message[3+len(prefix)+len(name)+len(repeat) : len(message.Message)]
// validateTimeFormat will be true if the repeat parameter is in
// the format of either 30m, 10h, or 10h30m.
validateTimeFormat, err := regexp.MatchString(`^(\d{1,2}[h])$|^(\d+[m])$|^(\d+[s])$|((\d{1,2}[h])((([0]?|[1-5]{1})[0-9])[m]))$`, repeat)
if err != nil {
app.Logger.Errorw("Received malformed time format in timer",
app.Log.Errorw("Received malformed time format in timer",
"repeat", repeat,
"error", err,
)
return
}
id := uuid.NewString()
timer := &data.Timer{
Name: name,
Text: text,
Channel: message.Channel,
Repeat: repeat,
Name: name,
Text: text,
Identifier: id,
Channel: message.Channel,
Repeat: repeat,
}
// Check if the time string we got is valid, this is important
@ -48,123 +57,123 @@ func (app *Application) AddTimer(name string, message twitch.PrivateMessage) {
// time format string is supplied.
if validateTimeFormat {
timer := &data.Timer{
Name: name,
Text: text,
Channel: message.Channel,
Repeat: repeat,
Name: name,
Text: text,
Identifier: id,
Channel: message.Channel,
Repeat: repeat,
}
err = app.Models.Timers.Insert(timer)
if err != nil {
app.Logger.Errorw("Error inserting new timer into database",
app.Log.Errorw("Error inserting new timer into database",
"timer", timer,
"error", err,
)
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
} else {
// cronName is the internal, unique tag/name for the timer.
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor"
cronName := fmt.Sprintf("%s-%s", message.Channel, name)
app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, cronName)
app.Logger.Infow("Added new timer",
app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, id)
app.Log.Infow("Added new timer",
"timer", timer,
)
if _, err := app.Rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error {
rdb.HSet(ctx, cronName, "timer-name", name)
rdb.HSet(ctx, cronName, "timer-cronname", cronName)
rdb.HSet(ctx, cronName, "timer-text", text)
rdb.HSet(ctx, cronName, "timer-channel", message.Channel)
rdb.HSet(ctx, cronName, "timer-repeat", repeat)
return nil
}); err != nil {
app.Logger.Panic(err)
}
app.Logger.Infow("Loaded timer into redis:",
"key", cronName,
"value", app.Rdb.HGetAll(ctx, cronName),
)
reply := fmt.Sprintf("Successfully added timer %s repeating every %s", name, repeat)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
} else {
app.Logger.Errorw("Received malformed time format in timer",
app.Log.Errorw("Received malformed time format in timer",
"timer", timer,
"error", err,
)
reply := "Something went wrong FeelsBadMan received wrong time format. Allowed formats: 30m, 10h, 10h30m"
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
}
// EditTimer just contains the logic for deleting a timer, and then adding a new one
// with the same name. It is technically not editing the timer.
func (app *Application) EditTimer(name string, message twitch.PrivateMessage) {
func (app *application) EditTimer(name, repeat string, message twitch.PrivateMessage) {
// Check if a timer with that name is in the database.
app.Log.Info(name)
old, err := app.Models.Timers.Get(name)
if err != nil {
app.Logger.Errorw("Could not get timer",
app.Log.Errorw("Could not get timer",
"timer", old,
"error", err,
)
reply := "Something went wrong FeelsBadMan"
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
// -----------------------
// Delete the old timer
cronName := fmt.Sprintf("%s-%s", message.Channel, name)
app.Scheduler.RemoveJob(cronName)
// -----------------------
identifier := old.Identifier
app.Scheduler.RemoveJob(identifier)
_ = app.Rdb.Del(ctx, cronName)
err = app.Models.Timers.Delete(name)
if err != nil {
app.Logger.Errorw("Error deleting timer from database",
app.Log.Errorw("Error deleting timer from database",
"name", name,
"cronName", cronName,
"identifier", identifier,
"error", err,
)
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
// -----------------------
// Add the new timer
cmdParams := strings.SplitN(message.Message, " ", 500)
// snipLength is the length of `()editcommand` plus +2 (for the space and zero based)
snipLength := 13
repeat := cmdParams[2]
// -----------------------
//cmdParams := strings.SplitN(message.Message, " ", 500)
// prefixLength is the length of `()editcommand` plus +2 (for the space and zero based)
prefix := "()edit timer"
prefixLength := len("()add timer")
nameLength := len(name)
repeatLength := len(repeat)
app.Log.Infow("Lengths",
"prefix", prefixLength,
"name", nameLength,
"repeat", repeatLength,
"sum", prefixLength+nameLength+repeatLength,
)
// Split the message into the parts we need.
//
// message: ()addtimer sponsor 20m hecking love my madmonq pills BatChest
// parts: | prefix | |name | |repeat | <----------- text -------------> |
text := message.Message[snipLength+len(name)+len(cmdParams[2]) : len(message.Message)]
text := message.Message[3+len(prefix)+len(name)+len(repeat) : len(message.Message)]
// validateTimeFormat will be true if the repeat parameter is in
// the format of either 30m, 10h, or 10h30m.
validateTimeFormat, err := regexp.MatchString(`^(\d{1,2}[h])$|^(\d+[m])$|^(\d+[s])$|((\d{1,2}[h])((([0]?|[1-5]{1})[0-9])[m]))$`, repeat)
if err != nil {
app.Logger.Errorw("Received malformed time format in timer",
app.Log.Errorw("Received malformed time format in timer",
"repeat", repeat,
"error", err,
)
return
}
id := uuid.NewString()
timer := &data.Timer{
Name: name,
Text: text,
Channel: message.Channel,
Repeat: repeat,
Name: name,
Text: text,
Identifier: id,
Channel: message.Channel,
Repeat: repeat,
}
// Check if the time string we got is valid, this is important
@ -172,71 +181,104 @@ func (app *Application) EditTimer(name string, message twitch.PrivateMessage) {
// time format string is supplied.
if validateTimeFormat {
timer := &data.Timer{
Name: name,
Text: text,
Channel: message.Channel,
Repeat: repeat,
Name: name,
Text: text,
Identifier: id,
Channel: message.Channel,
Repeat: repeat,
}
err = app.Models.Timers.Insert(timer)
if err != nil {
app.Logger.Errorw("Error inserting new timer into database",
app.Log.Errorw("Error inserting new timer into database",
"timer", timer,
"error", err,
)
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
} else { // this is a bit scuffed. The else here is the end of a successful call.
// cronName is the internal, unique tag/name for the timer.
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor"
cronName := fmt.Sprintf("%s-%s", message.Channel, name)
app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, id)
app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, cronName)
app.Logger.Infow("Updated a timer",
app.Log.Infow("Updated a timer",
"Name", name,
"Channel", message.Channel,
"Old timer", old,
"New timer", timer,
)
if _, err := app.Rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error {
rdb.HSet(ctx, cronName, "timer-name", name)
rdb.HSet(ctx, cronName, "timer-cronname", cronName)
rdb.HSet(ctx, cronName, "timer-text", text)
rdb.HSet(ctx, cronName, "timer-channel", message.Channel)
rdb.HSet(ctx, cronName, "timer-repeat", repeat)
return nil
}); err != nil {
app.Logger.Panic(err)
}
app.Logger.Infow("Loaded timer into redis:",
"key", cronName,
"value", app.Rdb.HGetAll(ctx, cronName),
)
reply := fmt.Sprintf("Successfully updated timer %s", name)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
} else {
app.Logger.Errorw("Received malformed time format in timer",
app.Log.Errorw("Received malformed time format in timer",
"timer", timer,
"error", err,
)
reply := "Something went wrong FeelsBadMan received wrong time format. Allowed formats: 30s, 30m, 10h, 10h30m"
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
}
// InitialTimers is called on startup and queries the database for a list of
// timers and then adds each onto the scheduler.
func (app *Application) InitialTimers() {
func (app *application) ListTimers() string {
timer, err := app.Models.Timers.GetAll()
if err != nil {
app.Logger.Errorw("Error trying to retrieve all timers from database", err)
app.Log.Errorw("Error trying to retrieve all timers from database", err)
return ""
}
// The slice of timers is only used to log them at
// the start so it looks a bit nicer.
var ts []string
// Iterate over all timers and then add them onto the scheduler.
for i, v := range timer {
// idk why this works but it does so no touchy touchy.
// https://github.com/robfig/cron/issues/420#issuecomment-940949195
i, v := i, v
_ = i
// cronName is the internal, unique tag/name for the timer.
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor"
t := fmt.Sprintf(
"ID: \t\t%v\n"+
"Name: \t\t%v\n"+
"Identifier: \t%v\n"+
"Text: \t\t%v\n"+
"Channel: \t%v\n"+
"Repeat: \t%v\n"+
"\n\n",
v.ID, v.Name, v.Identifier, v.Text, v.Channel, v.Repeat,
)
// Add new value to the slice
ts = append(ts, t)
//app.Scheduler.AddFunc(repeating, func() { app.newPrivateMessageTimer(v.Channel, v.Text) }, cronName)
}
reply, err := app.uploadPaste(strings.Join(ts, ""))
if err != nil {
app.Log.Errorw("Error trying to retrieve all timers from database", err)
return ""
}
return reply
}
// InitialTimers is called on startup and queries the database for a list of
// timers and then adds each onto the scheduler.
func (app *application) InitialTimers() {
timer, err := app.Models.Timers.GetAll()
if err != nil {
app.Log.Errorw("Error trying to retrieve all timers from database", err)
return
}
@ -253,73 +295,61 @@ func (app *Application) InitialTimers() {
// cronName is the internal, unique tag/name for the timer.
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor"
cronName := fmt.Sprintf("%s-%s", v.Channel, v.Name)
// Repeating is at what times the timer should repeat.
// 2 minute timer is @every 2m
repeating := fmt.Sprintf("@every %s", v.Repeat)
if _, err := app.Rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error {
rdb.HSet(ctx, cronName, "timer-id", v.ID)
rdb.HSet(ctx, cronName, "timer-name", v.Name)
rdb.HSet(ctx, cronName, "timer-text", v.Text)
rdb.HSet(ctx, cronName, "timer-channel", v.Channel)
rdb.HSet(ctx, cronName, "timer-repeat", v.Repeat)
return nil
}); err != nil {
app.Logger.Panic(err)
}
app.Logger.Infow("Loaded timer into redis:",
"key", cronName,
"value", app.Rdb.HGetAll(ctx, v.Channel),
)
app.Scheduler.AddFunc(repeating, func() { app.newPrivateMessageTimer(v.Channel, v.Text) }, cronName)
// Add new value to the slice
ts = append(ts, v)
app.Scheduler.AddFunc(repeating, func() { app.newPrivateMessageTimer(v.Channel, v.Text) }, v.Identifier)
}
// var model1 rdbVal
// if err := app.Rdb.HGetAll(ctx, cronName).Scan(&model1); err != nil {
// app.Logger.Panic(err)
// }
app.Logger.Infow("Initial timers",
app.Log.Infow("Initial timers",
"timer", ts,
)
}
// newPrivateMessageTimer is a helper function to set timers
// which trigger into sending a twitch PrivateMessage.
func (app *Application) newPrivateMessageTimer(channel, text string) {
common.Send(channel, text, app.TwitchClient)
func (app *application) newPrivateMessageTimer(channel, text string) {
app.SendNoContext(channel, text)
}
// DeleteTimer takes in the name of a timer and tries to delete the timer from the database.
func (app *Application) DeleteTimer(name string, message twitch.PrivateMessage) {
cronName := fmt.Sprintf("%s-%s", message.Channel, name)
app.Scheduler.RemoveJob(cronName)
func (app *application) DeleteTimer(name string, message twitch.PrivateMessage) {
app.Logger.Infow("Deleting timer",
identifier, err := app.Models.Timers.GetIdentifier(name)
if err != nil {
app.Log.Errorw("Error retrieving identifier rom database",
"name", name,
"identifier", identifier,
"error", err,
)
}
app.Scheduler.RemoveJob(identifier)
app.Log.Infow("Deleting timer",
"name", name,
"identifier", identifier,
"message.Channel", message.Channel,
"cronName", cronName,
)
_ = app.Rdb.Del(ctx, cronName)
err := app.Models.Timers.Delete(name)
err = app.Models.Timers.Delete(identifier)
if err != nil {
app.Logger.Errorw("Error deleting timer from database",
app.Log.Errorw("Error deleting timer from database",
"name", name,
"cronName", cronName,
"identifier", identifier,
"error", err,
)
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
return
}
reply := fmt.Sprintf("Deleted timer with name %s", name)
common.Send(message.Channel, reply, app.TwitchClient)
app.Send(message.Channel, reply, message)
}

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/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5 h1:kCvm3G3u+eTRbjfLPyfsfznJtraYEfZer/UvQ6CaQhI=
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5/go.mod h1:6DM2KNNK69jRu0lAHmYK9LYxmqpNjYHOaNp/ZxttD4U=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M=
github.com/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/nicklaw5/helix/v2 v2.25.1 h1:hccFfWf1kdPKeC/Zp8jNbOvqV0f6ya12hdeNHuQa5wg=
github.com/nicklaw5/helix/v2 v2.25.1/go.mod h1:zZcKsyyBWDli34x3QleYsVMiiNGMXPAEU5NjsiZDtvY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 h1:cgqwZtnR+IQfUYDLJ3Kiy4aE+O/wExTzEIg8xwC4Qfs=
github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0/go.mod h1:n3nudMl178cEvD44PaopxH9jhJaQzthSxUzLO5iKMy4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/wader/goutubedl v0.0.0-20230924165737-427b7fa536e6 h1:KHJV3fnnKsdWdGu5IKrDAA0Oa5RzGwrJpfx+bvVAjLA=
github.com/wader/goutubedl v0.0.0-20230924165737-427b7fa536e6/go.mod h1:5KXd5tImdbmz4JoVhePtbIokCwAfEhUVVx3WLHmjYuw=
github.com/wader/osleaktest v0.0.0-20191111175233-f643b0fed071 h1:QkrG4Zr5OVFuC9aaMPmFI0ibfhBZlAgtzDYWfu7tqQk=
github.com/wader/osleaktest v0.0.0-20191111175233-f643b0fed071/go.mod h1:XD6emOFPHVzb0+qQpiNOdPL2XZ0SRUM0N5JHuq6OmXo=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

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 (
"fmt"
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common"
)
func Bttv(target, query string, tc *twitch.Client) {
func Bttv(query string) string {
reply := fmt.Sprintf("https://betterttv.com/emotes/shared/search?query=%s", query)
common.Send(target, reply, tc)
return reply
}

View file

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

View file

@ -1,21 +1,30 @@
package commands
import (
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/commands/decapi"
"github.com/lyx0/nourybot/internal/common"
"go.uber.org/zap"
"fmt"
"io"
"net/http"
)
// ()currency 10 USD to EUR
func Currency(target, currAmount, currFrom, currTo string, tc *twitch.Client) {
sugar := zap.NewExample().Sugar()
defer sugar.Sync()
func Currency(currAmount, currFrom, currTo string) (string, error) {
basePath := "https://decapi.me/misc/currency/"
from := fmt.Sprintf("?from=%s", currFrom)
to := fmt.Sprintf("&to=%s", currTo)
value := fmt.Sprintf("&value=%s", currAmount)
resp, err := decapi.Currency(currAmount, currFrom, currTo)
// https://decapi.me/misc/currency/?from=usd&to=usd&value=10
resp, err := http.Get(fmt.Sprint(basePath + from + to + value))
if err != nil {
sugar.Error(err)
return "", ErrInternalServerError
}
common.Send(target, resp, tc)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", ErrInternalServerError
}
reply := string(body)
return reply, nil
}

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
import (
"fmt"
import "fmt"
"github.com/gempir/go-twitch-irc/v4"
"github.com/lyx0/nourybot/internal/common"
)
func Ffz(target, query string, tc *twitch.Client) {
reply := fmt.Sprintf("https://www.frankerfacez.com/emoticons/?q=%s", query)
common.Send(target, reply, tc)
return reply
}

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"
"os"
"github.com/gempir/go-twitch-irc/v4"
"github.com/joho/godotenv"
"github.com/lyx0/nourybot/internal/common"
"github.com/shkh/lastfm-go/lastfm"
"go.uber.org/zap"
)
func LastFmArtistTop(target string, message twitch.PrivateMessage, tc *twitch.Client) {
sugar := zap.NewExample().Sugar()
defer sugar.Sync()
// snipLength is the length we need to "snip" off of the start
// of `message` to only have the artists name left.
// `()lastfm artist top` = +20
// trailing space = +1
// zero-based = +1
// = 22
snipLength := 20
artist := message.Message[snipLength:len(message.Message)]
err := godotenv.Load()
if err != nil {
sugar.Error("Error loading OpenWeatherMap API key from .env file")
}
apiKey := os.Getenv("LAST_FM_API_KEY")
apiSecret := os.Getenv("LAST_FM_SECRET")
api := lastfm.New(apiKey, apiSecret)
result, _ := api.Artist.GetTopTracks(lastfm.P{"artist": artist}) //discarding error
for _, track := range result.Tracks {
sugar.Infow("Top tracks: ",
"artist:", artist,
"track", track.Name,
)
}
}
func LastFmUserRecent(target, user string, tc *twitch.Client) {
func LastFmUserRecent(target, user string) string {
sugar := zap.NewExample().Sugar()
defer sugar.Sync()
@ -68,9 +36,8 @@ func LastFmUserRecent(target, user string, tc *twitch.Client) {
)
reply = fmt.Sprintf("Most recently played track for user %v: %v - %v", user, track.Artist.Name, track.Name)
common.Send(target, reply, tc)
return
}
}
return reply
}

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

View file

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

View file

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

View file

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

View file

@ -5,35 +5,31 @@ import (
"os"
owm "github.com/briandowns/openweathermap"
"github.com/gempir/go-twitch-irc/v4"
"github.com/joho/godotenv"
"github.com/lyx0/nourybot/internal/common"
"go.uber.org/zap"
)
// Weather queries the OpenWeatherMap Api for the given location and sends the
// current weather response to the target twitch chat.
func Weather(target, location string, tc *twitch.Client) {
sugar := zap.NewExample().Sugar()
defer sugar.Sync()
func Weather(location string) (string, error) {
err := godotenv.Load()
if err != nil {
sugar.Error("Error loading OpenWeatherMap API key from .env file")
return "", ErrInternalServerError
}
owmKey := os.Getenv("OWM_KEY")
w, err := owm.NewCurrent("C", "en", owmKey)
if err != nil {
sugar.Error(err)
return "", ErrInternalServerError
}
if err := w.CurrentByName(location); err != nil {
return "", ErrInternalServerError
}
w.CurrentByName(location)
// Longitude and Latitude are returned as 0 when the supplied location couldn't be
// assigned to a OpenWeatherMap location.
if w.GeoPos.Longitude == 0 && w.GeoPos.Latitude == 0 {
reply := "Location not found FeelsBadMan"
common.Send(target, reply, tc)
return "", ErrWeatherLocationNotFound
} else {
// Weather for Vilnius, LT: Feels like: 29.67°C. Currently 29.49°C with a high of 29.84°C and a low of 29.49°C, humidity: 45%, wind: 6.17m/s.
reply := fmt.Sprintf("Weather for %s, %s: Feels like: %v°C. Currently %v°C with a high of %v°C and a low of %v°C, humidity: %v%%, wind: %vm/s.",
@ -46,6 +42,6 @@ func Weather(target, location string, tc *twitch.Client) {
w.Main.Humidity,
w.Wind.Speed,
)
common.Send(target, reply, tc)
return reply, nil
}
}

View file

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

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

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)
type Models struct {
Channels interface {
Insert(channel *Channel) error
Insert(login, id string) error
Get(login string) (*Channel, error)
GetAll() ([]*Channel, error)
GetJoinable() ([]string, error)
@ -47,18 +47,32 @@ type Models struct {
}
Timers interface {
Get(name string) (*Timer, error)
GetIdentifier(name string) (string, error)
Insert(timer *Timer) error
Update(timer *Timer) error
GetAll() ([]*Timer, error)
Delete(name string) error
}
Uploads interface {
Insert(twitchLogin, twitchID, twitchMessage, twitchChannel, filehoster, downloadURL, identifier string)
UpdateUploadURL(identifier, uploadURL string)
}
CommandsLogs interface {
Insert(twitchLogin, twitchId, twitchChannel, twitchMessage, commandName string, uLvl int, identifier, rawMsg string)
}
SentMessagesLogs interface {
Insert(twitchChannel, twitchMessage, ctxCommandName, ctxUser, ctxUserID, ctxMsg, identifier, ctxRaw string)
}
}
func NewModels(db *sql.DB) Models {
return Models{
Channels: ChannelModel{DB: db},
Users: UserModel{DB: db},
Commands: CommandModel{DB: db},
Timers: TimerModel{DB: db},
Channels: ChannelModel{DB: db},
Users: UserModel{DB: db},
Commands: CommandModel{DB: db},
Timers: TimerModel{DB: db},
Uploads: UploadModel{DB: db},
CommandsLogs: CommandsLogModel{DB: db},
SentMessagesLogs: SentMessagesLogModel{DB: db},
}
}

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

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

View file

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

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
);
INSERT INTO users (added_at,login,twitchid,"level") VALUES
(NOW(),'nourylul','31437432',1000),
(NOW(),'nourybot','596581605',1000),
(NOW(),'uudelleenkytkeytynyt','465178364',1000),
(NOW(),'xnoury','197780373',500),
(NOW(),'noemience','135447564',500);
UPDATE users SET location = 'vilnius' WHERE login = 'nourylul';
UPDATE users SET lastfm_username = 'nouryqt' WHERE login = 'nourylul';
INSERT INTO users (added_at,login,twitchid,"level",location,lastfm_username) VALUES
(NOW(),'nourylul','31437432',1000,'vilnius','nouryqt'),
(NOW(),'nourybot','596581605',1000,'',''),
(NOW(),'uudelleenkytkeytynyt','465178364',1000,'',''),
(NOW(),'xnoury','197780373',500,'',''),
(NOW(),'noemience','135447564',500,'','');

View file

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

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