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-15 21:58:57 +01:00
"github.com/lyx0/nourybot/pkg/ivr"
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-01-17 19:56:33 +01:00
// Serve files uploaded by the meme command, but don't list the directory contents.
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-28 18:46:53 +01:00
// eventsubMessageId stores the last message id of an event sub. Twitch resends events
// if it is unsure that you have gotten them so we check if the last event has the same
// message id and if it does discard the event.
2024-02-29 17:46:27 +01:00
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
}
// if there's a challenge in the request, respond with only the challenge to verify your eventsub.
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.
for i := 0 ; i < len ( lastEventSubSubscriptionId ) ; i ++ {
if vals . Subscription . ID == lastEventSubSubscriptionId [ i ] {
return
} else {
lastEventSubSubscriptionId [ i ] = vals . Subscription . ID
}
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 17:46:27 +01:00
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
game := ivr . GameByUsername ( liveEvent . BroadcasterUserLogin )
title := ivr . TitleByUsername ( liveEvent . BroadcasterUserLogin )
2024-02-29 17:46:27 +01:00
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
err = json . NewDecoder ( bytes . NewReader ( vals . Event ) ) . Decode ( & offlineEvent )
2024-02-29 17:46:27 +01:00
log . Printf ( "got stream online event webhook: [%s]: %s is live\n" , channel , offlineEvent . BroadcasterUserName )
2024-02-14 23:01:54 +01:00
w . WriteHeader ( 200 )
w . Write ( [ ] byte ( "ok" ) )
2024-02-29 17:46:27 +01:00
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 {
c = fmt . Sprintf ( "Name: %s\nDescription: %s\nLevel: %s\nUsage: %s\n\n" , i , v . Description , v . Level , v . Usage )
} else {
c = fmt . Sprintf ( "Name: %s\nAliases: %s\nDescription: %s\nLevel: %s\nUsage: %s\n\n" , i , v . Alias , v . Description , v . Level , v . Usage )
}
// 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 ( ) )
fmt . Fprintf ( w , fmt . Sprintf ( "started: \t%v\nenvironment: \t%v\ncommit: \t%v\ngithub: \t%v" , started , app . Environment , commit , commitLink ) )
}
2024-01-17 19:56:33 +01:00
// Since I want to serve the files that I uploaded with the meme command to the /public/uploads
// folder, but not list the directory on the `/uploads/` route I found this issue that solves
// that problem with the httprouter.
//
// 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
}