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 @@

Click the button to claim unclaimed points

Loading...

-

Here are the current activities:

- + Go betting! Privacy Policy Terms of Service + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html deleted file mode 100644 index e52623e..0000000 --- a/templates/login.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - Log in with Google - - -

Log in with Google

- - -

By logging in, you agree to our Privacy Policy and Terms of Service

- - - diff --git a/templates/plugin.html b/templates/plugin.html deleted file mode 100644 index 5c264b9..0000000 --- a/templates/plugin.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - {{ .Name }} - - - - - - - - - - {{ .PluginHTML }} -

Loading...

- - - \ No newline at end of file