819 lines
23 KiB
Go
819 lines
23 KiB
Go
package main
|
|
|
|
import (
|
|
"shoGambler/lib"
|
|
|
|
"errors"
|
|
"log"
|
|
"os"
|
|
"plugin"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"html/template"
|
|
"io/fs"
|
|
"math/big"
|
|
"net/http"
|
|
"path/filepath"
|
|
|
|
"github.com/MicahParks/keyfunc/v3"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func getYTChID(token string) (string, int, error) {
|
|
log.Println("[WARN] Scary, we are expending a Google API credit!")
|
|
|
|
// Ask Google for the user's channel ID
|
|
request, err := http.NewRequest("GET", "https://www.googleapis.com/youtube/v3/channels?mine=true", nil)
|
|
if err != nil {
|
|
return "", 500, errors.New("error creating Google auth request")
|
|
}
|
|
|
|
// Set the Authorization header
|
|
request.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
// Send the request
|
|
response, err := http.DefaultClient.Do(request)
|
|
if err != nil {
|
|
return "", 500, errors.New("error sending Google auth request")
|
|
}
|
|
|
|
// Check the status code
|
|
if response.StatusCode != 200 {
|
|
return "", response.StatusCode, errors.New("error getting Google auth response")
|
|
}
|
|
|
|
// Read the response
|
|
var responseJSON map[string]interface{}
|
|
err = json.NewDecoder(response.Body).Decode(&responseJSON)
|
|
if err != nil {
|
|
return "", 500, errors.New("error decoding Google auth response")
|
|
}
|
|
|
|
// Get the user's channel ID
|
|
channelID, ok := responseJSON["items"].([]interface{})[0].(map[string]interface{})["id"].(string)
|
|
if !ok {
|
|
return "", 400, errors.New("error getting channel ID")
|
|
}
|
|
|
|
return channelID, 200, nil
|
|
}
|
|
|
|
func giveUserPoints(channelID string, points *big.Int) {
|
|
// Add the points to the userPoints
|
|
var existingPoints []byte
|
|
err := conn.QueryRow("SELECT points FROM users WHERE channelID = ?", channelID).Scan(&existingPoints)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
_, err := conn.Exec("INSERT INTO users (channelID, points) VALUES (?, ?)", channelID, points.Bytes())
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error adding user to database: ", err)
|
|
}
|
|
} else if err != nil {
|
|
log.Fatal("[FATAL] Error querying database: ", err)
|
|
} else {
|
|
_, err := conn.Exec("UPDATE users SET points = ? WHERE channelID = ?", new(big.Int).Add(points, new(big.Int).SetBytes(existingPoints)).Bytes(), channelID)
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error updating user points: ", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func subtractUserPoints(channelID string, points *big.Int) {
|
|
// Subtract the points from the userPoints
|
|
var existingPoints []byte
|
|
err := conn.QueryRow("SELECT points FROM users WHERE channelID = ?", channelID).Scan(&existingPoints)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
_, err := conn.Exec("INSERT INTO users (channelID, points) VALUES (?, ?)", channelID, new(big.Int).Neg(points).Bytes())
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error adding user to database: ", err)
|
|
}
|
|
} else if err != nil {
|
|
log.Fatal("[FATAL] Error querying database: ", err)
|
|
} else {
|
|
_, err := conn.Exec("UPDATE users SET points = ? WHERE channelID = ?", new(big.Int).Add(new(big.Int).SetBytes(existingPoints), new(big.Int).Neg(points)).Bytes(), channelID)
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error updating user points: ", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getUserPoints(channelID string) *big.Int {
|
|
// Get the user's points
|
|
var points []byte
|
|
err := conn.QueryRow("SELECT points FROM users WHERE channelID = ?", channelID).Scan(&points)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return big.NewInt(0)
|
|
} else if err != nil {
|
|
log.Fatal("[FATAL] Error querying database: ", err)
|
|
}
|
|
|
|
return new(big.Int).SetBytes(points)
|
|
}
|
|
|
|
func userIsModerator(accessToken string) bool {
|
|
// Get the channel ID
|
|
channelID, ok := userSessions[accessToken]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if channelID != "UCHlTEt24Yb4ylJFuWz7hXIw" {
|
|
// Check if the user is a moderator
|
|
var channelIDCheck string
|
|
err := conn.QueryRow("SELECT channelID FROM moderators WHERE channelID = ?", channelID).Scan(&channelIDCheck)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return false
|
|
} else {
|
|
log.Fatal("[FATAL] Error querying database: ", err)
|
|
return false
|
|
}
|
|
} else {
|
|
return channelID == channelIDCheck
|
|
}
|
|
} else {
|
|
// Bro it's literally shounic, of course they're a moderator
|
|
return true
|
|
}
|
|
}
|
|
|
|
func checkForChatMessages(liveChatID string) {
|
|
for {
|
|
if streaming == true {
|
|
// Check for chat messages
|
|
log.Println("[INFO] Scary, we are expending a Google API credit (on stream)!")
|
|
|
|
// Keeping this in a comment so the compiler doesn't remember it:
|
|
key, err := base64.StdEncoding.DecodeString(configFile.ApiKey)
|
|
if err != nil {
|
|
log.Println("[ERROR] Error decoding API key: ", err)
|
|
}
|
|
|
|
response, err := http.Get("https://www.googleapis.com/youtube/v3/liveChat/messages?liveChatId=" + liveChatID + "&part=snippet,authorDetails&key=" + string(key))
|
|
if err != nil {
|
|
log.Println("[ERROR] Error getting chat messages: ", err)
|
|
return
|
|
}
|
|
|
|
// Read the response
|
|
var responseJSON map[string]interface{}
|
|
err = json.NewDecoder(response.Body).Decode(&responseJSON)
|
|
if err != nil {
|
|
log.Println("[ERROR] Error decoding chat messages: ", err)
|
|
return
|
|
}
|
|
|
|
// Check the status code
|
|
if response.StatusCode != 200 {
|
|
log.Println("[ERROR] Error getting chat messages: ", response.Status, responseJSON)
|
|
return
|
|
} else {
|
|
// Iterate through each live chat message
|
|
for _, item := range responseJSON["items"].([]interface{}) {
|
|
log.Println("[INFO] Processing message from ", item.(map[string]interface{})["authorDetails"].(map[string]interface{})["displayName"].(string))
|
|
earliestSentMessage, ok := earliestSentMsg[item.(map[string]interface{})["authorDetails"].(map[string]interface{})["channelId"].(string)]
|
|
publishedTime, err := time.Parse(time.RFC3339Nano, item.(map[string]interface{})["snippet"].(map[string]interface{})["publishedAt"].(string))
|
|
if err != nil {
|
|
log.Println("[ERROR] Error parsing time: ", err)
|
|
} else if !ok || publishedTime.Before(earliestSentMessage) {
|
|
if publishedTime.After(streamingSince) {
|
|
log.Println("[INFO] New message from ", item.(map[string]interface{})["authorDetails"].(map[string]interface{})["displayName"].(string))
|
|
earliestSentMsg[item.(map[string]interface{})["authorDetails"].(map[string]interface{})["channelId"].(string)] = publishedTime
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close the response body
|
|
err := response.Body.Close()
|
|
if err != nil {
|
|
log.Println("[ERROR] Error closing response body: ", err)
|
|
}
|
|
|
|
// Wait for the rate because Google likes to screw us over
|
|
time.Sleep(time.Second * time.Duration(configFile.Rate))
|
|
}
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
var (
|
|
plugins []lib.PluginData
|
|
conn *sql.DB
|
|
earliestSentMsg = make(map[string]time.Time)
|
|
userSessions = make(map[string]string)
|
|
streamingSince time.Time
|
|
configFile config
|
|
streaming bool
|
|
)
|
|
|
|
type config struct {
|
|
ApiKey string `json:"key"`
|
|
Rate int `json:"rate"`
|
|
}
|
|
|
|
func main() {
|
|
// Connect to the database
|
|
var err error
|
|
conn, err = sql.Open("sqlite", "database.db")
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error connecting to database: ", err)
|
|
}
|
|
|
|
// Read in config.json
|
|
configBytes, err := os.ReadFile("config.json")
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error reading config.json: ", err)
|
|
}
|
|
|
|
// Parse the JSON
|
|
err = json.Unmarshal(configBytes, &configFile)
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error parsing config.json: ", err)
|
|
}
|
|
|
|
// Create the blacklist table if it doesn't exist
|
|
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS blacklist (nonce TEXT NOT NULL UNIQUE)")
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error creating blacklist table: ", err)
|
|
}
|
|
|
|
// Create the plugins table if it doesn't exist
|
|
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS plugins (pluginName TEXT UNIQUE, pointsOverride BLOB)")
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error creating plugins table: ", err)
|
|
}
|
|
|
|
// Create the moderator table if it doesn't exist
|
|
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS moderators (channelID TEXT NOT NULL UNIQUE, isStreamer BOOLEAN NOT NULL DEFAULT FALSE)")
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error creating moderators table: ", err)
|
|
}
|
|
|
|
// Create the user table if it doesn't exist
|
|
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS users (channelID TEXT NOT NULL UNIQUE, points BLOB NOT NULL, sub TEXT NOT NULL UNIQUE)")
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error creating users table: ", err)
|
|
}
|
|
|
|
// Set up the JWT verification
|
|
keyVerifyFunction, err := keyfunc.NewDefault([]string{"https://www.googleapis.com/oauth2/v3/certs"})
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error setting up JWT verification: ", err)
|
|
}
|
|
|
|
// Set up plugins
|
|
err = filepath.WalkDir("plugins", func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ignore directories
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
// Load the plugin
|
|
gamblePlugin, err := plugin.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Run plugin.Metadata
|
|
metadata, err := gamblePlugin.Lookup("Metadata")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Declare it as a function
|
|
metadataFunc, ok := metadata.(func() (lib.PluginData, error))
|
|
if !ok {
|
|
return errors.New("metadata is not a function")
|
|
}
|
|
|
|
// Call the function
|
|
data, err := metadataFunc()
|
|
|
|
// Give them the add points function if they want it
|
|
if data.CanAddPoints {
|
|
addPoints, err := gamblePlugin.Lookup("SetAddPointsFunc")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addPointsFunc, ok := addPoints.(func(func(string, *big.Int)))
|
|
if !ok {
|
|
return errors.New("addPoints is not a function")
|
|
}
|
|
|
|
addPointsFunc(giveUserPoints)
|
|
}
|
|
|
|
// Give them the check moderator function if they want it
|
|
if data.CanCheckMod {
|
|
checkMod, err := gamblePlugin.Lookup("SetCheckModFunc")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
checkModFunc, ok := checkMod.(func(func(string) bool))
|
|
if !ok {
|
|
return errors.New("checkMod is not a function")
|
|
}
|
|
|
|
checkModFunc(userIsModerator)
|
|
}
|
|
|
|
// Append the plugin data to the plugins slice
|
|
plugins = append(plugins, data)
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error setting up plugins: ", err)
|
|
}
|
|
|
|
// Set up the router
|
|
gin.SetMode(gin.ReleaseMode)
|
|
router := gin.New()
|
|
|
|
// Set up the routes
|
|
router.Static("/static", "./static")
|
|
router.LoadHTMLGlob("./templates/*")
|
|
|
|
// Define routes for each plugin
|
|
for _, pluginData := range plugins {
|
|
log.Println("[INFO] Setting up plugin", pluginData.Name)
|
|
|
|
// Try to see if there is a point cost override
|
|
pointCost := pluginData.RecommendedPoints
|
|
var pointCostBytes []byte
|
|
err := conn.QueryRow("SELECT pointsOverride FROM plugins WHERE pluginName = ?", pluginData.Name).Scan(&pointCostBytes)
|
|
if err == nil {
|
|
pointCost = new(big.Int).SetBytes(pointCostBytes)
|
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
|
log.Fatal("[FATAL] Error querying database: ", err)
|
|
}
|
|
|
|
router.GET("/"+pluginData.Name, func(c *gin.Context) {
|
|
var costsSupported string
|
|
if pluginData.CanAcceptArbitraryPointAmount {
|
|
costsSupported = "true"
|
|
} else {
|
|
costsSupported = "false"
|
|
}
|
|
|
|
var canReturn string
|
|
if pluginData.CanReturnPoints {
|
|
canReturn = "true"
|
|
} else {
|
|
canReturn = "false"
|
|
}
|
|
|
|
c.HTML(200, "plugin.html", gin.H{
|
|
"Name": pluginData.Name,
|
|
"Cost": pointCost,
|
|
"PluginHTML": template.HTML(pluginData.PluginHTML),
|
|
"PluginScript": template.JS(pluginData.PluginScript),
|
|
"CanReturn": canReturn,
|
|
"MultipleCostsSupported": costsSupported,
|
|
"OnDataReturn": template.JS(pluginData.OnDataReturn),
|
|
})
|
|
})
|
|
|
|
router.POST("/api/"+pluginData.Name, func(c *gin.Context) {
|
|
var data map[string]interface{}
|
|
err := c.BindJSON(&data)
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": "Invalid JSON"})
|
|
return
|
|
}
|
|
|
|
// Get the user's access token
|
|
accessToken := c.GetHeader("Authorization")
|
|
if accessToken == "" {
|
|
c.JSON(400, gin.H{"error": "No token provided"})
|
|
return
|
|
}
|
|
|
|
// Get the user's channel ID
|
|
channelID, ok := userSessions[accessToken]
|
|
if !ok {
|
|
c.JSON(403, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
|
|
var inputPoints *big.Int
|
|
if pluginData.CanAcceptArbitraryPointAmount {
|
|
// Get the points
|
|
points, ok := data["points"].(string)
|
|
if !ok {
|
|
c.JSON(400, gin.H{"error": "Invalid JSON"})
|
|
return
|
|
}
|
|
|
|
// Parse the points
|
|
inputPoints, ok = new(big.Int).SetString(points, 10)
|
|
if !ok {
|
|
c.JSON(400, gin.H{"error": "Invalid points"})
|
|
return
|
|
}
|
|
|
|
// Subtract the point cost from the user's points
|
|
userPointAmount := getUserPoints(channelID)
|
|
if userPointAmount == big.NewInt(0) {
|
|
c.JSON(400, gin.H{"error": "No points"})
|
|
return
|
|
}
|
|
|
|
remaining := new(big.Int).Sub(userPointAmount, inputPoints)
|
|
if remaining.Cmp(big.NewInt(0)) == 1 {
|
|
subtractUserPoints(channelID, inputPoints)
|
|
} else {
|
|
c.JSON(400, gin.H{"error": "Not enough points, want " + inputPoints.String() + " have " + userPointAmount.String() + ", would leave you with " + remaining.String()})
|
|
return
|
|
}
|
|
} else {
|
|
// Subtract the point cost from the user's points
|
|
userPointAmount := getUserPoints(channelID)
|
|
if userPointAmount == big.NewInt(0) {
|
|
c.JSON(400, gin.H{"error": "No points"})
|
|
return
|
|
}
|
|
|
|
remaining := new(big.Int).Sub(userPointAmount, pointCost)
|
|
if remaining.Cmp(big.NewInt(0)) == 1 {
|
|
subtractUserPoints(channelID, pointCost)
|
|
} else {
|
|
c.JSON(400, gin.H{"error": "Not enough points, want " + pointCost.String() + " have " + userPointAmount.String() + ", would leave you with " + remaining.String()})
|
|
return
|
|
}
|
|
}
|
|
|
|
optionalData, ok := data["optional"].(string)
|
|
if !ok {
|
|
optionalData = "none"
|
|
}
|
|
|
|
if pluginData.CanReturnPoints {
|
|
profit, err := pluginData.ApiCode(c, lib.ApiInput{
|
|
InputPoints: inputPoints,
|
|
OptionalData: optionalData,
|
|
ChannelID: channelID,
|
|
})
|
|
if err != nil {
|
|
c.JSON(424, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
giveUserPoints(channelID, profit)
|
|
c.JSON(200, gin.H{"profit": profit.String()})
|
|
} else {
|
|
_, err := pluginData.ApiCode(c, lib.ApiInput{
|
|
InputPoints: inputPoints,
|
|
OptionalData: optionalData,
|
|
ChannelID: channelID,
|
|
})
|
|
if err != nil {
|
|
c.JSON(424, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"success": "true"})
|
|
}
|
|
|
|
})
|
|
|
|
if pluginData.HasExtraAPI {
|
|
router.POST("/api/extra/"+pluginData.Name, pluginData.ExtraAPICode)
|
|
}
|
|
}
|
|
|
|
// Define the route for /api/claimUnclaimedPoints
|
|
router.POST("/api/claimUnclaimedPoints", func(c *gin.Context) {
|
|
// Look for the token in the userMap
|
|
accessToken := c.GetHeader("Authorization")
|
|
id, ok := userSessions[accessToken]
|
|
if !ok {
|
|
c.JSON(403, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
|
|
// Get the users earliest sent message
|
|
earliestSentMessage, ok := earliestSentMsg[id]
|
|
if !ok {
|
|
c.JSON(400, gin.H{"error": "No messages sent"})
|
|
return
|
|
}
|
|
|
|
// Get the second difference between the earliest sent message and now
|
|
secondDifference := int64(time.Now().Sub(earliestSentMessage).Seconds())
|
|
|
|
// Clear the earliest sent message
|
|
delete(earliestSentMsg, id)
|
|
|
|
// Issue the points
|
|
giveUserPoints(id, big.NewInt(secondDifference))
|
|
|
|
// Return the points
|
|
c.JSON(200, gin.H{"points": strconv.FormatInt(secondDifference, 10)})
|
|
})
|
|
|
|
// Define the route for /api/getUnclaimedPoints
|
|
router.GET("/api/getUnclaimedPoints", func(c *gin.Context) {
|
|
// Look for the token in the userMap
|
|
accessToken := c.GetHeader("Authorization")
|
|
id, ok := userSessions[accessToken]
|
|
if !ok {
|
|
c.JSON(403, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
|
|
// Get the users earliest sent message
|
|
earliestSentMessage, ok := earliestSentMsg[id]
|
|
if !ok {
|
|
c.JSON(200, gin.H{"points": "0"})
|
|
return
|
|
}
|
|
|
|
// Get the second difference between the earliest sent message and now
|
|
secondDifference := int64(time.Now().Sub(earliestSentMessage).Seconds())
|
|
|
|
// Return the points
|
|
c.JSON(200, gin.H{"points": strconv.FormatInt(secondDifference, 10)})
|
|
})
|
|
|
|
// Define the route for /api/getPlugins
|
|
router.GET("/api/getPlugins", func(c *gin.Context) {
|
|
var pluginJSON []map[string]interface{}
|
|
for _, pluginData := range plugins {
|
|
// Try to see if there is a point cost override
|
|
pointCost := pluginData.RecommendedPoints
|
|
var pointCostBytes []byte
|
|
err := conn.QueryRow("SELECT pointsOverride FROM plugins WHERE pluginName = ?", pluginData.Name).Scan(&pointCostBytes)
|
|
if err == nil {
|
|
pointCost = new(big.Int).SetBytes(pointCostBytes)
|
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
|
log.Fatal("[FATAL] Error querying database: ", err)
|
|
}
|
|
|
|
// Append the plugin data to the pluginJSON slice
|
|
var costsSupported string
|
|
if pluginData.CanAcceptArbitraryPointAmount {
|
|
costsSupported = "true"
|
|
} else {
|
|
costsSupported = "false"
|
|
}
|
|
|
|
pluginJSON = append(pluginJSON, map[string]interface{}{
|
|
"Name": pluginData.Name,
|
|
"CanReturnPoints": pluginData.CanReturnPoints,
|
|
"Cost": pointCost,
|
|
"MultipleCostsSupported": costsSupported,
|
|
})
|
|
}
|
|
c.JSON(200, pluginJSON)
|
|
})
|
|
|
|
// Define the route for /api/getPoints
|
|
router.GET("/api/getPoints", func(c *gin.Context) {
|
|
// Look for the token in the userMap
|
|
accessToken := c.GetHeader("Authorization")
|
|
id, ok := userSessions[accessToken]
|
|
if !ok {
|
|
c.JSON(403, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
|
|
// Get the user's points
|
|
points := getUserPoints(id)
|
|
|
|
c.JSON(200, gin.H{"points": points.String()})
|
|
})
|
|
|
|
// Ugh, why do we have to do this legally
|
|
router.POST("/api/delete", func(c *gin.Context) {
|
|
// Look for the token in the userMap
|
|
accessToken := c.GetHeader("Authorization")
|
|
id, ok := userSessions[accessToken]
|
|
if !ok {
|
|
c.JSON(403, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
|
|
// Delete everything from every map with the user's channel ID
|
|
delete(earliestSentMsg, id)
|
|
_, err := conn.Exec("DELETE FROM users WHERE channelID = ?", id)
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error deleting user: ", err)
|
|
}
|
|
|
|
// Delete all access tokens with the user's channel ID
|
|
for key, value := range userSessions {
|
|
if value == id {
|
|
delete(userSessions, key)
|
|
}
|
|
}
|
|
|
|
c.JSON(200, "Data will be deleted in the time it takes for the garbage collector to do its thing")
|
|
})
|
|
|
|
// Define the route for /api/startStream
|
|
router.POST("/api/startStream", func(c *gin.Context) {
|
|
var data map[string]interface{}
|
|
err := c.BindJSON(&data)
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": "Invalid JSON"})
|
|
return
|
|
}
|
|
|
|
// Authenticate the user
|
|
accessToken := c.GetHeader("Authorization")
|
|
if accessToken == "" {
|
|
c.JSON(400, gin.H{"error": "No token provided"})
|
|
return
|
|
}
|
|
|
|
// Check if the user is a moderator
|
|
if !userIsModerator(accessToken) {
|
|
c.JSON(403, gin.H{"error": "You must be a moderator to start a stream"})
|
|
return
|
|
}
|
|
|
|
// Set the live status
|
|
streaming = true
|
|
streamingSince = time.Now()
|
|
liveChatID, ok := data["liveChatID"].(string)
|
|
if !ok {
|
|
c.JSON(400, gin.H{"error": "Invalid JSON"})
|
|
return
|
|
}
|
|
|
|
// Start the chat message checker
|
|
log.Println("Starting chat message checker: ", liveChatID)
|
|
go checkForChatMessages(liveChatID)
|
|
|
|
// Return 200
|
|
c.JSON(200, gin.H{"message": "Stream started"})
|
|
})
|
|
|
|
// Define the route for /api/endStream
|
|
router.POST("/api/endStream", func(c *gin.Context) {
|
|
// Authenticate the user
|
|
accessToken := c.GetHeader("Authorization")
|
|
if accessToken == "" {
|
|
c.JSON(400, gin.H{"error": "No token provided"})
|
|
return
|
|
}
|
|
|
|
// Check if the user is a moderator
|
|
if !userIsModerator(accessToken) {
|
|
c.JSON(403, gin.H{"error": "You must be a moderator to end a stream"})
|
|
return
|
|
}
|
|
|
|
// Set the live status
|
|
streaming = false
|
|
|
|
// Clear all unclaimed points
|
|
for key := range earliestSentMsg {
|
|
delete(earliestSentMsg, key)
|
|
}
|
|
|
|
// Return 200
|
|
c.JSON(200, gin.H{"message": "Stream ended"})
|
|
})
|
|
|
|
router.POST("/api/authorize", func(c *gin.Context) {
|
|
var data map[string]interface{}
|
|
err := c.BindJSON(&data)
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": "Invalid JSON"})
|
|
return
|
|
}
|
|
|
|
// Check if it's a valid google token via JWT
|
|
accessToken, ok := data["idToken"].(string)
|
|
if !ok {
|
|
c.JSON(400, gin.H{"error": "Invalid JSON"})
|
|
return
|
|
}
|
|
|
|
parsedToken, err := jwt.Parse(accessToken, keyVerifyFunction.Keyfunc)
|
|
if err != nil {
|
|
c.JSON(403, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
|
|
// Get the claims
|
|
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
c.JSON(403, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
|
|
// Check if the user is already registered
|
|
var channelID string
|
|
err = conn.QueryRow("SELECT channelID FROM users WHERE sub = ?", claims["sub"]).Scan(&channelID)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
// Get the at_hash
|
|
atHash, ok := claims["at_hash"].(string)
|
|
if !ok {
|
|
c.JSON(403, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
|
|
// Get the access token
|
|
accessToken, ok = data["accessToken"].(string)
|
|
if !ok {
|
|
c.JSON(403, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
|
|
// Hash the access token
|
|
hashedAccessToken := sha256.Sum256([]byte(accessToken))
|
|
|
|
// Check if the hash matches the at_hash
|
|
if strings.ReplaceAll(base64.URLEncoding.EncodeToString(hashedAccessToken[:16]), "=", "") != atHash {
|
|
c.JSON(403, gin.H{"error": "Non-matching access token"})
|
|
return
|
|
}
|
|
|
|
// Get the user's channel ID
|
|
channelID, response, err := getYTChID(accessToken)
|
|
if err != nil {
|
|
c.JSON(response, gin.H{"error": "Error getting channel ID"})
|
|
return
|
|
}
|
|
|
|
_, err = conn.Exec("INSERT INTO users (channelID, sub, points) VALUES (?, ?, ?)", channelID, claims["sub"], big.NewInt(0).Bytes())
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error registering user: ", err)
|
|
}
|
|
} else {
|
|
log.Fatal("[FATAL] Error querying database: ", err)
|
|
}
|
|
}
|
|
|
|
// Create a new random session token
|
|
sessionToken := make([]byte, 32)
|
|
_, err = rand.Read(sessionToken)
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error generating session token: ", err)
|
|
}
|
|
|
|
// Hex encode the session token
|
|
sessionTokenHex := hex.EncodeToString(sessionToken)
|
|
|
|
// Add the session token to the userSessions map
|
|
userSessions[sessionTokenHex] = channelID
|
|
|
|
// Return the session token
|
|
c.JSON(200, gin.H{"sessionToken": sessionTokenHex})
|
|
})
|
|
|
|
// Now some static routes
|
|
router.GET("/", func(c *gin.Context) {
|
|
c.HTML(200, "index.html", gin.H{})
|
|
})
|
|
|
|
router.GET("/admin", func(c *gin.Context) {
|
|
c.HTML(200, "admin.html", gin.H{})
|
|
})
|
|
|
|
router.GET("/login", func(c *gin.Context) {
|
|
c.HTML(200, "login.html", gin.H{})
|
|
})
|
|
|
|
router.GET("/privacy", func(c *gin.Context) {
|
|
c.HTML(200, "privacy.html", gin.H{})
|
|
})
|
|
|
|
router.GET("/tos", func(c *gin.Context) {
|
|
c.HTML(200, "tos.html", gin.H{})
|
|
})
|
|
|
|
// Start the server
|
|
var address string
|
|
if len(os.Args) < 2 {
|
|
address = ":8080"
|
|
} else {
|
|
address = os.Args[1]
|
|
}
|
|
|
|
log.Println("[INFO] Start server on " + address)
|
|
err = router.Run(address)
|
|
if err != nil {
|
|
log.Fatal("[FATAL] Error starting server: ", err)
|
|
}
|
|
}
|