Compare commits
9 Commits
466cfb197c
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
963e829b7f | ||
|
b0be1f99c8 | ||
|
049051a7e1 | ||
|
d67c30d8f4 | ||
|
0ef35de85b | ||
|
e6e6de0381 | ||
|
c704380ee3 | ||
|
b82d5dd0d4 | ||
|
5b286926a3 |
89
README.md
89
README.md
@@ -6,33 +6,106 @@ This bot is a CMDli or Terminal application that should run on most systems.
|
|||||||
The Theory is this should be a Light Weight application and to that point, it can only handle Text based (Chat) commands.
|
The Theory is this should be a Light Weight application and to that point, it can only handle Text based (Chat) commands.
|
||||||
|
|
||||||
>[!NOTE]
|
>[!NOTE]
|
||||||
>While there is a command response for greetings, the bot should greet every user upon FirstWords (of the stream).
|
>Key for entire document:
|
||||||
>The bot sohuld also NOT greet userNames that are removed from chat via WizeBot.
|
>[required] - you must use one of these
|
||||||
|
>
|
||||||
|
><optional> - this only needed with certain [required]
|
||||||
|
|
||||||
|
|
||||||
## The Commands it comes with:
|
>### CMDli interface / usage
|
||||||
|
> $twitchbot will start the bot and show bot:>
|
||||||
|
>
|
||||||
|
> the options you can use are:
|
||||||
|
>
|
||||||
|
> bot:>title [get|set] <newTitle> - view the Current Title or set a new one
|
||||||
|
>
|
||||||
|
> bot:>category [get|set] <category> - view the Current Category or set a new one
|
||||||
|
>
|
||||||
|
> bot:>list [categories|cmd] <permLevel> - catergories = view a list of categories from twitch | cmd <permLevel> = shows url for the commands list for that permission level or all commands if <premLevel> isn't specified
|
||||||
|
>
|
||||||
|
> bot:>stats [--watch | --interval=2m] - watch command usage & how many times the commands have been used in the lifetime of the bot
|
||||||
|
>
|
||||||
|
> bot:>uptime - shows your current stream uptime
|
||||||
|
>
|
||||||
|
> bot:>cmd [add|del|edit] [trigger] <response> (response only required if adding or editing a command)
|
||||||
|
>
|
||||||
|
> bot:>notif [get|set] <newMessage> - view or set the notification message
|
||||||
|
|
||||||
>[!NOTE]
|
>[!NOTE]
|
||||||
>< > = optional Input | [ ] = Required Options
|
>While there is a command response for greetings, the bot should greet every user upon FirstWords (of the stream).
|
||||||
|
>
|
||||||
|
>The bot sohuld also NOT greet userNames that are removed from chat via WizeBot.
|
||||||
|
|
||||||
> ### MOD COMMANDS ONLY
|
## The Commands it comes with:
|
||||||
|
> ### Broadcaster Commands (chat)
|
||||||
|
> @bot [mute|unmute]
|
||||||
|
>
|
||||||
|
> !fav [cmd|user] <trigger|username> - Username HAS to be in chat during the current stream
|
||||||
|
>
|
||||||
|
> ^rec [name - start|end] - posts an Announcement (ORANGE) to Chat reading <"Recording Started (Ended) at [timestamp] [name]>" and sets a stream marker with the title
|
||||||
|
>
|
||||||
|
> !upcoming [event details] - set or view a message with details of the next event happening on the channel
|
||||||
|
>
|
||||||
|
> !countdown or !timer [duration (HH:mm)] - set a timer to countdown for the set duration (updates every 5mins if time is over 30mins; then 3mins if time is 30mins or less; then 1min if time is 15mins or less; then 30sec from 5mins until 00:00)
|
||||||
|
|
||||||
|
> ### VIP Commands incl. Artist
|
||||||
|
> !quoteme [quote] - saves a quote and can recall a random quote or a specific quote using [!quoteme #00000]
|
||||||
|
>
|
||||||
|
> !lfg - posts a message letting chat know you are pumped and ready to rock 'n roll
|
||||||
|
>
|
||||||
|
> !ispy - starts a random VIP only game of the classic I-spy (rounds last 3mins)
|
||||||
|
> [VIPs use !guess [thing|emote|word] to guess (1st correct answer wins 100 points)]
|
||||||
|
|
||||||
|
> ### MOD COMMANDS
|
||||||
> !title <New Stream Title> - if no Input then it will post the current title to the chat
|
> !title <New Stream Title> - if no Input then it will post the current title to the chat
|
||||||
>
|
>
|
||||||
> !category or !game <Twitch Category>
|
> !category or !game <Twitch Category>
|
||||||
>
|
>
|
||||||
> !so [@ChatName] - shoutouts given to the channel (this has a 1hr cooldown per @chatName - the file is "so_cooldown.json")
|
> !so [@ChatName] - shoutouts given to the channel (this has a 1hr cooldown per @chatName - the file is "so_cooldown.json")
|
||||||
>
|
>
|
||||||
|
> !tso [@ChatName] - uses the twitch "/shoutout" to shout out a user - Cooldowns are managed by Twitch.tv (Can also be triggered by using "/shoutout in chat")
|
||||||
|
>
|
||||||
> !cmd [add | del | edit | pause | resume] [!commandName] <command response message> - Manage or Make a command (new commands will be marked with * in the list and have the Mod's Name (for streamer's reference))
|
> !cmd [add | del | edit | pause | resume] [!commandName] <command response message> - Manage or Make a command (new commands will be marked with * in the list and have the Mod's Name (for streamer's reference))
|
||||||
>
|
>
|
||||||
|
> !note [mods|caster] [message] - send Broadcaster (caster) or the other Mods a quick note in the respective private discord|whatsapp|element(matrix)|whisper or other msg channels
|
||||||
|
>
|
||||||
|
> !clip - creates a 30sec clip of what you just saw and posts it to #twitch-clips in discord
|
||||||
|
>
|
||||||
|
> !flag [username] - flag a user as suspicious (self deletes) reply = @user your wish has been carried out with precision
|
||||||
|
>
|
||||||
|
> !unflag [username] - remove flag from username (self deletes) reply = same as !flag
|
||||||
|
>
|
||||||
|
> !to or !timeout [user] [duration] - timeout a user in chat
|
||||||
|
>
|
||||||
|
> !cancel [to|timeout] [user] - cancel a timeout for user
|
||||||
|
>
|
||||||
|
> !warn [user] [reason] - give a warning to a user (logs # of warnings user recieved)
|
||||||
|
>
|
||||||
|
> !ban [user] [reason] - ban a user from chat - PLS leave a reason damnit
|
||||||
|
>
|
||||||
|
> !unban [user] [reason] - unban a user - reason is mandatory here
|
||||||
|
|
||||||
> ### ALL USERS
|
> ### ALL USERS
|
||||||
> !hi, !hello, !gday, !hola, !kiaora <@ChatName> - if no chatName is given the bot will greet the user who used the command
|
> !hi, !hello, !gday, !hola, !kiaora <@ChatName> - if no chatName is given the bot will greet the user who used the command
|
||||||
>
|
>
|
||||||
> !uptime - How Long has {$streamerName} been Live?
|
> !uptime - How Long has {$streamerName} been Live?
|
||||||
>
|
>
|
||||||
> !nextSong - request for the song to be skipped (if BG music is playing) this one plays a sound to alert the streamer as the bot doesn't have spotify integration (maybe in the future)
|
> !nextSong - request for the song to be skipped (if BG music is playing) - sends chat Message - the bot doesn't have spotify integration (maybe in the future)
|
||||||
>
|
>
|
||||||
> !AFOL - ID yourself as an AFOL (Adult Fan Of Lego) - this list will be saved to "AFOL.txt" for OBS / other reasons
|
> !AFOL - ID yourself as an AFOL (Adult Fan Of Lego) - this list will be saved to "AFOL.txt" for OBS / other reasons
|
||||||
>
|
>
|
||||||
> !countdown [MM:ss] - starts a countdown timer in chat (updated every 30 seconds via New Message) for a max of 60 Minutes (1H:00M) - Channel Announcement in Orange once timer is up.
|
|
||||||
>
|
|
||||||
> !cough [1-10/10]- you heard {$streamerName} cough on open mic ... Rate it 1 - 10 / 10 (anything over 10 is a coughing fit)
|
> !cough [1-10/10]- you heard {$streamerName} cough on open mic ... Rate it 1 - 10 / 10 (anything over 10 is a coughing fit)
|
||||||
|
>
|
||||||
|
> !hug [user] - give another viewer a hug
|
||||||
|
>
|
||||||
|
> !dice or !roll - rolls a 6-sided dice gives random number between 1 & 6
|
||||||
|
>
|
||||||
|
> !8ball [question] - ask 8ball a question and recieve a totally random answer
|
||||||
|
>
|
||||||
|
> !roulette or !spin [points] [color] [number] - spin the roulette wheel & 2x your bet if your color and number are correct
|
||||||
|
>
|
||||||
|
> !slots or !pull [points] - spin the slot machine - 3 of a kind wins up to 5x your bet
|
||||||
|
>
|
||||||
|
> !poker or !table [points] - be the closest to 21 and win 2x your bet
|
||||||
|
>
|
||||||
|
> !points - check how many points you have [useful for !dice, !roll, !roulette, !spin, !slots, !pull, !poker or !table]
|
||||||
|
@@ -4,9 +4,10 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"streambot_twitch/internal/auth"
|
auth "streambot_twitch/internal/auth"
|
||||||
"streambot_twitch/internal/chat"
|
"streambot_twitch/internal/chat"
|
||||||
"streambot_twitch/internal/commands"
|
"streambot_twitch/internal/commands"
|
||||||
|
"streambot_twitch/internal/points"
|
||||||
twitchapi "streambot_twitch/internal/twitchapi"
|
twitchapi "streambot_twitch/internal/twitchapi"
|
||||||
tests "streambot_twitch/internal/tests"
|
tests "streambot_twitch/internal/tests"
|
||||||
twitch "github.com/gempir/go-twitch-irc/v4"
|
twitch "github.com/gempir/go-twitch-irc/v4"
|
||||||
@@ -14,21 +15,26 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
go func() {
|
go func() {
|
||||||
if err := oauth.StartOAuthServer(":8080"); err != nil {
|
if err := auth.StartOAuthServer(":8080"); err != nil {
|
||||||
log.Fatalf("OAuth server error: %v", err)
|
log.Fatalf("OAuth server error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
_ = godotenv.Load()
|
||||||
|
|
||||||
log.Println("OAuth server started on port 8080")
|
log.Println("OAuth server started on port 8080")
|
||||||
|
|
||||||
botUsername := os.Getenv("nz_chatterbot")
|
botUsername := os.Getenv("BOT_USERNAME")
|
||||||
botOAuth :+ os.Getenv("BOT_OAUTH")
|
botAccess := os.Getenv("BOT_ACCESS_TOKEN")
|
||||||
channel := os Getenv("brycefromnz101")
|
botRefresh := os.Getenv("BOT_REFRESH_TOKEN")
|
||||||
clientID := os.Getenv("TWITCH_CLIENTID")
|
channel := os Getenv("CHANNEL")
|
||||||
|
clientID := os.Getenv("TWITCH_CLIENT_ID")
|
||||||
clientSecret := os.Getenv("TWITCH_CLIENT_SECRET")
|
clientSecret := os.Getenv("TWITCH_CLIENT_SECRET")
|
||||||
|
channelAccess := os.Getenv("STREAMER_ACCESS_TOKEN")
|
||||||
|
channelRefresh := os.Getenv("STREAMER_REFRESH_TOKEN")
|
||||||
botChannel := botUsername
|
botChannel := botUsername
|
||||||
|
|
||||||
if botUsername == "" || botOAuth == "" || channel == "" || clientID == "" || clientSecret == "" {
|
if botUsername == "" || botAccess == "" || channel == "" || clientID == "" || clientSecret == "" {
|
||||||
log.Fatal("Please set Bot_Username, Bot_OAuth, Channel, Twitch_Client_ID, and Twitch_Client_Secret env vars")
|
log.Fatal("Please set Bot_Username, Bot_OAuth, Channel, Twitch_Client_ID, and Twitch_Client_Secret env vars")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +58,14 @@ log.Println("OAuth server started on port 8080")
|
|||||||
log.Println("All startup tests passed. Starting bot. . .")
|
log.Println("All startup tests passed. Starting bot. . .")
|
||||||
// END of TESTS
|
// END of TESTS
|
||||||
|
|
||||||
|
// load chat points
|
||||||
|
if err := points.LoadPoints(); err != nil {
|
||||||
|
log.Fatalf("Failed to load points: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure points are saved on exit
|
||||||
|
defer points.SavePoints()
|
||||||
|
|
||||||
// load custom commands
|
// load custom commands
|
||||||
if err := commands.LoadCommands(); err != nil {
|
if err := commands.LoadCommands(); err != nil {
|
||||||
log.Fatalf("Failed to Load Custom Commands: %v", err)
|
log.Fatalf("Failed to Load Custom Commands: %v", err)
|
||||||
|
100
cmd/cli/main.go
Normal file
100
cmd/cli/main.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
twitchapi "streambot_twitch/internal/twitchapi"
|
||||||
|
"streambot_twitch/internal/commands"
|
||||||
|
"streambot_twitch/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// env setup
|
||||||
|
clientID := os.Getenv("TWITCH_CLIENT_ID")
|
||||||
|
clientSecret := os.Getenv("TWITCH_CLIENT_SECRET")
|
||||||
|
streamerChan := os.Getenv("CHANNEL")
|
||||||
|
userToken := os.Getenv("STREAMER_OAUTH")
|
||||||
|
|
||||||
|
if clientID == "" || clientSecret == "" || streamerChan == "" {
|
||||||
|
log.Fatal("Set TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET or cHANNEL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// init config, commands, usage
|
||||||
|
if err := config.Load(); err != nil {log.Fatal(err)}
|
||||||
|
if err := commands.LoadDefaultCommands(); err != nil {log.Fatal(err)}
|
||||||
|
if err := commands.LoadCustomCommands(); err != nil {lof.Fatal(err)}
|
||||||
|
if err := commands.LoadUsage(); err != nil {log.Fatal(err)}
|
||||||
|
|
||||||
|
// init api client
|
||||||
|
apiClient, err := twitchapi.NewClient(clientID, clientSecret)
|
||||||
|
if err != nil {log.Fatalf("API client: %v", err)}
|
||||||
|
if userToken != "" {
|
||||||
|
apiClient.SetUserAccessToken(userToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI flags
|
||||||
|
watch := flag.Bool("watch", false, "refresh stats periodically")
|
||||||
|
interval := flag.Duration("interval", 5*time.Minute, "refresh interval for watch mode")
|
||||||
|
flag.Parse()
|
||||||
|
args := flag.Args()
|
||||||
|
|
||||||
|
if len(args) == 0 || strings.ToLower(args[0]) == "shell" {
|
||||||
|
repl(apiClient, streamerChan, *watch, *interval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(apiClient, streamerChan, args, *watch, *interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// repl interactive prompt
|
||||||
|
func repl(apiClient *twitchapi.Client, streamerChan string, watch bool, interval time.Duration) {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
for {
|
||||||
|
fmt.Print("bot:> ")
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line == "exit" || line == "quit" {
|
||||||
|
fmt.Println("bye")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
dispatch(apiClient, streamerChan, parts, watch, interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch handles one command invocation
|
||||||
|
func dispatch(apiClient *twitchapi.Client, streamerChan string, args []string, watch bool, interval time.Duration) {
|
||||||
|
cmd := strings.ToLower(args[0])
|
||||||
|
switch cmd {
|
||||||
|
case "stats":
|
||||||
|
if watch {
|
||||||
|
printUsage()
|
||||||
|
t := time.NewTicker(interval)
|
||||||
|
for range t.C {
|
||||||
|
printUsage()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "title":
|
||||||
|
if len(args)<2 { fmt.Println("usage: title get|set <value>"); return }
|
||||||
|
sub := strings.ToLower(args[1])
|
||||||
|
switch sub {
|
||||||
|
case "get":
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
cmd/oauth/main.go
Normal file
13
cmd/oauth/main.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
auth "streambot_twitch/internal/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := auth.StartOAuthServer(":8080"); err != nil {
|
||||||
|
log.Fatalf("OAuth server error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
5
go.mod
5
go.mod
@@ -7,4 +7,7 @@ require (
|
|||||||
github.com/nicklaw5/helix v1.25.0
|
github.com/nicklaw5/helix v1.25.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
require (
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
)
|
||||||
|
5
go.sum
5
go.sum
@@ -1,6 +1,9 @@
|
|||||||
github.com/gempir/go-twitch-irc/v4 v4.2.0 h1:OCeff+1aH4CZIOxgKOJ8dQjh+1ppC6sLWrXOcpGZyq4=
|
github.com/gempir/go-twitch-irc/v4 v4.2.0 h1:OCeff+1aH4CZIOxgKOJ8dQjh+1ppC6sLWrXOcpGZyq4=
|
||||||
github.com/gempir/go-twitch-irc/v4 v4.2.0/go.mod h1:QsOMMAk470uxQ7EYD9GJBGAVqM/jDrXBNbuePfTauzg=
|
github.com/gempir/go-twitch-irc/v4 v4.2.0/go.mod h1:QsOMMAk470uxQ7EYD9GJBGAVqM/jDrXBNbuePfTauzg=
|
||||||
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
|
|
||||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M=
|
github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M=
|
||||||
github.com/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw=
|
github.com/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw=
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package oauth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -39,14 +39,15 @@ func StartOAuthServer(addr string) error {
|
|||||||
|
|
||||||
http.HandleFunc("/", handleIndex)
|
http.HandleFunc("/", handleIndex)
|
||||||
http.HandleFunc("/callback", handleCallback)
|
http.HandleFunc("/callback", handleCallback)
|
||||||
|
http.HandleFunc("/refresh", handleRefresh)
|
||||||
|
|
||||||
log.Printf("OAuth server listening on %s", addr)
|
log.Printf("OAuth server listening on %s", addr)
|
||||||
return http.ListenAndServe(addr, nil)
|
return http.ListenAndServe(addr, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleIndex(w http.ResponseWriter, r *http.Request) {
|
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
authURL := fmt.Sprintf(
|
authURL := fmt.Sprintf(
|
||||||
"https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s",
|
"https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&force_verify=true",
|
||||||
url.QueryEscape(clientID),
|
url.QueryEscape(clientID),
|
||||||
url.QueryEscape(redirectURI),
|
url.QueryEscape(redirectURI),
|
||||||
url.QueryEscape(strings.Join(scopes, " ")),
|
url.QueryEscape(strings.Join(scopes, " ")),
|
||||||
@@ -63,10 +64,10 @@ func HandleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(html))
|
w.Write([]byte(html))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCallback(w http.ResponseWriter, r &http.Request) {
|
func handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
code := r.URL.Query().Get("code")
|
code := r.URL.Query().Get("code")
|
||||||
if code == "" {
|
if code == "" {
|
||||||
http.Error(w, "Missing code in query", https.StatusBadRequest)
|
http.Error(w, "Missing code in query", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,14 +78,30 @@ func handleCallback(w http.ResponseWriter, r &http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jsonData, _ := json.MarshalIndent(token, "", " ")
|
jsonData, _ := json.MarshalIndent(token, "", " ")
|
||||||
|
w.Header().Set("Content-Type", "application/ijson")
|
||||||
|
w.Write(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRefresh(w http.ResponseWriter, r *http.Request) {
|
||||||
|
refresh := r.URL.Query().Get("refresh_token")
|
||||||
|
if refresh == "" {
|
||||||
|
http.Error(w, "Missing refresh_token parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr, err := ExchangeRefreshToken(refresh)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Refresh failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, _ := json.MarshalIndent(tr, "", " ")
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write([jsonData])
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func exchangeCodeForToken(code string) (*TokenResponse, error) {
|
func exchangeCodeForToken(code string) (*TokenResponse, error) {
|
||||||
data := url.Values{}
|
data := url.Values{}
|
||||||
data.Set("client_id", clientID)
|
data.Set("client_id", clientID)
|
||||||
data.Set("client_Secret", clientSecret)
|
data.Set("client_secret", clientSecret)
|
||||||
data.Set("code", code)
|
data.Set("code", code)
|
||||||
data.Set("grant_type", "authorization_code")
|
data.Set("grant_type", "authorization_code")
|
||||||
data.Set("redirect_uri", redirectURI)
|
data.Set("redirect_uri", redirectURI)
|
||||||
@@ -110,3 +127,31 @@ func exchangeCodeForToken(code string) (*TokenResponse, error) {
|
|||||||
|
|
||||||
return &tokenResp, nil
|
return &tokenResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refresh tokens programtically
|
||||||
|
func ExchangeRefreshToken(refreshToken string) (*TokenResponse, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("client_id", clientID)
|
||||||
|
form.Set("client_secret", clientSecret)
|
||||||
|
form.Set("grant_type", "refresh_token")
|
||||||
|
form.Set("refresh_token", refreshToken)
|
||||||
|
|
||||||
|
resp, err := http.Post(
|
||||||
|
"https://id.twitch.tv/oauth2/token",
|
||||||
|
"application/x-www-form-urlencoded",
|
||||||
|
strings.NewReader(form.Encode()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("refresh failed: %s", resp.Status)
|
||||||
|
}
|
||||||
|
var tr TokenResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tr, nil
|
||||||
|
}
|
||||||
|
@@ -91,13 +91,25 @@ func HandleMessage(client *twitch.Client, apiClient interface{}, message twitch.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dispatch poker/table commands
|
||||||
|
if strings.HasPrefix(strings.ToLower(msg), "!poker") || strings.HasPrefix(strings.ToLower(msg), "!table") {
|
||||||
|
HandlePokerCommand(client, message, user, channel, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Dispatch custom/default commands
|
// Dispatch custom/default commands
|
||||||
HandleCustomCommands(client, message, user, channel, botUsername)
|
HandleCustomCommands(client, message, user, channel, botUsername)
|
||||||
}
|
}
|
||||||
// check perms
|
// check perms
|
||||||
for _, cmd := range commands.GetAllCommands() {
|
for _, cmd := range commands.GetAllCommands() {
|
||||||
if messageMatchesCommand(msg, botName, cmd) {
|
if messageMatchesCommand(msg, botName, cmd) {
|
||||||
|
// track usage: use first alias as key
|
||||||
|
triggerKey := strings.ToLower(cmd.Triggers[0])
|
||||||
|
commands.IncrementUsage(triggerKey)
|
||||||
|
|
||||||
if userHasPermission(user, cmd.Permission) {
|
if userHasPermission(user, cmd.Permission) {
|
||||||
|
reply := ReplacePlaceholders(cmd.Replay, user.DisplayName, channel, botUsername)
|
||||||
|
|
||||||
client.Say(channel, cmd.Reply)
|
client.Say(channel, cmd.Reply)
|
||||||
} else {
|
} else {
|
||||||
client.Say(channel, fmt.Sprintf("@%s You don't have permission to use this command.", user.DisplayName))
|
client.Say(channel, fmt.Sprintf("@%s You don't have permission to use this command.", user.DisplayName))
|
||||||
|
154
internal/chat/poker.go
Normal file
154
internal/chat/poker.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
twitch "github.com/gempir/go-twitch-irc/v4"
|
||||||
|
"streambot_twitch/internal/points"
|
||||||
|
)
|
||||||
|
|
||||||
|
// card
|
||||||
|
type Card struct {
|
||||||
|
Rank, Suit string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
suits = []string{" of Hearts", " of Diamnods", " of Clubs", " of Spades"}
|
||||||
|
ranks = []string{"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"}
|
||||||
|
baseDeck []Card
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// build 2 decks (no Jokers) 104 cards
|
||||||
|
for d := 0; d < 2; d++ {
|
||||||
|
for _, s := range suits {
|
||||||
|
for _, r := range ranks {
|
||||||
|
baseDeck = append(baseDeck, Card{Rank: r, Suit: s})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
// impliment !poker or !table <bet>
|
||||||
|
func HandlePokerCommand(
|
||||||
|
client *twitch.Client,
|
||||||
|
message twitch.PrivateMessage,
|
||||||
|
user twitch.User,
|
||||||
|
channel, msg string,
|
||||||
|
) {
|
||||||
|
parts := strings.Fields(msg)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
client.Say(channel, fmt.Sprintf("@%s Usage: !poker <bet>", user.DisplayName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bet, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil || bet <= 0 {
|
||||||
|
client.Say(channel, fmt.Sprintf("@%s Invalid bet amount", user.DisplayName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
login := strings.ToLower(user.Name)
|
||||||
|
// load or init user points
|
||||||
|
balance := points.GetPoints(login)
|
||||||
|
if bet > ballance {
|
||||||
|
client.Say(channel, fmt.Sprintf("@%s You have %d points, bet lower", user.DisplayName, balance))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Deduct the bet
|
||||||
|
balance, err = points.AddPoints(login, -bet)
|
||||||
|
if err != nil {
|
||||||
|
client.Say(channel, fmt.Sprintf("@%s Error updating points: %v", user.DisplayName, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// shuffle deck
|
||||||
|
deck := make([]Card, len(baseDeck))
|
||||||
|
copy(deck, baseDeck)
|
||||||
|
rand.Shuffle(len(deck), func(i, j int) {deck[i], deck[j] = deck[j], deck[i]})
|
||||||
|
|
||||||
|
// deal 2 cards each
|
||||||
|
player := []Card{deck[0], deck[1]}
|
||||||
|
dealer := []Card{deck[2], deck[3]}
|
||||||
|
|
||||||
|
// compute hand values
|
||||||
|
pv := handValue(player)
|
||||||
|
dv := handValue(dealer)
|
||||||
|
|
||||||
|
// Determine outcome and payout multiplier
|
||||||
|
var net int
|
||||||
|
var outcome string
|
||||||
|
switch {
|
||||||
|
case pv > 21:
|
||||||
|
outcome = "FBPenalty Bust! You lose"
|
||||||
|
net = 0
|
||||||
|
case pv == 21:
|
||||||
|
outcome = "BLACKJACK! GoatEmotey"
|
||||||
|
net = bet * 2
|
||||||
|
case dv > 21:
|
||||||
|
outcome = "GoldPLZ Dealer bust! You win"
|
||||||
|
net = int(float64(bet) * 1.5)
|
||||||
|
case pv > dv:
|
||||||
|
outcome = "OhMyDog You Win"
|
||||||
|
net int(float64(bet) * 1.3)
|
||||||
|
case pv < dv:
|
||||||
|
outcome = "cmonBruh You lose"
|
||||||
|
net = 0
|
||||||
|
default:
|
||||||
|
outcome = "PunchTrees Push"
|
||||||
|
net = bet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add winnings
|
||||||
|
balance, err = points.AddPoints(login, net)
|
||||||
|
if err != nil {
|
||||||
|
client.Say(channel, fmt.Sprintf("@%s Error updating Points: %v", user.DisplayName, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// format a hand for display
|
||||||
|
fmtHand := func(h []Card) string {
|
||||||
|
var parts []string
|
||||||
|
for _, c := range h {
|
||||||
|
parts = append(parts, c.Rank+c.Suit)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// post result
|
||||||
|
client.Say(channel, fmt.Sprintf("@%s You: [%s]=%d Dealer: [%s]=%d => %s | Bet: %d Net: %d Ballance: %d", user.DisplayName,
|
||||||
|
fmtHand(player), pv,
|
||||||
|
fmtHand(dealer), dv,
|
||||||
|
outcome,
|
||||||
|
bet, net, balance,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute BlackJack style value (A = 11 or 1)
|
||||||
|
func handValue(hand []Card) int {
|
||||||
|
total := 0
|
||||||
|
aces := 0
|
||||||
|
for _, c := range hand {
|
||||||
|
switch c.Rank {
|
||||||
|
case "J", "Q", "K":
|
||||||
|
total += 10
|
||||||
|
case "A":
|
||||||
|
total += 11
|
||||||
|
aces++
|
||||||
|
default:
|
||||||
|
v, _ := strconv.Atoi(c.Rank)
|
||||||
|
total += v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert A from 11 to 1 while busting
|
||||||
|
for total > 21 && aces > 0 {
|
||||||
|
total -= 10
|
||||||
|
aces--
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
@@ -37,7 +37,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CustomCommand struct {
|
type CustomCommand struct {
|
||||||
Triggers string `json:"triggers"`
|
Triggers []string `json:"triggers"`
|
||||||
Reply string `json:"reply"`
|
Reply string `json:"reply"`
|
||||||
Permission PermissionLevel `json:"permission"`
|
Permission PermissionLevel `json:"permission"`
|
||||||
Match MatchType `json:"match"`
|
Match MatchType `json:"match"`
|
||||||
@@ -55,7 +55,7 @@ func LoadDefaultCommands() error {
|
|||||||
|
|
||||||
// Load Custom User Commands
|
// Load Custom User Commands
|
||||||
func LoadCustomCommands() error {
|
func LoadCustomCommands() error {
|
||||||
path := filepath.Join(commandDir, CustomCommandFile)
|
path := filepath.Join(commandDir, customCommandFile)
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
customCommands = []CustomCommand{}
|
customCommands = []CustomCommand{}
|
||||||
return nil
|
return nil
|
||||||
@@ -92,12 +92,12 @@ func GetAllCommands() []CustomCommand {
|
|||||||
func AddCustomCommand(trigger, reply string, permission PermissionLevel) bool {
|
func AddCustomCommand(trigger, reply string, permission PermissionLevel) bool {
|
||||||
trigger = strings.ToLower(trigger)
|
trigger = strings.ToLower(trigger)
|
||||||
for _, c := range defaultCommands {
|
for _, c := range defaultCommands {
|
||||||
if c.Trigger == trigger {
|
if c.Triggers == trigger {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, c := range customCommands {
|
for _, c := range customCommands {
|
||||||
if c.Trigger == tigger {
|
if c.Triggers == trigger {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ func AddCustomCommand(trigger, reply string, permission PermissionLevel) bool {
|
|||||||
func DeleteCustomCommand(trigger string) bool {
|
func DeleteCustomCommand(trigger string) bool {
|
||||||
tigger = strings.ToLower(trigger)
|
tigger = strings.ToLower(trigger)
|
||||||
for i, c := range customCommands {
|
for i, c := range customCommands {
|
||||||
if c.Trigger == trigger {
|
if c.Triggers == trigger {
|
||||||
customCommands = append(customCommands[:1], customCommands[+1:]...)
|
customCommands = append(customCommands[:1], customCommands[+1:]...)
|
||||||
SaveCustomCommands()
|
SaveCustomCommands()
|
||||||
return true
|
return true
|
||||||
@@ -127,12 +127,12 @@ func DeleteCustomCommand(trigger string) bool {
|
|||||||
func FindCommand(trigger string) *CustomCommand {
|
func FindCommand(trigger string) *CustomCommand {
|
||||||
trigger = strings.ToLower(trigger)
|
trigger = strings.ToLower(trigger)
|
||||||
for _, c := range defaultCommands {
|
for _, c := range defaultCommands {
|
||||||
if c.Trigger == trigger {
|
if c.Triggers == trigger {
|
||||||
return &c
|
return &c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, c:= range customCommands {
|
for _, c:= range customCommands {
|
||||||
if c.Trigger == trigger {
|
if c.Triggers == trigger {
|
||||||
return &c
|
return &c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
75
internal/commands/usage.go
Normal file
75
internal/commands/usage.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const usageFile = "internal/storage/usage.json"
|
||||||
|
|
||||||
|
var (
|
||||||
|
usageMu sybc.Mutex
|
||||||
|
usageCounts map[string]int
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadUsage() error {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(usageFile)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
usageCounts = make(map[string]int)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(dat, &usageCounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveUsage() error {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(usageFile), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(usageCounts, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(usageFile, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncrementUsage(trigger string) {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
if usageCounts == nil {
|
||||||
|
usageCounts = make(map[string]int)
|
||||||
|
}
|
||||||
|
usageCounts[trigger]++
|
||||||
|
SaveUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllUsage() map[string]int {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
m := make(map[string]int, len(usageCounts))
|
||||||
|
for k, v := range usageCounts {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func TotalUsage() int {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
sum := 0
|
||||||
|
for _, v := range usageCounts {
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
84
internal/config/config.go
Normal file
84
internal/config/config.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigDir = "internal/storage"
|
||||||
|
ConfigFile = "settings.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
TitleGetTemplate string `json:"title_get_template"`
|
||||||
|
TitleSetTemplate string `json:"title_set_template"`
|
||||||
|
CategoryGetTemplate string `json:"category_get_template"`
|
||||||
|
CategorySetTemplate string `json:"category_set_template"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Settings
|
||||||
|
|
||||||
|
func Load() error {
|
||||||
|
path := filepath.Join(ConfigDir, ConfigFile)
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// defaults
|
||||||
|
cfg = Settings{
|
||||||
|
TitleGetTemplate: "@user Current Title: $title",
|
||||||
|
TitleSetTemplate: "New Stream Title: $title",
|
||||||
|
CategoryGetTepmplate: "@user Current Category: $category",
|
||||||
|
CategorySetTemplate: "New StreamCategory: $category",
|
||||||
|
}
|
||||||
|
return Save()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, &cgf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Save() error {
|
||||||
|
if err := os.MkdirAll(ConfigDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := filepath.Join(ConfigDir, ConfigFile)
|
||||||
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(key string) (string bool) {
|
||||||
|
switch key {
|
||||||
|
case "title-get":
|
||||||
|
return cfg.TitleGetTemplate, true
|
||||||
|
case "title-set":
|
||||||
|
return cfg.TitleSetTemplate, true
|
||||||
|
case "category-get":
|
||||||
|
return cfg.CategoryGetTemplate, true
|
||||||
|
caes "categroy-set":
|
||||||
|
return cfg.CategorySetTemplate, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set(key, val string) bool {
|
||||||
|
switch key {
|
||||||
|
case "title-get":
|
||||||
|
cfg.TitleGetTemplate = val
|
||||||
|
case "tiel-set":
|
||||||
|
cfg.TitleSetTemplate = val
|
||||||
|
case "category-get":
|
||||||
|
cfg.CategoryGetTemplate = val
|
||||||
|
case "category-set":
|
||||||
|
cfg.CategorySetTemplate = val
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Save()
|
||||||
|
return true
|
||||||
|
}
|
84
internal/points/points.go
Normal file
84
internal/points/points.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package points
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dataDir = "internal/storage"
|
||||||
|
pointsFile = "points.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
pts map[string]int
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultPoints = 100000
|
||||||
|
|
||||||
|
// init or load points from disk
|
||||||
|
func LoadPoints() error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
pts = make(map[string]int)
|
||||||
|
path := filepath.Join(dataDir, pointsFile)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, &pts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save balances to disk
|
||||||
|
func SavePoints() error {
|
||||||
|
mu.Lock()
|
||||||
|
data, err := json.MarshalIndent(pts, "", " ")
|
||||||
|
mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(filepath.Join(dataDir, pointsFile), data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return point balance for user, init if needed
|
||||||
|
func GetPoints(user string) int {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
key := strings.ToLower(user)
|
||||||
|
bal, ok := pts[key]
|
||||||
|
if !ok {
|
||||||
|
pts[key] = DefaultPoints
|
||||||
|
bal = DefaultPoints
|
||||||
|
}
|
||||||
|
return bal
|
||||||
|
}
|
||||||
|
|
||||||
|
// add points to user's bal, save & return new bal
|
||||||
|
func AddPoints(user string, delta int) (int error) {
|
||||||
|
mu.Lock()
|
||||||
|
key := strings.ToLower(user)
|
||||||
|
bal, ok := pts[key]
|
||||||
|
if !ok {
|
||||||
|
bal = DefaultPoints
|
||||||
|
}
|
||||||
|
bal += delta
|
||||||
|
pts[key] = bal
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if err := SavePoints(); err != nil {
|
||||||
|
return bal, err
|
||||||
|
}
|
||||||
|
return bal, nil
|
||||||
|
}
|
0
internal/storage/settings.json
Normal file
0
internal/storage/settings.json
Normal file
0
internal/storage/usage.json
Normal file
0
internal/storage/usage.json
Normal file
@@ -7,16 +7,28 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
twitch "github.com/gempir/go-twitch-irc/v4"
|
twitch "github.com/gempir/go-twitch-irc/v4"
|
||||||
|
helix "github.com/nicklaw5/helix"
|
||||||
twitchapi "streambot_twitch/internal/twitchapi"
|
twitchapi "streambot_twitch/internal/twitchapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// check Twitch API connectivity
|
// check Twitch API connectivity
|
||||||
func TestTwitchAPI(client *twitchapi.Client, streamerChannel string) error {
|
func TestTwitchAPI(client *twitchapi.Client, streamerChannel string) error {
|
||||||
_, err := client.GetUserID(streamerChannel)
|
userID, err := client.GetUserID(streamerChannel)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Twitch API test failed: %w", err)
|
return fmt.Errorf("failed to get Usre ID: %w", err)
|
||||||
}
|
}
|
||||||
log.Println("Twitch API connection successful")
|
resp, err := client.api.GetChannelInformation(&helix.GetChannelInformationParams{
|
||||||
|
BroadcasterID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get channel information: %w", err)
|
||||||
|
}
|
||||||
|
if len(resp.Data.Channels) == 0 {
|
||||||
|
return fmt.Errorf("no channel information found")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Twitch API connection Test Successful for channel: %s", streamerChannel)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,34 +5,59 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
helix "github.com/nicklaw5/helix"
|
helix "github.com/nicklaw5/helix"
|
||||||
|
oauthsrv "streambot_twitch/internal/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
api *helix.Client
|
api *helix.Client
|
||||||
|
refreshToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(clientID, ClientSecret string) (*Client, error) {
|
func NewClient(clientID, clientSecret string) (*Client, error) {
|
||||||
apiClient, err := helix.NewClient(&helix.Options{
|
apiClient, err := helix.NewClient(&helix.Options{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nill, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := apiClient.RequestAppAccessToken([]string{})
|
resp, err := apiClient.RequestAppAccessToken([]string{})
|
||||||
if err != nil || resp.StatusCode != 200 {
|
if err != nil || resp.StatusCode != 200 {
|
||||||
return nil, fmt.Errorf("Failed to get app access token: %v", err)
|
return nil, fmt.Errorf("Failed to get app access token: %v", err)
|
||||||
}
|
}
|
||||||
apiClient.SetUserAccessToken(resp.Data.AcessToken)
|
apiClient.SetUserAccessToken(resp.Data.AccessToken)
|
||||||
|
|
||||||
return &Client{api: apiClient}, nil
|
return &Client{api: apiClient}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set User Tokens - Oauth and Refresh
|
||||||
|
func (c *Client) SetUserTokens(accessToken, refreshToken string, expiresIn int) {
|
||||||
|
at := strings.TrimPrefix(accessToken, "oauth:")
|
||||||
|
c.api.SetUserAccessToken(at)
|
||||||
|
c.accessToken = at
|
||||||
|
c.refreshToken = refreshToken
|
||||||
|
if expiresIn > 0 {
|
||||||
|
c.expiry = time.Now().Add(time.Duration(expiresIn) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use /refresh endpoint to renew tokens
|
||||||
|
func (c *Client) RefreshIfNeeded() error {
|
||||||
|
if time.Until(c.expiry) < 5*time.Minute {
|
||||||
|
tr, err := oauthsrv.ExchangeRefreshToken(c.refreshToken)
|
||||||
|
if err 1= nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.SetUserTokens(tr.AccessToken, tr.RefreshToken, tr.ExpiresIn)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Twitch API helpers
|
// Twitch API helpers
|
||||||
|
|
||||||
func getUserID(client *helix.Client, login String) (string, error) {
|
func (c *Client) GetUserID(login string) (string, error) {
|
||||||
resp, err := client.GetUsers(&helix.UsersParams{
|
resp, err := c.api.GetUsers(&helix.UsersParams{
|
||||||
Logins: []string{login},
|
Logins: []string{login},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -44,8 +69,8 @@ func getUserID(client *helix.Client, login String) (string, error) {
|
|||||||
return resp.Data.Users[0].ID, nil
|
return resp.Data.Users[0].ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStreamInfo(client *helix.Client, userID string) (*helix.Stream, error) {
|
func (c *Client) GetStreamInfo(userID string) (*helix.Stream, error) {
|
||||||
resp, err := clinet.GetStreams(&helix.StreamsParams{
|
rep, err := c.api.GetStreams(&helix.StreamsParams{
|
||||||
UserIDs: []string{userID},
|
UserIDs: []string{userID},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -57,29 +82,27 @@ func getStreamInfo(client *helix.Client, userID string) (*helix.Stream, error) {
|
|||||||
return &resp.Data.Streams[0], nil
|
return &resp.Data.Streams[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStreamUptime(client *helix.Client, channel string) (string, error) {
|
func (c *Client) GetStreamUptime(channel string) (string, error) {
|
||||||
userID, err := getUserID(client, channel)
|
userID, err := c.GetUserId(channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
stream, err := getStreamInfo(client, userID)
|
stream, err := c.GetStreamInfo(userID)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
startedAt, err := time.Parse(time.RFC3339, stream.StartedAt)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
startedAt := stream.StartedAt
|
||||||
|
|
||||||
uptime := time.Since(startedAt).Round(time.Second)
|
uptime := time.Since(startedAt).Round(time.Second)
|
||||||
return uptime.String(), nil
|
return uptime.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStreamTitle(client *helix.Client, channel string) (string, error) {
|
func (c *Client) GetStreamTitle(channel string) (string, error) {
|
||||||
userID, err := getUserID(client, channel)
|
userID, err := C. GetUserID(channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
resp, err := client.GetChannels(&helix.GetChannelsParams{
|
resp, err := c.api.GetChannelInformation(&helix.GetChannelInformationParams{
|
||||||
BroadcasterID: userID,
|
BroadcasterID: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -91,24 +114,24 @@ func getStreamTitle(client *helix.Client, channel string) (string, error) {
|
|||||||
return resp.Data.Channels[0].Title, nil
|
return resp.Data.Channels[0].Title, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setStreamTitle(client *helix.Client, channel, newTitle string) error {
|
func (c *Client) SetStreamTitle(channel, newTitle string) error {
|
||||||
userID, err := getUserID(client, channel)
|
userID, err := c.GetUserID(channel)
|
||||||
if err!= nil {
|
if err!= nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = client.UpdateChannelInformation(&helix.UpdateChannelInformationParams {
|
_, err = c.api.EditChannelInformation(&helix.EditChannelInformationParams {
|
||||||
BroadcasterID: userID,
|
BroadcasterID: userID,
|
||||||
Title: newTitle,
|
Title: newTitle,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStreamCategory(client *helix.Client, channel string) (srting, error) {
|
func (c *Client) GetStreamCategory(channel string) (string, error) {
|
||||||
userID, er := getUserID(client, channel)
|
userID, err := c.GetUserID(channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
resp, err := client.GetChannels(&helix.GetChannelsParams{
|
resp, err := c.api.GetChannelInformation(&helix.GetChannelInformationParams{
|
||||||
BroadcasterID: userID,
|
BroadcasterID: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,25 +143,25 @@ func getStreamCategory(client *helix.Client, channel string) (srting, error) {
|
|||||||
return resp.Data.Channels[0].GameName, nil
|
return resp.Data.Channels[0].GameName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setStreamCategory(client *helix.Client, channel,, newCategory string) error {
|
func (c *Client) SetStreamCategory(channel, newCategory string) error {
|
||||||
userID, err := getUserID(client, channel)
|
userID, err := c.GetUserID(channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// get Twitch GameID by Name from API
|
// get Twitch GameID by Name from API
|
||||||
gameID, err := getUserIDByName(client, newCategory)
|
gameID, err := c.GetGameIDByName(newCategory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = client.UpdateChannelInformation(&helix.UpdateChannelInformationParams{
|
_, err = c.api.EditChannelInformation(&helix.EditChannelInformationParams{
|
||||||
BroadcasterID: userID,
|
BroadcasterID: userID,
|
||||||
GameID: gameID,
|
GameID: gameID,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGameIDByName(client *helix.Client, name string) (string, error) {
|
func (c *Client) GetGameIDByName(name string) (string, error) {
|
||||||
resp, err := client.GetGames(&helix.GamesParams{
|
resp, err := c.api.GetGames(&helix.GamesParams{
|
||||||
Names: []string{name},
|
Names: []string{name},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Reference in New Issue
Block a user