mirror of
https://github.com/lyx0/nourybot.git
synced 2024-11-13 19:49:55 +01:00
start rewrite
This commit is contained in:
parent
0d313b7b0a
commit
b88b4345f5
10
Dockerfile
10
Dockerfile
|
@ -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
30
LICENSE
|
@ -1,15 +1,21 @@
|
|||
ISC License
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023, lyx0
|
||||
Copyright (c) 2023 lyx0
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
13
Makefile
13
Makefile
|
@ -1,21 +1,16 @@
|
|||
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} && ./${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} && ./${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} && ./${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} && ./${BINARY_NAME} -env="prod" | jq
|
||||
|
||||
jqapi:
|
||||
go build -o ${BINARY_NAME_API} cmd/api && ./${BINARY_NAME} | jq
|
||||
|
||||
|
|
|
@ -1,7 +1,2 @@
|
|||
# nourybot
|
||||
|
||||
Near future abandoned project in development.
|
||||
|
||||
### Make:
|
||||
Development:
|
||||
make jq
|
||||
Lidl Twitch bot
|
||||
|
|
|
@ -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{}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
110
cmd/api/main.go
110
cmd/api/main.go
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -1,86 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/commands/decapi"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"github.com/lyx0/nourybot/internal/data"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
reply := fmt.Sprintf("Something went wrong FeelsBadMan %s", err)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
app.TwitchClient.Join(login)
|
||||
reply := fmt.Sprintf("Added channel %s", login)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllChannels() queries the database and lists all channels.
|
||||
// Only used for debug/information purposes.
|
||||
func (app *Application) GetAllChannels() {
|
||||
channel, err := app.Models.Channels.GetAll()
|
||||
if err != nil {
|
||||
app.Logger.Error(err)
|
||||
return
|
||||
}
|
||||
app.Logger.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) {
|
||||
err := app.Models.Channels.Delete(login)
|
||||
if err != nil {
|
||||
common.Send(message.Channel, "Something went wrong FeelsBadMan", app.TwitchClient)
|
||||
app.Logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
app.TwitchClient.Depart(login)
|
||||
|
||||
reply := fmt.Sprintf("Deleted channel %s", login)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
}
|
||||
|
||||
// InitialJoin is called on startup and queries the database for a list of
|
||||
// channels which the TwitchClient then joins.
|
||||
func (app *Application) InitialJoin() {
|
||||
// GetJoinable returns a slice of channel names.
|
||||
channel, err := app.Models.Channels.GetJoinable()
|
||||
if err != nil {
|
||||
app.Logger.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",
|
||||
"channel", v)
|
||||
}
|
||||
}
|
|
@ -1,240 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"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) {
|
||||
// snipLength is the length we need to "snip" off of the start of `message`.
|
||||
// `()addcommand` = +12
|
||||
// trailing space = +1
|
||||
// zero-based = +1
|
||||
// = 14
|
||||
snipLength := 14
|
||||
|
||||
// Split the twitch message at `snipLength` plus length of the name of the
|
||||
// command that we want to add.
|
||||
// The part of the message we are left over with is then passed on to the database
|
||||
// handlers as the `text` part of the command.
|
||||
//
|
||||
// e.g. `()addcommand CoolSponsors Check out CoolSponsor.com they are the coolest sponsors!
|
||||
// | <- snipLength + name -> | <--- command text with however many characters ---> |
|
||||
// | <----- 14 + 12 ------> |
|
||||
text := message.Message[snipLength+len(name) : len(message.Message)]
|
||||
command := &data.Command{
|
||||
Name: name,
|
||||
Text: text,
|
||||
Category: "uncategorized",
|
||||
Level: 0,
|
||||
Help: "",
|
||||
}
|
||||
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)
|
||||
return
|
||||
} else {
|
||||
reply := fmt.Sprintf("Successfully added command: %s", name)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetCommand queries the database for a name. If an entry exists it checks
|
||||
// if the Command.Level is 0, if it is the command.Text value is returned.
|
||||
//
|
||||
// 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) {
|
||||
// Fetch the command from the database if it exists.
|
||||
command, err := app.Models.Commands.Get(name)
|
||||
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 {
|
||||
// Userlevel is sufficient so return the command.Text
|
||||
return command.Text, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Userlevel was not enough so return an empty string and error.
|
||||
return "", ErrUserInsufficientLevel
|
||||
}
|
||||
|
||||
// GetCommand queries the database for a name. If an entry exists it checks
|
||||
// if the Command.Level is 0, if it is the command.Text value is returned.
|
||||
//
|
||||
// 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) {
|
||||
// Fetch the command from the database if it exists.
|
||||
command, err := app.Models.Commands.Get(name)
|
||||
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.Help, 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 {
|
||||
// Userlevel is sufficient so return the command.Text
|
||||
return command.Help, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Userlevel was not enough so return an empty string and error.
|
||||
return "", ErrUserInsufficientLevel
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
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)
|
||||
return
|
||||
} else {
|
||||
reply := fmt.Sprintf("Updated command %s to level %v", name, level)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
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) {
|
||||
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)
|
||||
return
|
||||
} else {
|
||||
reply := fmt.Sprintf("Updated command %s to category %v", name, category)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
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) {
|
||||
// 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)
|
||||
return
|
||||
} else {
|
||||
reply := fmt.Sprintf("ID %v: Name %v, Level: %v, Category: %v, Text: %v, Help: %v",
|
||||
cmd.ID,
|
||||
cmd.Name,
|
||||
cmd.Level,
|
||||
cmd.Category,
|
||||
cmd.Text,
|
||||
cmd.Help,
|
||||
)
|
||||
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
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) {
|
||||
// snipLength is the length we need to "snip" off of the start of `message`.
|
||||
// `()editcommand` = +13
|
||||
// trailing space = +1
|
||||
// zero-based = +1
|
||||
// `help` = +4
|
||||
// = 19
|
||||
snipLength := 19
|
||||
|
||||
// Split the twitch message at `snipLength` plus length of the name of the
|
||||
// command that we want to set the help text for so that we get the
|
||||
// actual help message left over and then assign this string to `text`.
|
||||
//
|
||||
// e.g. `()editcommand help FeelsDankMan Returns a FeelsDankMan ascii art. Requires user level 500.`
|
||||
// | <---- snipLength + name ----> | <------ help text with however many characters. ----> |
|
||||
// | <--------- 19 + 12 --------> |
|
||||
text := message.Message[snipLength+len(name) : len(message.Message)]
|
||||
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)
|
||||
return
|
||||
} else {
|
||||
reply := fmt.Sprintf("Updated help text for command %s to: %v", name, text)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
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) {
|
||||
err := app.Models.Commands.Delete(name)
|
||||
if err != nil {
|
||||
common.Send(message.Channel, "Something went wrong FeelsBadMan", app.TwitchClient)
|
||||
app.Logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
reply := fmt.Sprintf("Deleted command %s", name)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
}
|
|
@ -1,561 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/commands"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
|
||||
// Increments the counter how many commands have been used, called in the ping command.
|
||||
common.CommandUsed()
|
||||
|
||||
// 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))
|
||||
|
||||
// target is the channelname the message originated from and
|
||||
// where the TwitchClient should send the response
|
||||
target := message.Channel
|
||||
|
||||
// Userlevel is the level set for a user in the database.
|
||||
// It is NOT same as twitch user/mod.
|
||||
// 1000 = admin
|
||||
// 500 = mod
|
||||
// 250 = vip
|
||||
// 100 = normal
|
||||
// If the level returned is -1 then the user was not found in the database.
|
||||
userLevel := app.GetUserLevel(message.User.ID)
|
||||
|
||||
app.Logger.Infow("Command received",
|
||||
// "message", message, // Pretty taxing
|
||||
"message.Message", message.Message,
|
||||
"message.Channel", target,
|
||||
"userLevel", userLevel,
|
||||
"commandName", commandName,
|
||||
"cmdParams", cmdParams,
|
||||
"msgLen", msgLen,
|
||||
)
|
||||
|
||||
// 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 {
|
||||
common.Send(target, "xd", app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
case "nourybot":
|
||||
common.Send(target, "Lidl Twitch bot made by @nourylul. Prefix: ()", app.TwitchClient)
|
||||
return
|
||||
|
||||
// ()bttv <emote name>
|
||||
case "bttv":
|
||||
if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()bttv <emote name>", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Bttv(target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
// Coinflip
|
||||
case "coin":
|
||||
commands.Coinflip(target, app.TwitchClient)
|
||||
return
|
||||
case "coinflip":
|
||||
commands.Coinflip(target, app.TwitchClient)
|
||||
return
|
||||
case "cf":
|
||||
commands.Coinflip(target, app.TwitchClient)
|
||||
return
|
||||
|
||||
// ()currency <amount> <input currency> to <output currency>
|
||||
case "currency":
|
||||
if msgLen < 4 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()currency 10 USD to EUR", app.TwitchClient)
|
||||
return
|
||||
}
|
||||
commands.Currency(target, cmdParams[1], cmdParams[2], cmdParams[4], app.TwitchClient)
|
||||
return
|
||||
|
||||
// ()ffz <emote name>
|
||||
case "ffz":
|
||||
if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()ffz <emote name>", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Ffz(target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
// ()followage <channel> <username>
|
||||
case "followage":
|
||||
if msgLen == 1 { // ()followage
|
||||
commands.Followage(target, target, message.User.Name, app.TwitchClient)
|
||||
return
|
||||
} else if msgLen == 2 { // ()followage forsen
|
||||
commands.Followage(target, target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
} else { // ()followage forsen pajlada
|
||||
commands.Followage(target, cmdParams[1], cmdParams[2], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
// First Line
|
||||
// ()firstline <channel> <username>
|
||||
case "firstline":
|
||||
if msgLen == 1 {
|
||||
common.Send(target, "Usage: ()firstline <channel> <user>", app.TwitchClient)
|
||||
return
|
||||
} else if msgLen == 2 {
|
||||
commands.FirstLine(target, target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.FirstLine(target, cmdParams[1], cmdParams[2], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
// ()fl <channel> <username>
|
||||
case "fl":
|
||||
if msgLen == 1 {
|
||||
common.Send(target, "Usage: ()firstline <channel> <user>", app.TwitchClient)
|
||||
return
|
||||
} else if msgLen == 2 {
|
||||
commands.FirstLine(target, target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.FirstLine(target, cmdParams[1], cmdParams[2], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
case "lastfm":
|
||||
if msgLen == 1 {
|
||||
app.UserCheckLastFM(message)
|
||||
return
|
||||
} else if cmdParams[1] == "artist" && cmdParams[2] == "top" {
|
||||
commands.LastFmArtistTop(target, message, app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
// Default to first argument supplied being the name
|
||||
// of the user to look up recently played.
|
||||
commands.LastFmUserRecent(target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
case "help":
|
||||
if msgLen == 1 {
|
||||
common.Send(target, "Provides information for a given command. Usage: ()help <commandname>", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
app.commandHelp(target, cmdParams[1], message.User.Name)
|
||||
return
|
||||
}
|
||||
|
||||
// ()ping
|
||||
case "ping":
|
||||
commands.Ping(target, app.TwitchClient)
|
||||
return
|
||||
|
||||
// Thumbnail
|
||||
// ()preview <live channel>
|
||||
case "preview":
|
||||
if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()preview <username>", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Preview(target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
case "set":
|
||||
if msgLen < 3 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else if cmdParams[1] == "lastfm" {
|
||||
app.SetUserLastFM(cmdParams[2], message)
|
||||
//app.SetLastFMUser(cmdParams[2], message)
|
||||
return
|
||||
} else if cmdParams[1] == "location" {
|
||||
app.SetUserLocation(message)
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// SevenTV
|
||||
// ()seventv <emote name>
|
||||
case "seventv":
|
||||
if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()seventv <emote name>", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Seventv(target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
// ()thumbnail <live channel>
|
||||
case "thumbnail":
|
||||
if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()thumbnail <username>", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Preview(target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
// ()7tv <emote name>
|
||||
case "7tv":
|
||||
if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()seventv <emote name>", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Seventv(target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
// ()tweet <username>
|
||||
case "tweet":
|
||||
if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()tweet <username>", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Tweet(target, cmdParams[1], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
case "phonetic":
|
||||
if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()phonetic <text>. ()help phonetic for more info", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Phonetic(target, message.Message[10:len(message.Message)], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
case "ph":
|
||||
if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided. Usage: ()ph <text>. ()help ph for more info", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Phonetic(target, message.Message[4:len(message.Message)], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
// ()weather <location>
|
||||
case "weather":
|
||||
if msgLen == 1 {
|
||||
// Default to first argument supplied being the name
|
||||
// of the user to look up recently played.
|
||||
app.UserCheckWeather(message)
|
||||
return
|
||||
} else if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Weather(target, message.Message[10:len(message.Message)], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
// Xkcd
|
||||
// Random Xkcd
|
||||
case "rxkcd":
|
||||
commands.RandomXkcd(target, app.TwitchClient)
|
||||
return
|
||||
case "randomxkcd":
|
||||
commands.RandomXkcd(target, app.TwitchClient)
|
||||
return
|
||||
// Latest Xkcd
|
||||
case "xkcd":
|
||||
commands.Xkcd(target, app.TwitchClient)
|
||||
return
|
||||
|
||||
// Commands with permission level or database from here on
|
||||
|
||||
//#################
|
||||
// 250 - VIP only
|
||||
//#################
|
||||
// ()debug user <username>
|
||||
// ()debug command <command name>
|
||||
case "debug":
|
||||
if userLevel < 250 {
|
||||
return
|
||||
} else if msgLen < 3 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else if cmdParams[1] == "user" {
|
||||
app.DebugUser(cmdParams[2], message)
|
||||
return
|
||||
} else if cmdParams[1] == "command" {
|
||||
app.DebugCommand(cmdParams[2], message)
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// ()echo <message>
|
||||
case "echo":
|
||||
if userLevel < 250 { // Limit to myself for now.
|
||||
return
|
||||
} else if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
commands.Echo(target, message.Message[7:len(message.Message)], app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
//###################
|
||||
// 1000 - Admin only
|
||||
//###################
|
||||
|
||||
// #####
|
||||
// Add
|
||||
// #####
|
||||
case "addchannel":
|
||||
if userLevel < 1000 {
|
||||
return
|
||||
} else if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
// ()addchannel noemience
|
||||
app.AddChannel(cmdParams[1], message)
|
||||
return
|
||||
}
|
||||
case "addcommand":
|
||||
if userLevel < 1000 {
|
||||
return
|
||||
} else if msgLen < 3 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
// ()addcommand dank FeelsDankMan xD
|
||||
app.AddCommand(cmdParams[1], message)
|
||||
return
|
||||
}
|
||||
case "addtimer":
|
||||
if userLevel < 1000 {
|
||||
return
|
||||
} else if msgLen < 4 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
// ()addtimer gfuel 5m sponsor XD xD
|
||||
app.AddTimer(cmdParams[1], message)
|
||||
return
|
||||
}
|
||||
|
||||
// ######
|
||||
// Edit
|
||||
// ######
|
||||
case "edituser":
|
||||
if userLevel < 1000 { // Limit to myself for now.
|
||||
return
|
||||
} else if msgLen < 4 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else if cmdParams[1] == "level" {
|
||||
// ()edituser level nourylul 1000
|
||||
app.EditUserLevel(cmdParams[2], cmdParams[3], message)
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
// ()edittimer testname 10m test text xd
|
||||
case "edittimer":
|
||||
if userLevel < 1000 { // Limit to myself for now.
|
||||
return
|
||||
} else if msgLen < 4 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
// ()edituser level nourylul 1000
|
||||
app.EditTimer(cmdParams[1], message)
|
||||
}
|
||||
case "editcommand": // ()editcommand level dankwave 1000
|
||||
if userLevel < 1000 {
|
||||
return
|
||||
} else if msgLen < 4 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else if cmdParams[1] == "level" {
|
||||
app.EditCommandLevel(cmdParams[2], cmdParams[3], message)
|
||||
return
|
||||
} else if cmdParams[1] == "category" {
|
||||
app.EditCommandCategory(cmdParams[2], cmdParams[3], message)
|
||||
return
|
||||
} else if cmdParams[1] == "help" {
|
||||
app.EditCommandHelp(cmdParams[2], message)
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// ########
|
||||
// Delete
|
||||
// ########
|
||||
case "deletechannel":
|
||||
if userLevel < 1000 { // Limit to myself for now.
|
||||
return
|
||||
} else if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
// ()deletechannel noemience
|
||||
app.DeleteChannel(cmdParams[1], message)
|
||||
return
|
||||
}
|
||||
case "deletecommand":
|
||||
if userLevel < 1000 { // Limit to myself for now.
|
||||
return
|
||||
} else if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
// ()deletecommand dank
|
||||
app.DeleteCommand(cmdParams[1], message)
|
||||
return
|
||||
}
|
||||
case "deleteuser":
|
||||
if userLevel < 1000 { // Limit to myself for now.
|
||||
return
|
||||
} else if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
// ()deleteuser noemience
|
||||
app.DeleteUser(cmdParams[1], message)
|
||||
return
|
||||
}
|
||||
case "deletetimer":
|
||||
if userLevel < 1000 { // Limit to myself for now.
|
||||
return
|
||||
} else if msgLen < 2 {
|
||||
common.Send(target, "Not enough arguments provided.", app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
// ()deletetimer dank
|
||||
app.DeleteTimer(cmdParams[1], message)
|
||||
return
|
||||
}
|
||||
|
||||
case "asd":
|
||||
app.Logger.Info(app.Scheduler.Entries())
|
||||
return
|
||||
|
||||
case "bttvemotes":
|
||||
if userLevel < 1000 {
|
||||
commands.Bttvemotes(target, app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
case "ffzemotes":
|
||||
if userLevel < 1000 {
|
||||
commands.Ffzemotes(target, app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// ##################
|
||||
// Check if the commandName exists as the "name" of a command in the database.
|
||||
// if it doesnt then ignore it.
|
||||
// ##################
|
||||
default:
|
||||
reply, err := app.GetCommand(commandName, message.User.Name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
common.SendNoLimit(message.Channel, reply, app.TwitchClient)
|
||||
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",
|
||||
}
|
||||
|
||||
func (app *Application) loadCommandHelp() {
|
||||
for k, v := range helpText {
|
||||
err := app.Rdb.HSet(ctx, "command-help", k, v).Err()
|
||||
if err != nil {
|
||||
app.Logger.Panic(err)
|
||||
}
|
||||
}
|
||||
commandHelpText := app.Rdb.HGetAll(ctx, "command-help").Val()
|
||||
app.Logger.Infow("Successfully loaded command help text into redis",
|
||||
"commandHelpText", commandHelpText,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// Help checks if a help text for a given command exists and replies with it.
|
||||
func (app *Application) commandHelp(target, name, username string) {
|
||||
// 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.Logger.Infow("commandHelp: no such command found",
|
||||
"err", err)
|
||||
return
|
||||
}
|
||||
|
||||
reply := fmt.Sprint(c)
|
||||
common.Send(target, reply, app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
reply := fmt.Sprint(i)
|
||||
common.Send(target, reply, app.TwitchClient)
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserLevelNotInteger = errors.New("user level must be a number")
|
||||
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")
|
||||
)
|
325
cmd/bot/timer.go
325
cmd/bot/timer.go
|
@ -1,325 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"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) {
|
||||
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]
|
||||
|
||||
// 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)]
|
||||
|
||||
// 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",
|
||||
"repeat", repeat,
|
||||
"error", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
timer := &data.Timer{
|
||||
Name: name,
|
||||
Text: text,
|
||||
Channel: message.Channel,
|
||||
Repeat: repeat,
|
||||
}
|
||||
|
||||
// Check if the time string we got is valid, this is important
|
||||
// because the Scheduler panics instead of erroring out if an invalid
|
||||
// time format string is supplied.
|
||||
if validateTimeFormat {
|
||||
timer := &data.Timer{
|
||||
Name: name,
|
||||
Text: text,
|
||||
Channel: message.Channel,
|
||||
Repeat: repeat,
|
||||
}
|
||||
|
||||
err = app.Models.Timers.Insert(timer)
|
||||
if err != nil {
|
||||
app.Logger.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)
|
||||
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",
|
||||
"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)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
app.Logger.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)
|
||||
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) {
|
||||
// Check if a timer with that name is in the database.
|
||||
old, err := app.Models.Timers.Get(name)
|
||||
if err != nil {
|
||||
app.Logger.Errorw("Could not get timer",
|
||||
"timer", old,
|
||||
"error", err,
|
||||
)
|
||||
reply := "Something went wrong FeelsBadMan"
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the old timer
|
||||
cronName := fmt.Sprintf("%s-%s", message.Channel, name)
|
||||
app.Scheduler.RemoveJob(cronName)
|
||||
|
||||
_ = app.Rdb.Del(ctx, cronName)
|
||||
err = app.Models.Timers.Delete(name)
|
||||
if err != nil {
|
||||
app.Logger.Errorw("Error deleting timer from database",
|
||||
"name", name,
|
||||
"cronName", cronName,
|
||||
"error", err,
|
||||
)
|
||||
|
||||
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
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]
|
||||
|
||||
// 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)]
|
||||
|
||||
// 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",
|
||||
"repeat", repeat,
|
||||
"error", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
timer := &data.Timer{
|
||||
Name: name,
|
||||
Text: text,
|
||||
Channel: message.Channel,
|
||||
Repeat: repeat,
|
||||
}
|
||||
|
||||
// Check if the time string we got is valid, this is important
|
||||
// because the Scheduler panics instead of erroring out if an invalid
|
||||
// time format string is supplied.
|
||||
if validateTimeFormat {
|
||||
timer := &data.Timer{
|
||||
Name: name,
|
||||
Text: text,
|
||||
Channel: message.Channel,
|
||||
Repeat: repeat,
|
||||
}
|
||||
|
||||
err = app.Models.Timers.Insert(timer)
|
||||
if err != nil {
|
||||
app.Logger.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)
|
||||
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) }, cronName)
|
||||
app.Logger.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)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
app.Logger.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)
|
||||
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() {
|
||||
timer, err := app.Models.Timers.GetAll()
|
||||
if err != nil {
|
||||
app.Logger.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 []*data.Timer
|
||||
|
||||
// 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"
|
||||
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)
|
||||
}
|
||||
|
||||
// var model1 rdbVal
|
||||
// if err := app.Rdb.HGetAll(ctx, cronName).Scan(&model1); err != nil {
|
||||
// app.Logger.Panic(err)
|
||||
// }
|
||||
|
||||
app.Logger.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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
app.Logger.Infow("Deleting timer",
|
||||
"name", name,
|
||||
"message.Channel", message.Channel,
|
||||
"cronName", cronName,
|
||||
)
|
||||
|
||||
_ = app.Rdb.Del(ctx, cronName)
|
||||
err := app.Models.Timers.Delete(name)
|
||||
if err != nil {
|
||||
app.Logger.Errorw("Error deleting timer from database",
|
||||
"name", name,
|
||||
"cronName", cronName,
|
||||
"error", err,
|
||||
)
|
||||
|
||||
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
reply := fmt.Sprintf("Deleted timer with name %s", name)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
}
|
213
cmd/bot/user.go
213
cmd/bot/user.go
|
@ -1,213 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/commands"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 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, message twitch.PrivateMessage) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
_, err := app.Models.Users.Check(twitchId)
|
||||
app.Logger.Error(err)
|
||||
if err != nil {
|
||||
app.Logger.Infow("InitUser: Adding new user:",
|
||||
"login: ", login,
|
||||
"twitchId: ", twitchId,
|
||||
)
|
||||
app.Models.Users.Insert(login, twitchId)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sugar.Infow("User Insert: User already registered: xd",
|
||||
"login: ", login,
|
||||
"twitchId: ", twitchId,
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
return
|
||||
} else {
|
||||
reply := fmt.Sprintf("User %v: ID %v, Login: %s, TwitchID: %v, Level: %v", login, user.ID, user.Login, user.TwitchID, user.Level)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
app.TwitchClient.Whisper(message.User.Name, reply)
|
||||
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 {
|
||||
common.Send(message.Channel, "Something went wrong FeelsBadMan", app.TwitchClient)
|
||||
app.Logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
reply := fmt.Sprintf("Deleted user %s", login)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
}
|
||||
|
||||
// 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.Logger.Error(err)
|
||||
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrUserLevelNotInteger), app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
err = app.Models.Users.SetLevel(login, level)
|
||||
if err != nil {
|
||||
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient)
|
||||
app.Logger.Error(err)
|
||||
return
|
||||
} else {
|
||||
reply := fmt.Sprintf("Updated user %s to level %v", login, level)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
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)]
|
||||
login := message.User.Name
|
||||
twitchId := message.User.ID
|
||||
|
||||
app.Logger.Infow("SetUserLocation",
|
||||
"location", location,
|
||||
"login", login,
|
||||
"twitchId", message.User.ID,
|
||||
)
|
||||
err := app.Models.Users.SetLocation(twitchId, location)
|
||||
if err != nil {
|
||||
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient)
|
||||
app.Logger.Error(err)
|
||||
return
|
||||
} else {
|
||||
reply := fmt.Sprintf("Successfully set your location to %v", location)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
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
|
||||
|
||||
app.Logger.Infow("SetUserLastFM",
|
||||
"lastfmUser", lastfmUser,
|
||||
"login", login,
|
||||
)
|
||||
err := app.Models.Users.SetLastFM(login, lastfmUser)
|
||||
if err != nil {
|
||||
common.Send(message.Channel, fmt.Sprintf("Something went wrong FeelsBadMan %s", ErrRecordNotFound), app.TwitchClient)
|
||||
app.Logger.Error(err)
|
||||
return
|
||||
} else {
|
||||
reply := fmt.Sprintf("Successfully set your lastfm username to %v", lastfmUser)
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserLevel takes in a login name and queries the database for an entry
|
||||
// with such a name value. 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) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
twitchLogin := message.User.Name
|
||||
twitchId := message.User.ID
|
||||
sugar.Infow("UserCheckWeather: ",
|
||||
"twitchLogin:", twitchLogin,
|
||||
"twitchId:", twitchId,
|
||||
)
|
||||
location, err := app.Models.Users.GetLocation(twitchId)
|
||||
if err != nil {
|
||||
sugar.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."
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
target := message.Channel
|
||||
sugar.Infow("Twitchlogin: ",
|
||||
"twitchLogin:", twitchLogin,
|
||||
"location:", location,
|
||||
)
|
||||
|
||||
commands.Weather(target, location, app.TwitchClient)
|
||||
}
|
||||
|
||||
func (app *Application) UserCheckLastFM(message twitch.PrivateMessage) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
twitchLogin := message.User.Name
|
||||
sugar.Infow("Twitchlogin: ",
|
||||
"twitchLogin:", twitchLogin,
|
||||
)
|
||||
lastfmUser, err := app.Models.Users.GetLastFM(twitchLogin)
|
||||
if err != nil {
|
||||
sugar.Errorw("No LastFM account registered for: ",
|
||||
"twitchLogin:", twitchLogin,
|
||||
)
|
||||
reply := "No lastfm account registered in my database. Use ()register lastfm <username> to register. (Not yet implemented) Otherwise use ()lastfm <username> without registering."
|
||||
common.Send(message.Channel, reply, app.TwitchClient)
|
||||
return
|
||||
}
|
||||
|
||||
target := message.Channel
|
||||
sugar.Infow("Twitchlogin: ",
|
||||
"twitchLogin:", twitchLogin,
|
||||
"user:", lastfmUser,
|
||||
)
|
||||
|
||||
commands.LastFmUserRecent(target, lastfmUser, app.TwitchClient)
|
||||
}
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
@ -12,10 +11,9 @@ import (
|
|||
"github.com/jakecoffman/cron"
|
||||
"github.com/joho/godotenv"
|
||||
_ "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/rs/zerolog/log"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -33,14 +31,14 @@ 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
|
||||
// Models data.Models
|
||||
Scheduler *cron.Cron
|
||||
// Rdb *redis.Client
|
||||
}
|
||||
|
||||
var envFlag string
|
||||
|
@ -50,10 +48,8 @@ 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()
|
||||
|
@ -62,7 +58,7 @@ func main() {
|
|||
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
sugar.Fatal("Error loading .env")
|
||||
}
|
||||
|
||||
// Twitch config variables
|
||||
|
@ -79,7 +75,6 @@ func main() {
|
|||
case "prod":
|
||||
cfg.db.dsn = os.Getenv("SUPABASE_DSN")
|
||||
}
|
||||
|
||||
// Database config variables
|
||||
cfg.db.maxOpenConns = 25
|
||||
cfg.db.maxIdleConns = 25
|
||||
|
@ -115,44 +110,28 @@ 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,
|
||||
sugar.Infow("db.Stats",
|
||||
"db.Stats", db.Stats(),
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
// Received a PrivateMessage (normal chat message).
|
||||
app.TwitchClient.OnPrivateMessage(func(message twitch.PrivateMessage) {
|
||||
|
||||
// app.Logger.Infow("Message received",
|
||||
// "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,
|
||||
// )
|
||||
|
||||
|
@ -160,73 +139,28 @@ func main() {
|
|||
// 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
|
||||
}
|
||||
|
||||
// Message was shorter than our prefix is therefore it's irrelevant for us.
|
||||
if len(message.Message) >= 2 {
|
||||
// This bots prefix is "()" configured above at cfg.commandPrefix,
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
|
||||
// Special rule for #pajlada.
|
||||
if message.Message == "!nourybot" {
|
||||
common.Send(message.Channel, "Lidl Twitch bot made by @nourylul. Prefix: ()", app.TwitchClient)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
// 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.Log.Infow("Successfully connected to Twitch Servers",
|
||||
"Bot username", cfg.twitchUsername,
|
||||
"Environment", envFlag,
|
||||
"Database Open Conns", cfg.db.maxOpenConns,
|
||||
"Database Idle Conns", cfg.db.maxIdleConns,
|
||||
"Database Idle Time", cfg.db.maxIdleTime,
|
||||
"Database", db.Stats(),
|
||||
"Helix", helixResp,
|
||||
)
|
||||
app.TwitchClient.OnConnect(func() {
|
||||
app.Logger.Infow("Successfully connected to Twitch Servers",
|
||||
"Bot username", cfg.twitchUsername,
|
||||
"Environment", envFlag,
|
||||
"Database Open Conns", cfg.db.maxOpenConns,
|
||||
"Database Idle Conns", cfg.db.maxIdleConns,
|
||||
"Database Idle Time", cfg.db.maxIdleTime,
|
||||
"Database", db.Stats(),
|
||||
"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)
|
||||
app.TwitchClient.Join("nourylul")
|
||||
app.TwitchClient.Say("nourylul", "xD!")
|
||||
// sugar.Infow("db.Stats",
|
||||
// "db.Stats", db.Stats(),
|
||||
// )
|
||||
})
|
||||
|
||||
// Actually connect to chat.
|
||||
err = app.TwitchClient.Connect()
|
||||
if err != nil {
|
|
@ -1,7 +0,0 @@
|
|||
version: "3.9"
|
||||
services:
|
||||
nourybot:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
# restart: unless-stopped
|
28
go.mod
28
go.mod
|
@ -3,23 +3,23 @@ module github.com/lyx0/nourybot
|
|||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/briandowns/openweathermap v0.18.0
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/gempir/go-twitch-irc/v3 v3.2.0
|
||||
github.com/gempir/go-twitch-irc/v3 v3.3.0
|
||||
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/lib/pq v1.10.6
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/nicklaw5/helix v1.25.0
|
||||
go.uber.org/zap v1.21.0
|
||||
github.com/rs/zerolog v1.29.1
|
||||
go.uber.org/zap v1.24.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||
github.com/redis/go-redis/v9 v9.0.3 // indirect
|
||||
github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
)
|
||||
|
|
95
go.sum
95
go.sum
|
@ -1,85 +1,44 @@
|
|||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/briandowns/openweathermap v0.18.0 h1:JYTtJ4bKjXZRmDTe7huJ5+dZ7CsjPUw10GUzMASkNV8=
|
||||
github.com/briandowns/openweathermap v0.18.0/go.mod h1:0GLnknqicWxXnGi1IqoOaZIw+kIe5hkt+YM5WY3j8+0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/gempir/go-twitch-irc/v3 v3.2.0 h1:ENhsa7RgBE1GMmDqe0iMkvcSYfgw6ZsXilt+sAg32/U=
|
||||
github.com/gempir/go-twitch-irc/v3 v3.2.0/go.mod h1:/W9KZIiyizVecp4PEb7kc4AlIyXKiCmvlXrzlpPUytU=
|
||||
github.com/gempir/go-twitch-irc/v3 v3.3.0 h1:iBOKSwNbgsE/zYwzyoHNhXBlf/kkzl3V3k6H2myENRU=
|
||||
github.com/gempir/go-twitch-irc/v3 v3.3.0/go.mod h1:/W9KZIiyizVecp4PEb7kc4AlIyXKiCmvlXrzlpPUytU=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
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/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/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/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/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/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=
|
||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
@ -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"}]}
|
|
@ -1,3 +0,0 @@
|
|||
# Insomnia
|
||||
|
||||
Api collection import/export for [insomnia](https://insomnia.rest/)
|
|
@ -1,14 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
)
|
||||
|
||||
func Bttv(target, query string, tc *twitch.Client) {
|
||||
reply := fmt.Sprintf("https://betterttv.com/emotes/shared/search?query=%s", query)
|
||||
|
||||
common.Send(target, reply, tc)
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/commands/decapi"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Bttvemotes(target string, tc *twitch.Client) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
resp, err := decapi.Bttvemotes(target)
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
|
||||
common.Send(target, resp, tc)
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
)
|
||||
|
||||
func Coinflip(target string, tc *twitch.Client) {
|
||||
flip := common.GenerateRandomNumber(2)
|
||||
|
||||
switch flip {
|
||||
case 0:
|
||||
common.Send(target, "Heads!", tc)
|
||||
return
|
||||
case 1:
|
||||
common.Send(target, "Tails!", tc)
|
||||
return
|
||||
default:
|
||||
common.Send(target, "Heads!", tc)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/commands/decapi"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ()currency 10 USD to EUR
|
||||
func Currency(target, currAmount, currFrom, currTo string, tc *twitch.Client) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
resp, err := decapi.Currency(currAmount, currFrom, currTo)
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
|
||||
common.Send(target, resp, tc)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package decapi
|
||||
|
||||
var (
|
||||
twitterUserNotFoundError = "[Error] - [34] Sorry, that page does not exist."
|
||||
followageUserCannotFollowOwn = "A user cannot follow themself."
|
||||
)
|
|
@ -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
|
||||
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
)
|
||||
|
||||
func Echo(target, message string, tc *twitch.Client) {
|
||||
common.Send(target, message, tc)
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"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)
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/commands/decapi"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Ffzemotes(target string, tc *twitch.Client) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
resp, err := decapi.Ffzemotes(target)
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
|
||||
common.Send(target, resp, tc)
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/commands/ivr"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
)
|
||||
|
||||
func FirstLine(target, channel, username string, tc *twitch.Client) {
|
||||
ivrResponse, err := ivr.FirstLine(channel, username)
|
||||
|
||||
if err != nil {
|
||||
common.Send(channel, fmt.Sprint(err), tc)
|
||||
return
|
||||
}
|
||||
|
||||
common.Send(target, ivrResponse, tc)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/commands/decapi"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ()currency 10 USD to EUR
|
||||
func Followage(target, channel, username string, tc *twitch.Client) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
resp, err := decapi.Followage(channel, username)
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
|
||||
common.Send(target, resp, tc)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"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) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
sugar.Error("Error loading LASTFM API keys from .env file")
|
||||
}
|
||||
|
||||
apiKey := os.Getenv("LAST_FM_API_KEY")
|
||||
apiSecret := os.Getenv("LAST_FM_SECRET")
|
||||
|
||||
api := lastfm.New(apiKey, apiSecret)
|
||||
result, _ := api.User.GetRecentTracks(lastfm.P{"user": user}) //discarding error
|
||||
|
||||
var reply string
|
||||
for i, track := range result.Tracks {
|
||||
// The 0th result is the most recent one since it goes from most recent
|
||||
// to least recent.
|
||||
if i == 0 {
|
||||
sugar.Infow("Most recent: ",
|
||||
"user:", user,
|
||||
"track", track.Name,
|
||||
"artist", track.Artist.Name,
|
||||
)
|
||||
|
||||
reply = fmt.Sprintf("Most recently played track for user %v: %v - %v", user, track.Artist.Name, track.Name)
|
||||
common.Send(target, reply, tc)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
)
|
||||
|
||||
var cm = map[string]string{
|
||||
"`": "ё",
|
||||
"~": "Ё",
|
||||
"=": "ъ",
|
||||
"+": "Ъ",
|
||||
"[": "ю",
|
||||
"]": "щ",
|
||||
`\`: "э",
|
||||
"{": "Ю",
|
||||
"}": "Щ",
|
||||
"|": "Э",
|
||||
";": "ь",
|
||||
":": "Ь",
|
||||
"'": "ж",
|
||||
`"`: "Ж",
|
||||
|
||||
"q": "я",
|
||||
"w": "ш",
|
||||
"e": "е",
|
||||
"r": "р",
|
||||
"t": "т",
|
||||
"y": "ы",
|
||||
"u": "у",
|
||||
"i": "и",
|
||||
"o": "о",
|
||||
"p": "п",
|
||||
"a": "а",
|
||||
"s": "с",
|
||||
"d": "д",
|
||||
"f": "ф",
|
||||
"g": "г",
|
||||
"h": "ч",
|
||||
"j": "й",
|
||||
"k": "к",
|
||||
"l": "л",
|
||||
"z": "з",
|
||||
"x": "х",
|
||||
"c": "ц",
|
||||
"v": "в",
|
||||
"b": "б",
|
||||
"n": "н",
|
||||
"m": "м",
|
||||
"Q": "Я",
|
||||
"W": "Ш",
|
||||
"E": "Е",
|
||||
"R": "Р",
|
||||
"T": "Т",
|
||||
"Y": "Ы",
|
||||
"U": "У",
|
||||
"I": "И",
|
||||
"O": "О",
|
||||
"P": "П",
|
||||
"A": "А",
|
||||
"S": "С",
|
||||
"D": "Д",
|
||||
"F": "Ф",
|
||||
"G": "Г",
|
||||
"H": "Ч",
|
||||
"J": "Й",
|
||||
"K": "К",
|
||||
"L": "Л",
|
||||
"Z": "З",
|
||||
"X": "Х",
|
||||
"C": "Ц",
|
||||
"V": "В",
|
||||
"B": "Б",
|
||||
"N": "Н",
|
||||
"M": "М",
|
||||
}
|
||||
|
||||
func Phonetic(target, message string, tc *twitch.Client) {
|
||||
var ts string
|
||||
|
||||
for _, c := range message {
|
||||
if _, ok := cm[string(c)]; ok {
|
||||
ts = ts + cm[string(c)]
|
||||
} else {
|
||||
ts = ts + string(c)
|
||||
|
||||
}
|
||||
//ts = append(ts, cm[string(c)])
|
||||
}
|
||||
|
||||
common.Send(target, fmt.Sprint(ts), tc)
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"github.com/lyx0/nourybot/internal/humanize"
|
||||
)
|
||||
|
||||
func Ping(target string, tc *twitch.Client) {
|
||||
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)
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
)
|
||||
|
||||
func Preview(target, channel string, tc *twitch.Client) {
|
||||
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)
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"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)
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/commands/decapi"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Tweet(target, username string, tc *twitch.Client) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
resp, err := decapi.Tweet(username)
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
|
||||
common.Send(target, resp, tc)
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
owm "github.com/briandowns/openweathermap"
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"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()
|
||||
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
sugar.Error("Error loading OpenWeatherMap API key from .env file")
|
||||
}
|
||||
owmKey := os.Getenv("OWM_KEY")
|
||||
|
||||
w, err := owm.NewCurrent("C", "en", owmKey)
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
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)
|
||||
} 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.",
|
||||
w.Name,
|
||||
w.Sys.Country,
|
||||
w.Main.FeelsLike,
|
||||
w.Main.Temp,
|
||||
w.Main.TempMax,
|
||||
w.Main.TempMin,
|
||||
w.Main.Humidity,
|
||||
w.Wind.Speed,
|
||||
)
|
||||
common.Send(target, reply, tc)
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/lyx0/nourybot/internal/common"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type xkcdResponse struct {
|
||||
Num int `json:"num"`
|
||||
SafeTitle string `json:"safe_title"`
|
||||
Img string `json:"img"`
|
||||
}
|
||||
|
||||
func Xkcd(target string, tc *twitch.Client) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
response, err := http.Get("https://xkcd.com/info.0.json")
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
responseData, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
var responseObject xkcdResponse
|
||||
json.Unmarshal(responseData, &responseObject)
|
||||
|
||||
reply := fmt.Sprint("Current Xkcd #", responseObject.Num, " Title: ", responseObject.SafeTitle, " ", responseObject.Img)
|
||||
|
||||
common.Send(target, reply, tc)
|
||||
}
|
||||
|
||||
func RandomXkcd(target string, tc *twitch.Client) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
comicNum := fmt.Sprint(common.GenerateRandomNumber(2655))
|
||||
|
||||
response, err := http.Get(fmt.Sprint("http://xkcd.com/" + comicNum + "/info.0.json"))
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
responseData, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
sugar.Error(err)
|
||||
}
|
||||
var responseObject xkcdResponse
|
||||
json.Unmarshal(responseData, &responseObject)
|
||||
|
||||
reply := fmt.Sprint("Random Xkcd #", responseObject.Num, " Title: ", responseObject.SafeTitle, " ", responseObject.Img)
|
||||
|
||||
common.Send(target, reply, tc)
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package common
|
||||
|
||||
var (
|
||||
tempCommands = 0
|
||||
)
|
||||
|
||||
// CommandUsed is called on every command incremenenting tempCommands.
|
||||
func CommandUsed() {
|
||||
tempCommands++
|
||||
}
|
||||
|
||||
// GetCommandsUsed returns the amount of commands that have been used
|
||||
// since the last restart.
|
||||
func GetCommandsUsed() int {
|
||||
return tempCommands
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package common
|
||||
|
||||
import "github.com/gempir/go-twitch-irc/v3"
|
||||
|
||||
// ElevatedPrivsMessage is checking a given message twitch.PrivateMessage
|
||||
// if it came from a moderator/vip/or broadcaster and returns a bool
|
||||
func ElevatedPrivsMessage(message twitch.PrivateMessage) bool {
|
||||
if message.User.Badges["moderator"] == 1 ||
|
||||
message.User.Badges["vip"] == 1 ||
|
||||
message.User.Badges["broadcaster"] == 1 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ModPrivsMessage is checking a given message twitch.PrivateMessage
|
||||
// if it came from a moderator or broadcaster and returns a bool
|
||||
func ModPrivsMessage(message twitch.PrivateMessage) bool {
|
||||
if message.User.Badges["moderator"] == 1 ||
|
||||
message.User.Badges["broadcaster"] == 1 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StrGenerateRandomNumber generates a random number from
|
||||
// a given max value as a string
|
||||
func StrGenerateRandomNumber(max string) int {
|
||||
num, err := strconv.Atoi(max)
|
||||
if num < 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Supplied value %v is not a number", num)
|
||||
return 0
|
||||
} else {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return rand.Intn(num)
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateRandomNumber returns a random number from
|
||||
// a given max value as a int
|
||||
func GenerateRandomNumber(max int) int {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return rand.Intn(max)
|
||||
}
|
||||
|
||||
// GenerateRandomNumberRange returns a random number
|
||||
// over a given minimum and maximum range.
|
||||
func GenerateRandomNumberRange(min int, max int) int {
|
||||
return (rand.Intn(max-min) + min)
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 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 checkMessage(text string) (bool, string) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
// {"message": "AHAHAHAHA LUL"}
|
||||
reqBody, err := json.Marshal(map[string]string{
|
||||
"message": text,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(banPhraseUrl, "application/json", bytes.NewBuffer(reqBody))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
var responseObject banphraseResponse
|
||||
json.Unmarshal(body, &responseObject)
|
||||
|
||||
// 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 Send(target, message string, tc *twitch.Client) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
// 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 := checkMessage(message)
|
||||
if messageBanned {
|
||||
// Bad message, replace message and log it.
|
||||
tc.Say(target, "[BANPHRASED] monkaS")
|
||||
sugar.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.
|
||||
if len(message) > 500 {
|
||||
firstMessage := message[0:499]
|
||||
secondMessage := message[499:]
|
||||
|
||||
tc.Say(target, firstMessage)
|
||||
tc.Say(target, secondMessage)
|
||||
|
||||
return
|
||||
}
|
||||
// Message was fine.
|
||||
tc.Say(target, message)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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 SendNoLimit(target, message string, tc *twitch.Client) {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
|
||||
// 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 := checkMessage(message)
|
||||
if messageBanned {
|
||||
// Bad message, replace message and log it.
|
||||
tc.Say(target, "[BANPHRASED] monkaS")
|
||||
sugar.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.
|
||||
tc.Say(target, message)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package common
|
||||
|
||||
import "time"
|
||||
|
||||
var (
|
||||
uptime time.Time
|
||||
)
|
||||
|
||||
func StartTime() {
|
||||
uptime = time.Now()
|
||||
}
|
||||
|
||||
func GetUptime() time.Time {
|
||||
return uptime
|
||||
}
|
|
@ -1,211 +0,0 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
ID int `json:"id"`
|
||||
AddedAt time.Time `json:"-"`
|
||||
Login string `json:"login"`
|
||||
TwitchID string `json:"twitchid"`
|
||||
}
|
||||
|
||||
type ChannelModel struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// Get takes the login name for a channel and queries the database for an
|
||||
// existing entry with that login value. If it exists it returns a
|
||||
// pointer to a Channel.
|
||||
func (c ChannelModel) Get(login string) (*Channel, error) {
|
||||
query := `
|
||||
SELECT id, added_at, login, twitchid
|
||||
FROM channels
|
||||
WHERE login = $1`
|
||||
|
||||
var channel Channel
|
||||
|
||||
err := c.DB.QueryRow(query, login).Scan(
|
||||
&channel.ID,
|
||||
&channel.AddedAt,
|
||||
&channel.Login,
|
||||
&channel.TwitchID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, ErrRecordNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
// Insert takes in a channel struct and inserts it into the database.
|
||||
func (c ChannelModel) Insert(channel *Channel) error {
|
||||
query := `
|
||||
INSERT INTO channels(login, twitchid)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (login)
|
||||
DO NOTHING
|
||||
RETURNING id, added_at;
|
||||
`
|
||||
|
||||
args := []interface{}{channel.Login, channel.TwitchID}
|
||||
|
||||
// Execute the query returning the number of affected rows.
|
||||
result, err := c.DB.Exec(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check how many rows were affected.
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return ErrChannelRecordAlreadyExists
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAll() returns a pointer to a slice of all channels (`[]*Channel`) in the database.
|
||||
func (c ChannelModel) GetAll() ([]*Channel, error) {
|
||||
query := `
|
||||
SELECT id, added_at, login, twitchid
|
||||
FROM channels
|
||||
ORDER BY id`
|
||||
|
||||
// Create a context with 3 seconds timeout.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Use QueryContext() the context and query. This returns a
|
||||
// sql.Rows resultset containing our channels.
|
||||
rows, err := c.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Need to defer a call to rows.Close() to ensure the resultset
|
||||
// is closed before GetAll() returns.
|
||||
defer rows.Close()
|
||||
|
||||
// Initialize an empty slice to hold the data.
|
||||
channels := []*Channel{}
|
||||
|
||||
// Iterate over the resultset.
|
||||
for rows.Next() {
|
||||
// Initialize an empty Channel struct where we put on
|
||||
// a single channel value.
|
||||
var channel Channel
|
||||
|
||||
// Scan the values onto the channel struct
|
||||
err := rows.Scan(
|
||||
&channel.ID,
|
||||
&channel.AddedAt,
|
||||
&channel.Login,
|
||||
&channel.TwitchID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Add the single movie struct onto the slice.
|
||||
channels = append(channels, &channel)
|
||||
}
|
||||
|
||||
// When rows.Next() finishes call rows.Err() to retrieve any errors.
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
// GetJoinable() returns a slice of channel names (Channel.Login) in the database.
|
||||
func (c ChannelModel) GetJoinable() ([]string, error) {
|
||||
query := `
|
||||
SELECT login
|
||||
FROM channels
|
||||
ORDER BY id`
|
||||
|
||||
// Create a context with 3 seconds timeout.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Use QueryContext() the context and query. This returns a
|
||||
// sql.Rows resultset containing our channels.
|
||||
rows, err := c.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Need to defer a call to rows.Close() to ensure the resultset
|
||||
// is closed before GetAll() returns.
|
||||
defer rows.Close()
|
||||
|
||||
// Initialize an empty slice to hold the data.
|
||||
channels := []string{}
|
||||
|
||||
// Iterate over the resultset.
|
||||
for rows.Next() {
|
||||
// Initialize an empty Channel struct where we put on
|
||||
// a single channel value.
|
||||
var channel Channel
|
||||
|
||||
// Scan the values onto the channel struct
|
||||
err := rows.Scan(
|
||||
&channel.Login,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Add the single movie struct onto the slice.
|
||||
channels = append(channels, channel.Login)
|
||||
}
|
||||
|
||||
// When rows.Next() finishes call rows.Err() to retrieve any errors.
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
// Delete takes in a login name and queries the database and if there is an
|
||||
// entry with that login name deletes the entry.
|
||||
func (c ChannelModel) Delete(login string) error {
|
||||
// Prepare the statement.
|
||||
query := `
|
||||
DELETE FROM channels
|
||||
WHERE login = $1`
|
||||
|
||||
// Execute the query returning the number of affected rows.
|
||||
result, err := c.DB.Exec(query, login)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check how many rows were affected.
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want atleast 1, if it is 0 the entry did not exist.
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,205 +0,0 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Level int `json:"level,omitempty"`
|
||||
Help string `json:"help,omitempty"`
|
||||
}
|
||||
|
||||
type CommandModel struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// Get tries to find a command in the database with the provided name.
|
||||
func (c CommandModel) Get(name string) (*Command, error) {
|
||||
query := `
|
||||
SELECT id, name, text, category, level, help
|
||||
FROM commands
|
||||
WHERE name = $1`
|
||||
|
||||
var command Command
|
||||
|
||||
err := c.DB.QueryRow(query, name).Scan(
|
||||
&command.ID,
|
||||
&command.Name,
|
||||
&command.Text,
|
||||
&command.Category,
|
||||
&command.Level,
|
||||
&command.Help,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, ErrRecordNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &command, nil
|
||||
}
|
||||
|
||||
// Insert adds a command into the database.
|
||||
func (c CommandModel) Insert(command *Command) error {
|
||||
query := `
|
||||
INSERT into commands(name, text, category, level, help)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (name)
|
||||
DO NOTHING
|
||||
RETURNING id;
|
||||
`
|
||||
|
||||
args := []interface{}{command.Name, command.Text, command.Category, command.Level, command.Help}
|
||||
|
||||
result, err := c.DB.Exec(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return ErrCommandRecordAlreadyExists
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c CommandModel) Update(command *Command) error {
|
||||
query := `
|
||||
UPDATE commands
|
||||
SET text = $2
|
||||
WHERE name = $1
|
||||
RETURNING id`
|
||||
|
||||
args := []interface{}{
|
||||
command.Name,
|
||||
command.Text,
|
||||
}
|
||||
|
||||
err := c.DB.QueryRow(query, args...).Scan(&command.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return ErrEditConflict
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCategory queries the database for an entry with the provided name,
|
||||
// if there is one it updates the categories level with the provided level.
|
||||
func (c CommandModel) SetCategory(name string, category string) error {
|
||||
query := `
|
||||
UPDATE commands
|
||||
SET category = $2
|
||||
WHERE name = $1`
|
||||
|
||||
result, err := c.DB.Exec(query, name, category)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLevel queries the database for an entry with the provided name,
|
||||
// if there is one it updates the entrys level with the provided level.
|
||||
func (c CommandModel) SetLevel(name string, level int) error {
|
||||
query := `
|
||||
UPDATE commands
|
||||
SET level = $2
|
||||
WHERE name = $1`
|
||||
|
||||
result, err := c.DB.Exec(query, name, level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetHelp sets the help text for a given name of a command in the database.
|
||||
func (c CommandModel) SetHelp(name string, helptext string) error {
|
||||
query := `
|
||||
UPDATE commands
|
||||
SET help = $2
|
||||
WHERE name = $1`
|
||||
|
||||
result, err := c.DB.Exec(query, name, helptext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete takes in a command name and queries the database for an entry with
|
||||
// the same name and tries to delete that entry.
|
||||
func (c CommandModel) Delete(name string) error {
|
||||
// Prepare the statement.
|
||||
query := `
|
||||
DELETE FROM commands
|
||||
WHERE name = $1`
|
||||
|
||||
// Execute the query returning the number of affected rows.
|
||||
result, err := c.DB.Exec(query, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check how many rows were affected.
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want atleast 1, if it is 0 the entry did not exist.
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRecordNotFound = errors.New("record not found")
|
||||
ErrChannelRecordAlreadyExists = errors.New("channel already in database")
|
||||
ErrEditConflict = errors.New("edit conflict")
|
||||
ErrCommandRecordAlreadyExists = errors.New("command already exists")
|
||||
ErrLastFMUserRecordAlreadyExists = errors.New("lastfm connection already set")
|
||||
ErrUserAlreadyExists = errors.New("user already in database")
|
||||
)
|
||||
|
||||
// struct Models wraps the models, making them callable
|
||||
// as app.models.Channels.Get(login)
|
||||
type Models struct {
|
||||
Channels interface {
|
||||
Insert(channel *Channel) error
|
||||
Get(login string) (*Channel, error)
|
||||
GetAll() ([]*Channel, error)
|
||||
GetJoinable() ([]string, error)
|
||||
Delete(login string) error
|
||||
}
|
||||
Users interface {
|
||||
Insert(login, twitchId string) error
|
||||
Get(login string) (*User, error)
|
||||
Check(twitchId string) (*User, error)
|
||||
SetLevel(login string, level int) error
|
||||
GetLevel(twitchId string) (int, error)
|
||||
SetLocation(twitchId, location string) error
|
||||
GetLocation(twitchId string) (string, error)
|
||||
SetLastFM(login, lastfmUser string) error
|
||||
GetLastFM(login string) (string, error)
|
||||
Delete(login string) error
|
||||
}
|
||||
Commands interface {
|
||||
Get(name string) (*Command, error)
|
||||
Insert(command *Command) error
|
||||
Update(command *Command) error
|
||||
SetLevel(name string, level int) error
|
||||
SetCategory(name, category string) error
|
||||
SetHelp(name, helptext string) error
|
||||
Delete(name string) error
|
||||
}
|
||||
Timers interface {
|
||||
Get(name string) (*Timer, error)
|
||||
Insert(timer *Timer) error
|
||||
Update(timer *Timer) error
|
||||
GetAll() ([]*Timer, error)
|
||||
Delete(name string) error
|
||||
}
|
||||
}
|
||||
|
||||
func NewModels(db *sql.DB) Models {
|
||||
return Models{
|
||||
Channels: ChannelModel{DB: db},
|
||||
Users: UserModel{DB: db},
|
||||
Commands: CommandModel{DB: db},
|
||||
Timers: TimerModel{DB: db},
|
||||
}
|
||||
}
|
|
@ -1,185 +0,0 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type TimerModel struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func (t TimerModel) Get(name string) (*Timer, error) {
|
||||
query := `
|
||||
SELECT id, name, text, channel, repeat
|
||||
FROM timers
|
||||
WHERE name = $1
|
||||
`
|
||||
|
||||
var timer Timer
|
||||
|
||||
err := t.DB.QueryRow(query, name).Scan(
|
||||
&timer.ID,
|
||||
&timer.Name,
|
||||
&timer.Text,
|
||||
&timer.Channel,
|
||||
&timer.Repeat,
|
||||
)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, ErrRecordNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &timer, 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)
|
||||
RETURNING id;
|
||||
`
|
||||
|
||||
args := []interface{}{timer.Name, timer.Text, timer.Channel, timer.Repeat}
|
||||
|
||||
result, err := t.DB.Exec(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return ErrCommandRecordAlreadyExists
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
FROM timers
|
||||
ORDER BY id`
|
||||
|
||||
// Create a context with 3 seconds timeout.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Use QueryContext() the context and query. This returns a
|
||||
// sql.Rows resultset containing our channels.
|
||||
rows, err := t.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Need to defer a call to rows.Close() to ensure the resultset
|
||||
// is closed before GetAll() returns.
|
||||
defer rows.Close()
|
||||
|
||||
// Initialize an empty slice to hold the data.
|
||||
timers := []*Timer{}
|
||||
|
||||
// Iterate over the resultset.
|
||||
for rows.Next() {
|
||||
// Initialize an empty Channel struct where we put on
|
||||
// a single channel value.
|
||||
var timer Timer
|
||||
|
||||
// Scan the values onto the channel struct
|
||||
err := rows.Scan(
|
||||
&timer.ID,
|
||||
&timer.Name,
|
||||
&timer.Text,
|
||||
&timer.Channel,
|
||||
&timer.Repeat,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Add the single movie struct onto the slice.
|
||||
timers = append(timers, &timer)
|
||||
}
|
||||
|
||||
// When rows.Next() finishes call rows.Err() to retrieve any errors.
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return timers, nil
|
||||
}
|
||||
|
||||
func (t TimerModel) Update(timer *Timer) error {
|
||||
query := `
|
||||
UPDATE timers
|
||||
SET text = $2, channel = $3, repeat = $4
|
||||
WHERE name = $1
|
||||
RETURNING id`
|
||||
|
||||
args := []interface{}{
|
||||
timer.Name,
|
||||
timer.Text,
|
||||
timer.Channel,
|
||||
timer.Repeat,
|
||||
}
|
||||
|
||||
err := t.DB.QueryRow(query, args...).Scan(&timer.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return ErrEditConflict
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Prepare the statement.
|
||||
query := `
|
||||
DELETE FROM timers
|
||||
WHERE name = $1`
|
||||
|
||||
// Execute the query returning the number of affected rows.
|
||||
result, err := t.DB.Exec(query, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check how many rows were affected.
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want atleast 1, if it is 0 the entry did not exist.
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,296 +0,0 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
AddedAt time.Time `json:"-"`
|
||||
Login string `json:"login"`
|
||||
TwitchID string `json:"twitchid"`
|
||||
Level int `json:"level"`
|
||||
Location string `json:"location,omitempty"`
|
||||
LastFMUsername string `json:"lastfm_username,omitempty"`
|
||||
}
|
||||
|
||||
type UserModel struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// 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)
|
||||
ON CONFLICT (login)
|
||||
DO NOTHING
|
||||
RETURNING id, added_at;
|
||||
`
|
||||
|
||||
args := []interface{}{login, twitchId}
|
||||
|
||||
// Execute the query returning the number of affected rows.
|
||||
result, err := u.DB.Exec(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check how many rows were affected.
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return ErrUserAlreadyExists
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLocation searches the database for a record with the provided login value
|
||||
// and if that exists sets the location to the supplied
|
||||
func (u UserModel) SetLocation(twitchId, location string) error {
|
||||
query := `
|
||||
UPDATE users
|
||||
SET location = $2
|
||||
WHERE twitchId = $1`
|
||||
|
||||
result, err := u.DB.Exec(query, twitchId, location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check how many rows were affected.
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want atleast 1, if it is 0 the entry did not exist.
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLocation searches the database for a record with the provided login value
|
||||
// and if that exists sets the location to the supplied
|
||||
func (u UserModel) GetLocation(twitchId string) (string, error) {
|
||||
query := `
|
||||
SELECT location
|
||||
FROM users
|
||||
WHERE twitchid = $1`
|
||||
|
||||
var user User
|
||||
|
||||
err := u.DB.QueryRow(query, twitchId).Scan(
|
||||
&user.Location,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return "", ErrRecordNotFound
|
||||
default:
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return user.Location, nil
|
||||
}
|
||||
|
||||
// SetLocation searches the database for a record with the provided login value
|
||||
// and if that exists sets the location to the supplied
|
||||
func (u UserModel) SetLastFM(login, lastfmUser string) error {
|
||||
query := `
|
||||
UPDATE users
|
||||
SET lastfm_username = $2
|
||||
WHERE login = $1`
|
||||
|
||||
result, err := u.DB.Exec(query, login, lastfmUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check how many rows were affected.
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want atleast 1, if it is 0 the entry did not exist.
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLocation searches the database for a record with the provided login value
|
||||
// and if that exists sets the location to the supplied
|
||||
func (u UserModel) GetLastFM(login string) (string, error) {
|
||||
query := `
|
||||
SELECT lastfm_username
|
||||
FROM users
|
||||
WHERE login = $1`
|
||||
|
||||
var user User
|
||||
|
||||
err := u.DB.QueryRow(query, login).Scan(
|
||||
&user.LastFMUsername,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return "", ErrRecordNotFound
|
||||
default:
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return user.LastFMUsername, nil
|
||||
}
|
||||
|
||||
// SetLocation searches the database for a record with the provided login value
|
||||
// and if that exists sets the location to the supplied
|
||||
func (u UserModel) GetLevel(twitchId string) (int, error) {
|
||||
query := `
|
||||
SELECT level
|
||||
FROM users
|
||||
WHERE twitchid = $1`
|
||||
|
||||
var user User
|
||||
|
||||
err := u.DB.QueryRow(query, twitchId).Scan(
|
||||
&user.Level,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return -1, ErrRecordNotFound
|
||||
default:
|
||||
return -1, err
|
||||
}
|
||||
}
|
||||
|
||||
return user.Level, nil
|
||||
}
|
||||
|
||||
// Setlevel searches the database for a record with the provided login value
|
||||
// and if that exists sets the level to the supplied level value.
|
||||
func (u UserModel) SetLevel(login string, level int) error {
|
||||
query := `
|
||||
UPDATE users
|
||||
SET level = $2
|
||||
WHERE login = $1`
|
||||
|
||||
// err := u.DB.QueryRow(query, args...).Scan(&user)
|
||||
result, err := u.DB.Exec(query, login, level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check how many rows were affected.
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want atleast 1, if it is 0 the entry did not exist.
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
FROM users
|
||||
WHERE login = $1`
|
||||
|
||||
var user User
|
||||
|
||||
err := u.DB.QueryRow(query, login).Scan(
|
||||
&user.ID,
|
||||
&user.AddedAt,
|
||||
&user.Login,
|
||||
&user.TwitchID,
|
||||
&user.Level,
|
||||
&user.Location,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, ErrRecordNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Check checks the database for a record with the given login name.
|
||||
func (u UserModel) Check(twitchId string) (*User, error) {
|
||||
query := `
|
||||
SELECT id, login
|
||||
FROM users
|
||||
WHERE twitchid = $1`
|
||||
|
||||
var user User
|
||||
|
||||
err := u.DB.QueryRow(query, twitchId).Scan(
|
||||
&user.ID,
|
||||
&user.Login,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, ErrRecordNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Delete searches the database for a value with the supplied login name and if
|
||||
// one exists deletes the record, returning any errors that might occur.
|
||||
func (u UserModel) Delete(login string) error {
|
||||
// Prepare the statement.
|
||||
query := `
|
||||
DELETE FROM users
|
||||
WHERE login = $1`
|
||||
|
||||
// Execute the query returning the number of affected rows.
|
||||
result, err := u.DB.Exec(query, login)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check how many rows were affected.
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want atleast 1, if it is 0 the entry did not exist.
|
||||
if rowsAffected == 0 {
|
||||
return ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
func Time(t time.Time) string {
|
||||
return humanize.Time(t)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE IF EXISTS channels;
|
|
@ -1,13 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id bigserial PRIMARY KEY,
|
||||
added_at timestamp(0) with time zone NOT NULL DEFAULT NOW(),
|
||||
login text UNIQUE NOT NULL,
|
||||
twitchid text NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO channels (added_at,login,twitchid) VALUES
|
||||
(NOW(),'nourylul','31437432'),
|
||||
(NOW(),'nourybot','596581605'),
|
||||
(NOW(),'uudelleenkytkeytynyt','465178364'),
|
||||
(NOW(),'xnoury','197780373');
|
||||
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE IF EXISTS users;
|
|
@ -1,19 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS users (
|
||||
id bigserial PRIMARY KEY,
|
||||
added_at timestamp(0) with time zone NOT NULL DEFAULT NOW(),
|
||||
login text UNIQUE NOT NULL,
|
||||
twitchid text NOT NULL,
|
||||
level integer,
|
||||
location text,
|
||||
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';
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE IF EXISTS commands;
|
|
@ -1,39 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS commands (
|
||||
id bigserial PRIMARY KEY,
|
||||
name text UNIQUE NOT NULL,
|
||||
text text NOT NULL,
|
||||
category text NOT NULL,
|
||||
level integer NOT NULL,
|
||||
help text NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO commands (name,"text","category","level","help") VALUES
|
||||
('repeat','xset r rate 175 50','default',0,'Command to set my keyboard repeat rate'),
|
||||
('xset','xset r rate 175 50','default',0,'Command to set my keyboard repeat rate'),
|
||||
('eurkey','setxkbmap -layout eu','default',0,'Command to enable the EURKey keyboard layout'),
|
||||
('clueless','ch02 ch21 ch31','default',0,'Clueless'),
|
||||
('justinfan','64537','default',0,'pajaDink :tf:'),
|
||||
('kek','lmao','default',0,'kek'),
|
||||
('lmao','kek','default',0,'lmao'),
|
||||
('streamlink','https://haste.zneix.eu/udajirixep put this in ~/.config/streamlink/config on Linux (or %appdata%\streamlink\streamlinkrc on Windows)','default',0,'Returns a optimized streamlink config for Twitch.'),
|
||||
('gyazo','Gyazo is the worst screenshot uploader in human history. At best, it’s inconvenient, slow, and missing features: at worst, it’s a bandwidth-draining malware risk for everyone who views your images. There is absolutely no reason to use it unless you’re too lazy to spend 5 minutes installing another program.','pasta',250,'Dumb copy pasta about gyazo being garbage.'),
|
||||
('arch','Your friend isnt wrong. Being on the actual latest up to date software, having a single unified community repository for out of repo software (AUR) instead of a bunch of scattered broken PPAs for extra software, not having so many hard dependencies that removing GNOME removes basic system utilities, broader customization support and other things is indeed, pretty nice.','pasta',250,'Copy pasta about arch having the superior package manager.'),
|
||||
('arch2','One time I was ordering coffee and suddenly realised the barista didnt know I use Arch. Needless to say, I stopped mid-order to inform her that I do indeed use Arch. I must have spoken louder than I intended because the whole café instantly erupted into a prolonged applause. I walked outside with my head held high. I never did finish my order that day, but just knowing that everyone around me was aware that I use Arch was more energising than a simple cup of coffee could ever be.','pasta',250,'Copy pasta about arch linux users.'),
|
||||
('feelsdankman','⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⢀⣾⣿⣿⣿⣿⣷⣄⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⠟⢁⣴⣿⣿⣿⣿⣿⣿⣿⣿⣦⡈⢻⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⡿⠁⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠙⢿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⡿⠃⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡈⢻⣿⣿⣿⣿ ⣿⣿⣿⣿⡿⢁⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠹⣿⣿⣿ ⣿⣿⣿⣿⠁⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠿⠿⠿⠆⠘⢿⣿ ⣿⣿⠟⠉⠄⠄⠄⠄⢤⣀⣦⣤⣤⣤⣤⣀⣀⡀⠄⠄⡀⠄⠄⠄⠄⠄⠄⠄⠙ ⣿⠃⠄⠄⠄⠄⠄⠄⠙⠿⣿⣿⠋⠩⠉⠉⢹⣿⣧⣤⣴⣶⣷⣿⠟⠛⠛⣿⣷ ⠇⠄⠄⠄⠄⠄⠄⠄⠄⠄⠁⠒⠄⠄⠄⠄⠈⠉⠛⢻⣿⣿⢿⠁⠄⠄⠁⠘⢁ ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣂⣀⣐⣂⣐⣒⣃⠠⠥⠤⠴⠶⠖⠦⠤⠖⢂⣽ ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠛⠂⠐⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣠⣴⣶⣿⣿ ⠃⣠⣄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣠⣤⣄⠚⢿⣿⣿⣿⣿ ⣾⣿⣿⣿⣶⣦⣤⣤⣄⣀⣀⣀⣀⣀⣀⣠⣤⣤⣶⣿⣿⣿⣿⣷⡄⢻⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠈⣿⣿⣿','ascii',500,'Posts a FeelsDankMan ascii'),
|
||||
('dankhug','⣼⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣾⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀ ⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀ ⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀ ⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠺⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠟⠛⠛⠀⠀ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠆⠒⠀⠀⠶⢶⣶⣶⣭⣤⠹⠟⣛⢉⣉⣉⣀⣀ ⣿⣿⣾⣿⣶⣶⣶⣶⣶⣶⣿⣿⣶⠀⢬⣒⣂⡀⠀⠀⠀⠀⣈⣉⣉⣉⣉⡉⠅ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⢭⣭⠭⠭⠉⠠⣷⣆⣂⣐⣐⣒⣒⡈ ⢿⣿⣿⣿⠋⢁⣄⡈⠉⠛⠛⠻⡿⠟⢠⡻⣿⣿⣛⣛⡋⠉⣀⠤⣚⠙⠛⠉⠁ ⠀⠙⠛⠛⠀⠘⠛⠛⠛⠛⠋⠀⠨⠀⠀⠀⠒⠒⠒⠒⠒⠒⠒⠊⡀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠰⣾⣿⣿⣷⣦⠀⠀⠀⠀⠀⠀⠀⢀⣠⠴⢊⣭⣦⡀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⣠⣌⠻⣿⣿⣷⣞⣠⣖⣠⣶⣴⣶⣶⣶⣾⣿⣿⣿⣿⡀⠀ ⠀⢀⣀⣀⣠⣴⣾⣿⣿⣷⣌⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⢻⣿⣷⡁ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢻⣶⣬⣭⣉⣛⠛⠛⢛⣛⣉⣭⣴⣾⣿⣿⣿⡇','ascii',500,'Posts a dankHug ascii'),
|
||||
('shotgun','⡏⠛⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣧⣀⡀⠄⠹⠟⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣧⠄⢈⡄⣄⠄⠙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⠄⢸⣧⠘⢹⣦⣄⠈⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⠄⢸⣿⡇⢸⣿⣿⣿⣶⣄⠉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⠄⢸⣿⣷⠄⣿⣿⣿⣿⣿⣷⣦⡈⣙⠟⠉⠉⠙⠋⠉⠹⣿⣿ ⣿⣿⣿⠄⢸⣿⣿⡄⠸⣿⣿⣿⣿⣿⣿⡿⠃⠄⠄⣀⠄⢠⣀⠄⡨⣹ ⣿⣿⣿⠄⢸⣿⣿⣇⠄⠹⣿⣿⣿⣿⣿⠁⠄⠄⠄⠈⠄⠄⠄⠄⠠⣾ ⣿⣿⣿⠄⠈⣿⣿⣿⣆⠄⠈⠛⠿⣿⣿⠄⠄⠄⠄⠄⠄⠄⠄⠄⢀⣿ ⣿⣿⣿⠄⠄⣿⣿⣿⣿⣦⣀⠄⠄⠈⠉⠄⠄⠄⠄⠄⠄⠄⠤⣶⣿⣿ ⣿⣿⣿⠄⠄⢻⣿⣿⣿⣿⣿⠷⠂⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠘⣿⣿ ⣿⣿⣿⣇⠄⠈⠻⣿⣿⠟⠁⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢸⣿ ⣿⣿⣿⣿⣦⠄⠄⠈⠋⠄⠄⣠⣄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣼⣿','ascii',500,'Posts an ascii of pepe sucking on a shotgun.'),
|
||||
('rope','⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⡇⠄⣿⣿⣿⡿⠟⠋⣉⣉⣉⡙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⠃⠄⠹⠟⣡⣶⡿⢟⣛⣛⡻⢿⣦⣩⣤⣤⣤⣬⡉⢻⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⠄⢀⢤⣾⣿⣿⣿⣿⡿⠿⠿⠿⢮⡃⣛⣛⡻⠿⢿⠈⣿⣿⣿⣿⣿⣿⣿ ⣿⡟⢡⣴⣯⣿⣿⣿⣉⠤⣤⣭⣶⣶⣶⣮⣔⡈⠛⠛⠛⢓⠦⠈⢻⣿⣿⣿⣿⣿ ⠏⣠⣿⣿⣿⣿⣿⣿⣿⣯⡪⢛⠿⢿⣿⣿⣿⡿⣼⣿⣿⣿⣶⣮⣄⠙⣿⣿⣿⣿ ⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣾⡭⠴⣶⣶⣽⣽⣛⡿⠿⠿⠿⠿⠇⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣝⣛⢛⡛⢋⣥⣴⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⢿⠱⣿⣿⣛⠾⣭⣛⡿⢿⣿⣿⣿⣿⣿⣿⣿⡀⣿⣿⣿⣿⣿⣿⣿ ⠑⠽⡻⢿⣿⣮⣽⣷⣶⣯⣽⣳⠮⣽⣟⣲⠯⢭⣿⣛⣛⣿⡇⢸⣿⣿⣿⣿⣿⣿ ⠄⠄⠈⠑⠊⠉⠟⣻⠿⣿⣿⣿⣿⣷⣾⣭⣿⣛⠷⠶⠶⠂⣴⣿⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠄⠄⠄⠄⠄⠁⠙⠒⠙⠯⠍⠙⢉⣉⣡⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿','ascii',500,'Posts an ascii of pepe roping out of desperation'),
|
||||
('porosad','⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⢀⣠⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠒⣛⠐⣠⣾⣿⣤⣾⣿⣿⣿⣿⣷⣠⣤⣤⢀⣀⣖⠒⠒⠀⠀⠀⠀⠀ ⠀⠀⠀⢀⣘⣘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣬⡕⠀⠀⠀⠀⣀⠠⠀⠀ ⠀⣠⣾⣿⣿⣿⣿⠿⠿⣿⣿⣿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀ ⠐⢛⣿⡟⠋⢉⠉⠀⠔⣿⣿⣿⠆⠀⠩⡉⠙⢛⣿⠿⠿⠿⢛⣃⣀⡀⠀⠀⠀ ⠀⣾⣟⠁⢤⣀⣔⣤⣼⣿⣿⣿⣆⣀⠂⠴⣶⡁⠸⣿⣶⣾⣿⣿⣿⣿⠷⠂⠀ ⠀⣿⡿⡿⣧⣵⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣝⡛⣸⣿⣿⣿⣿⣿⣿⣷⡀⠀ ⠀⢿⢃⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠸⣿⣿⣿⣿⣿⣿⣿⡇⠀ ⠀⠻⢸⣿⣿⣿⣿⣿⣿⠋⠻⣿⣿⡻⢿⣿⣿⣿⣿⡆⣿⣿⣿⠿⣿⠟⢿⡇⠀ ⠀⠠⣸⣿⣿⠟⣩⣈⣩⣴⣶⣌⣁⣄⡉⠻⣿⣿⣿⣧⢸⣿⣧⣬⣤⣤⣦⣤⠀ ⠀⠀⠸⢫⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⢻⡇⣼⣿⣿⣿⣿⣿⣿⡟⠀ ⠀⠀⠀⠈⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣱⣿⣿⣿⣿⣿⣿⡟⠀⠀ ⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀ ⠀⠀⠀⠀⠀⠈⠉⠉⠛⠛⠛⠛⠛⠛⠉⠉⠉⠛⠛⠉⠁⠀⠀⠈⠉⠁⠀⠀⠀ ','ascii',500,'Posts a PoroSad ascii.. Why is he always sad? PoroSad'),
|
||||
('toucan','░░░░░░░░▄▄▄▀▀▀▄▄███▄░░░░░░░░░░ ░░░░░▄▀▀░░░░░░░▐░▀██▌░░░░░░░░░ ░░░▄▀░░░░▄▄███░▌▀▀░▀█░░░░░░░░░ ░░▄█░░▄▀▀▒▒▒▒▒▄▐░░░░█▌░░░░░░░░ ░▐█▀▄▀▄▄▄▄▀▀▀▀▌░░░░░▐█▄░░░░░░░ ░▌▄▄▀▀░░░░░░░░▌░░░░▄███████▄░░ ░░░░░░░░░░░░░▐░░░░▐███████████ ░░░░░le░░░░░░░▐░░░░▐███████████ ░░░░toucan░░░░░░▀▄░░░▐██████████ ░░░░has arrived░░░░░░▀▄▄███████████','ascii',500,'le budget toucan has arrived <(*)'),
|
||||
('harrypottah','⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠋⣉⣠⣤⣤⣴⣦⣤⡈⠛⢿⣿⣿⣿⣿ ⣿⣿⠋⢉⣉⣉⡉⠛⠛⠟⠉⣠⣄⣚⡛⠿⢿⣿⣿⣿⣿⡛⢿⣧⠄⢿⣿⣿⣿ .⣿⣿⣄⠘⢿⣿⣿⣿⣿⣶⣶⣦⣬⣍⣙⠛⢶⣾⣿⣿⣿⠇⠈⢻⠄⢸⣿⣿⣿ ⣿⣿⣿⣶⣄⣉⠛⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⡛⠂⠄⣦⣤⣾⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⠛⠒⠂⠄⠄⠈⣉⣉⣉⣙⣛⣛⠛⠿⠿⠷⣦⣄⡈⠛⢿⣿⣿ ⣿⣿⣿⣿⣿⣿⠄⠄⢀⣶⣿⣿⣿⣿⣿⣯⣿⣿⣿⣿⣷⣶⠄⢀⣈⠄⠄⣿⣿ ⣿⣿⣿⣿⣿⣿⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢏⣴⣾⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⠄⢀⣾⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⣧⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⠄⠄⠻⢿⣿⡟⠋⠉⡁⠄⢉⡢⠿⠿⣫⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⢻⣿⣦⣬⣉⣛⣿⠛⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⠸⣿⣛⣻⣿⡟⠁⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⡟⢻⢻⡿⢻⡟⠛⢿⠛⠻⡟⠿⢻⣿⠛⠻⡿⠛⢿⠛⠛⠛⠛⢻⠛⣿⠛⡟⣿ ⡇⢈⢸⠇⠈⡇⠄⣸⠄⢀⣷⠄⣾⣿⠄⣀⡇⣶⢸⡇⢸⣿⢸⡿⠄⢹⠄⡁⣿ ⣧⣼⣼⣤⣧⣥⣤⣼⣤⣤⣿⣤⣿⣿⣤⣿⣧⣀⣼⣧⣼⣿⣼⣧⣼⣼⣤⣧⣿','ascii',500,'Posts forsens Harry potter ascii'),
|
||||
('borgir','⣿⣿⣿⣿⠄⠄⠄⠄⠄⠄⠄⠄⣀⡀⠄⠄⠄⣀⣀⡀⠄⠄⠄⠄⢹⣿⣿⣿⣿ ⣿⣿⣿⣿⠄⠄⠄⠄⢀⣴⣾⣿⣿⡿⠿⠾⣿⣿⣿⣿⣿⣦⡄⠄⠄⠉⠙⠛⢿ ⣿⣿⣿⡿⠑⠢⠄⢠⣿⣿⣿⣛⠻⠷⠦⠄⢈⣿⠋⠉⡉⠉⢡⠄⣤⣤⣤⠄⠘ ⣿⣿⣿⣷⠶⣤⣰⣾⣿⣿⣿⣿⣷⣶⣶⣾⣿⣿⣄⡉⠁⣠⡽⠈⠉⠉⠉⠄⣰ ⣿⣿⠟⠋⡞⠛⠋⢿⣿⣿⣿⣿⣿⣿⣷⣥⣍⣉⣹⣿⣿⣿⠁⣤⣤⣤⣴⣾⣿ ⠄⠞⠓⠂⠄⠂⠄⠄⠄⠈⠉⢛⣋⣀⣀⠄⠈⠉⠛⠻⣿⣿⣤⣾⡿⠏⠉⠈⣿ ⠄⠄⠄⠄⠄⢀⣤⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣦⠄⠄⠙⠓⠄⢛⣆⠄⠄⠈ ⠄⠄⠄⣤⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠉⣠⣄⠄⠄⢠⣭⣭⣷⣤⣄ ⠄⠄⣸⣿⣿⣿⣿⣿⣿⣿⡿⠿⠟⠛⠋⠉⠄⠐⠚⠛⢉⠄⣴⣶⣾⡛⠿⣿⣿ ⢀⣾⣯⣉⣀⡀⠄⠄⠄⠄⠄⢀⣀⣀⣀⣠⡤⠴⠶⠾⣟⠹⠏⠛⠛⠛⠄⠙⠿ ⡿⠿⠿⠿⠿⠿⠿⣶⣦⣭⣭⣽⠏⠁⠄⠄⠄⠄⢤⣶⣶⡘⠚⠄⠄⠄⠄⠄⠄ ⣿⣿⠉⡙⠛⣿⡟⠛⠙⠻⣿⡏⢉⠛⢿⣿⠛⢋⠛⣿⣿⠉⣿⡏⢉⠛⢿⣿⣿ ⣿⣿⠄⣅⠐⣿⠄⢾⡿⠄⣿⡇⠈⠁⣼⡇⠰⣏⠉⣿⣿⠄⣿⡇⠈⠁⣼⣿⣿ ⣿⣿⣤⣥⣤⣿⣧⣤⣤⣼⣿⣧⣼⣤⣼⣿⣤⣬⣤⣿⣿⣤⣿⣧⣼⣤⣼⣿⣿','ascii',500,'Posts a Borgir ascii'),
|
||||
('dankwave','FeelsDankMan PowerUpR DANK WAVE▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂','ascii',500,'dank wave coming through'),
|
||||
('forsenhead','⠄⠄⠄⠄⠄⠄⢠⣤⣀⣤⣤⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠄ ⠄⠄⠄⠄⠄⠔⣺⣿⣿⣿⣿⣿⢿⡉⠁⠉⠉⠛⢿⣿⣿⣿⣿⣿⣿⣿⡷⡀ ⠄⠄⠄⠄⣀⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣷⣿⣿⣿⣿⣿⠟⠛⡇⣥ ⠄⠄⠄⠄⣿⣿⣿⣿⣿⣿⣟⠝⠻⠿⣿⣿⣟⣹⣿⣿⣿⣿⣿⣿⣿⣶⣿⣿ ⠄⠄⠄⣰⣻⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣶⣿⣿⢿⣸⣿⡟⠿⣿⣿⣿⣿⣿ ⠄⠄⠄⢼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣫⣾⣿⣿⣿⣷⣬⣿⣿⣿⣿ ⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⢩⣽⣿⣹⣿⣿⣿⣿⣿⣿⣿⣿ ⠄⠄⢿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠿⠛⠄⠩⠛⠛⠿⢿⣿⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠙⠻⢟⣿⣿⡿⠉⠄⠄⠄⠄⠄⠄⠄⠄⠄⠴⠞⢻⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠄⠄⠙⠿⠟⠄⠄⠄⠄⢠⣤⣤⣠⣠⣄⡂⠄⢀⣼⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠄⠄⠄⠄⠄⣾⣧⡀⢀⡀⠈⠉⠻⢿⡟⠁⣷⣿⣿⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠄⠄⠄⠄⠄⣿⢿⢿⣿⣷⣶⣶⣤⣿⢡⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠄⠄⠄⠄⢰⣿⠄⠙⠿⣿⣿⣿⣿⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠄⠄⠄⠄⠄⠙⠄⠄⠄⠄⠄⡻⠇⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿','ascii',500,'Posts a forsenHead ascii'),
|
||||
('hewillnever','⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣏⡛⢿⣿⣿⣿⣿⣿⣿⡙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣦⣉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣯⣍⣙⡛⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣶⣶⣬⣿⣿⣿⡿⠛⠛⠛⣛⣋⣙⠛⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠃⠰⢾⣶⣧⣴⢦⣌⣶⣦⠘⣿⣿⣿⣿⣿⣿⣿ ⠿⠿⠿⠿⠿⠿⢻⣿⣯⠄⠠⠶⠆⢸⠋⠠⣦⠄⢹⣿⣿⠄⣿⣿⣿⣿⣿⣿⣿ ⣾⣿⣿⣿⣿⣿⣿⣿⡟⢠⣶⣶⣶⣿⣶⣬⣉⣀⠾⢿⣿⡄⠛⠛⠻⢿⣿⣿⣿ ⣿⢿⣿⣿⠛⠿⠿⠉⠁⠘⠻⠯⠄⢹⣛⣿⣟⣩⣷⣦⢹⡇⣸⣷⠄⠄⣀⠙⠛ ⢓⣚⡃⣩⣭⠄⣀⣀⢀⠄⠄⢐⠙⠊⣿⣶⣒⣿⡟⢋⡼⢁⣿⣿⠄⠄⠘⠗⠄ ⢚⣛⣫⣿⣽⣿⣽⠗⢺⢖⣴⣄⡙⠛⠛⠛⠛⠛⢛⣉⣤⣾⣿⣿⠄⢸⡄⠸⣿ ⣿⣿⣯⣟⣛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠸⣷⠄⠻ ⣿⣟⣷⣿⣿⠷⠾⠿⠿⠿⠿⠿⠟⠛⠛⠛⠛⠻⠿⠛⠋⠉⠉⠁⠄⠄⠁⠄⠄ ⢉⣁⣠⣤⣤⣤⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⡶⠒⠄⠄⢤⣀⣀⣀⣀⣀⠄⠄⠄','ascii',500,'Posts a he will never ascii'),
|
||||
('mylifeisgazatu','⠀⠀⠀⠀⠀⣠⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣿⣷⡀⠀⠀⠀⠀ ⠀⠀⠀⠀⣾⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀ ⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀ ⠀⠀⠘⠛⣛⣛⣻⣿⡛⠛⠛⠉⠁⠀⠀⠀⠀⠛⢛⣛⣛⣿⣟⠛⠛⠋⠉⠀⠀ ⠀⠀⠀⠀⠙⠻⠉⠀⠹⢷⣶⡿⠉⠹⠇⠀⠀⠀⠈⠛⠏⠁⠈⠿⣶⣾⠏⠉⠿ ⠀⠀⠀⠀⠀⢠⡤⠤⠤⠀⠉⠒⠔⠂⠀⠀⠀⠀⠀⠀⣤⠤⠤⠀⠈⠑⠢⠒⠀ ⢠⣀⠀⠀⠀⠀⠀⠠⠀⢀⣠⣀⠀⠀⠀⣄⡀⠀⠀⠀⠀⠀⠀⠀⣀⣄⡀⠀⠀ ⣿⣿⣿⣿⣶⣶⣶⣶⣿⣿⣿⣿⡆⠀⢸⣿⣿⣿⣷⣶⣶⣶⣾⣿⣿⣿⣷⠀⠀ ⠀⣀⡀⠀⡀⠀⡀⢀⡀⠀⠀⢰⠀⢰⡆⢰⣞⠂⠀⡀⠀⠀⠀⣶⠀⠀⡀⠀⠀ ⠀⣿⢻⡟⣿⠀⢿⣸⠇⠀⠀⢸⠀⢸⡇⢸⡏⠀⣿⠽⠇⠀⠀⣿⠀⠾⣽⡁⠀ ⠀⠛⠘⠃⠛⠀⢸⡟⠀⠀⠀⠘⠂⠘⠃⠘⠃⠀⠛⠛⠃⠀⠀⠛⠀⠛⠚⠃⠀ ⠀⢠⣤⣦⡄⠀⢠⣶⡀⠀⠰⠶⣶⠆⠀⢰⣦⠀⠀⠶⣶⠶⠀⢰⡆⠀⣦⠀⠀ ⠀⣿⡱⢶⡆⠀⣾⣿⣧⠀⢀⣼⠋⠀⢀⣿⣿⡆⠀⠀⣿⠀⠀⢸⡇⢀⡿⠀⠀ ⠀⠘⠛⠛⠃⠘⠋⠀⠛⠀⠚⠓⠒⠂⠘⠃⠈⠛⠀⠀⠛⠀⠀⠈⠛⠛⠃⠀⠀','ascii',500,'Probably the worlds smolest ascii miniDank'),
|
||||
('ohno','⣿⣿⣿⠋⡡⢲⣶⣮⣙⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⢉⣠⣶⣿⠆⣿⣿ ⣿⣿⠇⡔⠄⣿⣿⣿⣿⣆⠟⡛⠋⠉⠉⢉⠍⣟⡩⡅⢀⣴⣿⣿⣿⡟⣼⣿⣿ ⡿⠋⠰⡧⡀⣿⣿⠿⠛⠉⠄⠄⣠⣴⣶⣶⣿⣿⣷⣦⣿⣿⣿⣿⡟⣾⣿⣿⣿ ⢁⠂⠄⣳⠇⠈⠁⠄⠄⠄⣠⣴⣶⣿⣿⢿⣿⣿⣿⣿⣿⣟⢹⡽⢸⣿⣿⣿⣿ ⠃⠄⠠⠿⠆⠄⠄⠄⠄⢀⣿⠟⢥⣾⡿⣿⣿⠿⢿⣿⣿⠿⡘⢷⡜⢿⣿⣿⣿ ⠄⠄⣤⣶⠆⠄⠄⠄⢀⣾⣿⣸⣽⣝⠁⣾⡹⡧⢿⣽⣿⣦⣄⠹⣿⣄⢻⣿⣿ ⠄⠄⣿⣿⠄⠄⠄⢀⣼⣿⡟⣿⣿⣿⡀⣿⣿⣷⢸⣿⣿⣿⣿⡷⠈⣿⡄⣿⣿ ⠄⠄⠈⠄⠄⢀⣴⣿⣿⣿⠇⠉⠄⠈⠱⣿⡿⣇⣼⡟⠉⠉⠉⢿⠄⣿⣷⢹⣿ ⠄⣠⣴⣦⣴⣿⣿⣿⣿⠏⡄⠄⠄⠄⠄⣿⣯⣡⣿⡅⠄⠄⣸⠏⢰⣿⣿⡆⣿ ⣿⣿⣿⣿⣿⣿⡿⠛⢑⣻⣿⣶⣶⣶⠂⠙⠛⠛⡟⠳⣶⣾⡟⠄⢸⣿⣿⡇⣿ ⣿⣿⣿⣿⣿⡏⠄⠄⠉⠛⠿⣿⠿⠋⢶⣤⠄⠄⢁⣴⣿⣿⠁⠄⣸⣿⣿⢃⣿ ⣿⣿⣿⣿⣿⣷⠄⠄⠄⡠⣞⢴⣾⣽⣽⣿⡿⠄⣾⣿⣿⠃⠄⠄⣿⣿⣿⢸⣿ ⣿⣿⣿⡿⠟⠋⠐⣤⣞⣁⠄⠁⠉⠐⠛⠉⠄⠄⠈⠉⠁⠄⠄⠄⠘⣿⣿⡈⢿ ⣿⠟⠋⠄⠄⠄⠄⠄⠙⠋⠰⠄⢀⡀⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠙⣿⣿⣄','ascii',500,'Posts a Ohno ascii'),
|
||||
('weirddoc','⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠹⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⣰⣆⠀⠀⠀⣤⠀⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⢛⣟⣛⣁⠀⠀⣿⣿⡄⠀⢸⣿⡄⠀⢀⣭⣭⣭⡛⢿⣿⣿⣿ ⣿⣿⣿⣿⡟⣱⣿⣿⣿⣿⣤⣀⡟⠙⠀⠀⠘⢹⣧⣤⣿⣿⣿⣿⠿⠎⠙⣿⣿ ⣿⣿⣿⣿⣷⣮⡛⢶⣮⣭⣿⣿⣿⣿⣷⣶⣿⣿⣟⣛⣩⣥⣴⡶⣢⣴⣾⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣶⣾⣭⣟⣻⠿⢿⣿⣿⣿⡟⣛⣿⣭⣿⣶⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠂⠈⢹⣿⣿⣧⣈⡁⠈⠉⠉⠉⠉⠉⠛⠛⠿ ⣿⣿⣿⠿⠿⠿⠋⠉⠁⠁⢀⣠⣶⣿⣿⣿⣿⣿⣿⠙⠛⠻⠶⠦⢄⠀⠀⠀⠀ ⠁⠁⠀⠀⠀⢀⣠⣴⠶⠟⠛⠉⠉⢠⣿⣿⣿⣿⣿⡝⣷⣾⣶⠆⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠒⣉⣁⣤⣶⣴⠀⠀⠀⣸⣿⣿⣿⣿⣿⣇⢺⣿⠏⠀⠀⠀⠀⣠⣾ ⣷⣤⠀⠀⠀⠈⠻⣿⣿⠟⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⡄⣏⠀⠀⠀⣰⣾⣿⣿ ⣿⣿⣷⣤⡀⠀⠀⠈⠁⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣟⡟⠿⣿⣿ ⣿⣿⣾⣿⣿⣿⣶⡶⠂⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢋⣼⣿ ⣿⣿⣿⡜⣾⣿⣿⡁⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠩⣛⣛⣛⣴⣿⣿⣿','ascii',500,'Posts a WeirdDoc ascii'),
|
||||
('weirddude','⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣿⣿⠀⠀⠀⠀⠀⠀⣀⣴⣾⣿⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⢠⢞⣻⡿⢋⣉⡀⠀⠀⢀⣠⣬⠽⠿⣟⡻⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⢀⣵⣿⣿⣾⣿⣿⠿⣷⣶⣜⢭⣶⣾⣿⣷⠾⡥⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣟⣫⡽⣟⡫⠿⢯⣥⣘⠋⣪⠶⠾⢿⣷⣔⣂⠀⠀ ⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣭⢕⡾⣿⣄⣀⣠⣿⡿⠿⣿⣤⣠⠴⠿⢿⡛⠁⠀ ⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣷⣤⣴⣤⣍⡙⣩⣴⡾⣷⣶⣶⡿⠛⠉⠀⠀⠀ ⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠭⠷⣿⣿⣿⣿⣶⣶⣶⣶⣿⡶⠀⠀⠀ ⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⢰⣞⣛⣛⣒⣀⡀⠭⠭⠭⠥⠤⠤⢐⣛⠁⠀ ⠀⠀⠀⠀⠈⠛⠿⠿⢿⣿⣷⣝⣶⣶⣶⣶⣶⣶⣦⣭⣭⣭⣭⠭⠉⠉⠀⠀⠀ ⠀⠀⣠⣤⣤⣤⣤⣤⣤⣌⠻⣿⣿⣿⣿⣿⣿⣿⡿⠿⠟⢛⣥⣤⣤⣀⠀⠀⠀ ⢀⣿⣿⣿⠁⢸⣿⣿⣿⣿⣷⣶⣮⣭⣭⣭⣽⣷⣶⣶⣾⣿⣿⡏⠻⢿⣷⣆⠀ ⠀⠻⣿⣿⡀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⡹⠟⠁ ⠀⠀⠈⠛⠿⣦⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⣴⠞⠀⠀⠀ ⠀⠀⢈⣵⣦⠹⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⣵⣶⣤⠀⠀','ascii',500,'Posts a WeirdDude ascii'),
|
||||
('beefalarm','⡏⢀⣶⡶⠒⠒⢶⣶⡄⠄⣰⣶⠶⠶⠶⠂⢠⣶⡶⠶⠶⠖⠄⣴⣶⠶⠶⠶⠂ ⠁⣸⣿⣇⣀⣠⡞⠛⣡⢇⣿⣿⣄⣋⡉⡉⣸⣿⣧⣈⣁⢀⢠⣿⣿⣀⣀⡀⢻ ⠄⣿⣿⢡⣦⠈⣿⣷⠈⣸⣿⠏⢭⣿⠥⢃⣿⡿⠩⢭⡵⠎⣸⣿⠋⢉⠉⣡⣼ ⠘⠛⠛⠒⣒⣚⣛⣭⣆⠻⠛⢛⣒⣒⢀⠘⢛⠿⢓⡒⠂⣀⣛⡛⢸⣿⣿⣿⣿ ⠿⢿⣿⣿⣿⣿⣿⣿⠏⠾⠿⠿⠿⠿⠿⠿⠿⣿⣷⡝⢿⣿⣿⡿⠿⢿⠿⠯⠩ ⠾⠷⠮⣿⣿⣿⣿⡿⠄⠄⠄⠄⠄⠄⠄⠄⢸⣿⣿⡇⢸⣿⣿⣿⣫⣥⡄⠄⠄ ⣭⣟⣿⣿⣿⣿⣿⡇⠄⠄⠄⠄⠄⠄⠄⠄⢸⣿⣿⣇⢸⣿⣿⣿⣿⠷⢖⡀⠄ ⠷⠶⣿⣿⣿⣿⣿⠄⠄⠄⠄⣶⠄⠄⠄⢸⢸⣿⣿⣿⢸⣿⣿⣿⣿⣛⣛⠄⠄ ⢟⣻⣽⣿⣿⣿⡇⠄⠄⠄⢰⣿⠄⠄⠄⠸⢸⣿⣿⣿⠸⣿⣿⣿⡿⢿⣶⠄⠄ ⣭⣶⣶⣾⣿⣿⠄⣤⣤⣤⣬⣥⣤⣤⣤⣤⣼⣿⣿⣿⣤⣿⣿⣿⣿⣷⣮⣤⣠ ⣿⣿⡿⠿⠿⠃⢰⠿⢿⣿⣿⣿⡿⠿⠿⣿⣿⡿⠿⣿⡇⣿⠿⠿⢿⣿⠿⠿⠿ ⣾⡟⣰⠛⣇⠄⠄⣾⢰⣿⣿⡟⣱⠛⡇⣿⡏⣾⢫⡻⣦⠉⣼⢻⡀⡟⡰⢻⡇ ⠋⣰⡃⠂⣿⠄⢠⡟⠘⠛⠋⣱⡃⠃⣷⠙⢱⡷⢭⡜⠁⢀⡇⠄⡇⢠⠁⣸⠃ ⠰⠏⢉⠉⠻⠄⠸⠧⠤⠄⠰⠋⠉⠉⠿⠄⠼⠁⠄⠿⠈⠸⠁⠄⠷⠃⠄⠿⢀','ascii',500,'Posts a beef alarm ascii'),
|
||||
('feelswowman','⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣶⣶⣶⣦⣤⡀⠀⣀⣤⣤⣴⣤⣄⡀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⣰⣿⣿⠿⢟⣛⣛⣛⣛⠿⣎⢻⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⣼⣿⣿⡷⣺⣵⣾⡿⠟⠷⢿⣦⡐⠶⡶⣒⣦⣶⣴⣤⣆⣀⠀⠀ ⠀⠀⠀⣠⢸⣿⣿⣭⣾⣿⣿⣿⡅⠈⠐⠀⢻⣿⣷⣸⣿⣿⣿⠋⠀⠍⢻⣷⣄ ⠀⢀⣾⣧⣾⣿⣿⣎⠿⣿⣿⣿⣷⣦⣤⣴⣿⣿⣿⣿⣿⣿⣿⣦⣁⣀⣼⣿⡿ ⠀⣾⣿⣿⣿⣿⣿⣿⣷⣌⣙⠛⠿⢿⣿⣿⣿⣿⠿⣻⣿⣿⣿⣿⣿⣿⡿⠟⠁ ⢸⣿⣿⣿⣿⣿⠟⣛⠻⣿⣿⣿⣶⣦⣤⣤⣴⣾⣿⣷⣍⣙⠛⣛⣋⡉⠀⠀⠀ ⢸⣿⣿⣿⣿⣿⢸⣟⢷⣍⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀ ⢸⠏⡌⡟⡉⡿⡂⡙⢷⣌⠻⠶⣬⣝⡛⠿⠿⢿⣿⣿⣿⣿⣿⣿⡿⠿⢟⣀⠀ ⠸⢠⣧⣴⡇⢡⡇⣿⣦⣙⡻⠶⣶⣬⣙⣛⣓⠶⠶⠶⠶⠶⠶⠶⠶⢛⡛⣅⡀ ⠐⠘⣿⣿⣷⣿⣧⡙⠻⢿⣿⣷⣶⣤⣭⣍⣉⣛⣛⣛⣛⣛⣛⡛⢛⣛⠅⠋⠀ ⢲⣤⣘⠻⣿⣿⣿⣿⣿⣶⡌⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠭⠶⢟⡁⠀⠀⠀ ⢸⣿⣿⡷⢈⡻⠿⣿⡿⠿⢑⣒⣂⣦⣤⣄⣐⣒⣢⡾⣹⣿⣿⣿⣿⡇⠀⠀⠀ ⢸⣿⣿⢣⣿⣿⣷⣶⣶⡇⢿⣿⣿⣿⣿⣿⣿⣿⡿⢡⣿⣿⣿⣿⡿⠀⠀⠀⠀','ascii',500,'Posts a FeelsWowMan ascii');
|
||||
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE IF EXISTS timers;
|
|
@ -1,16 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS timers (
|
||||
id bigserial PRIMARY KEY,
|
||||
name 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');
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
# Migrations
|
||||
|
||||
Tool: [golang-migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate)
|
||||
|
||||
## Create Database
|
||||
```sql
|
||||
$ sudo -u postgres psql
|
||||
psql (14.3)
|
||||
Type "help" for help.
|
||||
|
||||
postgres=# CREATE DATABASE nourybot;
|
||||
CREATE DATABASE
|
||||
postgres=# \c nourybot;
|
||||
You are now connected to database "nourybot" as user "postgres".
|
||||
nourybot=# CREATE ROLE username WITH LOGIN PASSWORD 'password';
|
||||
CREATE ROLE
|
||||
nourybot=# CREATE EXTENSION IF NOT EXISTS citext;
|
||||
CREATE EXTENSION
|
||||
nourybot=#
|
||||
```
|
||||
|
||||
## Connect to Database
|
||||
```sh
|
||||
$ psql --host=localhost --dbname=nourybot --username=username
|
||||
psql (14.3)
|
||||
Type "help" for help.
|
||||
|
||||
nourybot=>
|
||||
```
|
||||
|
||||
## Apply migrations
|
||||
```sh
|
||||
$ migrate -path=./migrations -database="postgres://username:password@localhost/nourybot?sslmode=disable" up
|
||||
```
|
||||
|
||||
```sh
|
||||
$ migrate -path=./migrations -database="postgres://username:password@localhost/nourybot?sslmode=disable" down
|
||||
```
|
||||
|
||||
## Fix Dirty database
|
||||
```sh
|
||||
$ migrate -path=./migrations -database="postgres://username:password@localhost/nourybot?sslmode=disable" force 1
|
||||
```
|
Loading…
Reference in a new issue