Compare commits
7 Commits
b82d5dd0d4
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
963e829b7f | ||
|
b0be1f99c8 | ||
|
049051a7e1 | ||
|
d67c30d8f4 | ||
|
0ef35de85b | ||
|
e6e6de0381 | ||
|
c704380ee3 |
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.
|
||||
|
||||
>[!NOTE]
|
||||
>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.
|
||||
>Key for entire document:
|
||||
>[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]
|
||||
>< > = 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
|
||||
>
|
||||
> !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")
|
||||
>
|
||||
> !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))
|
||||
>
|
||||
> !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
|
||||
> !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?
|
||||
>
|
||||
> !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
|
||||
>
|
||||
> !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)
|
||||
>
|
||||
> !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]
|
||||
|
@@ -1,12 +1,13 @@
|
||||
package main
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"streambot_twitch/internal/auth"
|
||||
auth "streambot_twitch/internal/auth"
|
||||
"streambot_twitch/internal/chat"
|
||||
"streambot_twitch/internal/commands"
|
||||
"streambot_twitch/internal/points"
|
||||
twitchapi "streambot_twitch/internal/twitchapi"
|
||||
tests "streambot_twitch/internal/tests"
|
||||
twitch "github.com/gempir/go-twitch-irc/v4"
|
||||
@@ -14,21 +15,26 @@ import (
|
||||
|
||||
func main() {
|
||||
go func() {
|
||||
if err := oauth.StartOAuthServer(":8080"); err != nil {
|
||||
if err := auth.StartOAuthServer(":8080"); err != nil {
|
||||
log.Fatalf("OAuth server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = godotenv.Load()
|
||||
|
||||
log.Println("OAuth server started on port 8080")
|
||||
|
||||
botUsername := os.Getenv("nz_chatterbot")
|
||||
botOAuth :+ os.Getenv("BOT_OAUTH")
|
||||
channel := os Getenv("brycefromnz101")
|
||||
clientID := os.Getenv("TWITCH_CLIENTID")
|
||||
clientSecret := os.Getenv("TWITCH_CLIENT_SECRET")
|
||||
botUsername := os.Getenv("BOT_USERNAME")
|
||||
botAccess := os.Getenv("BOT_ACCESS_TOKEN")
|
||||
botRefresh := os.Getenv("BOT_REFRESH_TOKEN")
|
||||
channel := os Getenv("CHANNEL")
|
||||
clientID := os.Getenv("TWITCH_CLIENT_ID")
|
||||
clientSecret := os.Getenv("TWITCH_CLIENT_SECRET")
|
||||
channelAccess := os.Getenv("STREAMER_ACCESS_TOKEN")
|
||||
channelRefresh := os.Getenv("STREAMER_REFRESH_TOKEN")
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -52,6 +58,14 @@ log.Println("OAuth server started on port 8080")
|
||||
log.Println("All startup tests passed. Starting bot. . .")
|
||||
// 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
|
||||
if err := commands.LoadCommands(); err != nil {
|
||||
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
|
||||
)
|
||||
|
||||
require github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
require (
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
)
|
||||
|
2
go.sum
2
go.sum
@@ -3,5 +3,7 @@ github.com/gempir/go-twitch-irc/v4 v4.2.0/go.mod h1:QsOMMAk470uxQ7EYD9GJBGAVqM/j
|
||||
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/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw=
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package oauth
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -39,6 +39,7 @@ func StartOAuthServer(addr string) error {
|
||||
|
||||
http.HandleFunc("/", handleIndex)
|
||||
http.HandleFunc("/callback", handleCallback)
|
||||
http.HandleFunc("/refresh", handleRefresh)
|
||||
|
||||
log.Printf("OAuth server listening on %s", addr)
|
||||
return http.ListenAndServe(addr, nil)
|
||||
@@ -46,7 +47,7 @@ func StartOAuthServer(addr string) error {
|
||||
|
||||
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
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(redirectURI),
|
||||
url.QueryEscape(strings.Join(scopes, " ")),
|
||||
@@ -77,10 +78,26 @@ func handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
jsonData, _ := json.MarshalIndent(token, "", " ")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
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.Write(data)
|
||||
}
|
||||
|
||||
func exchangeCodeForToken(code string) (*TokenResponse, error) {
|
||||
data := url.Values{}
|
||||
data.Set("client_id", clientID)
|
||||
@@ -110,3 +127,31 @@ func exchangeCodeForToken(code string) (*TokenResponse, error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
HandleCustomCommands(client, message, user, channel, botUsername)
|
||||
}
|
||||
// check perms
|
||||
for _, cmd := range commands.GetAllCommands() {
|
||||
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) {
|
||||
reply := ReplacePlaceholders(cmd.Replay, user.DisplayName, channel, botUsername)
|
||||
|
||||
client.Say(channel, cmd.Reply)
|
||||
} else {
|
||||
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
|
||||
}
|
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
@@ -5,10 +5,12 @@ import (
|
||||
"time"
|
||||
|
||||
helix "github.com/nicklaw5/helix"
|
||||
oauthsrv "streambot_twitch/internal/auth"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
api *helix.Client
|
||||
refreshToken string
|
||||
}
|
||||
|
||||
func NewClient(clientID, clientSecret string) (*Client, error) {
|
||||
@@ -29,6 +31,29 @@ func NewClient(clientID, clientSecret string) (*Client, error) {
|
||||
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
|
||||
|
||||
func (c *Client) GetUserID(login string) (string, error) {
|
||||
@@ -118,25 +143,25 @@ func (c *Client) GetStreamCategory(channel string) (string, error) {
|
||||
return resp.Data.Channels[0].GameName, nil
|
||||
}
|
||||
|
||||
func setStreamCategory(client *helix.Client, channel, newCategory string) error {
|
||||
userID, err := GetUserID(client, channel)
|
||||
func (c *Client) SetStreamCategory(channel, newCategory string) error {
|
||||
userID, err := c.GetUserID(channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// get Twitch GameID by Name from API
|
||||
gameID, err := getGameIDByName(client, newCategory)
|
||||
gameID, err := c.GetGameIDByName(newCategory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.EditChannelInformation(&helix.EditChannelInformationParams{
|
||||
_, err = c.api.EditChannelInformation(&helix.EditChannelInformationParams{
|
||||
BroadcasterID: userID,
|
||||
GameID: gameID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func getGameIDByName(client *helix.Client, name string) (string, error) {
|
||||
resp, err := GetGames(&helix.GamesParams{
|
||||
func (c *Client) GetGameIDByName(name string) (string, error) {
|
||||
resp, err := c.api.GetGames(&helix.GamesParams{
|
||||
Names: []string{name},
|
||||
})
|
||||
if err != nil {
|
||||
|
Reference in New Issue
Block a user