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