Revamped some things
This commit is contained in:
parent
885efd959a
commit
2d832736df
15 changed files with 761 additions and 1333 deletions
7
build.sh
7
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
|
||||
|
|
5
go.mod
5
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
|
||||
|
|
10
go.sum
10
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=
|
||||
|
|
38
lib/main.go
38
lib/main.go
|
@ -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
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
go build -ldflags "-s -w" -buildmode=plugin -o bet.so main.go
|
|
@ -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: `
|
||||
<h1>Bet on it</h1>
|
||||
<p>Vote for what you think will happen</p>
|
||||
<p>If you lose the bet, you lose your points</p>
|
||||
<p>If you win the bet, you get double your points back</p>
|
||||
<p id="question"></p>
|
||||
<p id="possible"></p>
|
||||
<p id="timer"></p>
|
||||
<button id="bet" disabled>Bet</button>
|
||||
`,
|
||||
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!")
|
||||
}
|
||||
})
|
||||
</script>
|
||||
`,
|
||||
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
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
go build -ldflags "-s -w" -buildmode=plugin -o diceroll.so main.go
|
|
@ -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: `
|
||||
<h1>Dice Roll</h1>
|
||||
<p>Roll a dice</p>
|
||||
<button id="roll">Roll</button>
|
||||
`,
|
||||
// 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);
|
||||
})
|
||||
</script>
|
||||
`,
|
||||
// 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")
|
||||
}
|
||||
}
|
|
@ -6,86 +6,74 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>Admin Panel</h1>
|
||||
<p>Note: this admin panel will only work for Shounic!</p>
|
||||
<p>Note: this admin panel will only work for moderators and streamers</p>
|
||||
<button id="startPoll">Start Bet</button>
|
||||
<button id="endPoll">End Bet</button>
|
||||
<button id="startStream">Start Stream</button>
|
||||
<button id="endStream">End Stream</button>
|
||||
<script>
|
||||
document.getElementById('startPoll').addEventListener('click', async () => {
|
||||
let question = prompt("What is the question of the bet?")
|
||||
let outcomes = prompt("What outcomes can the poll have (comma seperated)?")
|
||||
let time = prompt("How long should the poll last (in seconds)?")
|
||||
let response = await fetch("/api/extra/bet-on-it", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"Action": "startBet",
|
||||
"question": question,
|
||||
"possible": outcomes.split(", "),
|
||||
"timeToBet": Math.floor(new Date().getTime() / 1000) + parseInt(time)
|
||||
})
|
||||
})
|
||||
if (response.ok) {
|
||||
alert("Poll started!")
|
||||
} else {
|
||||
alert("Error starting poll")
|
||||
}
|
||||
})
|
||||
let possible = [];
|
||||
|
||||
document.getElementById('endPoll').addEventListener('click', async () => {
|
||||
let response = await fetch("/api/extra/bet-on-it", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"Action": "endBet",
|
||||
"answer": prompt("What was the result of the bet (case sensitive)?")
|
||||
})
|
||||
})
|
||||
if (response.ok) {
|
||||
alert("Poll ended!")
|
||||
} else {
|
||||
alert("Error ending poll")
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('startStream').addEventListener('click', async () => {
|
||||
// First, fetch the broadcasts from YouTube
|
||||
let response = await fetch("https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet&mine=true", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("oauthToken")
|
||||
}
|
||||
})
|
||||
let data = await response.json()
|
||||
let liveChatID = data.items[0]["snippet"]["liveChatId"]
|
||||
// Now that we have the live chat ID, we can start the stream
|
||||
let response2 = await fetch("/api/startStream", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"liveChatID": liveChatID
|
||||
document.getElementById('startPoll').addEventListener('click', async () => {
|
||||
let question = prompt("What is the question of the bet?")
|
||||
possible = prompt("What outcomes can the poll have (comma seperated)?").split(", ")
|
||||
let time = prompt("How long should the poll last (in seconds)?")
|
||||
let response = await fetch("/api/startBet", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"question": question,
|
||||
"possible": possible,
|
||||
"timeToBet": parseInt(time)
|
||||
})
|
||||
})
|
||||
if (response.ok) {
|
||||
alert("Poll started!")
|
||||
} else {
|
||||
alert("Error starting poll")
|
||||
}
|
||||
})
|
||||
|
||||
if (response2.ok) {
|
||||
alert("Stream started!")
|
||||
} else {
|
||||
alert("Error starting stream")
|
||||
}
|
||||
})
|
||||
document.getElementById('endPoll').addEventListener('click', async () => {
|
||||
let response = await fetch("/api/endBet", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"correct": possible.index(prompt("What was the correct answer?"))
|
||||
})
|
||||
})
|
||||
if (response.ok) {
|
||||
alert("Poll ended!")
|
||||
} else {
|
||||
alert("Error ending poll")
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('startStream').addEventListener('click', async () => {
|
||||
// First, fetch the broadcasts from YouTube
|
||||
let response = await fetch("https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet&mine=true", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("oauthToken")
|
||||
}
|
||||
})
|
||||
let data = await response.json()
|
||||
let liveChatID = data.items[0]["snippet"]["liveChatId"]
|
||||
// Now that we have the live chat ID, we can start the stream
|
||||
let response2 = await fetch("/api/startStream", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"liveChatID": prompt("What is the live chat ID? (Get this from youtuber api tester)"),
|
||||
})
|
||||
})
|
||||
|
||||
if (response2.ok) {
|
||||
alert("Stream started!")
|
||||
} else {
|
||||
alert("Error starting stream")
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('endStream').addEventListener('click', async () => {
|
||||
let response = await fetch("/api/endStream", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
}
|
||||
method: "GET"
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
|
|
73
templates/bet.html
Normal file
73
templates/bet.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Betting</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Betting</h1>
|
||||
<p>Vote for what you think will happen</p>
|
||||
<p>If you lose the bet, you lose your points</p>
|
||||
<p>If you win the bet, you get your points back and more, depending on the stakes</p>
|
||||
<p id="question"></p>
|
||||
<p id="possible"></p>
|
||||
<p id="timer"></p>
|
||||
<button id="bet" disabled>Bet</button>
|
||||
<a href="/privacy">Privacy Policy</a>
|
||||
<a href="/tos">Terms of Service</a>
|
||||
<script>
|
||||
let outcomes = [];
|
||||
|
||||
async function updateTimer(time) {
|
||||
// noinspection InfiniteLoopJS
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
async function getBet() {
|
||||
let response = await fetch("/api/getCurrentBet");
|
||||
if (response.ok) {
|
||||
let data = await response.json();
|
||||
document.getElementById("question").innerText = data.question;
|
||||
outcomes = data.possible;
|
||||
document.getElementById("possible").innerText = "Possible outcomes: " + outcomes.join(", ");
|
||||
// noinspection ES6MissingAwait
|
||||
updateTimer(data.timeToBet);
|
||||
} else {
|
||||
document.getElementById("question").innerText = "No bet is currently active";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("bet").addEventListener("click", async () => {
|
||||
let outcome = prompt("What do you think will happen?");
|
||||
if (outcomes.includes(outcome)) {
|
||||
let response = await fetch("/api/bet", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"amount": parseInt(prompt("How many points do you want to bet?")),
|
||||
"answer": outcomes.indexOf(outcome)
|
||||
})
|
||||
});
|
||||
if (response.ok) {
|
||||
alert("Bet placed!");
|
||||
} else {
|
||||
alert("Error placing bet");
|
||||
}
|
||||
} else {
|
||||
alert("Invalid outcome");
|
||||
}
|
||||
});
|
||||
|
||||
getBet();
|
||||
</script>
|
||||
</body>
|
|
@ -10,10 +10,7 @@
|
|||
<p>Click the button to claim unclaimed points</p>
|
||||
<button id="claimPoints">Claim Points</button>
|
||||
<p id="pointAmount">Loading...</p>
|
||||
<p>Here are the current activities:</p>
|
||||
<ul id="fillIn">
|
||||
<!-- Fill in the activities here with JavaScript -->
|
||||
</ul>
|
||||
<a href="/bet">Go betting!</a>
|
||||
<a href="/privacy">Privacy Policy</a>
|
||||
<a href="/tos">Terms of Service</a>
|
||||
<script>
|
||||
|
@ -64,34 +61,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (localStorage.getItem("accessToken") === null) {
|
||||
window.location.href = "/login"
|
||||
}
|
||||
|
||||
let fillIn = document.getElementById("fillIn")
|
||||
await fetch("/api/getPlugins", {
|
||||
await fetch("/api/loggedIn", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
}
|
||||
}).then(async (response) => {
|
||||
let data = await response.json()
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let li = document.createElement("li")
|
||||
let a = document.createElement("a")
|
||||
a.href = "/" + data[i]["Name"]
|
||||
a.innerText = data[i]["Name"]
|
||||
li.appendChild(a)
|
||||
fillIn.appendChild(li)
|
||||
if (!response.ok) {
|
||||
window.location.href = "/link"
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('claimPoints').addEventListener('click', async () => {
|
||||
let response = await fetch('/api/claimUnclaimedPoints', {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
}})
|
||||
"method": "GET"
|
||||
})
|
||||
if (response.ok) {
|
||||
alert("Claimed " + data['points'] + " points")
|
||||
window.location.reload()
|
||||
|
@ -99,26 +80,18 @@
|
|||
alert("Error claiming points")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function updatePoints() {
|
||||
while (true) {
|
||||
let response = await fetch('/api/getPoints', {
|
||||
"method": "GET",
|
||||
"headers": {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
}
|
||||
"method": "GET"
|
||||
})
|
||||
let data = await response.json()
|
||||
document.getElementById('pointAmount').innerText = "You have " + data['points'] + " points"
|
||||
|
||||
response = await fetch('/api/getUnclaimedPoints', {
|
||||
"method": "GET",
|
||||
"headers": {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
}
|
||||
"method": "GET"
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
34
templates/link.html
Normal file
34
templates/link.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Link your account</title>
|
||||
</head>
|
||||
<body>
|
||||
<p id="status">Linking your account...</p>
|
||||
<script>
|
||||
let status = document.getElementById("status");
|
||||
let ws = new WebSocket("ws://" + window.location.host + "/api/link");
|
||||
ws.onmessage = (event) => {
|
||||
let data = JSON.parse(event.data);
|
||||
switch (data["type"]) {
|
||||
case "ping":
|
||||
ws.send(JSON.stringify({"type": "pong"}));
|
||||
break;
|
||||
case "nonce":
|
||||
status.innerText = "To link your account, type \"!link " + data["nonce"] + "\" in the chat of the stream you are watching";
|
||||
ws.send(JSON.stringify({"type": "success"}));
|
||||
break;
|
||||
case "success":
|
||||
status.innerText = "Account linked successfully!";
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 1000);
|
||||
break;
|
||||
case "error":
|
||||
status.innerText = "Error linking account: " + data["error"];
|
||||
break;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
|
@ -1,170 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Log in with Google</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="text">Log in with Google</h1>
|
||||
<button id="authorize">Authorize</button>
|
||||
<button id="delete">Delete every last byte of my data</button>
|
||||
<p>By logging in, you agree to our <a href="/privacy">Privacy Policy</a> and <a href="/tos">Terms of Service</a></p>
|
||||
<script>
|
||||
// Configuration
|
||||
const clientId = '165295772598-ua5imd4pduoapsh0bnu96lul2j6bhsul.apps.googleusercontent.com';
|
||||
const redirectUri = 'https://sho.ailur.dev/login';
|
||||
const authorizationEndpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
const tokenEndpoint = 'https://oauth2.googleapis.com/token';
|
||||
const userinfoEndpoint = 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json';
|
||||
|
||||
// Generate a random code verifier
|
||||
function generateCodeVerifier() {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
|
||||
const length = 128;
|
||||
return Array.from(crypto.getRandomValues(new Uint8Array(length)))
|
||||
.map((x) => charset[x % charset.length])
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Create a code challenge from the code verifier using SHA-256
|
||||
async function createCodeChallenge(codeVerifier) {
|
||||
const buffer = new TextEncoder().encode(codeVerifier);
|
||||
const hashArrayBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
||||
return btoa(String.fromCharCode(...new Uint8Array(hashArrayBuffer)))
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
// Authorization function with PKCE
|
||||
document.getElementById('authorize').addEventListener('click', () => {
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
localStorage.setItem('codeVerifier', codeVerifier); // Store code verifier
|
||||
createCodeChallenge(codeVerifier)
|
||||
.then((codeChallenge) => {
|
||||
window.location.href = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256&scope=https://www.googleapis.com/auth/youtube.readonly%20https://www.googleapis.com/auth/userinfo.profile`;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error generating code challenge:', error);
|
||||
});
|
||||
})
|
||||
|
||||
// Delete every last byte of user data
|
||||
document.getElementById('delete').addEventListener('click', async () => {
|
||||
document.getElementById("text").innerText = "Deleting every last byte of your data..."
|
||||
let response = await fetch("/api/delete", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
}
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
document.getElementById("text").innerText = "Theres probably an outage or something. Try again later. Can't say I didn't try."
|
||||
} else {
|
||||
localStorage.clear()
|
||||
document.getElementById("text").innerText = "Deleted every last byte of your data, even from memory (well, when the garbage collector kicks in)."
|
||||
}
|
||||
})
|
||||
|
||||
// Parse the authorization code from the URL
|
||||
function parseCodeFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('code');
|
||||
}
|
||||
|
||||
// Exchange authorization code for access token
|
||||
async function exchangeCodeForToken(code) {
|
||||
const codeVerifier = localStorage.getItem('codeVerifier'); // Retrieve code verifier
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('client_id', String(clientId));
|
||||
formData.append('code', String(code));
|
||||
formData.append('redirect_uri', String(redirectUri));
|
||||
formData.append('grant_type', 'authorization_code');
|
||||
formData.append('code_verifier', String(codeVerifier));
|
||||
|
||||
// Google, for some godforsaken reason, wants client_secret during a PKCE flow.
|
||||
// What the hell? That's not remotely compatible with the PKCE RFC.
|
||||
// I hope to god that Google is still doing PKCE properly even though they're asking for client_secret.
|
||||
// #### you, Google, the hate is absolutely justified.
|
||||
formData.append('client_secret', String('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'))
|
||||
|
||||
let response = await fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.error('Failed to exchange code for token:', response.status, await response.text());
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Successfully exchanged code for token:', data);
|
||||
const accessToken = data['access_token'];
|
||||
|
||||
// Request userinfo with ACCESS TOKEN in bearer format
|
||||
// For some other godforsaken reason, they want the access token, not the id token.
|
||||
// #### you, Google.
|
||||
// THAT'S NOT HOW USERINFO WORKS, GOOGLE. YOU IDIOT!
|
||||
fetch(userinfoEndpoint, {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + accessToken
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
async function doStuff() {
|
||||
if (response.status === 200) {
|
||||
const userinfoData = await response.json();
|
||||
console.log("Userinfo:", userinfoData);
|
||||
console.log(accessToken)
|
||||
console.log("User:", userinfoData["name"]);
|
||||
console.log("Sub:", userinfoData["id"]);
|
||||
localStorage.removeItem("codeVerifier")
|
||||
document.getElementById("text").innerText = "Authenticated, " + userinfoData.name + ", now logging into the server..."
|
||||
await fetch("/api/authorize", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"idToken": data['id_token'],
|
||||
"accessToken": accessToken,
|
||||
}),
|
||||
}).then(response => {
|
||||
response.json().then(data => {
|
||||
if (response.status === 200) {
|
||||
localStorage.setItem("accessToken", data["sessionToken"])
|
||||
localStorage.setItem("user", userinfoData["name"])
|
||||
localStorage.setItem("oauthToken", accessToken)
|
||||
localStorage.setItem("sub", userinfoData["id"])
|
||||
window.location.href = "/"
|
||||
} else {
|
||||
document.getElementById("text").innerText = "Authentication failed"
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
document.getElementById("text").innerText = "Authentication failed"
|
||||
}
|
||||
}
|
||||
doStuff()
|
||||
});
|
||||
}
|
||||
|
||||
// Main function to handle OAuth2 flow
|
||||
async function main() {
|
||||
if (localStorage.getItem("user") !== null) {
|
||||
document.getElementById("text").innerText = "Welcome back, " + localStorage.getItem("user") + ". Aren't you already logged in?"
|
||||
}
|
||||
const code = parseCodeFromUrl();
|
||||
if (code) {
|
||||
await exchangeCodeForToken(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the main function on page load
|
||||
main();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,62 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ .Name }}</title>
|
||||
<!-- Pass through the cost of the plugin to the page -->
|
||||
<meta id="cost" content="{{ .Cost }}">
|
||||
<meta id="multipleCostsSupported" content="{{ .MultipleCostsSupported }}">
|
||||
<meta id="name" content="{{ .Name }}">
|
||||
<meta id="canReturnTokens" content="{{ .CanReturn }}">
|
||||
<script src="/static/js/jwt.min.js"></script>
|
||||
<script>
|
||||
async function sendCost(amount, optional) {
|
||||
if (document.getElementById('multipleCostsSupported').content !== 'true') {
|
||||
amount = BigInt(document.getElementById('cost').content)
|
||||
}
|
||||
|
||||
// Save updated points to local storage
|
||||
let response = await fetch('/api/' + document.getElementById('name').content, {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
},
|
||||
"body": JSON.stringify({
|
||||
"points": amount.toString(),
|
||||
"optional": optional || "none"
|
||||
})
|
||||
})
|
||||
if (response.status === 200) {
|
||||
let data = await response.json()
|
||||
if (document.getElementById('canReturnTokens').content === 'true') {
|
||||
alert("You got " + data['profit'] + " points")
|
||||
} else {
|
||||
{{ .OnDataReturn }}
|
||||
}
|
||||
} else {
|
||||
let data = await response.json()
|
||||
alert("Failed to activate plugin: " + data['error'])
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
let response = await fetch('/api/getPoints', {
|
||||
"method": "GET",
|
||||
"headers": {
|
||||
"Authorization": localStorage.getItem("accessToken")
|
||||
}
|
||||
})
|
||||
let data = await response.json()
|
||||
document.getElementById('pointAmount').innerText = "You have " + data['points'] + " points"
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{ .PluginHTML }}
|
||||
<p id="pointAmount">Loading...</p>
|
||||
<script>
|
||||
{{ .PluginScript }}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue