Revamped some things

This commit is contained in:
Tracker-Friendly 2025-03-24 18:56:52 +00:00
parent 885efd959a
commit 2d832736df
15 changed files with 761 additions and 1333 deletions

View file

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

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

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

View file

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

1186
main.go

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
#!/bin/sh
go build -ldflags "-s -w" -buildmode=plugin -o bet.so main.go

View file

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

View file

@ -1,2 +0,0 @@
#!/bin/sh
go build -ldflags "-s -w" -buildmode=plugin -o diceroll.so main.go

View file

@ -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")
}
}

View file

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

View file

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

View file

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

View file

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