Add ticker symbol voting (e.g. QPL3++, TMP2--)
Extend vote detection to recognize game ticker symbols alongside the existing thisgame++/-- syntax. Each symbol maps to a specific game so users can vote for any game by its stock-style ticker. The matched ticker is sent to the API via a new optional `ticker` field in the vote request. Made-with: Cursor
This commit is contained in:
@@ -252,16 +252,15 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
|
|||||||
rmsg.Text = string(output)
|
rmsg.Text = string(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for votes (thisgame++ or thisgame--)
|
// Check for votes (thisgame++/-- or ticker symbol++/--)
|
||||||
// Only process votes from non-relayed messages
|
// Only process votes from non-relayed messages
|
||||||
if !jackbox.IsRelayedMessage(rmsg.Text) {
|
if !jackbox.IsRelayedMessage(rmsg.Text) {
|
||||||
if isVote, voteType := jackbox.DetectVote(rmsg.Text); isVote {
|
if isVote, voteType, ticker := jackbox.DetectVote(rmsg.Text); isVote {
|
||||||
b.Log.Debugf("Detected vote from %s: %s", event.Source.Name, voteType)
|
b.Log.Debugf("Detected vote from %s: %s (ticker=%q)", event.Source.Name, voteType, ticker)
|
||||||
if b.jackboxClient != nil {
|
if b.jackboxClient != nil {
|
||||||
go func() {
|
go func() {
|
||||||
// Use current time as timestamp for IRC messages
|
|
||||||
timestamp := time.Now()
|
timestamp := time.Now()
|
||||||
if err := b.jackboxClient.SendVote(event.Source.Name, voteType, timestamp); err != nil {
|
if err := b.jackboxClient.SendVote(event.Source.Name, voteType, timestamp, ticker); err != nil {
|
||||||
b.Log.Errorf("Failed to send vote to Jackbox API: %v", err)
|
b.Log.Errorf("Failed to send vote to Jackbox API: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ type AuthResponse struct {
|
|||||||
// VoteRequest represents a vote submission to the API
|
// VoteRequest represents a vote submission to the API
|
||||||
type VoteRequest struct {
|
type VoteRequest struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Vote string `json:"vote"` // "up" or "down"
|
Vote string `json:"vote"` // "up" or "down"
|
||||||
Timestamp string `json:"timestamp"`
|
Timestamp string `json:"timestamp"`
|
||||||
|
Ticker string `json:"ticker,omitempty"` // ticker symbol targeting a specific game
|
||||||
}
|
}
|
||||||
|
|
||||||
// VoteResponse represents the API response to a vote submission
|
// VoteResponse represents the API response to a vote submission
|
||||||
@@ -214,8 +215,9 @@ func (c *Client) ensureAuthenticated() error {
|
|||||||
return c.Authenticate()
|
return c.Authenticate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendVote sends a vote to the Jackbox API
|
// SendVote sends a vote to the Jackbox API.
|
||||||
func (c *Client) SendVote(username, voteType string, timestamp time.Time) error {
|
// ticker is optional; when non-empty the API resolves the target game by symbol.
|
||||||
|
func (c *Client) SendVote(username, voteType string, timestamp time.Time, ticker string) error {
|
||||||
// Ensure we're authenticated
|
// Ensure we're authenticated
|
||||||
if err := c.ensureAuthenticated(); err != nil {
|
if err := c.ensureAuthenticated(); err != nil {
|
||||||
return fmt.Errorf("authentication failed: %w", err)
|
return fmt.Errorf("authentication failed: %w", err)
|
||||||
@@ -230,6 +232,7 @@ func (c *Client) SendVote(username, voteType string, timestamp time.Time) error
|
|||||||
Username: username,
|
Username: username,
|
||||||
Vote: voteType,
|
Vote: voteType,
|
||||||
Timestamp: timestamp.Format(time.RFC3339),
|
Timestamp: timestamp.Format(time.RFC3339),
|
||||||
|
Ticker: ticker,
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonBody, err := json.Marshal(voteReq)
|
jsonBody, err := json.Marshal(voteReq)
|
||||||
@@ -266,7 +269,7 @@ func (c *Client) SendVote(username, voteType string, timestamp time.Time) error
|
|||||||
return fmt.Errorf("re-authentication failed: %w", err)
|
return fmt.Errorf("re-authentication failed: %w", err)
|
||||||
}
|
}
|
||||||
// Retry the vote
|
// Retry the vote
|
||||||
return c.SendVote(username, voteType, timestamp)
|
return c.SendVote(username, voteType, timestamp, ticker)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusConflict {
|
if resp.StatusCode == http.StatusConflict {
|
||||||
|
|||||||
72
bridge/jackbox/tickers.go
Normal file
72
bridge/jackbox/tickers.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package jackbox
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// tickerSymbols maps uppercase ticker symbols to their canonical game titles.
|
||||||
|
var tickerSymbols = map[string]string{
|
||||||
|
"QPL3": "Quiplash 3",
|
||||||
|
"QPL2": "Quiplash 2",
|
||||||
|
"QLXL": "Quiplash XL",
|
||||||
|
"FBXL": "Fibbage XL",
|
||||||
|
"FBG2": "Fibbage 2",
|
||||||
|
"FBG3": "Fibbage 3",
|
||||||
|
"FBG4": "Fibbage 4",
|
||||||
|
"TMP1": "Trivia Murder Party",
|
||||||
|
"TMP2": "Trivia Murder Party 2",
|
||||||
|
"DRWF": "Drawful",
|
||||||
|
"DRWA": "Drawful Animate",
|
||||||
|
"DD": "Dirty Drawful",
|
||||||
|
"DOOM": "Doominate",
|
||||||
|
"JJ": "Job Job",
|
||||||
|
"TKO2": "Tee K.O. 2",
|
||||||
|
"TKOX": "Tee K.O. T-Shirt Knock Out",
|
||||||
|
"CU": "Champ'd Up",
|
||||||
|
"BR": "Blather 'Round",
|
||||||
|
"STR": "Split the Room",
|
||||||
|
"ROOM": "Roomerang",
|
||||||
|
"BRKT": "Bracketeering",
|
||||||
|
"NNSR": "Nonsensory",
|
||||||
|
"QXRT": "Quixort",
|
||||||
|
"JNKT": "Junktopia",
|
||||||
|
"TP": "Talking Points",
|
||||||
|
"PS": "Patently Stupid",
|
||||||
|
"PTB": "Push the Button",
|
||||||
|
"WD": "Weapons Drawn",
|
||||||
|
"HPNT": "Hypnotorious",
|
||||||
|
"DCTN": "Dictionarium",
|
||||||
|
"RM": "Role Models",
|
||||||
|
"JB": "Joke Boat",
|
||||||
|
"GSPN": "Guesspionage",
|
||||||
|
"MVC": "Mad Verse City",
|
||||||
|
"HRSY": "Hear Say",
|
||||||
|
"CH": "Cookie Haus",
|
||||||
|
"SPCT": "Suspectives",
|
||||||
|
"LOT": "Legends of Trivia",
|
||||||
|
"STI": "Survive the Internet",
|
||||||
|
"CVDL": "Civic Doodle",
|
||||||
|
"MSM": "Monster Seeking Monster",
|
||||||
|
"TPM": "The Poll Mine",
|
||||||
|
"TWEP": "The Wheel of Enormous Proportions",
|
||||||
|
"TJ": "Time Jinx",
|
||||||
|
"DRM": "Dodo Re Mi",
|
||||||
|
"FT": "Fixy Text",
|
||||||
|
"SS": "Survey Scramble",
|
||||||
|
"WS": "Word Spud",
|
||||||
|
"LS": "Lie Swatter",
|
||||||
|
"FI": "Fakin' It!",
|
||||||
|
"FANL": "Fakin' It All Night Long",
|
||||||
|
"LMF": "Let Me Finish",
|
||||||
|
"BDTS": "Bidiots",
|
||||||
|
"BC": "Bomb Corp.",
|
||||||
|
"YDK1": "You Don't Know Jack® 2015",
|
||||||
|
"YDKJ": "You Don't Know Jack® Full Stream",
|
||||||
|
"ZPDM": "Zeeple Dome",
|
||||||
|
"EW": "Earwax™",
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupTicker returns the game title for a ticker symbol.
|
||||||
|
// The symbol is matched case-insensitively.
|
||||||
|
func LookupTicker(symbol string) (title string, ok bool) {
|
||||||
|
title, ok = tickerSymbols[strings.ToUpper(symbol)]
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -1,23 +1,42 @@
|
|||||||
package jackbox
|
package jackbox
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tickerVoteRe matches a ticker symbol (2-4 uppercase alphanumeric chars)
|
||||||
|
// immediately followed by ++ or --.
|
||||||
|
var tickerVoteRe = regexp.MustCompile(`(?i)\b([A-Z0-9]{2,4})(\+\+|--)`)
|
||||||
|
|
||||||
// DetectVote checks if a message contains a vote and returns the vote type
|
// DetectVote checks if a message contains a vote and returns the vote type
|
||||||
// Returns (true, "up") for thisgame++
|
// and optional ticker symbol.
|
||||||
// Returns (true, "down") for thisgame--
|
//
|
||||||
// Returns (false, "") for non-vote messages
|
// For "thisgame++" / "thisgame--": returns (true, "up"/"down", "")
|
||||||
func DetectVote(text string) (isVote bool, voteType string) {
|
// For "QPL3++" / "tmp2--": returns (true, "up"/"down", "QPL3"/"TMP2")
|
||||||
|
// For non-vote messages: returns (false, "", "")
|
||||||
|
func DetectVote(text string) (isVote bool, voteType string, ticker string) {
|
||||||
lower := strings.ToLower(text)
|
lower := strings.ToLower(text)
|
||||||
|
|
||||||
if strings.Contains(lower, "thisgame++") {
|
if strings.Contains(lower, "thisgame++") {
|
||||||
return true, "up"
|
return true, "up", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(lower, "thisgame--") {
|
if strings.Contains(lower, "thisgame--") {
|
||||||
return true, "down"
|
return true, "down", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, ""
|
if m := tickerVoteRe.FindStringSubmatch(text); m != nil {
|
||||||
|
sym := strings.ToUpper(m[1])
|
||||||
|
if _, ok := LookupTicker(sym); ok {
|
||||||
|
if m[2] == "++" {
|
||||||
|
return true, "up", sym
|
||||||
|
}
|
||||||
|
return true, "down", sym
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRelayedMessage checks if a message is relayed from another chat
|
// IsRelayedMessage checks if a message is relayed from another chat
|
||||||
|
|||||||
118
bridge/jackbox/votes_test.go
Normal file
118
bridge/jackbox/votes_test.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package jackbox
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDetectVote_ThisGame(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
isVote bool
|
||||||
|
voteType string
|
||||||
|
ticker string
|
||||||
|
}{
|
||||||
|
{"thisgame++", true, "up", ""},
|
||||||
|
{"thisgame--", true, "down", ""},
|
||||||
|
{"THISGAME++", true, "up", ""},
|
||||||
|
{"ThisGame--", true, "down", ""},
|
||||||
|
{"I love thisgame++ so much", true, "up", ""},
|
||||||
|
{"honestly thisgame-- was rough", true, "down", ""},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
isVote, voteType, ticker := DetectVote(tt.input)
|
||||||
|
if isVote != tt.isVote || voteType != tt.voteType || ticker != tt.ticker {
|
||||||
|
t.Errorf("DetectVote(%q) = (%v, %q, %q), want (%v, %q, %q)",
|
||||||
|
tt.input, isVote, voteType, ticker, tt.isVote, tt.voteType, tt.ticker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectVote_Ticker(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
isVote bool
|
||||||
|
voteType string
|
||||||
|
ticker string
|
||||||
|
}{
|
||||||
|
{"QPL3++", true, "up", "QPL3"},
|
||||||
|
{"qpl3++", true, "up", "QPL3"},
|
||||||
|
{"TMP2--", true, "down", "TMP2"},
|
||||||
|
{"tmp2--", true, "down", "TMP2"},
|
||||||
|
{"DD++", true, "up", "DD"},
|
||||||
|
{"dd--", true, "down", "DD"},
|
||||||
|
{"YDKJ++", true, "up", "YDKJ"},
|
||||||
|
{"EW--", true, "down", "EW"},
|
||||||
|
{"let's go FBG4++", true, "up", "FBG4"},
|
||||||
|
{"TWEP++ is great", true, "up", "TWEP"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
isVote, voteType, ticker := DetectVote(tt.input)
|
||||||
|
if isVote != tt.isVote || voteType != tt.voteType || ticker != tt.ticker {
|
||||||
|
t.Errorf("DetectVote(%q) = (%v, %q, %q), want (%v, %q, %q)",
|
||||||
|
tt.input, isVote, voteType, ticker, tt.isVote, tt.voteType, tt.ticker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectVote_UnknownSymbol(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"ZZZZ++",
|
||||||
|
"ABCD--",
|
||||||
|
"XY++",
|
||||||
|
"NOPE--",
|
||||||
|
}
|
||||||
|
for _, input := range tests {
|
||||||
|
isVote, voteType, ticker := DetectVote(input)
|
||||||
|
if isVote {
|
||||||
|
t.Errorf("DetectVote(%q) = (%v, %q, %q), want (false, \"\", \"\")",
|
||||||
|
input, isVote, voteType, ticker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectVote_NoVote(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"hello world",
|
||||||
|
"this game is fun",
|
||||||
|
"QPL3",
|
||||||
|
"++QPL3",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
for _, input := range tests {
|
||||||
|
isVote, voteType, ticker := DetectVote(input)
|
||||||
|
if isVote {
|
||||||
|
t.Errorf("DetectVote(%q) = (%v, %q, %q), want (false, \"\", \"\")",
|
||||||
|
input, isVote, voteType, ticker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectVote_ThisGameTakesPriority(t *testing.T) {
|
||||||
|
// When a message contains both thisgame++ and a ticker, thisgame wins
|
||||||
|
isVote, voteType, ticker := DetectVote("thisgame++ QPL3++")
|
||||||
|
if !isVote || voteType != "up" || ticker != "" {
|
||||||
|
t.Errorf("DetectVote with both patterns = (%v, %q, %q), want (true, \"up\", \"\")",
|
||||||
|
isVote, voteType, ticker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupTicker(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
symbol string
|
||||||
|
title string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"QPL3", "Quiplash 3", true},
|
||||||
|
{"qpl3", "Quiplash 3", true},
|
||||||
|
{"DD", "Dirty Drawful", true},
|
||||||
|
{"EW", "Earwax™", true},
|
||||||
|
{"TWEP", "The Wheel of Enormous Proportions", true},
|
||||||
|
{"ZZZZ", "", false},
|
||||||
|
{"", "", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
title, ok := LookupTicker(tt.symbol)
|
||||||
|
if ok != tt.ok || title != tt.title {
|
||||||
|
t.Errorf("LookupTicker(%q) = (%q, %v), want (%q, %v)",
|
||||||
|
tt.symbol, title, ok, tt.title, tt.ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -204,14 +204,14 @@ func (b *Bkosmi) handleIncomingMessage(payload *NewMessagePayload) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for votes (thisgame++ or thisgame--)
|
// Check for votes (thisgame++/-- or ticker symbol++/--)
|
||||||
// Only process votes from non-relayed messages
|
// Only process votes from non-relayed messages
|
||||||
if !jackbox.IsRelayedMessage(body) {
|
if !jackbox.IsRelayedMessage(body) {
|
||||||
if isVote, voteType := jackbox.DetectVote(body); isVote {
|
if isVote, voteType, ticker := jackbox.DetectVote(body); isVote {
|
||||||
b.Log.Debugf("Detected vote from %s: %s", username, voteType)
|
b.Log.Debugf("Detected vote from %s: %s (ticker=%q)", username, voteType, ticker)
|
||||||
if b.jackboxClient != nil {
|
if b.jackboxClient != nil {
|
||||||
go func() {
|
go func() {
|
||||||
if err := b.jackboxClient.SendVote(username, voteType, timestamp); err != nil {
|
if err := b.jackboxClient.SendVote(username, voteType, timestamp, ticker); err != nil {
|
||||||
b.Log.Errorf("Failed to send vote to Jackbox API: %v", err)
|
b.Log.Errorf("Failed to send vote to Jackbox API: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
Reference in New Issue
Block a user