From 2d832736df5617b2cff1fe6fe7b266d208dad91c Mon Sep 17 00:00:00 2001
From: arzumify
Date: Mon, 24 Mar 2025 18:56:52 +0000
Subject: [PATCH] Revamped some things
---
build.sh | 7 -
go.mod | 5 +-
go.sum | 10 +-
lib/main.go | 38 --
main.go | 1186 ++++++++++++++++-----------------
plugins-src/bet/build.sh | 2 -
plugins-src/bet/main.go | 252 -------
plugins-src/diceroll/build.sh | 2 -
plugins-src/diceroll/main.go | 86 ---
templates/admin.html | 124 ++--
templates/bet.html | 73 ++
templates/index.html | 43 +-
templates/link.html | 34 +
templates/login.html | 170 -----
templates/plugin.html | 62 --
15 files changed, 761 insertions(+), 1333 deletions(-)
delete mode 100644 lib/main.go
delete mode 100755 plugins-src/bet/build.sh
delete mode 100644 plugins-src/bet/main.go
delete mode 100755 plugins-src/diceroll/build.sh
delete mode 100644 plugins-src/diceroll/main.go
create mode 100644 templates/bet.html
create mode 100644 templates/link.html
delete mode 100644 templates/login.html
delete mode 100644 templates/plugin.html
diff --git a/build.sh b/build.sh
index f9f9af1..64a6a23 100755
--- a/build.sh
+++ b/build.sh
@@ -1,9 +1,2 @@
#!/bin/sh
go build -ldflags "-s -w"
-cd plugins-src/diceroll || exit
-./build.sh
-mv diceroll.so ../../plugins/diceroll.so
-cd ../bet || exit
-./build.sh
-mv bet.so ../../plugins/bet.so
-echo Done
diff --git a/go.mod b/go.mod
index 897c09c..edde771 100644
--- a/go.mod
+++ b/go.mod
@@ -3,14 +3,12 @@ module shoGambler
go 1.23.0
require (
- github.com/MicahParks/keyfunc/v3 v3.3.5
github.com/gin-gonic/gin v1.10.0
- github.com/golang-jwt/jwt/v5 v5.2.0
+ github.com/gorilla/websocket v1.5.3
modernc.org/sqlite v1.32.0
)
require (
- github.com/MicahParks/jwkset v0.5.19 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
@@ -40,7 +38,6 @@ require (
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.15.0 // indirect
- golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
diff --git a/go.sum b/go.sum
index 68cda32..a061417 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,3 @@
-github.com/MicahParks/jwkset v0.5.19 h1:XZCsgJv05DBCvxEHYEHlSafqiuVn5ESG0VRB331Fxhw=
-github.com/MicahParks/jwkset v0.5.19/go.mod h1:q8ptTGn/Z9c4MwbcfeCDssADeVQb3Pk7PnVxrvi+2QY=
-github.com/MicahParks/keyfunc/v3 v3.3.5 h1:7ceAJLUAldnoueHDNzF8Bx06oVcQ5CfJnYwNt1U3YYo=
-github.com/MicahParks/keyfunc/v3 v3.3.5/go.mod h1:SdCCyMJn/bYqWDvARspC6nCT8Sk74MjuAY22C7dCST8=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
@@ -31,8 +27,6 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
-github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -40,6 +34,8 @@ github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlG
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -96,8 +92,6 @@ golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
diff --git a/lib/main.go b/lib/main.go
deleted file mode 100644
index 9789d37..0000000
--- a/lib/main.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package lib
-
-import (
- "math/big"
-
- "github.com/gin-gonic/gin"
-)
-
-type Date struct {
- DaysSinceEpoch uint64
-}
-
-type PluginData struct {
- Name string
- CanReturnPoints bool
- CanAddPoints bool
- CanCheckMod bool
- OnDataReturn string
- CanAcceptArbitraryPointAmount bool
- RecommendedPoints *big.Int
- PluginHTML string
- PluginScript string
- ApiCode func(*gin.Context, ApiInput) (*big.Int, error)
- HasExtraAPI bool
- ExtraAPICode func(*gin.Context)
-}
-
-type ApiInput struct {
- InputPoints *big.Int
- AddPointsFunction func(string, *big.Int)
- ChannelID string
- OptionalData string
-}
-
-type DateAndStream struct {
- Date Date
- Stream *big.Int
-}
diff --git a/main.go b/main.go
index ec44dc5..dd0bbe7 100644
--- a/main.go
+++ b/main.go
@@ -1,193 +1,158 @@
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"
+ "github.com/gorilla/websocket"
_ "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)
+func initUser(channelID string, isModerator bool, isStreamer bool) {
+ _, err := conn.Exec("INSERT INTO users (channelID, points, isModerator, isStreamer) VALUES (?, ?, ?, ?)", channelID, 0, isModerator, isStreamer)
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)
- }
+ panic("Error inserting moderator: " + err.Error())
}
}
-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
+func getPoints(channelID string) int64 {
+ var points int64
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)
+ if err != nil {
+ panic("Error querying database: " + err.Error())
}
- return new(big.Int).SetBytes(points)
+ return 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 updatePoints(channelID string, points int64) {
+ _, err := conn.Exec("UPDATE users SET points = ? WHERE channelID = ?", points, channelID)
+ if err != nil {
+ panic("Error updating user points: " + err.Error())
}
}
-func checkForChatMessages(liveChatID string) {
+func addPoints(channelID string, points int64) {
+ updatePoints(channelID, getPoints(channelID)+points)
+}
+
+func minusPoints(channelID string, points int64) {
+ updatePoints(channelID, getPoints(channelID)-points)
+}
+
+//goland:noinspection GoUnusedFunction
+func userIsStreamer(channelID string) bool {
+ var isStreamer bool
+ err := conn.QueryRow("SELECT isStreamer FROM users WHERE channelID = ?", channelID).Scan(&isStreamer)
+ if err != nil {
+ panic("Error querying database: " + err.Error())
+ }
+
+ return isStreamer
+}
+
+func userIsModerator(channelID string) bool {
+ var isModerator bool
+ err := conn.QueryRow("SELECT isModerator FROM users WHERE channelID = ?", channelID).Scan(&isModerator)
+ if err != nil {
+ panic("Error querying database: " + err.Error())
+ }
+
+ return isModerator
+}
+
+type LiveChatMessages struct {
+ Items []Items `json:"items"`
+}
+
+type Items struct {
+ AutorDetails AuthorDetails `json:"authorDetails"`
+ Snippet Snippet `json:"snippet"`
+}
+
+type AuthorDetails struct {
+ DisplayName string `json:"displayName"`
+ ChannelID string `json:"channelId"`
+}
+
+type Snippet struct {
+ PublishedAt string `json:"publishedAt"`
+ TextMessageDetails TextMessageDetails `json:"textMessageDetails"`
+}
+
+type TextMessageDetails struct {
+ MessageText string `json:"messageText"`
+}
+
+func scrape(liveChatID string, key string) {
for {
if streaming == true {
// Check for chat messages
- log.Println("[INFO] Scary, we are expending a Google API credit (on stream)!")
+ log.Println("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)
+ response, err := http.Get("https://www.googleapis.com/youtube/v3/liveChat/messages?liveChatId=" + liveChatID + "&part=snippet,authorDetails&key=" + key)
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
+ log.Println("Error getting chat messages: " + err.Error() + ", trying again in 20 seconds")
+ time.Sleep(time.Second * 20)
+ continue
}
// Read the response
- var responseJSON map[string]interface{}
+ var responseJSON LiveChatMessages
err = json.NewDecoder(response.Body).Decode(&responseJSON)
if err != nil {
- log.Println("[ERROR] Error decoding chat messages: ", err)
- return
+ panic("Error decoding JSON: " + err.Error())
}
// Check the status code
if response.StatusCode != 200 {
- log.Println("[ERROR] Error getting chat messages: ", response.Status, responseJSON)
+ log.Println("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
+ for _, item := range responseJSON.Items {
+ log.Println("Processing message from ", item.AutorDetails.DisplayName)
+ // Check if the message starts with !verify
+ if strings.HasPrefix(item.Snippet.TextMessageDetails.MessageText, "!verify ") {
+ // Get the channel ID
+ channelID := item.AutorDetails.ChannelID
+
+ // Check if the user is in pendingSessions
+ pendingSession, ok := pendingSessions[strings.TrimPrefix("!verify ", item.Snippet.TextMessageDetails.MessageText)]
+ if !ok {
+ // This user does not need to be verified
+ continue
+ }
+
+ // Add the user to the sessions map
+ sessions[pendingSession.IP] = channelID
+
+ // Call the event callback
+ pendingSession.EventCallback()
+
+ // Remove the user from the pendingSessions map
+ delete(pendingSessions, strings.TrimPrefix("!verify ", item.Snippet.TextMessageDetails.MessageText))
+ } else {
+ earliestSentMessage, ok := unclaimedMessages[item.AutorDetails.ChannelID]
+ publishedTime, err := time.Parse(time.RFC3339Nano, item.Snippet.PublishedAt)
+ if err != nil {
+ log.Println("Error parsing time: " + err.Error())
+ } else if !ok || publishedTime.Before(earliestSentMessage) {
+ if publishedTime.After(streamingSince) {
+ log.Println("New message from ", item.AutorDetails.DisplayName)
+ unclaimedMessages[item.AutorDetails.ChannelID] = publishedTime
+ }
}
}
}
@@ -195,7 +160,7 @@ func checkForChatMessages(liveChatID string) {
// Close the response body
err := response.Body.Close()
if err != nil {
- log.Println("[ERROR] Error closing response body: ", err)
+ log.Println("Error closing response body: " + err.Error())
}
// Wait for the rate because Google likes to screw us over
@@ -207,19 +172,111 @@ func checkForChatMessages(liveChatID string) {
}
}
+func getUserSecondDifference(channelID string) (int64, bool) {
+ earliestSentMessage, ok := unclaimedMessages[channelID]
+ if !ok {
+ return 0, false
+ }
+
+ return int64(time.Now().Sub(earliestSentMessage).Seconds()), true
+}
+
+type PendingSession struct {
+ IP string
+ EventCallback func()
+}
+
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
+ conn *sql.DB
+ unclaimedMessages = make(map[string]time.Time)
+ sessions = make(map[string]string)
+ pendingSessions = make(map[string]PendingSession)
+ streamingSince time.Time
+ configFile Config
+ streaming bool
+ upgrade = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ }
)
-type config struct {
- ApiKey string `json:"key"`
- Rate int `json:"rate"`
+func giveBetPoints(correct int) {
+ for better, bet := range bets {
+ if bet.answer == correct {
+ addPoints(better, bet.amount*stakesMultiplier)
+ }
+ }
+
+ endBet()
+}
+
+func endBet() {
+ bettingOpen = false
+ question = ""
+ possibleAnswers = nil
+ timeToBet = 0
+ bets = nil
+}
+
+func startBet(q string, a []string, d time.Duration) {
+ bettingOpen = true
+ question = q
+ possibleAnswers = make(map[int]string)
+ for i, answer := range a {
+ possibleAnswers[i] = answer
+ }
+ timeToBet = d
+ bets = make(map[string]Bet)
+}
+
+func getLivestream(channelID string) string {
+ // Get the channel's livestream
+ response, err := http.Get("https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=" + channelID + "&eventType=live&type=video&key=" + configFile.ApiKey)
+ if err != nil {
+ panic("Error getting livestream: " + err.Error())
+ }
+
+ // Read the response
+ var responseJSON map[string]interface{}
+ err = json.NewDecoder(response.Body).Decode(&responseJSON)
+ if err != nil {
+ panic("Error decoding JSON: " + err.Error())
+ }
+
+ // Check the status code
+ if response.StatusCode != 200 {
+ panic("Error getting livestream: " + response.Status)
+ }
+
+ // Get the video ID
+ items := responseJSON["items"].([]interface{})
+ if len(items) == 0 {
+ return ""
+ }
+
+ videoID := items[0].(map[string]interface{})["id"].(map[string]interface{})["videoId"].(string)
+
+ return videoID
+}
+
+var (
+ stakesMultiplier int64
+ bettingOpen bool
+ question string
+ possibleAnswers map[int]string
+ timeToBet time.Duration
+ bets map[string]Bet
+)
+
+type Bet struct {
+ amount int64
+ answer int
+}
+
+type Config struct {
+ ApiKey string `json:"key"`
+ Rate float64 `json:"rate"`
+ Multiplier float64 `json:"multiplier"`
}
func main() {
@@ -227,120 +284,31 @@ func main() {
var err error
conn, err = sql.Open("sqlite", "database.db")
if err != nil {
- log.Fatal("[FATAL] Error connecting to database: ", err)
+ panic("Error connecting to database: " + err.Error())
}
// Read in config.json
configBytes, err := os.ReadFile("config.json")
if err != nil {
- log.Fatal("[FATAL] Error reading config.json: ", err)
+ panic("Error reading config.json: " + err.Error())
}
// 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)
+ panic("Error parsing config.json: " + err.Error())
}
// 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)")
+ _, err = conn.Exec("CREATE TABLE IF NOT EXISTS users (channelID TEXT NOT NULL UNIQUE, points INTEGER NOT NULL, isModerator BOOLEAN NOT NULL DEFAULT FALSE, isStreamer BOOLEAN NOT NULL DEFAULT FALSE)")
if err != nil {
- log.Fatal("[FATAL] Error creating users table: ", err)
+ panic("Error creating users table: " + err.Error())
}
- // Set up the JWT verification
- keyVerifyFunction, err := keyfunc.NewDefault([]string{"https://www.googleapis.com/oauth2/v3/certs"})
+ // Add shounic by default
+ _, err = conn.Exec("INSERT OR IGNORE INTO users (channelID, points, isModerator, isStreamer) VALUES ('UCHlTEt24Yb4ylJFuWz7hXIw', 0, 1, 1)")
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)
+ panic("Error inserting shounic: " + err.Error())
}
// Set up the router
@@ -351,283 +319,279 @@ func main() {
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]
+ router.GET("/delete", func(c *gin.Context) {
+ // Get the user's channel ID
+ id, ok := sessions[c.ClientIP()]
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)
+ delete(unclaimedMessages, id)
_, err := conn.Exec("DELETE FROM users WHERE channelID = ?", id)
if err != nil {
- log.Fatal("[FATAL] Error deleting user: ", err)
+ panic("Error deleting user: " + err.Error())
}
// Delete all access tokens with the user's channel ID
- for key, value := range userSessions {
+ for key, value := range sessions {
if value == id {
- delete(userSessions, key)
+ delete(sessions, key)
}
}
- c.JSON(200, "Data will be deleted in the time it takes for the garbage collector to do its thing")
+ c.JSON(200, "Data deleted")
})
- // Define the route for /api/startStream
- router.POST("/api/startStream", func(c *gin.Context) {
+ // Link the user's account
+ router.GET("/api/link", func(c *gin.Context) {
+ // Kick over to a WebSocket
+ conn, err := upgrade.Upgrade(c.Writer, c.Request, nil)
+ if err != nil {
+ panic("Error upgrading connection: " + err.Error())
+ }
+
+ // Create a new nonce
+ nonce := make([]byte, 16)
+ _, err = rand.Read(nonce)
+ if err != nil {
+ panic("Error generating nonce: " + err.Error())
+ }
+
+ // Base64 encode the nonce
+ nonceBase64 := base64.StdEncoding.EncodeToString(nonce)
+
+ // Set a read deadline
+ var read bool
+ go func() {
+ time.Sleep(time.Second * 5)
+ if !read {
+ err := conn.WriteJSON(gin.H{
+ "type": "error",
+ "error": "Did not respond to nonce",
+ })
+ if err != nil {
+ panic("Error writing back to WebSocket: " + err.Error())
+ }
+ }
+ }()
+
+ // Send the nonce
+ err = conn.WriteJSON(gin.H{
+ "type": "nonce",
+ "nonce": nonceBase64,
+ })
+ if err != nil {
+ return
+ }
+
+ _, nonceResponse, err := conn.ReadMessage()
+ if err != nil {
+ if !errors.Is(err, websocket.ErrCloseSent) {
+ panic("Error reading nonce response: " + err.Error())
+ } else {
+ return
+ }
+ }
+
+ read = true
+
+ jsonResponse := make(map[string]interface{})
+ err = json.Unmarshal(nonceResponse, &jsonResponse)
+ if err != nil {
+ panic("Error unmarshalling nonce response: " + err.Error())
+ }
+
+ heartbeatStop := make(chan struct{})
+
+ // If the response is ok, add the user to the pendingSessions map and start heartbeats
+ if jsonResponse["type"] == "success" {
+ pendingSessions[nonceBase64] = PendingSession{
+ IP: c.ClientIP(),
+ EventCallback: func() {
+ // Write back to the WebSocket
+ err = conn.WriteJSON(gin.H{"type": "success"})
+ if err != nil {
+ panic("Error writing back to WebSocket: " + err.Error())
+ }
+
+ err = conn.Close()
+ if err != nil {
+ panic("Error closing WebSocket: " + err.Error())
+ }
+
+ // Tell the heartbeat loop to stop
+ close(heartbeatStop)
+ },
+ }
+ }
+
+ for {
+ select {
+ case <-heartbeatStop:
+ return
+ case <-time.After(time.Second * 5):
+ err := conn.WriteJSON(gin.H{
+ "type": "ping",
+ })
+ if err != nil {
+ return
+ }
+
+ var read bool
+
+ go func() {
+ time.Sleep(time.Second * 5)
+ if !read {
+ err := conn.WriteJSON(gin.H{
+ "type": "error",
+ "error": "Did not respond to ping",
+ })
+ if err != nil {
+ panic("Error writing back to WebSocket: " + err.Error())
+ }
+ }
+ }()
+
+ _, _, err = conn.ReadMessage()
+ if err != nil {
+ return
+ }
+
+ read = true
+ }
+ }
+ })
+
+ // Set up the betting route
+ router.POST("/api/bet", 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 betting is open
+ if bettingOpen {
+ // Get the user's channel ID
+ id, ok := sessions[c.ClientIP()]
+ if !ok {
+ c.JSON(403, gin.H{"error": "Invalid token"})
+ return
+ }
+
+ // Get the user's bet amount
+ amountFloat, ok := data["amount"].(float64)
+ if !ok {
+ c.JSON(400, gin.H{"error": "Invalid bet"})
+ return
+ }
+
+ amount := int64(amountFloat)
+
+ // Check if the user has enough points
+ if getPoints(id) < amount {
+ c.JSON(400, gin.H{"error": "Not enough points"})
+ return
+ }
+
+ // Get the user's bet answer
+ answerFloat, ok := data["answer"].(float64)
+ if !ok {
+ c.JSON(400, gin.H{"error": "Invalid answer"})
+ return
+ }
+
+ answer := int(answerFloat)
+
+ // Check if the answer is valid
+ if _, ok := possibleAnswers[answer]; !ok {
+ c.JSON(400, gin.H{"error": "Invalid answer"})
+ return
+ }
+
+ // Add the bet to the bets map
+ bets[id] = Bet{
+ amount: amount,
+ answer: answer,
+ }
+
+ // Subtract the points from the user
+ minusPoints(id, amount)
+
+ c.JSON(200, gin.H{"message": "Bet placed"})
+ } else {
+ c.JSON(400, gin.H{"error": "Betting is closed"})
+ }
+ })
+
+ router.POST("/api/startBet", func(c *gin.Context) {
+ // Parse the JSON
+ 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 channel ID
+ id, ok := sessions[c.ClientIP()]
+ if !ok {
+ c.JSON(403, gin.H{"error": "Invalid token"})
+ return
+ }
+
+ // Check if the user is a moderator
+ if !userIsModerator(id) {
+ c.JSON(403, gin.H{"error": "You must be a moderator to start a bet"})
+ return
+ }
+
+ // Check if betting is open
+ if bettingOpen {
+ c.JSON(206, gin.H{"error": "There is already a running bet"})
+ return
+ }
+
+ question := data["question"].(string)
+ possibleAnswersRaw := data["possible"].([]interface{})
+ var possibleAnswers []string
+ for _, answer := range possibleAnswersRaw {
+ possibleAnswers = append(possibleAnswers, answer.(string))
+ }
+ timeToBetFloat := data["timeToBet"].(float64)
+ timeToBet := time.Duration(timeToBetFloat) * time.Second
+
+ startBet(question, possibleAnswers, timeToBet)
+
+ c.JSON(200, gin.H{"message": "Bet started"})
+ })
+
+ // Define the route for /api/getCurrentBet
+ router.GET("/api/getCurrentBet", func(c *gin.Context) {
+ if bettingOpen {
+ var possibleAnswersList []string
+ for _, answer := range possibleAnswers {
+ possibleAnswersList = append(possibleAnswersList, answer)
+ }
+ c.JSON(200, gin.H{
+ "question": question,
+ "possible": possibleAnswersList,
+ "timeToBet": timeToBet.Seconds(),
+ })
+ return
+ } else {
+ c.JSON(206, gin.H{"error": "There is not a running bet"})
+ return
+ }
+ })
+
+ // Define the route for /api/endBet
+ router.POST("/api/endBet", func(c *gin.Context) {
+ // Parse the JSON
var data map[string]interface{}
err := c.BindJSON(&data)
if err != nil {
@@ -644,6 +608,100 @@ func main() {
// Check if the user is a moderator
if !userIsModerator(accessToken) {
+ c.JSON(403, gin.H{"error": "You must be a moderator to end a bet"})
+ return
+ }
+
+ correctFloat, ok := data["correct"].(float64)
+ if !ok {
+ c.JSON(400, gin.H{"error": "Invalid correct answer"})
+ return
+ }
+
+ correct := int(correctFloat)
+
+ // Give the points to the correct betters
+ giveBetPoints(correct)
+
+ c.JSON(200, gin.H{"message": "Bet ended"})
+ })
+
+ // Define the route for /api/claimUnclaimedPoints
+ router.GET("/api/claimUnclaimedPoints", func(c *gin.Context) {
+ // Get the user's channel ID
+ id, ok := sessions[c.ClientIP()]
+ if !ok {
+ c.JSON(403, gin.H{"error": "Invalid token"})
+ return
+ }
+
+ // Get the user's second difference
+ secondDifference, ok := getUserSecondDifference(id)
+ if !ok {
+ c.JSON(200, gin.H{"points": "0"})
+ return
+ }
+
+ // Clear the earliest sent message
+ delete(unclaimedMessages, id)
+
+ // Issue the points
+ addPoints(id, secondDifference*int64(configFile.Multiplier))
+
+ // 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) {
+ // Get the user's channel ID
+ id, ok := sessions[c.ClientIP()]
+ if !ok {
+ c.JSON(403, gin.H{"error": "Invalid token"})
+ return
+ }
+
+ // Get the user's second difference
+ secondDifference, ok := getUserSecondDifference(id)
+ if !ok {
+ c.JSON(200, gin.H{"points": "0"})
+ return
+ }
+
+ c.JSON(200, gin.H{"points": strconv.FormatInt(secondDifference, 10)})
+ })
+
+ // Define the route for /api/getPoints
+ router.GET("/api/getPoints", func(c *gin.Context) {
+ // Get the user's channel ID
+ id, ok := sessions[c.ClientIP()]
+ if !ok {
+ c.JSON(403, gin.H{"error": "Invalid token"})
+ return
+ }
+
+ c.JSON(200, gin.H{"points": getPoints(id)})
+ })
+
+ // Define the route for /api/startStream
+ router.GET("/api/startStream", func(c *gin.Context) {
+ // Parse the JSON
+ 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 channel ID
+ id, ok := sessions[c.ClientIP()]
+ if !ok {
+ c.JSON(403, gin.H{"error": "Invalid token"})
+ return
+ }
+
+ // Check if the user is a streamer
+ if !userIsModerator(id) {
c.JSON(403, gin.H{"error": "You must be a moderator to start a stream"})
return
}
@@ -659,14 +717,15 @@ func main() {
// Start the chat message checker
log.Println("Starting chat message checker: ", liveChatID)
- go checkForChatMessages(liveChatID)
+
+ go scrape(liveChatID, configFile.ApiKey)
// Return 200
c.JSON(200, gin.H{"message": "Stream started"})
})
// Define the route for /api/endStream
- router.POST("/api/endStream", func(c *gin.Context) {
+ router.GET("/api/endStream", func(c *gin.Context) {
// Authenticate the user
accessToken := c.GetHeader("Authorization")
if accessToken == "" {
@@ -684,101 +743,26 @@ func main() {
streaming = false
// Clear all unclaimed points
- for key := range earliestSentMsg {
- delete(earliestSentMsg, key)
+ for key := range unclaimedMessages {
+ delete(unclaimedMessages, key)
}
+ // Clear the bets
+ endBet()
+
// 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)
+ router.GET("/api/loggedIn", func(c *gin.Context) {
+ // Get the user's channel ID
+ _, ok := sessions[c.ClientIP()]
if !ok {
- c.JSON(400, gin.H{"error": "Invalid JSON"})
+ c.JSON(403, gin.H{"loggedIn": false})
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})
+ c.JSON(200, gin.H{"loggedIn": true})
})
// Now some static routes
@@ -790,8 +774,12 @@ func main() {
c.HTML(200, "admin.html", gin.H{})
})
- router.GET("/login", func(c *gin.Context) {
- c.HTML(200, "login.html", gin.H{})
+ router.GET("/link", func(c *gin.Context) {
+ c.HTML(200, "link.html", gin.H{})
+ })
+
+ router.GET("/bet", func(c *gin.Context) {
+ c.HTML(200, "bet.html", gin.H{})
})
router.GET("/privacy", func(c *gin.Context) {
@@ -810,9 +798,9 @@ func main() {
address = os.Args[1]
}
- log.Println("[INFO] Start server on " + address)
+ log.Println("Start server on " + address)
err = router.Run(address)
if err != nil {
- log.Fatal("[FATAL] Error starting server: ", err)
+ panic("Error starting server: " + err.Error())
}
}
diff --git a/plugins-src/bet/build.sh b/plugins-src/bet/build.sh
deleted file mode 100755
index dbf25f0..0000000
--- a/plugins-src/bet/build.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-go build -ldflags "-s -w" -buildmode=plugin -o bet.so main.go
\ No newline at end of file
diff --git a/plugins-src/bet/main.go b/plugins-src/bet/main.go
deleted file mode 100644
index ffb94b3..0000000
--- a/plugins-src/bet/main.go
+++ /dev/null
@@ -1,252 +0,0 @@
-package main
-
-import (
- "errors"
- "math/big"
- "shoGambler/lib"
- "time"
-
- "github.com/gin-gonic/gin"
-)
-
-type bet struct {
- amount *big.Int
- answer string
-}
-
-var (
- bets = make(map[string]bet)
- possibleAnswers []string
- bettingIsOpen = false
- question string
- addPoints func(string, *big.Int)
- userIsModerator func(string) bool
- timeToBet time.Time
-)
-
-func Metadata() (lib.PluginData, error) {
- return lib.PluginData{
- Name: "bet-on-it",
- CanReturnPoints: false,
- RecommendedPoints: big.NewInt(10),
- CanAcceptArbitraryPointAmount: true,
- PluginHTML: `
-
Bet on it
-
Vote for what you think will happen
-
If you lose the bet, you lose your points
-
If you win the bet, you get double your points back
-
-
-
-
- `,
- PluginScript: `
- async function updateTimer(time) {
- while (true) {
- let timeLeft = time - Math.floor(Date.now() / 1000);
- if (timeLeft <= 0) {
- document.getElementById("timer").innerText = "Betting is now closed";
- document.getElementById("bet").disabled = true;
- } else {
- document.getElementById("timer").innerText = "Betting closes in " + timeLeft + " seconds";
- }
- await new Promise(r => setTimeout(r, 1000));
- }
- }
-
- let data
- fetch("/api/extra/bet-on-it", {
- method: "POST",
- body: JSON.stringify({
- Action: "getCurrentBet",
- }),
- headers: {
- "Content-Type": "application/json",
- },
- })
- .then(async (response) => {
- if (response.status == 206) {
- alert("There is not a running bet");
- window.location.href = "/";
- } else if (response.status != 200) {
- alert("Error: " + response.statusText);
- window.location.href = "/";
- }
- data = await response.json();
- document.getElementById("question").innerText = data["question"];
- document.getElementById("possible").innerText = data["possible"].join(", ");
- updateTimer(data["timeToBet"]);
- document.getElementById("bet").disabled = false;
- })
- document.getElementById("bet").addEventListener("click", async () => {
- let points = BigInt(0);
- try {
- points = BigInt(prompt("How many points do you want to spend?"));
- } catch (e) {
- alert("Invalid number");
- return;
- }
- let candidate = prompt(data["question"] + data["possible"].join(", ") + "(case sensitive)");
- if (data["possible"].includes(candidate)) {
- sendCost(points, candidate);
- } else {
- alert("That's not an option!")
- }
- })
-
- `,
- ApiCode: ApiCode,
- HasExtraAPI: true,
- ExtraAPICode: ExtraAPICode,
- OnDataReturn: "alert('Bet placed. Good luck!')",
- CanAddPoints: true,
- CanCheckMod: true,
- }, nil
-}
-
-func ApiCode(_ *gin.Context, input lib.ApiInput) (*big.Int, error) {
- if time.Now().Before(timeToBet) {
- // See which option the user bet on
- candidate := input.OptionalData
-
- // Add the user's bet to the map
- validBet := false
- for _, possibleAnswer := range possibleAnswers {
- if candidate == possibleAnswer {
- validBet = true
- break
- }
- }
-
- if !validBet {
- return nil, errors.New("invalid bet")
- } else {
- bets[input.ChannelID] = bet{
- amount: input.InputPoints,
- answer: candidate,
- }
- return nil, nil
- }
- } else {
- return nil, errors.New("betting is closed")
- }
-}
-
-func ExtraAPICode(c *gin.Context) {
- var data map[string]interface{}
- err := c.BindJSON(&data)
- if err != nil {
- c.JSON(400, gin.H{
- "error": "Invalid JSON",
- })
- return
- }
- action, ok := data["Action"].(string)
- if !ok {
- c.JSON(400, gin.H{
- "error": "Invalid action",
- })
- return
- }
- switch action {
- case "getCurrentBet":
- if bettingIsOpen {
- c.JSON(200, gin.H{
- "question": question,
- "possible": possibleAnswers,
- "timeToBet": timeToBet.Unix(),
- })
- return
- } else {
- c.JSON(206, gin.H{
- "error": "There is not a running bet",
- })
- return
- }
- case "startBet":
- if bettingIsOpen {
- c.JSON(206, gin.H{
- "error": "There is already a running bet",
- })
- return
- } else {
- accessToken := c.GetHeader("Authorization")
- if !userIsModerator(accessToken) || accessToken == "" {
- c.JSON(403, gin.H{
- "error": "You must be a moderator to start a bet",
- })
- return
- }
- question, ok = data["question"].(string)
- if !ok {
- c.JSON(400, gin.H{
- "error": "Invalid question",
- })
- return
- }
- possibleAnswersJSON, ok := data["possible"].([]interface{})
- if !ok {
- c.JSON(400, gin.H{
- "error": "Invalid possible answers",
- })
- return
- }
- possibleAnswers = make([]string, len(possibleAnswersJSON))
- for i, possibleAnswer := range possibleAnswersJSON {
- possibleAnswers[i], ok = possibleAnswer.(string)
- if !ok {
- c.JSON(400, gin.H{
- "error": "Invalid possible answer",
- })
- return
- }
- }
-
- timeToBet = time.Unix(int64(data["timeToBet"].(float64)), 0)
- bettingIsOpen = true
- c.JSON(200, gin.H{
- "success": true,
- })
- return
- }
- case "endBet":
- if bettingIsOpen {
- accessToken := c.GetHeader("Authorization")
- if !userIsModerator(accessToken) || accessToken == "" {
- c.JSON(403, gin.H{
- "error": "You must be a moderator to end a bet",
- })
- return
- }
- bettingIsOpen = false
- // Give the winners double their points
- for channelID, bet := range bets {
- if bet.answer == data["answer"].(string) {
- addPoints(channelID, new(big.Int).Mul(bet.amount, big.NewInt(2)))
- }
- }
- // Clear the bets
- bets = make(map[string]bet)
- question = ""
- possibleAnswers = nil
- // Return success
- c.JSON(200, gin.H{
- "success": true,
- })
- return
- }
- default:
- c.JSON(400, gin.H{
- "error": "Invalid action",
- })
- return
- }
-}
-
-func SetAddPointsFunc(addPointsFunc func(string, *big.Int)) {
- addPoints = addPointsFunc
-}
-
-func SetCheckModFunc(userIsModeratorFunc func(string) bool) {
- userIsModerator = userIsModeratorFunc
-}
diff --git a/plugins-src/diceroll/build.sh b/plugins-src/diceroll/build.sh
deleted file mode 100755
index ce121c1..0000000
--- a/plugins-src/diceroll/build.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-go build -ldflags "-s -w" -buildmode=plugin -o diceroll.so main.go
\ No newline at end of file
diff --git a/plugins-src/diceroll/main.go b/plugins-src/diceroll/main.go
deleted file mode 100644
index 327ffd4..0000000
--- a/plugins-src/diceroll/main.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package main
-
-import (
- "crypto/rand"
- "errors"
- "fmt"
- "math/big"
-
- "shoGambler/lib"
-
- "github.com/gin-gonic/gin"
-)
-
-func Metadata() (lib.PluginData, error) {
- return lib.PluginData{
- Name: "dice-roll",
- // It does return points
- CanReturnPoints: true,
- // It does not add points outside out /api/dice-roll
- CanAddPoints: false,
- // It does not need an arbitrary API, it's fine within the constraints of the plugin API
- HasExtraAPI: false,
- // It does not need an arbitrary API, so this is nil
- ExtraAPICode: nil,
- // It recommends 10 points to be spent
- RecommendedPoints: big.NewInt(10),
- // You can input arbitrary point amounts
- CanAcceptArbitraryPointAmount: true,
- // Very simple HTML
- PluginHTML: `
-
Dice Roll
-
Roll a dice
-
- `,
- // Very simple script
- PluginScript: `
- document.getElementById("roll").addEventListener("click", async () => {
- let points = BigInt(0);
- try {
- points = BigInt(prompt("How many points do you want to spend?"));
- } catch (e) {
- alert("Invalid number");
- return;
- }
- sendCost(points);
- })
-
- `,
- // The API code is the function ApiCode
- ApiCode: ApiCode,
- // If the plugin html says the plugin has returned data rather than points, it's an error
- OnDataReturn: "alert('Error: this plugin should be returning points'); throw new Error('Error: this plugin should be returning points')",
- }, nil
-}
-
-func ApiCode(_ *gin.Context, input lib.ApiInput) (*big.Int, error) {
- // Roll a die
- diceRoll, err := rand.Int(rand.Reader, big.NewInt(6))
- if err != nil {
- return nil, err
- }
-
- // 1, 2 and 3 - lose all points
- // 4 and 5 - keep points
- // 6 - win 125% points
-
- if input.InputPoints != nil {
- switch diceRoll.Uint64() + 1 {
- case 1, 2, 3:
- // Lose all points
- return big.NewInt(0), nil
- case 4, 5:
- // Keep points
- return input.InputPoints, nil
- case 6:
- // Win 125% points
- result, _ := new(big.Float).Mul(new(big.Float).SetInt(input.InputPoints), big.NewFloat(1.25)).Int(nil)
- return result, nil
- }
-
- return nil, errors.New("dice roll out of range: " + diceRoll.String())
- } else {
- fmt.Println(input.InputPoints)
- return nil, errors.New("input points is nil")
- }
-}
diff --git a/templates/admin.html b/templates/admin.html
index 724eebc..d7001ef 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -6,86 +6,74 @@
Admin Panel
-
Note: this admin panel will only work for Shounic!
+
Note: this admin panel will only work for moderators and streamers
+
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
index 7c6b308..1518166 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -10,10 +10,7 @@