mirror of
https://github.com/lyx0/nourybot.git
synced 2024-11-13 19:49:55 +01:00
add database models, add timer functionality
This commit is contained in:
parent
ec86f396bd
commit
b330d5aa56
|
@ -114,7 +114,16 @@ func (app *application) handleCommand(message twitch.PrivateMessage) {
|
||||||
case "xkcd":
|
case "xkcd":
|
||||||
reply, _ = commands.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)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the commandName exists as the "name" of a command in the database.
|
// Check if the commandName exists as the "name" of a command in the database.
|
||||||
// if it doesnt then ignore it.
|
// if it doesnt then ignore it.
|
||||||
// ##################
|
// ##################
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
_ "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/nicklaw5/helix"
|
"github.com/nicklaw5/helix"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ type application struct {
|
||||||
HelixClient *helix.Client
|
HelixClient *helix.Client
|
||||||
Log *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
|
||||||
}
|
}
|
||||||
|
@ -128,6 +129,8 @@ func main() {
|
||||||
HelixClient: helixClient,
|
HelixClient: helixClient,
|
||||||
Log: sugar,
|
Log: sugar,
|
||||||
Db: db,
|
Db: db,
|
||||||
|
Models: data.NewModels(db),
|
||||||
|
Scheduler: cron.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Received a PrivateMessage (normal chat message).
|
// Received a PrivateMessage (normal chat message).
|
||||||
|
@ -180,6 +183,12 @@ func main() {
|
||||||
"Database", db.Stats(),
|
"Database", db.Stats(),
|
||||||
"Helix", helixResp,
|
"Helix", helixResp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Load the initial timers from the database.
|
||||||
|
app.InitialTimers()
|
||||||
|
|
||||||
|
// Start the timers.
|
||||||
|
app.Scheduler.Start()
|
||||||
})
|
})
|
||||||
// Actually connect to chat.
|
// Actually connect to chat.
|
||||||
err = app.TwitchClient.Connect()
|
err = app.TwitchClient.Connect()
|
||||||
|
|
277
cmd/nourybot/timer.go
Normal file
277
cmd/nourybot/timer.go
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gempir/go-twitch-irc/v4"
|
||||||
|
"github.com/lyx0/nourybot/internal/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddTimer slices the message into relevant parts, adding the values onto a
|
||||||
|
// new data.Timer struct so that the timer can be inserted into the database.
|
||||||
|
func (app *application) AddTimer(name, repeat string, message twitch.PrivateMessage) {
|
||||||
|
cmdParams := strings.SplitN(message.Message, " ", 500)
|
||||||
|
// prefixLength is the length of `()add timer` plus +2 (for the space and zero based)
|
||||||
|
prefixLength := 13
|
||||||
|
|
||||||
|
// Split the message into the parts we need.
|
||||||
|
//
|
||||||
|
// message: ()addtimer sponsor 20m hecking love my madmonq pills BatChest
|
||||||
|
// parts: | prefix | |name | |repeat | <----------- text -------------> |
|
||||||
|
text := message.Message[prefixLength+len(name)+len(cmdParams[2]) : len(message.Message)]
|
||||||
|
|
||||||
|
// validateTimeFormat will be true if the repeat parameter is in
|
||||||
|
// the format of either 30m, 10h, or 10h30m.
|
||||||
|
validateTimeFormat, err := regexp.MatchString(`^(\d{1,2}[h])$|^(\d+[m])$|^(\d+[s])$|((\d{1,2}[h])((([0]?|[1-5]{1})[0-9])[m]))$`, repeat)
|
||||||
|
if err != nil {
|
||||||
|
app.Log.Errorw("Received malformed time format in timer",
|
||||||
|
"repeat", repeat,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timer := &data.Timer{
|
||||||
|
Name: name,
|
||||||
|
Text: text,
|
||||||
|
Channel: message.Channel,
|
||||||
|
Repeat: repeat,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the time string we got is valid, this is important
|
||||||
|
// because the Scheduler panics instead of erroring out if an invalid
|
||||||
|
// time format string is supplied.
|
||||||
|
if validateTimeFormat {
|
||||||
|
timer := &data.Timer{
|
||||||
|
Name: name,
|
||||||
|
Text: text,
|
||||||
|
Channel: message.Channel,
|
||||||
|
Repeat: repeat,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.Models.Timers.Insert(timer)
|
||||||
|
if err != nil {
|
||||||
|
app.Log.Errorw("Error inserting new timer into database",
|
||||||
|
"timer", timer,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
|
||||||
|
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// cronName is the internal, unique tag/name for the timer.
|
||||||
|
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor"
|
||||||
|
cronName := fmt.Sprintf("%s%s", message.Channel, name)
|
||||||
|
|
||||||
|
app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, cronName)
|
||||||
|
app.Log.Infow("Added new timer",
|
||||||
|
"timer", timer,
|
||||||
|
)
|
||||||
|
|
||||||
|
reply := fmt.Sprintf("Successfully added timer %s repeating every %s", name, repeat)
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.Log.Errorw("Received malformed time format in timer",
|
||||||
|
"timer", timer,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
reply := "Something went wrong FeelsBadMan received wrong time format. Allowed formats: 30m, 10h, 10h30m"
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditTimer just contains the logic for deleting a timer, and then adding a new one
|
||||||
|
// with the same name. It is technically not editing the timer.
|
||||||
|
func (app *application) EditTimer(name, repeat string, message twitch.PrivateMessage) {
|
||||||
|
// Check if a timer with that name is in the database.
|
||||||
|
app.Log.Info(name)
|
||||||
|
|
||||||
|
old, err := app.Models.Timers.Get(name)
|
||||||
|
if err != nil {
|
||||||
|
app.Log.Errorw("Could not get timer",
|
||||||
|
"timer", old,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
reply := "Something went wrong FeelsBadMan"
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Delete the old timer
|
||||||
|
// -----------------------
|
||||||
|
cronName := fmt.Sprintf("%s%s", message.Channel, name)
|
||||||
|
app.Scheduler.RemoveJob(cronName)
|
||||||
|
|
||||||
|
err = app.Models.Timers.Delete(name)
|
||||||
|
if err != nil {
|
||||||
|
app.Log.Errorw("Error deleting timer from database",
|
||||||
|
"name", name,
|
||||||
|
"cronName", cronName,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
|
||||||
|
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Add the new timer
|
||||||
|
// -----------------------
|
||||||
|
cmdParams := strings.SplitN(message.Message, " ", 500)
|
||||||
|
// prefixLength is the length of `()editcommand` plus +2 (for the space and zero based)
|
||||||
|
prefixLength := 14
|
||||||
|
|
||||||
|
// Split the message into the parts we need.
|
||||||
|
//
|
||||||
|
// message: ()addtimer sponsor 20m hecking love my madmonq pills BatChest
|
||||||
|
// parts: | prefix | |name | |repeat | <----------- text -------------> |
|
||||||
|
text := message.Message[prefixLength+len(name)+len(cmdParams[2]) : len(message.Message)]
|
||||||
|
|
||||||
|
// validateTimeFormat will be true if the repeat parameter is in
|
||||||
|
// the format of either 30m, 10h, or 10h30m.
|
||||||
|
validateTimeFormat, err := regexp.MatchString(`^(\d{1,2}[h])$|^(\d+[m])$|^(\d+[s])$|((\d{1,2}[h])((([0]?|[1-5]{1})[0-9])[m]))$`, repeat)
|
||||||
|
if err != nil {
|
||||||
|
app.Log.Errorw("Received malformed time format in timer",
|
||||||
|
"repeat", repeat,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timer := &data.Timer{
|
||||||
|
Name: name,
|
||||||
|
Text: text,
|
||||||
|
Channel: message.Channel,
|
||||||
|
Repeat: repeat,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the time string we got is valid, this is important
|
||||||
|
// because the Scheduler panics instead of erroring out if an invalid
|
||||||
|
// time format string is supplied.
|
||||||
|
if validateTimeFormat {
|
||||||
|
timer := &data.Timer{
|
||||||
|
Name: name,
|
||||||
|
Text: text,
|
||||||
|
Channel: message.Channel,
|
||||||
|
Repeat: repeat,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.Models.Timers.Insert(timer)
|
||||||
|
if err != nil {
|
||||||
|
app.Log.Errorw("Error inserting new timer into database",
|
||||||
|
"timer", timer,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
|
||||||
|
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
return
|
||||||
|
} else { // this is a bit scuffed. The else here is the end of a successful call.
|
||||||
|
// cronName is the internal, unique tag/name for the timer.
|
||||||
|
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor"
|
||||||
|
cronName := fmt.Sprintf("%s%s", message.Channel, name)
|
||||||
|
|
||||||
|
app.Scheduler.AddFunc(fmt.Sprintf("@every %s", repeat), func() { app.newPrivateMessageTimer(message.Channel, text) }, cronName)
|
||||||
|
|
||||||
|
app.Log.Infow("Updated a timer",
|
||||||
|
"Name", name,
|
||||||
|
"Channel", message.Channel,
|
||||||
|
"Old timer", old,
|
||||||
|
"New timer", timer,
|
||||||
|
)
|
||||||
|
|
||||||
|
reply := fmt.Sprintf("Successfully updated timer %s", name)
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.Log.Errorw("Received malformed time format in timer",
|
||||||
|
"timer", timer,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
reply := "Something went wrong FeelsBadMan received wrong time format. Allowed formats: 30s, 30m, 10h, 10h30m"
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitialTimers is called on startup and queries the database for a list of
|
||||||
|
// timers and then adds each onto the scheduler.
|
||||||
|
func (app *application) InitialTimers() {
|
||||||
|
timer, err := app.Models.Timers.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
app.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 []*data.Timer
|
||||||
|
|
||||||
|
// Iterate over all timers and then add them onto the scheduler.
|
||||||
|
for i, v := range timer {
|
||||||
|
// idk why this works but it does so no touchy touchy.
|
||||||
|
// https://github.com/robfig/cron/issues/420#issuecomment-940949195
|
||||||
|
i, v := i, v
|
||||||
|
_ = i
|
||||||
|
|
||||||
|
// cronName is the internal, unique tag/name for the timer.
|
||||||
|
// A timer named "sponsor" in channel "forsen" will be named "forsensponsor"
|
||||||
|
cronName := fmt.Sprintf("%s%s", v.Channel, v.Name)
|
||||||
|
|
||||||
|
// Repeating is at what times the timer should repeat.
|
||||||
|
// 2 minute timer is @every 2m
|
||||||
|
repeating := fmt.Sprintf("@every %s", v.Repeat)
|
||||||
|
|
||||||
|
// Add new value to the slice
|
||||||
|
ts = append(ts, v)
|
||||||
|
|
||||||
|
app.Scheduler.AddFunc(repeating, func() { app.newPrivateMessageTimer(v.Channel, v.Text) }, cronName)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Log.Infow("Initial timers",
|
||||||
|
"timer", ts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPrivateMessageTimer is a helper function to set timers
|
||||||
|
// which trigger into sending a twitch PrivateMessage.
|
||||||
|
func (app *application) newPrivateMessageTimer(channel, text string) {
|
||||||
|
app.Send(channel, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTimer takes in the name of a timer and tries to delete the timer from the database.
|
||||||
|
func (app *application) DeleteTimer(name string, message twitch.PrivateMessage) {
|
||||||
|
cronName := fmt.Sprintf("%s%s", message.Channel, name)
|
||||||
|
app.Scheduler.RemoveJob(cronName)
|
||||||
|
|
||||||
|
app.Log.Infow("Deleting timer",
|
||||||
|
"name", name,
|
||||||
|
"message.Channel", message.Channel,
|
||||||
|
"cronName", cronName,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := app.Models.Timers.Delete(name)
|
||||||
|
if err != nil {
|
||||||
|
app.Log.Errorw("Error deleting timer from database",
|
||||||
|
"name", name,
|
||||||
|
"cronName", cronName,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
|
||||||
|
reply := fmt.Sprintln("Something went wrong FeelsBadMan")
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := fmt.Sprintf("Deleted timer with name %s", name)
|
||||||
|
app.Send(message.Channel, reply)
|
||||||
|
}
|
211
internal/data/channel.go
Normal file
211
internal/data/channel.go
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
AddedAt time.Time `json:"-"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
TwitchID string `json:"twitchid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelModel struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get takes the login name for a channel and queries the database for an
|
||||||
|
// existing entry with that login value. If it exists it returns a
|
||||||
|
// pointer to a Channel.
|
||||||
|
func (c ChannelModel) Get(login string) (*Channel, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, added_at, login, twitchid
|
||||||
|
FROM channels
|
||||||
|
WHERE login = $1`
|
||||||
|
|
||||||
|
var channel Channel
|
||||||
|
|
||||||
|
err := c.DB.QueryRow(query, login).Scan(
|
||||||
|
&channel.ID,
|
||||||
|
&channel.AddedAt,
|
||||||
|
&channel.Login,
|
||||||
|
&channel.TwitchID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return nil, ErrRecordNotFound
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &channel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert takes in a channel struct and inserts it into the database.
|
||||||
|
func (c ChannelModel) Insert(channel *Channel) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO channels(login, twitchid)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (login)
|
||||||
|
DO NOTHING
|
||||||
|
RETURNING id, added_at;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := []interface{}{channel.Login, channel.TwitchID}
|
||||||
|
|
||||||
|
// Execute the query returning the number of affected rows.
|
||||||
|
result, err := c.DB.Exec(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check how many rows were affected.
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrChannelRecordAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll() returns a pointer to a slice of all channels (`[]*Channel`) in the database.
|
||||||
|
func (c ChannelModel) GetAll() ([]*Channel, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, added_at, login, twitchid
|
||||||
|
FROM channels
|
||||||
|
ORDER BY id`
|
||||||
|
|
||||||
|
// Create a context with 3 seconds timeout.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Use QueryContext() the context and query. This returns a
|
||||||
|
// sql.Rows resultset containing our channels.
|
||||||
|
rows, err := c.DB.QueryContext(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to defer a call to rows.Close() to ensure the resultset
|
||||||
|
// is closed before GetAll() returns.
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// Initialize an empty slice to hold the data.
|
||||||
|
channels := []*Channel{}
|
||||||
|
|
||||||
|
// Iterate over the resultset.
|
||||||
|
for rows.Next() {
|
||||||
|
// Initialize an empty Channel struct where we put on
|
||||||
|
// a single channel value.
|
||||||
|
var channel Channel
|
||||||
|
|
||||||
|
// Scan the values onto the channel struct
|
||||||
|
err := rows.Scan(
|
||||||
|
&channel.ID,
|
||||||
|
&channel.AddedAt,
|
||||||
|
&channel.Login,
|
||||||
|
&channel.TwitchID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Add the single movie struct onto the slice.
|
||||||
|
channels = append(channels, &channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When rows.Next() finishes call rows.Err() to retrieve any errors.
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJoinable() returns a slice of channel names (Channel.Login) in the database.
|
||||||
|
func (c ChannelModel) GetJoinable() ([]string, error) {
|
||||||
|
query := `
|
||||||
|
SELECT login
|
||||||
|
FROM channels
|
||||||
|
ORDER BY id`
|
||||||
|
|
||||||
|
// Create a context with 3 seconds timeout.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Use QueryContext() the context and query. This returns a
|
||||||
|
// sql.Rows resultset containing our channels.
|
||||||
|
rows, err := c.DB.QueryContext(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to defer a call to rows.Close() to ensure the resultset
|
||||||
|
// is closed before GetAll() returns.
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// Initialize an empty slice to hold the data.
|
||||||
|
channels := []string{}
|
||||||
|
|
||||||
|
// Iterate over the resultset.
|
||||||
|
for rows.Next() {
|
||||||
|
// Initialize an empty Channel struct where we put on
|
||||||
|
// a single channel value.
|
||||||
|
var channel Channel
|
||||||
|
|
||||||
|
// Scan the values onto the channel struct
|
||||||
|
err := rows.Scan(
|
||||||
|
&channel.Login,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Add the single movie struct onto the slice.
|
||||||
|
channels = append(channels, channel.Login)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When rows.Next() finishes call rows.Err() to retrieve any errors.
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete takes in a login name and queries the database and if there is an
|
||||||
|
// entry with that login name deletes the entry.
|
||||||
|
func (c ChannelModel) Delete(login string) error {
|
||||||
|
// Prepare the statement.
|
||||||
|
query := `
|
||||||
|
DELETE FROM channels
|
||||||
|
WHERE login = $1`
|
||||||
|
|
||||||
|
// Execute the query returning the number of affected rows.
|
||||||
|
result, err := c.DB.Exec(query, login)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check how many rows were affected.
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want atleast 1, if it is 0 the entry did not exist.
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
205
internal/data/commands.go
Normal file
205
internal/data/commands.go
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
Level int `json:"level,omitempty"`
|
||||||
|
Help string `json:"help,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandModel struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tries to find a command in the database with the provided name.
|
||||||
|
func (c CommandModel) Get(name string) (*Command, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, name, text, category, level, help
|
||||||
|
FROM commands
|
||||||
|
WHERE name = $1`
|
||||||
|
|
||||||
|
var command Command
|
||||||
|
|
||||||
|
err := c.DB.QueryRow(query, name).Scan(
|
||||||
|
&command.ID,
|
||||||
|
&command.Name,
|
||||||
|
&command.Text,
|
||||||
|
&command.Category,
|
||||||
|
&command.Level,
|
||||||
|
&command.Help,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return nil, ErrRecordNotFound
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &command, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert adds a command into the database.
|
||||||
|
func (c CommandModel) Insert(command *Command) error {
|
||||||
|
query := `
|
||||||
|
INSERT into commands(name, text, category, level, help)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
ON CONFLICT (name)
|
||||||
|
DO NOTHING
|
||||||
|
RETURNING id;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := []interface{}{command.Name, command.Text, command.Category, command.Level, command.Help}
|
||||||
|
|
||||||
|
result, err := c.DB.Exec(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrCommandRecordAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CommandModel) Update(command *Command) error {
|
||||||
|
query := `
|
||||||
|
UPDATE commands
|
||||||
|
SET text = $2
|
||||||
|
WHERE name = $1
|
||||||
|
RETURNING id`
|
||||||
|
|
||||||
|
args := []interface{}{
|
||||||
|
command.Name,
|
||||||
|
command.Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.DB.QueryRow(query, args...).Scan(&command.ID)
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return ErrEditConflict
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCategory queries the database for an entry with the provided name,
|
||||||
|
// if there is one it updates the categories level with the provided level.
|
||||||
|
func (c CommandModel) SetCategory(name string, category string) error {
|
||||||
|
query := `
|
||||||
|
UPDATE commands
|
||||||
|
SET category = $2
|
||||||
|
WHERE name = $1`
|
||||||
|
|
||||||
|
result, err := c.DB.Exec(query, name, category)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLevel queries the database for an entry with the provided name,
|
||||||
|
// if there is one it updates the entrys level with the provided level.
|
||||||
|
func (c CommandModel) SetLevel(name string, level int) error {
|
||||||
|
query := `
|
||||||
|
UPDATE commands
|
||||||
|
SET level = $2
|
||||||
|
WHERE name = $1`
|
||||||
|
|
||||||
|
result, err := c.DB.Exec(query, name, level)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHelp sets the help text for a given name of a command in the database.
|
||||||
|
func (c CommandModel) SetHelp(name string, helptext string) error {
|
||||||
|
query := `
|
||||||
|
UPDATE commands
|
||||||
|
SET help = $2
|
||||||
|
WHERE name = $1`
|
||||||
|
|
||||||
|
result, err := c.DB.Exec(query, name, helptext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete takes in a command name and queries the database for an entry with
|
||||||
|
// the same name and tries to delete that entry.
|
||||||
|
func (c CommandModel) Delete(name string) error {
|
||||||
|
// Prepare the statement.
|
||||||
|
query := `
|
||||||
|
DELETE FROM commands
|
||||||
|
WHERE name = $1`
|
||||||
|
|
||||||
|
// Execute the query returning the number of affected rows.
|
||||||
|
result, err := c.DB.Exec(query, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check how many rows were affected.
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want atleast 1, if it is 0 the entry did not exist.
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
64
internal/data/models.go
Normal file
64
internal/data/models.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrRecordNotFound = errors.New("record not found")
|
||||||
|
ErrChannelRecordAlreadyExists = errors.New("channel already in database")
|
||||||
|
ErrEditConflict = errors.New("edit conflict")
|
||||||
|
ErrCommandRecordAlreadyExists = errors.New("command already exists")
|
||||||
|
ErrLastFMUserRecordAlreadyExists = errors.New("lastfm connection already set")
|
||||||
|
ErrUserAlreadyExists = errors.New("user already in database")
|
||||||
|
)
|
||||||
|
|
||||||
|
// struct Models wraps the models, making them callable
|
||||||
|
// as app.models.Channels.Get(login)
|
||||||
|
type Models struct {
|
||||||
|
Channels interface {
|
||||||
|
Insert(channel *Channel) error
|
||||||
|
Get(login string) (*Channel, error)
|
||||||
|
GetAll() ([]*Channel, error)
|
||||||
|
GetJoinable() ([]string, error)
|
||||||
|
Delete(login string) error
|
||||||
|
}
|
||||||
|
Users interface {
|
||||||
|
Insert(login, twitchId string) error
|
||||||
|
Get(login string) (*User, error)
|
||||||
|
Check(twitchId string) (*User, error)
|
||||||
|
SetLevel(login string, level int) error
|
||||||
|
GetLevel(twitchId string) (int, error)
|
||||||
|
SetLocation(twitchId, location string) error
|
||||||
|
GetLocation(twitchId string) (string, error)
|
||||||
|
SetLastFM(login, lastfmUser string) error
|
||||||
|
GetLastFM(login string) (string, error)
|
||||||
|
Delete(login string) error
|
||||||
|
}
|
||||||
|
Commands interface {
|
||||||
|
Get(name string) (*Command, error)
|
||||||
|
Insert(command *Command) error
|
||||||
|
Update(command *Command) error
|
||||||
|
SetLevel(name string, level int) error
|
||||||
|
SetCategory(name, category string) error
|
||||||
|
SetHelp(name, helptext string) error
|
||||||
|
Delete(name string) error
|
||||||
|
}
|
||||||
|
Timers interface {
|
||||||
|
Get(name string) (*Timer, error)
|
||||||
|
Insert(timer *Timer) error
|
||||||
|
Update(timer *Timer) error
|
||||||
|
GetAll() ([]*Timer, error)
|
||||||
|
Delete(name string) error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModels(db *sql.DB) Models {
|
||||||
|
return Models{
|
||||||
|
Channels: ChannelModel{DB: db},
|
||||||
|
Users: UserModel{DB: db},
|
||||||
|
Commands: CommandModel{DB: db},
|
||||||
|
Timers: TimerModel{DB: db},
|
||||||
|
}
|
||||||
|
}
|
185
internal/data/timers.go
Normal file
185
internal/data/timers.go
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Timer struct {
|
||||||
|
ID int `json:"id" redis:"timer-id"`
|
||||||
|
Name string `json:"name" redis:"timer-name"`
|
||||||
|
CronName string `redis:"timer-cronname"`
|
||||||
|
Text string `json:"text" redis:"timer-text"`
|
||||||
|
Channel string `json:"channel" redis:"timer-channel"`
|
||||||
|
Repeat string `json:"repeat" redis:"timer-repeat"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimerModel struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TimerModel) Get(name string) (*Timer, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, name, text, channel, repeat
|
||||||
|
FROM timers
|
||||||
|
WHERE name = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
var timer Timer
|
||||||
|
|
||||||
|
err := t.DB.QueryRow(query, name).Scan(
|
||||||
|
&timer.ID,
|
||||||
|
&timer.Name,
|
||||||
|
&timer.Text,
|
||||||
|
&timer.Channel,
|
||||||
|
&timer.Repeat,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return nil, ErrRecordNotFound
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &timer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert adds a command into the database.
|
||||||
|
func (t TimerModel) Insert(timer *Timer) error {
|
||||||
|
query := `
|
||||||
|
INSERT into timers(name, text, channel, repeat)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := []interface{}{timer.Name, timer.Text, timer.Channel, timer.Repeat}
|
||||||
|
|
||||||
|
result, err := t.DB.Exec(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrCommandRecordAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll() returns a pointer to a slice of all channels (`[]*Channel`) in the database.
|
||||||
|
func (t TimerModel) GetAll() ([]*Timer, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, name, text, channel, repeat
|
||||||
|
FROM timers
|
||||||
|
ORDER BY id`
|
||||||
|
|
||||||
|
// Create a context with 3 seconds timeout.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Use QueryContext() the context and query. This returns a
|
||||||
|
// sql.Rows resultset containing our channels.
|
||||||
|
rows, err := t.DB.QueryContext(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to defer a call to rows.Close() to ensure the resultset
|
||||||
|
// is closed before GetAll() returns.
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// Initialize an empty slice to hold the data.
|
||||||
|
timers := []*Timer{}
|
||||||
|
|
||||||
|
// Iterate over the resultset.
|
||||||
|
for rows.Next() {
|
||||||
|
// Initialize an empty Channel struct where we put on
|
||||||
|
// a single channel value.
|
||||||
|
var timer Timer
|
||||||
|
|
||||||
|
// Scan the values onto the channel struct
|
||||||
|
err := rows.Scan(
|
||||||
|
&timer.ID,
|
||||||
|
&timer.Name,
|
||||||
|
&timer.Text,
|
||||||
|
&timer.Channel,
|
||||||
|
&timer.Repeat,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Add the single movie struct onto the slice.
|
||||||
|
timers = append(timers, &timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When rows.Next() finishes call rows.Err() to retrieve any errors.
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return timers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TimerModel) Update(timer *Timer) error {
|
||||||
|
query := `
|
||||||
|
UPDATE timers
|
||||||
|
SET text = $2, channel = $3, repeat = $4
|
||||||
|
WHERE name = $1
|
||||||
|
RETURNING id`
|
||||||
|
|
||||||
|
args := []interface{}{
|
||||||
|
timer.Name,
|
||||||
|
timer.Text,
|
||||||
|
timer.Channel,
|
||||||
|
timer.Repeat,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := t.DB.QueryRow(query, args...).Scan(&timer.ID)
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return ErrEditConflict
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete takes in a command name and queries the database for an entry with
|
||||||
|
// the same name and tries to delete that entry.
|
||||||
|
func (t TimerModel) Delete(name string) error {
|
||||||
|
// Prepare the statement.
|
||||||
|
query := `
|
||||||
|
DELETE FROM timers
|
||||||
|
WHERE name = $1`
|
||||||
|
|
||||||
|
// Execute the query returning the number of affected rows.
|
||||||
|
result, err := t.DB.Exec(query, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check how many rows were affected.
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want atleast 1, if it is 0 the entry did not exist.
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
296
internal/data/user.go
Normal file
296
internal/data/user.go
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
AddedAt time.Time `json:"-"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
TwitchID string `json:"twitchid"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
Location string `json:"location,omitempty"`
|
||||||
|
LastFMUsername string `json:"lastfm_username,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserModel struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts a user model into the database.
|
||||||
|
func (u UserModel) Insert(login, twitchId string) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO users(login, twitchid)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (login)
|
||||||
|
DO NOTHING
|
||||||
|
RETURNING id, added_at;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := []interface{}{login, twitchId}
|
||||||
|
|
||||||
|
// Execute the query returning the number of affected rows.
|
||||||
|
result, err := u.DB.Exec(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check how many rows were affected.
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrUserAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocation searches the database for a record with the provided login value
|
||||||
|
// and if that exists sets the location to the supplied
|
||||||
|
func (u UserModel) SetLocation(twitchId, location string) error {
|
||||||
|
query := `
|
||||||
|
UPDATE users
|
||||||
|
SET location = $2
|
||||||
|
WHERE twitchId = $1`
|
||||||
|
|
||||||
|
result, err := u.DB.Exec(query, twitchId, location)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check how many rows were affected.
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want atleast 1, if it is 0 the entry did not exist.
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocation searches the database for a record with the provided login value
|
||||||
|
// and if that exists sets the location to the supplied
|
||||||
|
func (u UserModel) GetLocation(twitchId string) (string, error) {
|
||||||
|
query := `
|
||||||
|
SELECT location
|
||||||
|
FROM users
|
||||||
|
WHERE twitchid = $1`
|
||||||
|
|
||||||
|
var user User
|
||||||
|
|
||||||
|
err := u.DB.QueryRow(query, twitchId).Scan(
|
||||||
|
&user.Location,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return "", ErrRecordNotFound
|
||||||
|
default:
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.Location, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocation searches the database for a record with the provided login value
|
||||||
|
// and if that exists sets the location to the supplied
|
||||||
|
func (u UserModel) SetLastFM(login, lastfmUser string) error {
|
||||||
|
query := `
|
||||||
|
UPDATE users
|
||||||
|
SET lastfm_username = $2
|
||||||
|
WHERE login = $1`
|
||||||
|
|
||||||
|
result, err := u.DB.Exec(query, login, lastfmUser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check how many rows were affected.
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want atleast 1, if it is 0 the entry did not exist.
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocation searches the database for a record with the provided login value
|
||||||
|
// and if that exists sets the location to the supplied
|
||||||
|
func (u UserModel) GetLastFM(login string) (string, error) {
|
||||||
|
query := `
|
||||||
|
SELECT lastfm_username
|
||||||
|
FROM users
|
||||||
|
WHERE login = $1`
|
||||||
|
|
||||||
|
var user User
|
||||||
|
|
||||||
|
err := u.DB.QueryRow(query, login).Scan(
|
||||||
|
&user.LastFMUsername,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return "", ErrRecordNotFound
|
||||||
|
default:
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.LastFMUsername, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocation searches the database for a record with the provided login value
|
||||||
|
// and if that exists sets the location to the supplied
|
||||||
|
func (u UserModel) GetLevel(twitchId string) (int, error) {
|
||||||
|
query := `
|
||||||
|
SELECT level
|
||||||
|
FROM users
|
||||||
|
WHERE twitchid = $1`
|
||||||
|
|
||||||
|
var user User
|
||||||
|
|
||||||
|
err := u.DB.QueryRow(query, twitchId).Scan(
|
||||||
|
&user.Level,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return -1, ErrRecordNotFound
|
||||||
|
default:
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.Level, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setlevel searches the database for a record with the provided login value
|
||||||
|
// and if that exists sets the level to the supplied level value.
|
||||||
|
func (u UserModel) SetLevel(login string, level int) error {
|
||||||
|
query := `
|
||||||
|
UPDATE users
|
||||||
|
SET level = $2
|
||||||
|
WHERE login = $1`
|
||||||
|
|
||||||
|
// err := u.DB.QueryRow(query, args...).Scan(&user)
|
||||||
|
result, err := u.DB.Exec(query, login, level)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check how many rows were affected.
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want atleast 1, if it is 0 the entry did not exist.
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get searches the database for a login name and returns the user struct on success.
|
||||||
|
func (u UserModel) Get(login string) (*User, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, added_at, login, twitchid, level, location
|
||||||
|
FROM users
|
||||||
|
WHERE login = $1`
|
||||||
|
|
||||||
|
var user User
|
||||||
|
|
||||||
|
err := u.DB.QueryRow(query, login).Scan(
|
||||||
|
&user.ID,
|
||||||
|
&user.AddedAt,
|
||||||
|
&user.Login,
|
||||||
|
&user.TwitchID,
|
||||||
|
&user.Level,
|
||||||
|
&user.Location,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return nil, ErrRecordNotFound
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check checks the database for a record with the given login name.
|
||||||
|
func (u UserModel) Check(twitchId string) (*User, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, login
|
||||||
|
FROM users
|
||||||
|
WHERE twitchid = $1`
|
||||||
|
|
||||||
|
var user User
|
||||||
|
|
||||||
|
err := u.DB.QueryRow(query, twitchId).Scan(
|
||||||
|
&user.ID,
|
||||||
|
&user.Login,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return nil, ErrRecordNotFound
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete searches the database for a value with the supplied login name and if
|
||||||
|
// one exists deletes the record, returning any errors that might occur.
|
||||||
|
func (u UserModel) Delete(login string) error {
|
||||||
|
// Prepare the statement.
|
||||||
|
query := `
|
||||||
|
DELETE FROM users
|
||||||
|
WHERE login = $1`
|
||||||
|
|
||||||
|
// Execute the query returning the number of affected rows.
|
||||||
|
result, err := u.DB.Exec(query, login)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check how many rows were affected.
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want atleast 1, if it is 0 the entry did not exist.
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue