2023-12-17 19:07:05 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-02-13 19:13:44 +01:00
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
2023-12-17 19:07:05 +01:00
|
|
|
"fmt"
|
2023-12-20 02:06:05 +01:00
|
|
|
"html/template"
|
2024-02-20 19:15:14 +01:00
|
|
|
"io"
|
2024-02-13 19:13:44 +01:00
|
|
|
"log"
|
2023-12-17 19:07:05 +01:00
|
|
|
"net/http"
|
2024-01-17 19:56:33 +01:00
|
|
|
"os"
|
2023-12-20 02:06:05 +01:00
|
|
|
"sort"
|
2023-12-17 19:07:05 +01:00
|
|
|
|
|
|
|
"github.com/julienschmidt/httprouter"
|
2023-12-20 02:06:05 +01:00
|
|
|
"github.com/lyx0/nourybot/internal/data"
|
2024-02-20 22:36:33 +01:00
|
|
|
"github.com/lyx0/nourybot/pkg/common"
|
2024-02-13 19:13:44 +01:00
|
|
|
"github.com/nicklaw5/helix/v2"
|
2023-12-17 19:07:05 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func (app *application) startRouter() {
|
|
|
|
router := httprouter.New()
|
2023-12-20 02:06:05 +01:00
|
|
|
router.GET("/", app.homeRoute)
|
2023-12-17 19:07:05 +01:00
|
|
|
router.GET("/status", app.statusPageRoute)
|
2024-02-29 17:46:27 +01:00
|
|
|
router.POST("/eventsub/:channel", app.eventsubFollow)
|
2023-12-18 00:04:27 +01:00
|
|
|
router.GET("/commands", app.commandsRoute)
|
2024-02-11 16:54:53 +01:00
|
|
|
router.GET("/commands/:channel", app.channelCommandsRoute)
|
2024-02-10 21:36:46 +01:00
|
|
|
router.GET("/timer", app.timersRoute)
|
2023-12-17 20:11:02 +01:00
|
|
|
router.GET("/timer/:channel", app.channelTimersRoute)
|
2023-12-17 19:07:05 +01:00
|
|
|
|
2024-02-29 21:17:39 +01:00
|
|
|
// Serve files uploaded by the meme command, but don't list directory contents.
|
2024-01-17 19:56:33 +01:00
|
|
|
fs := justFilesFilesystem{http.Dir("/public/uploads/")}
|
|
|
|
router.Handler("GET", "/uploads/*filepath", http.StripPrefix("/uploads", http.FileServer(fs)))
|
|
|
|
|
2024-02-10 21:36:46 +01:00
|
|
|
app.Log.Info("Serving on :8080")
|
2023-12-17 19:07:05 +01:00
|
|
|
app.Log.Fatal(http.ListenAndServe(":8080", router))
|
|
|
|
}
|
|
|
|
|
2024-02-13 19:13:44 +01:00
|
|
|
type eventSubNotification struct {
|
|
|
|
Subscription helix.EventSubSubscription `json:"subscription"`
|
|
|
|
Challenge string `json:"challenge"`
|
|
|
|
Event json.RawMessage `json:"event"`
|
|
|
|
}
|
|
|
|
|
2024-02-29 21:17:39 +01:00
|
|
|
// eventsubSubscriptionID stores the received eventsub subscription ids since
|
|
|
|
// last restart. Twitch resends events if it is unsure that we have gotten them
|
|
|
|
// so we check if the received eventsub subscription id has already
|
|
|
|
// been recorded and discard them if so.
|
|
|
|
var lastEventSubSubscriptionID = []string{"xd"}
|
2024-02-28 18:46:53 +01:00
|
|
|
|
2024-02-29 17:46:27 +01:00
|
|
|
func (app *application) eventsubFollow(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
|
|
channel := ps.ByName("channel")
|
2024-02-20 19:15:14 +01:00
|
|
|
body, err := io.ReadAll(r.Body)
|
2024-02-13 19:13:44 +01:00
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
|
|
// verify that the notification came from twitch using the secret.
|
|
|
|
if !helix.VerifyEventSubNotification(app.Config.eventSubSecret, r.Header, string(body)) {
|
|
|
|
log.Println("no valid signature on subscription")
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
log.Println("verified signature for subscription")
|
|
|
|
}
|
2024-02-14 23:01:54 +01:00
|
|
|
|
2024-02-13 19:13:44 +01:00
|
|
|
var vals eventSubNotification
|
|
|
|
err = json.NewDecoder(bytes.NewReader(body)).Decode(&vals)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
2024-02-29 21:17:39 +01:00
|
|
|
// if there's a challenge in the request, respond
|
|
|
|
// with only the challenge to verify the request is genuine.
|
2024-02-13 19:13:44 +01:00
|
|
|
if vals.Challenge != "" {
|
|
|
|
w.Write([]byte(vals.Challenge))
|
|
|
|
return
|
|
|
|
}
|
2024-02-29 17:46:27 +01:00
|
|
|
//r.Body.Close()
|
|
|
|
|
|
|
|
// Check if the current events subscription id equals the last events.
|
|
|
|
// If it does ignore the event since it's a repeated event.
|
2024-02-29 21:17:39 +01:00
|
|
|
for i := 0; i < len(lastEventSubSubscriptionID); i++ {
|
|
|
|
if vals.Subscription.ID == lastEventSubSubscriptionID[i] {
|
2024-02-29 17:46:27 +01:00
|
|
|
return
|
|
|
|
} else {
|
2024-02-29 21:17:39 +01:00
|
|
|
lastEventSubSubscriptionID[i] = vals.Subscription.ID
|
2024-02-29 17:46:27 +01:00
|
|
|
}
|
2024-02-28 18:46:53 +01:00
|
|
|
}
|
|
|
|
|
2024-02-14 23:01:54 +01:00
|
|
|
switch vals.Subscription.Type {
|
|
|
|
case helix.EventSubTypeStreamOnline:
|
|
|
|
var liveEvent helix.EventSubStreamOnlineEvent
|
2024-02-28 18:46:53 +01:00
|
|
|
|
2024-02-14 23:01:54 +01:00
|
|
|
err = json.NewDecoder(bytes.NewReader(vals.Event)).Decode(&liveEvent)
|
2024-02-29 21:17:39 +01:00
|
|
|
if err != nil {
|
|
|
|
app.Log.Errorln(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("got stream online event webhook: [%s]: %s is live\n",
|
|
|
|
channel, liveEvent.BroadcasterUserName)
|
2024-02-14 23:01:54 +01:00
|
|
|
w.WriteHeader(200)
|
|
|
|
w.Write([]byte("ok"))
|
2024-02-15 21:58:57 +01:00
|
|
|
|
2024-02-29 19:32:43 +01:00
|
|
|
game := app.getChannelGame(liveEvent.BroadcasterUserID)
|
|
|
|
title := app.getChannelTitle(liveEvent.BroadcasterUserID)
|
2024-02-15 21:58:57 +01:00
|
|
|
|
2024-02-29 21:17:39 +01:00
|
|
|
go app.SendNoBanphrase(channel,
|
|
|
|
fmt.Sprintf("@%s went live FeelsGoodMan Game: %s; Title: %s; https://twitch.tv/%s",
|
|
|
|
liveEvent.BroadcasterUserName, game, title, liveEvent.BroadcasterUserLogin))
|
2024-02-14 23:01:54 +01:00
|
|
|
|
|
|
|
case helix.EventSubTypeStreamOffline:
|
|
|
|
var offlineEvent helix.EventSubStreamOfflineEvent
|
2024-02-29 21:17:39 +01:00
|
|
|
|
2024-02-14 23:01:54 +01:00
|
|
|
err = json.NewDecoder(bytes.NewReader(vals.Event)).Decode(&offlineEvent)
|
2024-02-29 21:17:39 +01:00
|
|
|
if err != nil {
|
|
|
|
app.Log.Errorln(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("got stream offline event webhook: [%s]: %s is offline\n",
|
|
|
|
channel, offlineEvent.BroadcasterUserName)
|
2024-02-14 23:01:54 +01:00
|
|
|
w.WriteHeader(200)
|
|
|
|
w.Write([]byte("ok"))
|
2024-02-29 21:17:39 +01:00
|
|
|
|
|
|
|
go app.SendNoBanphrase(channel,
|
|
|
|
fmt.Sprintf("%s went offline FeelsBadMan", offlineEvent.BroadcasterUserName))
|
2024-02-14 23:01:54 +01:00
|
|
|
}
|
2024-02-13 19:13:44 +01:00
|
|
|
}
|
|
|
|
|
2024-02-10 21:36:46 +01:00
|
|
|
type timersRouteData struct {
|
|
|
|
Timers []data.Timer
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) timersRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
2024-02-12 20:04:02 +01:00
|
|
|
t, err := template.ParseFiles(
|
|
|
|
"./web/templates/base.template.gohtml",
|
|
|
|
"./web/templates/header.partial.gohtml",
|
|
|
|
"./web/templates/footer.partial.gohtml",
|
|
|
|
"./web/templates/timers.page.gohtml",
|
|
|
|
)
|
2024-02-10 21:36:46 +01:00
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var ts []data.Timer
|
|
|
|
|
|
|
|
timerData, err := app.Models.Timers.GetAll()
|
|
|
|
if err != nil {
|
|
|
|
app.Log.Errorw("Error trying to retrieve all timer for a channel from database", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Iterate over all timers and then add them onto the scheduler.
|
|
|
|
for i, v := range timerData {
|
|
|
|
// 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
|
|
|
|
var t data.Timer
|
|
|
|
t.Name = v.Name
|
|
|
|
t.Text = v.Text
|
|
|
|
t.Repeat = v.Repeat
|
|
|
|
|
|
|
|
// Add new value to the slice
|
|
|
|
ts = append(ts, t)
|
|
|
|
}
|
|
|
|
|
|
|
|
data := &timersRouteData{ts}
|
|
|
|
err = t.Execute(w, data)
|
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-11 16:54:53 +01:00
|
|
|
type channelTimersRouteData struct {
|
|
|
|
Timers []data.Timer
|
|
|
|
Channel string
|
|
|
|
}
|
|
|
|
|
2024-02-11 15:48:32 +01:00
|
|
|
func (app *application) channelTimersRoute(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
|
|
channel := ps.ByName("channel")
|
2024-02-12 20:04:02 +01:00
|
|
|
t, err := template.ParseFiles(
|
|
|
|
"./web/templates/base.template.gohtml",
|
|
|
|
"./web/templates/header.partial.gohtml",
|
|
|
|
"./web/templates/footer.partial.gohtml",
|
|
|
|
"./web/templates/channeltimers.page.gohtml",
|
|
|
|
)
|
2024-02-11 15:48:32 +01:00
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var ts []data.Timer
|
|
|
|
|
|
|
|
timerData, err := app.Models.Timers.GetChannelTimer(channel)
|
|
|
|
if err != nil {
|
|
|
|
app.Log.Errorw("Error trying to retrieve all timer for a channel from database", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Iterate over all timers and then add them onto the scheduler.
|
|
|
|
for i, v := range timerData {
|
|
|
|
// 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
|
|
|
|
var t data.Timer
|
|
|
|
t.Name = v.Name
|
|
|
|
t.Text = v.Text
|
|
|
|
t.Repeat = v.Repeat
|
|
|
|
|
|
|
|
// Add new value to the slice
|
|
|
|
ts = append(ts, t)
|
|
|
|
}
|
|
|
|
|
|
|
|
data := &channelTimersRouteData{ts, channel}
|
|
|
|
err = t.Execute(w, data)
|
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-20 02:06:05 +01:00
|
|
|
type commandsRouteData struct {
|
|
|
|
Commands map[string]command
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) commandsRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
2024-02-12 20:04:02 +01:00
|
|
|
t, err := template.ParseFiles(
|
|
|
|
"./web/templates/base.template.gohtml",
|
|
|
|
"./web/templates/header.partial.gohtml",
|
|
|
|
"./web/templates/footer.partial.gohtml",
|
|
|
|
"./web/templates/commands.page.gohtml",
|
|
|
|
)
|
2023-12-20 02:06:05 +01:00
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-12-18 00:04:27 +01:00
|
|
|
var cs []string
|
|
|
|
|
2023-12-20 02:06:05 +01:00
|
|
|
for i, v := range helpText {
|
|
|
|
// 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
|
|
|
|
var c string
|
|
|
|
|
|
|
|
if v.Alias == nil {
|
2024-02-29 21:17:39 +01:00
|
|
|
c = fmt.Sprintf(
|
|
|
|
"Name: %s\nDescription: %s\nLevel: %s\nUsage: %s\n\n",
|
|
|
|
i, v.Description, v.Level, v.Usage)
|
2023-12-20 02:06:05 +01:00
|
|
|
} else {
|
2024-02-29 21:17:39 +01:00
|
|
|
c = fmt.Sprintf(
|
|
|
|
"Name: %s\nAliases: %s\nDescription: %s\nLevel: %s\nUsage: %s\n\n",
|
|
|
|
i, v.Alias, v.Description, v.Level, v.Usage)
|
2023-12-20 02:06:05 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add new value to the slice
|
|
|
|
cs = append(cs, c)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(cs)
|
|
|
|
data := &commandsRouteData{helpText}
|
|
|
|
|
|
|
|
err = t.Execute(w, data)
|
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
type homeRouteData struct {
|
|
|
|
Channels []*data.Channel
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) homeRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
2024-02-11 19:18:40 +01:00
|
|
|
t, err := template.ParseFiles(
|
|
|
|
"./web/templates/base.template.gohtml",
|
|
|
|
"./web/templates/header.partial.gohtml",
|
|
|
|
"./web/templates/footer.partial.gohtml",
|
|
|
|
"./web/templates/home.page.gohtml",
|
|
|
|
)
|
|
|
|
|
2023-12-20 02:06:05 +01:00
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
allChannel, err := app.Models.Channels.GetAll()
|
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
app.Log.Infow("All channels:",
|
|
|
|
"channel", allChannel)
|
2023-12-20 02:08:14 +01:00
|
|
|
data := &homeRouteData{allChannel}
|
2023-12-18 00:04:27 +01:00
|
|
|
|
2023-12-20 02:06:05 +01:00
|
|
|
err = t.Execute(w, data)
|
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
2023-12-18 00:04:27 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2024-02-11 16:54:53 +01:00
|
|
|
type channelCommandsRouteData struct {
|
|
|
|
Commands []data.Command
|
|
|
|
Channel string
|
|
|
|
}
|
|
|
|
|
2023-12-17 19:07:05 +01:00
|
|
|
func (app *application) channelCommandsRoute(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
|
|
channel := ps.ByName("channel")
|
2024-02-12 20:04:02 +01:00
|
|
|
t, err := template.ParseFiles(
|
|
|
|
"./web/templates/base.template.gohtml",
|
|
|
|
"./web/templates/header.partial.gohtml",
|
|
|
|
"./web/templates/footer.partial.gohtml",
|
|
|
|
"./web/templates/channelcommands.page.gohtml",
|
|
|
|
)
|
2024-02-11 16:54:53 +01:00
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var cs []data.Command
|
2023-12-17 19:07:05 +01:00
|
|
|
|
2024-02-11 16:54:53 +01:00
|
|
|
commandData, err := app.Models.Commands.GetAllChannel(channel)
|
2023-12-17 19:07:05 +01:00
|
|
|
if err != nil {
|
2024-02-11 16:54:53 +01:00
|
|
|
app.Log.Errorw("Error trying to retrieve all timer for a channel from database", err)
|
2023-12-17 19:07:05 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-11 16:54:53 +01:00
|
|
|
for i, v := range commandData {
|
2023-12-17 19:07:05 +01:00
|
|
|
// 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
|
2024-02-11 16:54:53 +01:00
|
|
|
var c data.Command
|
|
|
|
c.Name = v.Name
|
|
|
|
c.Level = v.Level
|
|
|
|
c.Description = v.Description
|
|
|
|
c.Text = v.Text
|
2023-12-17 19:07:05 +01:00
|
|
|
|
|
|
|
cs = append(cs, c)
|
|
|
|
}
|
|
|
|
|
2024-02-11 16:54:53 +01:00
|
|
|
data := &channelCommandsRouteData{cs, channel}
|
|
|
|
err = t.Execute(w, data)
|
|
|
|
if err != nil {
|
|
|
|
app.Log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
2023-12-17 19:07:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) statusPageRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
|
|
commit := common.GetVersion()
|
|
|
|
started := common.GetUptime().Format("2006-1-2 15:4:5")
|
|
|
|
commitLink := fmt.Sprintf("https://github.com/lyx0/nourybot/commit/%v", common.GetVersionPure())
|
|
|
|
|
2024-02-29 21:17:39 +01:00
|
|
|
fmt.Print(w, fmt.Sprint(
|
|
|
|
"started: \t"+started+"\n"+
|
|
|
|
"environment: \t"+app.Environment+"\n"+
|
|
|
|
"commit: \t"+commit+"\n"+
|
|
|
|
"github: \t"+commitLink,
|
|
|
|
))
|
2023-12-17 19:07:05 +01:00
|
|
|
}
|
2024-01-17 19:56:33 +01:00
|
|
|
|
2024-02-29 21:17:39 +01:00
|
|
|
// Since I want to serve the files that I upload with the meme command to
|
|
|
|
// the /public/uploads folder but not list the directory contents of
|
|
|
|
// the `/uploads/` route I found this issue that solves this.
|
2024-01-17 19:56:33 +01:00
|
|
|
//
|
|
|
|
// https://github.com/julienschmidt/httprouter/issues/25#issuecomment-74977940
|
|
|
|
type justFilesFilesystem struct {
|
|
|
|
fs http.FileSystem
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fs justFilesFilesystem) Open(name string) (http.File, error) {
|
|
|
|
f, err := fs.fs.Open(name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return neuteredReaddirFile{f}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type neuteredReaddirFile struct {
|
|
|
|
http.File
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|