From 562b90ebf777f74c733b54f97b92f9889a1678af Mon Sep 17 00:00:00 2001 From: tyler Date: Tue, 28 May 2024 16:44:31 -0400 Subject: [PATCH] Implemented chatbot event triggers on follow, raid, rant, and subscribe --- v1/app.go | 82 ++- .../src/assets/icons/twbs/chevron-down.png | Bin 0 -> 1741 bytes v1/frontend/src/assets/index.js | 2 + v1/frontend/src/components/ChatBot.css | 105 +++ v1/frontend/src/components/ChatBot.jsx | 603 ++++++++++++++++-- v1/frontend/src/components/DropDown.css | 80 +++ v1/frontend/src/components/DropDown.jsx | 97 +++ v1/frontend/src/components/PageSideBar.jsx | 4 + v1/go.mod | 2 +- v1/go.sum | 4 +- v1/internal/chatbot/chatbot.go | 511 ++++++++++++++- v1/internal/chatbot/rule.go | 79 ++- v1/internal/chatbot/runner.go | 287 ++++++++- v1/internal/events/api.go | 4 + .../rumble-livestream-lib-go/chat.go | 35 +- .../rumble-livestream-lib-go/client.go | 18 +- v1/vendor/modules.txt | 2 +- 17 files changed, 1791 insertions(+), 124 deletions(-) create mode 100644 v1/frontend/src/assets/icons/twbs/chevron-down.png create mode 100644 v1/frontend/src/components/DropDown.css create mode 100644 v1/frontend/src/components/DropDown.jsx diff --git a/v1/app.go b/v1/app.go index f892fec..52c2fff 100644 --- a/v1/app.go +++ b/v1/app.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "sync" "time" @@ -190,7 +191,7 @@ func (a *App) processChat(event events.Chat) { // TODO: implement this func (a *App) chatbotApiProcessor(event events.Api) { - return + a.chatbot.HandleApi(event) } func (a *App) chatbotChatProcessor(event events.Chat) { @@ -356,9 +357,9 @@ func (a *App) verifyAccounts() (int, error) { } loggedIn, err := client.LoggedIn() if err != nil { - return -1, fmt.Errorf("error check if account is logged in: %v", err) + return -1, fmt.Errorf("error checking if account is logged in: %v", err) } - if loggedIn { + if loggedIn.User.LoggedIn { a.clients[*account.Username] = client } else { account.Cookies = nil @@ -493,7 +494,24 @@ func (a *App) Login(username string, password string) error { return fmt.Errorf("Error logging in. Try again.") } if acct == nil { - acct = &models.Account{nil, nil, &username, &cookiesS, nil, nil} + loggedIn, err := client.LoggedIn() + if err != nil { + a.logError.Println("error checking if account is logged in:", err) + return fmt.Errorf("Error logging in. Try again.") + } + + uid, found := strings.CutPrefix(loggedIn.User.ID, "_u") + if !found { + a.logError.Println("did not find uid prefix '_u' in response after checking if accounts is logged in") + return fmt.Errorf("Error logging in. Try again.") + } + rumbleUsername := loggedIn.Data.Username + if rumbleUsername == "" { + a.logError.Println("username is empty in response after checking if accounts is logged in") + return fmt.Errorf("Error logging in. Try again.") + } + + acct = &models.Account{nil, &uid, &rumbleUsername, &cookiesS, nil, nil} id, err := a.services.AccountS.Create(acct) if err != nil { a.logError.Println("error creating account:", err) @@ -789,6 +807,27 @@ func (a *App) ActivateChannel(id int64) error { return nil } +func (a *App) startPageApi(pi PageInfo) error { + name := pi.String() + if name == nil { + return fmt.Errorf("page name is nil") + } + url := pi.KeyUrl() + if url == nil { + return fmt.Errorf("page key url is nil") + } + + if !a.producers.ApiP.Active(*name) { + err := a.producers.ApiP.Start(*name, *url, 10*time.Second) + if err != nil { + return fmt.Errorf("error starting api: %v", err) + } + runtime.EventsEmit(a.wails, "ApiActive-"+*name, true) + } + + return nil +} + // If page is inactivate, activate. // If page is active, deactivate. func (a *App) activatePage(pi PageInfo) error { @@ -1400,6 +1439,41 @@ func (a *App) RunChatbotRule(rule *chatbot.Rule) error { a.logError.Println("error starting chat producer:", err) // TODO: send error to UI that chatbot URL could not be started //runtime.EventsEmit("Ch") + return fmt.Errorf("Error connecting to chat. Try again.") + } + + page := rule.Page() + if page != nil { + switch page.Prefix { + case chatbot.PrefixAccount: + acct, err := a.services.AccountS.ByUsername(page.Name) + if err != nil { + a.logError.Println("error getting account by username:", err) + return fmt.Errorf("Error getting account to monitor. Check rule and try again.") + } + if acct == nil { + return fmt.Errorf("Account to monitor does not exist. Check rule and try again.") + } + err = a.startPageApi(acct) + if err != nil { + a.logError.Println("error starting page api:", err) + return fmt.Errorf("Error starting API for account in rule. Try again.") + } + case chatbot.PrefixChannel: + channel, err := a.services.ChannelS.ByName(page.Name) + if err != nil { + a.logError.Println("error getting channel by name:", err) + return fmt.Errorf("Error getting channel to monitor. Check rule and try again.") + } + if channel == nil { + return fmt.Errorf("Channel to monitor does not exist. Check rule and try again.") + } + err = a.startPageApi(channel) + if err != nil { + a.logError.Println("error starting page api:", err) + return fmt.Errorf("Error starting API for channel in rule. Try again.") + } + } } err = a.chatbot.Run(rule, *mChatbot.Url) diff --git a/v1/frontend/src/assets/icons/twbs/chevron-down.png b/v1/frontend/src/assets/icons/twbs/chevron-down.png new file mode 100644 index 0000000000000000000000000000000000000000..4744882261087890d25b43bbb336f7ca652080d9 GIT binary patch literal 1741 zcmc(g`#aMM0LH&FJS#kF>2xB~HaZ-aaA>Y=$6CzUiKw-RT+5|pR1_woRan|eMmZTu z<&sMzg>Ps)2`SVtxom~SXfF9m>FBTMhxhZozrD}<)0?)>-BoM7?s@WT{v;fmu zTQl@FfN>IOr;7a>v(yx@ucS`(unK*&it60zi}A55J)7(oJI{3cqw2QKVz5gu&x2O{ z&r55s6UB+I z_YtQbZmR_kTv}YJ);Gkli2tESh=UGb0j%V(r2bNgmw#7p=A6rfOy)ZGsN+zK4*ho1 z=(~4~vsoqjuZZB+N!GV+G1llt79WlY1wHPJK7YExuRxcTGZ}sj@yj+Ld~9%tKQp;` z@YS&Pz)JO3S?Nqg8B*xi;IQ1Wtzg0`lG2SP$&#-*x9w9drxqK@%wJ?2XZsCLY?c!MNx0qT=xT7AfChJp_ zV;b{}A$C_(qGnMaFH|P82zi!otD6G1f%C1$_Tx_P;xsaeT#vZcrCIL8v1kWK>mpvo z<{G&_!NVP%iX&J{YgG(M_hcheZEr;4ST?!;IQ<|}pT_c2rEm_S7an?^R>u5>L4E0= z%g&qJP`pg2|L$%#o>AE)X4}pZ89^r#=4V>vY0Kuik%202+Qc7qKBspQNi;>p#=t}I zbA&x*p7<^oEA@UNXlj$@31>N3}7`JEpW1SmD!@F6N_}MXnm2xZU=# zcbi#Vr0+1*X3nY^j{7Ez(G`~bSnw%RF2<>A=ApI6IF@oK2w^!XultP1huigh36u=ImuvsZlswgH9803tW@8k5FVpwRvVn5VW5t_&{Wsyx{9?6A3XO9>Q&w49dP@etKlEG_`l zE0!ye*Sbm(f}3hl1W?l}b09B`$_#`pPKfT<`PfJtvN{xV)p)!}uqiw6sZZfDMGzL0 z5vA)-b7%dT2a_BsOtWgi$*A^CukXonaAtV058jDwDU@|!o30y}5kOy+f2~N;+{T-{ z#cY%>9qW@C1?4>%s_ce4&IC&xuMLzxX?W0U{vYR?`kmHih$Pk@0veY4r|+lRqkvEduiXa1!oAV^_C66r>#lS>Ks}2h5!vzWJ5yS z*Pf$kE>g*gyTAvmu~Qf0+oOw%?i|gzA_(jI-i)3?get?O)sWouqh^20#ta%q6pDJE zn|pn8z-VuDFh?}8ML5tP0_s!wkdxHZz6oz&Na%(-BmeR=T^;|i4Nw0XmVRIViHQI& zc`%P{D!zP%Q$DsgMX~&_ju1Q(A@uTx4=*f$n=_`8vEWpYDl~X|9+5Vy*hW`ri0t{f z>Bo~4U?ett7fD*0R6t_SpYY-Gp_~5OwF4SGqBx?*XVYEST`t3@jU!U1bF*q)f4eW6`QX*IUZ7*M-JP;-of04?v(h^-%c`E48m-J{M^#fnP#2Win`;c z5}UIY$E@RaD-s7{4zK#Y7)i=p66*_jJmlW{k!RZ9As~&MrJgwA`w`wQ)ZYM1b;B=* b!v+YMVm5W$SsAZf`zOF{k2|@X6p->iCt30X literal 0 HcmV?d00001 diff --git a/v1/frontend/src/assets/index.js b/v1/frontend/src/assets/index.js index f4e317b..cc81523 100644 --- a/v1/frontend/src/assets/index.js +++ b/v1/frontend/src/assets/index.js @@ -1,4 +1,5 @@ import chess_rook from './icons/Font-Awesome/chess-rook.png'; +import chevron_down from './icons/twbs/chevron-down.png'; import chevron_left from './icons/twbs/chevron-left.png'; import chevron_right from './icons/twbs/chevron-right.png'; import circle_green_background from './icons/twbs/circle-green-background.png'; @@ -25,6 +26,7 @@ import logo from './logo/logo.png'; export const ChessRook = chess_rook; export const ChevronLeft = chevron_left; +export const ChevronDown = chevron_down; export const ChevronRight = chevron_right; export const CircleGreenBackground = circle_green_background; export const CircleRedBackground = circle_red_background; diff --git a/v1/frontend/src/components/ChatBot.css b/v1/frontend/src/components/ChatBot.css index 1fab05b..4c77b04 100644 --- a/v1/frontend/src/components/ChatBot.css +++ b/v1/frontend/src/components/ChatBot.css @@ -184,6 +184,100 @@ width: 100%; } +.chatbot-modal-event-body { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: space-between; + width: 100%; +} + +.chatbot-modal-event-body-bottom { + align-items: center; + display: flex; + flex-direction: column; + height: 50%; + justify-content: space-between; + width: 100%; +} + +.chatbot-modal-event-body-top { + align-items: center; + display: flex; + flex-direction: column; + height: 50%; + justify-content: space-evenly; + width: 100%; +} + +.chatbot-modal-event-setting { + align-items: center; + box-sizing: border-box; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + +.chatbot-modal-event-options { + align-items: center; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: space-evenly; + height: 158.5px; + width: 100%; +} + +.chatbot-modal-event-options-follow { + align-items: center; + display: flex; + flex-direction: column; + justify-content: space-evenly; + height: 100%; + width: 100%; +} + +.chatbot-modal-event-options-label { + align-items: center; + color: #eee; + display: flex; + font-family: sans-serif; + font-size: 16px; + font-weight: bold; +} + +.chatbot-modal-event-options-label-warning { + align-items: center; + color: #f23160; + display: flex; + font-family: sans-serif; + font-size: 16px; + font-weight: bold; +} + +.chatbot-modal-option-label { + align-items: center; + color: #eee; + display: flex; + font-family: sans-serif; + font-size: 16px; + font-weight: bold; + height: 32px; +} + +.chatbot-modal-option-label-warning { + align-items: center; + color: #f23160; + display: flex; + font-family: sans-serif; + font-size: 16px; + font-style: italic; + font-weight: bold; + height: 32px; +} + .chatbot-modal-pages { background-color: white; /* border: 1px solid #D6E0EA; */ @@ -248,6 +342,13 @@ font-size: 16px; } +.chatbot-modal-setting-description-warning { + color: #f23160; + font-family: sans-serif; + font-size: 16px; + font-style: italic; +} + .chatbot-modal-textarea { border: none; border-radius: 5px; @@ -461,6 +562,10 @@ input:checked + .chatbot-modal-toggle-slider:before { padding-right: 1px; } +.dropdown-option-hide { + display: none; +} + .timer-input { border: none; border-radius: 34px; diff --git a/v1/frontend/src/components/ChatBot.jsx b/v1/frontend/src/components/ChatBot.jsx index 82f0b13..d79b266 100644 --- a/v1/frontend/src/components/ChatBot.jsx +++ b/v1/frontend/src/components/ChatBot.jsx @@ -30,6 +30,7 @@ import { StopBigRed, } from '../assets'; import './ChatBot.css'; +import { DropDown } from './DropDown'; function ChatBot(props) { const [chatbots, setChatbots] = useState([]); @@ -326,12 +327,52 @@ function ChatbotRule(props) { return hours + 'h ' + minutes + 'm ' + seconds + 's'; }; + const printTriggerEvent = () => { + const onEvent = props.rule.parameters.trigger.on_event; + switch (true) { + case onEvent.from_account !== undefined && onEvent.from_account !== null: + const fromAccount = props.rule.parameters.trigger.on_event.from_account; + switch (true) { + case fromAccount.on_follow !== undefined && fromAccount.on_follow !== null: + return 'Follow'; + default: + return ''; + } + break; + case onEvent.from_channel !== undefined && onEvent.from_channel !== null: + const fromChannel = props.rule.parameters.trigger.on_event.from_channel; + switch (true) { + case fromChannel.on_follow !== undefined && fromChannel.on_follow !== null: + return 'Follow'; + default: + return ''; + } + break; + case onEvent.from_live_stream !== undefined && onEvent.from_live_stream !== null: + const fromLiveStream = props.rule.parameters.trigger.on_event.from_live_stream; + switch (true) { + case fromLiveStream.on_raid !== undefined && fromLiveStream.on_raid !== null: + return 'Raid'; + case fromLiveStream.on_rant !== undefined && fromLiveStream.on_rant !== null: + return 'Rant'; + case fromLiveStream.on_sub !== undefined && fromLiveStream.on_sub !== null: + return 'Sub'; + default: + return ''; + } + default: + return ''; + } + }; + const printTrigger = () => { let trigger = props.rule.parameters.trigger; switch (true) { case trigger.on_command !== undefined && trigger.on_command !== null: return trigger.on_command.command; + case trigger.on_event !== undefined && trigger.on_event !== null: + return printTriggerEvent(); case trigger.on_timer !== undefined && trigger.on_timer !== null: return prettyTimer(props.rule.parameters.trigger.on_timer); } @@ -372,6 +413,8 @@ function ChatbotRule(props) { switch (true) { case trigger.on_command !== undefined && trigger.on_command !== null: return 'on_command'; + case trigger.on_event !== undefined && trigger.on_event !== null: + return 'on_event'; case trigger.on_timer !== undefined && trigger.on_timer !== null: return 'on_timer'; } @@ -600,6 +643,7 @@ function ModalRule(props) { }); }; + console.log('back:', back); return ( <> {error !== '' && ( @@ -613,6 +657,49 @@ function ModalRule(props) { onSubmit={() => setError('')} /> )} + {stage === 'event-from_stream' && ( + + )} + {stage === 'message' && ( + + )} + {stage === 'review' && ( + + )} + {stage === 'sender' && ( + + )} {stage === 'trigger' && ( )} + {stage === 'trigger-on_event' && ( + + )} {stage === 'trigger-on_timer' && ( )} - {stage === 'message' && ( - - )} - {stage === 'sender' && ( - - )} - {stage === 'review' && ( - - )} ); } @@ -707,6 +771,20 @@ function ModalRuleTrigger(props) { next('trigger-on_command'); }; + const triggerOnEvent = () => { + const rule = props.rule; + if (rule.trigger == undefined || rule.trigger == null) { + rule.trigger = {}; + } + + rule.trigger.on_command = null; + rule.trigger.on_timer = null; + + props.setRule(rule); + + next('trigger-on_event'); + }; + const triggerOnTimer = () => { const rule = props.rule; if (rule.trigger == undefined || rule.trigger == null) { @@ -739,18 +817,15 @@ function ModalRuleTrigger(props) { src={ChevronRight} /> - {/* */} + + + ))} + + + ); +} + +function EventOptionsRant(props) { + const [minAmount, setMinAmount] = useState( + isNaN(props.options.min_amount) ? 0 : props.options.min_amount + ); + const updateMinAmount = (event) => { + let amount = parseInt(event.target.value); + if (isNaN(amount)) { + amount = 0; + } + + if (maxAmount !== 0 && amount > maxAmount) { + setValidMaxAmount(false); + } else { + setValidMaxAmount(true); + } + + setMinAmount(event.target.value); + props.setOptions({ min_amount: amount, max_amount: maxAmount }); + }; + const [maxAmount, setMaxAmount] = useState( + isNaN(props.options.max_amount) ? 0 : props.options.max_amount + ); + const updateMaxAmount = (event) => { + let amount = parseInt(event.target.value); + if (isNaN(amount)) { + amount = 0; + } + + if (amount !== 0) { + if (amount < minAmount) { + setValidMaxAmount(false); + } else { + setValidMaxAmount(true); + } + } else { + setValidMaxAmount(true); + } + + setMaxAmount(amount); + props.setOptions({ min_amount: minAmount, max_amount: amount }); + }; + const [validMaxAmount, setValidMaxAmount] = useState(true); + + return ( + <> +
+ +
+ $ + +
+
+
+ +
+ $ + +
+
+ + ); +} + function ModalRuleTriggerTimer(props) { const prependZero = (value) => { if (value < 10) { diff --git a/v1/frontend/src/components/DropDown.css b/v1/frontend/src/components/DropDown.css new file mode 100644 index 0000000..7b4e797 --- /dev/null +++ b/v1/frontend/src/components/DropDown.css @@ -0,0 +1,80 @@ +.dropdown { + width: 100%; +} + +.dropdown-menu { + align-items: center; + background-color: white; + border: 1px solid #061726; + border-radius: 5px; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + padding: 5px; + position: fixed; + z-index: 10; +} + +.dropdown-menu-container { + width: 100%; +} + +.dropdown-menu-background { + align-items: center; + display: flex; + height: 100vh; + justify-content: center; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100vw; + z-index: 8; +} + +.dropdown-menu-option { + background-color: white; + border: none; + border-radius: 5px; + box-sizing: border-box; + color: #061726; + font-family: sans-serif; + font-size: 16px; + font-weight: bold; + padding: 5px; + width: 100%; +} + +.dropdown-menu-option-selected { + background-color: #77b23b; +} + +.dropdown-menu-option:hover { + background-color: #77b23b; + cursor: pointer; +} + +.dropdown-toggle { + align-items: center; + background-color: white; + border: 1px solid #061726; + border-radius: 5px; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 5px; + width: 100%; +} + +.dropdown-toggle-text { + color: #061726; + font-family: sans-serif; + font-size: 16px; + font-weight: bold; +} + +.dropdown-toggle-icon { + height: 20px; + width: 20px; +} \ No newline at end of file diff --git a/v1/frontend/src/components/DropDown.jsx b/v1/frontend/src/components/DropDown.jsx new file mode 100644 index 0000000..41a792a --- /dev/null +++ b/v1/frontend/src/components/DropDown.jsx @@ -0,0 +1,97 @@ +import { useEffect, useRef, useState } from 'react'; + +import { ChevronDown } from '../assets'; +import './DropDown.css'; + +export function DropDown(props) { + const [options, setOptions] = useState(props.options !== undefined ? props.options : []); + const [selected, setSelected] = useState(props.selected !== undefined ? props.selected : ''); + const [toggled, setToggled] = useState(false); + const toggle = () => { + setToggled(!toggled); + }; + + useEffect(() => { + setSelected(props.selected !== undefined ? props.selected : ''); + }, [props.selected]); + + useEffect(() => { + setOptions(props.options !== undefined ? props.options : []); + }, [props.options]); + + const select = (option) => { + props.select(option); + setSelected(option); + toggle(); + }; + + return ( +
+ + {toggled && ( + + )} +
+ ); +} + +function DropDownMenu(props) { + const menuRef = useRef(); + const { width } = menuWidth(menuRef); + + return ( +
+ {width !== undefined && ( +
+ {props.options.map((option, index) => ( + + ))} +
+ )} +
+
+ ); +} + +export const menuWidth = (menuRef) => { + const [width, setWidth] = useState(0); + + useEffect(() => { + const getWidth = () => ({ width: menuRef.current.offsetWidth }); + + const handleResize = () => { + setWidth(getWidth()); + }; + + if (menuRef.current) { + setWidth(getWidth()); + } + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [menuRef]); + + return width; +}; diff --git a/v1/frontend/src/components/PageSideBar.jsx b/v1/frontend/src/components/PageSideBar.jsx index 48ceb23..023fd07 100644 --- a/v1/frontend/src/components/PageSideBar.jsx +++ b/v1/frontend/src/components/PageSideBar.jsx @@ -190,6 +190,10 @@ function AccountIcon(props) { setUsername(props.account.username); }, [props.account.username]); + useEffect(() => { + setLoggedIn(props.account.cookies !== null); + }, [props.account.cookies]); + useEffect(() => { if (username !== '') { PageStatus(pageName(username)); diff --git a/v1/go.mod b/v1/go.mod index fee5275..acaea6b 100644 --- a/v1/go.mod +++ b/v1/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.0 require ( github.com/mattn/go-sqlite3 v1.14.22 - github.com/tylertravisty/rumble-livestream-lib-go v0.7.2 + github.com/tylertravisty/rumble-livestream-lib-go v0.9.0 github.com/wailsapp/wails/v2 v2.8.1 ) diff --git a/v1/go.sum b/v1/go.sum index e0f7763..992b3f7 100644 --- a/v1/go.sum +++ b/v1/go.sum @@ -60,8 +60,8 @@ github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQ github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g= github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk= -github.com/tylertravisty/rumble-livestream-lib-go v0.7.2 h1:TRGTKhxB+uK0gnIC+rXbRxfFjMJxPHhjZzbsjDSpK+o= -github.com/tylertravisty/rumble-livestream-lib-go v0.7.2/go.mod h1:Odkqvsn+2eoWV3ePcj257Ga0bdOqV4JBTfOJcQ+Sqf8= +github.com/tylertravisty/rumble-livestream-lib-go v0.9.0 h1:G1b/uac43dq7BG7NzcLeRLPOfOu8GyjViE9s48qhwhw= +github.com/tylertravisty/rumble-livestream-lib-go v0.9.0/go.mod h1:Odkqvsn+2eoWV3ePcj257Ga0bdOqV4JBTfOJcQ+Sqf8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= diff --git a/v1/internal/chatbot/chatbot.go b/v1/internal/chatbot/chatbot.go index 073e563..c80551a 100644 --- a/v1/internal/chatbot/chatbot.go +++ b/v1/internal/chatbot/chatbot.go @@ -43,12 +43,32 @@ func (c clients) byUsernameLivestream(username string, url string) *rumblelivest return user.byLivestream(url) } +type followReceiver struct { + apiCh chan events.ApiFollower + latest time.Time +} + type receiver struct { onCommand map[string]map[int64]chan events.Chat onCommandMu sync.Mutex - //onFollow []chan ??? - //onRant []chan events.Chat - //onSubscribe []chan events.Chat + onFollow map[int64]*followReceiver + onFollowMu sync.Mutex + onRaid map[int64]chan events.Chat + onRaidMu sync.Mutex + onRant map[int64]chan events.Chat + onRantMu sync.Mutex + onSub map[int64]chan events.Chat + onSubMu sync.Mutex +} + +func newReceiver() *receiver { + return &receiver{ + onCommand: map[string]map[int64]chan events.Chat{}, + onFollow: map[int64]*followReceiver{}, + onRaid: map[int64]chan events.Chat{}, + onRant: map[int64]chan events.Chat{}, + onSub: map[int64]chan events.Chat{}, + } } type Bot struct { @@ -107,6 +127,10 @@ func (cb *Chatbot) addClient(username string, livestreamUrl string) (*rumblelive return nil, fmt.Errorf("error querying account by username: %v", err) } + if account.Cookies == nil { + return nil, fmt.Errorf("account cookies are nil") + } + var cookies []*http.Cookie err = json.Unmarshal([]byte(*account.Cookies), &cookies) if err != nil { @@ -154,10 +178,17 @@ func (cb *Chatbot) Run(rule *Rule, url string) error { } } + page := "" + rulePage := rule.Page() + if rulePage != nil { + page = rulePage.Prefix + strings.ReplaceAll(rulePage.Name, " ", "") + } + ctx, cancel := context.WithCancel(context.Background()) runner := &Runner{ cancel: cancel, client: client, + page: page, rule: *rule, wails: cb.wails, } @@ -187,13 +218,18 @@ func (cb *Chatbot) initRunner(runner *Runner) error { runner.channelIDMu.Unlock() switch { - case runner.rule.Parameters.Trigger.OnTimer != nil: - runner.run = runner.runOnTimer case runner.rule.Parameters.Trigger.OnCommand != nil: err = cb.initRunnerCommand(runner) if err != nil { return fmt.Errorf("error initializing command: %v", err) } + case runner.rule.Parameters.Trigger.OnEvent != nil: + err = cb.initRunnerEvent(runner) + if err != nil { + return fmt.Errorf("error initializing event: %v", err) + } + case runner.rule.Parameters.Trigger.OnTimer != nil: + runner.run = runner.runOnTimer } // cb.runnersMu.Lock() @@ -233,12 +269,12 @@ func (cb *Chatbot) initRunnerCommand(runner *Runner) error { defer cb.receiversMu.Unlock() rcvr, exists := cb.receivers[runner.client.LiveStreamUrl] if !exists { - rcvr = &receiver{ - onCommand: map[string]map[int64]chan events.Chat{}, - } + rcvr = newReceiver() cb.receivers[runner.client.LiveStreamUrl] = rcvr } + rcvr.onCommandMu.Lock() + defer rcvr.onCommandMu.Unlock() chans, exists := rcvr.onCommand[cmd] if !exists { chans = map[int64]chan events.Chat{} @@ -249,6 +285,164 @@ func (cb *Chatbot) initRunnerCommand(runner *Runner) error { return nil } +func (cb *Chatbot) initRunnerEvent(runner *Runner) error { + event := runner.rule.Parameters.Trigger.OnEvent + switch { + case event.FromAccount != nil: + return cb.initRunnerEventFromAccount(runner) + case event.FromChannel != nil: + return cb.initRunnerEventFromChannel(runner) + case event.FromLiveStream != nil: + return cb.initRunnerEventFromLiveStream(runner) + } + + return fmt.Errorf("runner event not supported") +} + +func (cb *Chatbot) initRunnerEventFromAccount(runner *Runner) error { + fromAccount := runner.rule.Parameters.Trigger.OnEvent.FromAccount + switch { + case fromAccount.OnFollow != nil: + return cb.initRunnerEventFromAccountOnFollow(runner) + } + + return fmt.Errorf("runner event not supported") +} + +func (cb *Chatbot) initRunnerEventFromAccountOnFollow(runner *Runner) error { + runner.run = runner.runOnEventFromAccountOnFollow + + apiCh := make(chan events.ApiFollower, 10) + runner.apiCh = apiCh + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + rcvr, exists := cb.receivers[runner.page] + if !exists { + rcvr = newReceiver() + cb.receivers[runner.page] = rcvr + } + + // TODO: should I check if channel already exists, if so delete it? + rcvr.onFollowMu.Lock() + defer rcvr.onFollowMu.Unlock() + rcvr.onFollow[*runner.rule.ID] = &followReceiver{apiCh, time.Now()} + + return nil +} + +func (cb *Chatbot) initRunnerEventFromChannel(runner *Runner) error { + fromChannel := runner.rule.Parameters.Trigger.OnEvent.FromChannel + switch { + case fromChannel.OnFollow != nil: + return cb.initRunnerEventFromChannelOnFollow(runner) + } + + return fmt.Errorf("runner event not supported") +} + +func (cb *Chatbot) initRunnerEventFromChannelOnFollow(runner *Runner) error { + runner.run = runner.runOnEventFromChannelOnFollow + + apiCh := make(chan events.ApiFollower, 10) + runner.apiCh = apiCh + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + rcvr, exists := cb.receivers[runner.page] + if !exists { + rcvr = newReceiver() + cb.receivers[runner.page] = rcvr + } + + // TODO: should I check if channel already exists, if so delete it? + rcvr.onFollowMu.Lock() + defer rcvr.onFollowMu.Unlock() + rcvr.onFollow[*runner.rule.ID] = &followReceiver{apiCh, time.Now()} + + return nil +} + +func (cb *Chatbot) initRunnerEventFromLiveStream(runner *Runner) error { + fromLiveStream := runner.rule.Parameters.Trigger.OnEvent.FromLiveStream + switch { + case fromLiveStream.OnRaid != nil: + return cb.initRunnerEventFromLiveStreamOnRaid(runner) + case fromLiveStream.OnRant != nil: + return cb.initRunnerEventFromLiveStreamOnRant(runner) + case fromLiveStream.OnSub != nil: + return cb.initRunnerEventFromLiveStreamOnSub(runner) + } + + return fmt.Errorf("runner event not supported") +} + +func (cb *Chatbot) initRunnerEventFromLiveStreamOnRaid(runner *Runner) error { + runner.run = runner.runOnEventFromLiveStreamOnRaid + + chatCh := make(chan events.Chat, 10) + runner.chatCh = chatCh + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + rcvr, exists := cb.receivers[runner.client.LiveStreamUrl] + if !exists { + rcvr = newReceiver() + cb.receivers[runner.client.LiveStreamUrl] = rcvr + } + + // TODO: should I check if channel already exists, if so delete it? + rcvr.onRaidMu.Lock() + defer rcvr.onRaidMu.Unlock() + rcvr.onRaid[*runner.rule.ID] = chatCh + + return nil +} + +func (cb *Chatbot) initRunnerEventFromLiveStreamOnRant(runner *Runner) error { + runner.run = runner.runOnEventFromLiveStreamOnRant + + chatCh := make(chan events.Chat, 10) + runner.chatCh = chatCh + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + rcvr, exists := cb.receivers[runner.client.LiveStreamUrl] + if !exists { + rcvr = newReceiver() + cb.receivers[runner.client.LiveStreamUrl] = rcvr + } + + // TODO: should I check if channel already exists, if so delete it? + rcvr.onRantMu.Lock() + defer rcvr.onRantMu.Unlock() + rcvr.onRant[*runner.rule.ID] = chatCh + + return nil +} + +func (cb *Chatbot) initRunnerEventFromLiveStreamOnSub(runner *Runner) error { + runner.run = runner.runOnEventFromLiveStreamOnSub + + chatCh := make(chan events.Chat, 10) + runner.chatCh = chatCh + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + rcvr, exists := cb.receivers[runner.client.LiveStreamUrl] + if !exists { + rcvr = newReceiver() + cb.receivers[runner.client.LiveStreamUrl] = rcvr + } + + // TODO: should I check if channel already exists, if so delete it? + rcvr.onSubMu.Lock() + defer rcvr.onSubMu.Unlock() + rcvr.onSub[*runner.rule.ID] = chatCh + + return nil +} + func (cb *Chatbot) run(ctx context.Context, runner *Runner) { if runner == nil || runner.rule.ID == nil || runner.run == nil { cb.logError.Println("invalid runner") @@ -312,12 +506,6 @@ func (cb *Chatbot) stop(rule *Rule) error { } func (cb *Chatbot) stopRunner(chatbotID int64, ruleID int64) bool { - // cb.runnersMu.Lock() - // defer cb.runnersMu.Unlock() - // runner, exists := cb.runners[id] - // if !exists { - // return - // } cb.botsMu.Lock() defer cb.botsMu.Unlock() bot, exists := cb.bots[chatbotID] @@ -334,7 +522,6 @@ func (cb *Chatbot) stopRunner(chatbotID int64, ruleID int64) bool { stopped := true runner.stop() - // delete(cb.runners, id) delete(bot.runners, ruleID) switch { @@ -343,6 +530,11 @@ func (cb *Chatbot) stopRunner(chatbotID int64, ruleID int64) bool { if err != nil { cb.logError.Println("error closing runner command:", err) } + case runner.rule.Parameters.Trigger.OnEvent != nil: + err := cb.closeRunnerEvent(runner) + if err != nil { + cb.logError.Println("error closing runner event:", err) + } } return stopped @@ -361,6 +553,9 @@ func (cb *Chatbot) closeRunnerCommand(runner *Runner) error { return fmt.Errorf("receiver for runner does not exist") } + rcvr.onCommandMu.Lock() + defer rcvr.onCommandMu.Unlock() + cmd := runner.rule.Parameters.Trigger.OnCommand.Command chans, exists := rcvr.onCommand[cmd] if !exists { @@ -378,8 +573,193 @@ func (cb *Chatbot) closeRunnerCommand(runner *Runner) error { return nil } -func (cb *Chatbot) HandleChat(event events.Chat) { +func (cb *Chatbot) closeRunnerEvent(runner *Runner) error { + if runner == nil || runner.rule.ID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil || runner.rule.Parameters.Trigger.OnEvent == nil { + return fmt.Errorf("invalid runner event") + } + switch { + case runner.rule.Parameters.Trigger.OnEvent.FromAccount != nil: + return cb.closeRunnerEventFromAccount(runner) + case runner.rule.Parameters.Trigger.OnEvent.FromChannel != nil: + return cb.closeRunnerEventFromChannel(runner) + case runner.rule.Parameters.Trigger.OnEvent.FromLiveStream != nil: + return cb.closeRunnerEventFromLiveStream(runner) + } + + return fmt.Errorf("runner event not supported") +} + +func (cb *Chatbot) closeRunnerEventFromAccount(runner *Runner) error { + if runner == nil || runner.rule.ID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil || runner.rule.Parameters.Trigger.OnEvent == nil || runner.rule.Parameters.Trigger.OnEvent.FromAccount == nil { + return fmt.Errorf("invalid runner event") + } + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + + rcvr, exists := cb.receivers[runner.page] + if !exists { + return fmt.Errorf("receiver for runner does not exist") + } + + fromAccount := runner.rule.Parameters.Trigger.OnEvent.FromAccount + switch { + case fromAccount.OnFollow != nil: + rcvr.onFollowMu.Lock() + defer rcvr.onFollowMu.Unlock() + followR, exists := rcvr.onFollow[*runner.rule.ID] + if !exists { + return fmt.Errorf("channel for runner does not exist") + } + close(followR.apiCh) + delete(rcvr.onFollow, *runner.rule.ID) + } + + return nil +} + +func (cb *Chatbot) closeRunnerEventFromChannel(runner *Runner) error { + if runner == nil || runner.rule.ID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil || runner.rule.Parameters.Trigger.OnEvent == nil || runner.rule.Parameters.Trigger.OnEvent.FromChannel == nil { + return fmt.Errorf("invalid runner event") + } + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + + rcvr, exists := cb.receivers[runner.page] + if !exists { + return fmt.Errorf("receiver for runner does not exist") + } + + fromChannel := runner.rule.Parameters.Trigger.OnEvent.FromChannel + switch { + case fromChannel.OnFollow != nil: + rcvr.onFollowMu.Lock() + defer rcvr.onFollowMu.Unlock() + followR, exists := rcvr.onFollow[*runner.rule.ID] + if !exists { + return fmt.Errorf("channel for runner does not exist") + } + close(followR.apiCh) + delete(rcvr.onFollow, *runner.rule.ID) + } + + return nil +} + +func (cb *Chatbot) closeRunnerEventFromLiveStream(runner *Runner) error { + if runner == nil || runner.rule.ID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil || runner.rule.Parameters.Trigger.OnEvent == nil || runner.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil { + return fmt.Errorf("invalid runner event") + } + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + + rcvr, exists := cb.receivers[runner.client.LiveStreamUrl] + if !exists { + return fmt.Errorf("receiver for runner does not exist") + } + + fromLiveStream := runner.rule.Parameters.Trigger.OnEvent.FromLiveStream + switch { + case fromLiveStream.OnRaid != nil: + rcvr.onRaidMu.Lock() + defer rcvr.onRaidMu.Unlock() + ch, exists := rcvr.onRaid[*runner.rule.ID] + if !exists { + return fmt.Errorf("channel for runner does not exist") + } + close(ch) + delete(rcvr.onRaid, *runner.rule.ID) + case fromLiveStream.OnRant != nil: + rcvr.onRantMu.Lock() + defer rcvr.onRantMu.Unlock() + ch, exists := rcvr.onRant[*runner.rule.ID] + if !exists { + return fmt.Errorf("channel for runner does not exist") + } + close(ch) + delete(rcvr.onRant, *runner.rule.ID) + case fromLiveStream.OnSub != nil: + rcvr.onSubMu.Lock() + defer rcvr.onSubMu.Unlock() + ch, exists := rcvr.onSub[*runner.rule.ID] + if !exists { + return fmt.Errorf("channel for runner does not exist") + } + close(ch) + delete(rcvr.onSub, *runner.rule.ID) + } + + return nil +} + +func (cb *Chatbot) HandleApi(event events.Api) { + errs := cb.runApiFuncs( + event, + cb.handleApiFollow, + ) + + for _, err := range errs { + cb.logError.Println("chatbot: error handling api event:", err) + } +} + +type apiFunc func(api events.Api) error + +func (cb *Chatbot) runApiFuncs(api events.Api, fns ...apiFunc) []error { + // TODO: validate api response? + + errs := []error{} + for _, fn := range fns { + err := fn(api) + if err != nil { + errs = append(errs, err) + } + } + + return errs +} + +func (cb *Chatbot) handleApiFollow(api events.Api) error { + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + + rcvr, exists := cb.receivers[api.Name] + if !exists { + return nil + } + if rcvr == nil { + return fmt.Errorf("receiver is nil for API: %s", api.Name) + } + + rcvr.onFollowMu.Lock() + defer rcvr.onFollowMu.Unlock() + + for _, runner := range rcvr.onFollow { + latest := runner.latest + for _, follower := range api.Resp.Followers.RecentFollowers { + followedOn, err := time.Parse(time.RFC3339, follower.FollowedOn) + // TODO: fix this in the API, not in the code + followedOn = followedOn.Add(-4 * time.Hour) + if err != nil { + return fmt.Errorf("error parsing followed_on time: %v", err) + } + if followedOn.After(runner.latest) { + if followedOn.After(latest) { + latest = followedOn + } + runner.apiCh <- events.ApiFollower{Username: follower.Username} + } + } + runner.latest = latest + } + + return nil +} + +func (cb *Chatbot) HandleChat(event events.Chat) { switch event.Message.Type { case rumblelivestreamlib.ChatTypeMessages: cb.handleMessage(event) @@ -390,6 +770,9 @@ func (cb *Chatbot) handleMessage(event events.Chat) { errs := cb.runMessageFuncs( event, cb.handleMessageCommand, + cb.handleMessageEventRaid, + cb.handleMessageEventRant, + cb.handleMessageEventSub, ) for _, err := range errs { @@ -397,12 +780,12 @@ func (cb *Chatbot) handleMessage(event events.Chat) { } } -func (cb *Chatbot) runMessageFuncs(event events.Chat, fns ...messageFunc) []error { +func (cb *Chatbot) runMessageFuncs(chat events.Chat, fns ...messageFunc) []error { // TODO: validate message errs := []error{} for _, fn := range fns { - err := fn(event) + err := fn(chat) if err != nil { errs = append(errs, err) } @@ -411,25 +794,25 @@ func (cb *Chatbot) runMessageFuncs(event events.Chat, fns ...messageFunc) []erro return errs } -type messageFunc func(event events.Chat) error +type messageFunc func(chat events.Chat) error -func (cb *Chatbot) handleMessageCommand(event events.Chat) error { - if strings.Index(event.Message.Text, "!") != 0 { +func (cb *Chatbot) handleMessageCommand(chat events.Chat) error { + if strings.Index(chat.Message.Text, "!") != 0 { return nil } - words := strings.Split(event.Message.Text, " ") + words := strings.Split(chat.Message.Text, " ") cmd := words[0] cb.receiversMu.Lock() defer cb.receiversMu.Unlock() - receiver, exists := cb.receivers[event.Livestream] + receiver, exists := cb.receivers[chat.Livestream] if !exists { return nil } if receiver == nil { - return fmt.Errorf("receiver is nil for livestream: %s", event.Livestream) + return fmt.Errorf("receiver is nil for livestream: %s", chat.Livestream) } receiver.onCommandMu.Lock() @@ -440,7 +823,85 @@ func (cb *Chatbot) handleMessageCommand(event events.Chat) error { } for _, runner := range runners { - runner <- event + runner <- chat + } + + return nil +} + +func (cb *Chatbot) handleMessageEventRaid(chat events.Chat) error { + if !chat.Message.Raid { + return nil + } + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + + receiver, exists := cb.receivers[chat.Livestream] + if !exists { + return nil + } + if receiver == nil { + return fmt.Errorf("receiver is nil for livestream: %s", chat.Livestream) + } + + receiver.onRaidMu.Lock() + defer receiver.onRaidMu.Unlock() + + for _, runner := range receiver.onRaid { + runner <- chat + } + + return nil +} + +func (cb *Chatbot) handleMessageEventRant(chat events.Chat) error { + if chat.Message.Rant == 0 { + return nil + } + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + + receiver, exists := cb.receivers[chat.Livestream] + if !exists { + return nil + } + if receiver == nil { + return fmt.Errorf("receiver is nil for livestream: %s", chat.Livestream) + } + + receiver.onRantMu.Lock() + defer receiver.onRantMu.Unlock() + + for _, runner := range receiver.onRant { + runner <- chat + } + + return nil +} + +func (cb *Chatbot) handleMessageEventSub(chat events.Chat) error { + if !chat.Message.Sub { + return nil + } + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + + receiver, exists := cb.receivers[chat.Livestream] + if !exists { + return nil + } + if receiver == nil { + return fmt.Errorf("receiver is nil for livestream: %s", chat.Livestream) + } + + receiver.onSubMu.Lock() + defer receiver.onSubMu.Unlock() + + for _, runner := range receiver.onSub { + runner <- chat } return nil diff --git a/v1/internal/chatbot/rule.go b/v1/internal/chatbot/rule.go index b80928d..eeb4b3a 100644 --- a/v1/internal/chatbot/rule.go +++ b/v1/internal/chatbot/rule.go @@ -16,6 +16,11 @@ import ( "github.com/tylertravisty/rum-goggles/v1/internal/models" ) +const ( + PrefixAccount = "/user/" + PrefixChannel = "/c/" +) + func SortRules(rules []Rule) { slices.SortFunc(rules, func(a, b Rule) int { return cmp.Compare(strings.ToLower(a.Display), strings.ToLower(b.Display)) @@ -30,12 +35,33 @@ type Rule struct { Running bool `json:"running"` } +type Page struct { + Name string + Prefix string +} + +func (r *Rule) Page() *Page { + if r.Parameters != nil { + return r.Parameters.Page() + } + + return nil +} + type RuleParameters struct { Message *RuleMessage `json:"message"` SendAs *RuleSender `json:"send_as"` Trigger *RuleTrigger `json:"trigger"` } +func (rp *RuleParameters) Page() *Page { + if rp.Trigger != nil { + return rp.Trigger.Page() + } + + return nil +} + type RuleMessage struct { FromFile *RuleMessageFile `json:"from_file"` FromText string `json:"from_text"` @@ -131,6 +157,14 @@ type RuleTrigger struct { OnTimer *time.Duration `json:"on_timer"` } +func (rt *RuleTrigger) Page() *Page { + if rt.OnEvent != nil { + return rt.OnEvent.Page() + } + + return nil +} + type RuleTriggerCommand struct { Command string `json:"command"` Restrict *RuleTriggerCommandRestriction `json:"restrict"` @@ -154,12 +188,49 @@ type RuleTriggerCommandRestrictionBypass struct { } type RuleTriggerEvent struct { - OnFollow bool `json:"on_follow"` - OnSubscribe bool `json:"on_subscribe"` - OnRaid bool `json:"on_raid"` - OnRant int `json:"on_rant"` + FromAccount *RuleTriggerEventAccount `json:"from_account"` + FromChannel *RuleTriggerEventChannel `json:"from_channel"` + FromLiveStream *RuleTriggerEventLiveStream `json:"from_live_stream"` } +func (rte *RuleTriggerEvent) Page() *Page { + switch { + case rte.FromAccount != nil: + return &Page{rte.FromAccount.Name, PrefixAccount} + case rte.FromChannel != nil: + return &Page{rte.FromChannel.Name, PrefixChannel} + default: + return nil + } +} + +type RuleTriggerEventAccount struct { + Name string `json:"name"` + OnFollow *RuleTriggerEventAccountFollow `json:"on_follow"` +} + +type RuleTriggerEventAccountFollow struct{} + +type RuleTriggerEventChannel struct { + Name string `json:"name"` + OnFollow *RuleTriggerEventChannelFollow `json:"on_follow"` +} + +type RuleTriggerEventChannelFollow struct{} + +type RuleTriggerEventLiveStream struct { + OnRaid *RuleTriggerEventLiveStreamRaid `json:"on_raid"` + OnRant *RuleTriggerEventLiveStreamRant `json:"on_rant"` + OnSub *RuleTriggerEventLiveStreamSub `json:"on_sub"` +} + +type RuleTriggerEventLiveStreamRaid struct{} +type RuleTriggerEventLiveStreamRant struct { + MinAmount int `json:"min_amount"` + MaxAmount int `json:"max_amount"` +} +type RuleTriggerEventLiveStreamSub struct{} + func (rule *Rule) ToModelsChatbotRule() (*models.ChatbotRule, error) { modelsRule := &models.ChatbotRule{ ID: rule.ID, diff --git a/v1/internal/chatbot/runner.go b/v1/internal/chatbot/runner.go index 6611003..0c79d8d 100644 --- a/v1/internal/chatbot/runner.go +++ b/v1/internal/chatbot/runner.go @@ -14,13 +14,14 @@ import ( ) type Runner struct { - apiCh chan events.Api + apiCh chan events.ApiFollower cancel context.CancelFunc cancelMu sync.Mutex channelID *int channelIDMu sync.Mutex chatCh chan events.Chat client *rumblelivestreamlib.Client + page string rule Rule run runFunc wails context.Context @@ -61,29 +62,31 @@ func (r *Runner) chat(fields *chatFields) error { return nil } -func (r *Runner) init() error { - if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { - return fmt.Errorf("invalid rule") - } +// func (r *Runner) init() error { +// if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { +// return fmt.Errorf("invalid rule") +// } - channelID, err := r.rule.Parameters.SendAs.ChannelIDInt() - if err != nil { - return fmt.Errorf("error converting channel ID to int: %v", err) - } +// channelID, err := r.rule.Parameters.SendAs.ChannelIDInt() +// if err != nil { +// return fmt.Errorf("error converting channel ID to int: %v", err) +// } - r.channelIDMu.Lock() - r.channelID = channelID - r.channelIDMu.Unlock() +// r.channelIDMu.Lock() +// r.channelID = channelID +// r.channelIDMu.Unlock() - switch { - case r.rule.Parameters.Trigger.OnTimer != nil: - r.run = r.runOnTimer - case r.rule.Parameters.Trigger.OnCommand != nil: - r.run = r.runOnCommand - } +// switch { +// case r.rule.Parameters.Trigger.OnTimer != nil: +// r.run = r.runOnTimer +// case r.rule.Parameters.Trigger.OnEvent != nil: +// r.run = r.runOnEvent +// case r.rule.Parameters.Trigger.OnCommand != nil: +// r.run = r.runOnCommand +// } - return nil -} +// return nil +// } type runFunc func(ctx context.Context) error @@ -102,18 +105,18 @@ func (r *Runner) runOnCommand(ctx context.Context) error { select { case <-ctx.Done(): return nil - case event := <-r.chatCh: + case chat := <-r.chatCh: now := time.Now() if now.Sub(prev) < r.rule.Parameters.Trigger.OnCommand.Timeout*time.Second { break } - if block := r.blockCommand(event); block { - // if bypass := r.bypassCommand(event); !bypass {break} + if block := r.blockCommand(chat); block { + // if bypass := r.bypassCommand(chat); !bypass {break} break } - err := r.handleCommand(event) + err := r.handleCommand(chat) if err != nil { return fmt.Errorf("error handling command: %v", err) } @@ -122,18 +125,18 @@ func (r *Runner) runOnCommand(ctx context.Context) error { } } -func (r *Runner) blockCommand(event events.Chat) bool { +func (r *Runner) blockCommand(chat events.Chat) bool { if r.rule.Parameters.Trigger.OnCommand.Restrict == nil { return false } if r.rule.Parameters.Trigger.OnCommand.Restrict.ToFollower && - !event.Message.IsFollower { + !chat.Message.IsFollower { return true } subscriber := false - for _, badge := range event.Message.Badges { + for _, badge := range chat.Message.Badges { if badge == rumblelivestreamlib.ChatBadgeLocalsSupporter || badge == rumblelivestreamlib.ChatBadgeRecurringSubscription { subscriber = true } @@ -144,24 +147,238 @@ func (r *Runner) blockCommand(event events.Chat) bool { return true } - if event.Message.Rant < r.rule.Parameters.Trigger.OnCommand.Restrict.ToRant*100 { + if chat.Message.Rant < r.rule.Parameters.Trigger.OnCommand.Restrict.ToRant*100 { return true } return false } -func (r *Runner) handleCommand(event events.Chat) error { - displayName := event.Message.Username - if event.Message.ChannelName != "" { - displayName = event.Message.ChannelName +func (r *Runner) handleCommand(chat events.Chat) error { + displayName := chat.Message.Username + if chat.Message.ChannelName != "" { + displayName = chat.Message.ChannelName } fields := &chatFields{ - ChannelName: event.Message.ChannelName, + ChannelName: chat.Message.ChannelName, DisplayName: displayName, - Username: event.Message.Username, - Rant: event.Message.Rant / 100, + Username: chat.Message.Username, + Rant: chat.Message.Rant / 100, + } + + err := r.chat(fields) + if err != nil { + return fmt.Errorf("error sending chat: %v", err) + } + + return nil +} + +func (r *Runner) runOnEventFromAccountOnFollow(ctx context.Context) error { + if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { + return fmt.Errorf("invalid rule") + } + if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromAccount == nil || r.rule.Parameters.Trigger.OnEvent.FromAccount.OnFollow == nil { + return fmt.Errorf("event is nil") + } + + for { + runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true) + + select { + case <-ctx.Done(): + return nil + case api := <-r.apiCh: + err := r.handleEventOnFollow(api) + if err != nil { + return fmt.Errorf("error handling event: %v", err) + } + } + } +} + +func (r *Runner) runOnEventFromChannelOnFollow(ctx context.Context) error { + if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { + return fmt.Errorf("invalid rule") + } + if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromChannel == nil || r.rule.Parameters.Trigger.OnEvent.FromChannel.OnFollow == nil { + return fmt.Errorf("event is nil") + } + + for { + runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true) + + select { + case <-ctx.Done(): + return nil + case api := <-r.apiCh: + err := r.handleEventOnFollow(api) + if err != nil { + return fmt.Errorf("error handling event: %v", err) + } + } + } +} + +func (r *Runner) handleEventOnFollow(follower events.ApiFollower) error { + fields := &chatFields{ + DisplayName: follower.Username, + Username: follower.Username, + } + + err := r.chat(fields) + if err != nil { + return fmt.Errorf("error sending chat: %v", err) + } + + return nil +} + +func (r *Runner) runOnEventFromLiveStreamOnRaid(ctx context.Context) error { + if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { + return fmt.Errorf("invalid rule") + } + if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRaid == nil { + return fmt.Errorf("event is nil") + } + + for { + runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true) + + select { + case <-ctx.Done(): + return nil + case chat := <-r.chatCh: + err := r.handleEventFromLiveStreamOnRaid(chat) + if err != nil { + return fmt.Errorf("error handling event: %v", err) + } + } + } +} + +func (r *Runner) handleEventFromLiveStreamOnRaid(chat events.Chat) error { + if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil || r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRaid == nil { + return fmt.Errorf("invalid rule") + } + + displayName := chat.Message.Username + if chat.Message.ChannelName != "" { + displayName = chat.Message.ChannelName + } + + fields := &chatFields{ + ChannelName: chat.Message.ChannelName, + DisplayName: displayName, + Username: chat.Message.Username, + Rant: chat.Message.Rant / 100, + } + + err := r.chat(fields) + if err != nil { + return fmt.Errorf("error sending chat: %v", err) + } + + return nil +} + +func (r *Runner) runOnEventFromLiveStreamOnRant(ctx context.Context) error { + if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { + return fmt.Errorf("invalid rule") + } + if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRant == nil { + return fmt.Errorf("event is nil") + } + + for { + runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true) + + select { + case <-ctx.Done(): + return nil + case chat := <-r.chatCh: + err := r.handleEventFromLiveStreamOnRant(chat) + if err != nil { + return fmt.Errorf("error handling event: %v", err) + } + } + } +} + +func (r *Runner) handleEventFromLiveStreamOnRant(chat events.Chat) error { + if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil || r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRant == nil { + return fmt.Errorf("invalid rule") + } + + rant := chat.Message.Rant / 100 + minAmount := r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRant.MinAmount + maxAmount := r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRant.MaxAmount + if minAmount != 0 && rant < minAmount { + return nil + } + if maxAmount != 0 && rant > maxAmount { + return nil + } + + displayName := chat.Message.Username + if chat.Message.ChannelName != "" { + displayName = chat.Message.ChannelName + } + + fields := &chatFields{ + ChannelName: chat.Message.ChannelName, + DisplayName: displayName, + Username: chat.Message.Username, + Rant: chat.Message.Rant / 100, + } + + err := r.chat(fields) + if err != nil { + return fmt.Errorf("error sending chat: %v", err) + } + + return nil +} + +func (r *Runner) runOnEventFromLiveStreamOnSub(ctx context.Context) error { + if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { + return fmt.Errorf("invalid rule") + } + if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnSub == nil { + return fmt.Errorf("event is nil") + } + + for { + runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true) + + select { + case <-ctx.Done(): + return nil + case chat := <-r.chatCh: + err := r.handleEventFromLiveStreamOnSub(chat) + if err != nil { + return fmt.Errorf("error handling event: %v", err) + } + } + } +} + +func (r *Runner) handleEventFromLiveStreamOnSub(chat events.Chat) error { + if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil || r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnSub == nil { + return fmt.Errorf("invalid rule") + } + + displayName := chat.Message.Username + if chat.Message.ChannelName != "" { + displayName = chat.Message.ChannelName + } + + fields := &chatFields{ + ChannelName: chat.Message.ChannelName, + DisplayName: displayName, + Username: chat.Message.Username, + Rant: chat.Message.Rant / 100, } err := r.chat(fields) diff --git a/v1/internal/events/api.go b/v1/internal/events/api.go index 94702c7..1fbefa8 100644 --- a/v1/internal/events/api.go +++ b/v1/internal/events/api.go @@ -16,6 +16,10 @@ type Api struct { Stop bool } +type ApiFollower struct { + Username string +} + type apiProducer struct { cancel context.CancelFunc cancelMu sync.Mutex diff --git a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go index c839a48..b05e050 100644 --- a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go +++ b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go @@ -258,6 +258,15 @@ type ChatEventBlock struct { Type string `json:"type"` } +type ChatEventNotification struct { + Badge string `json:"badge"` + Text string `json:"text"` +} + +type ChatEventRaidNotification struct { + StartTs int64 `json:"start_ts"` +} + type ChatEventRant struct { Duration int `json:"duration"` ExpiresOn string `json:"expires_on"` @@ -265,13 +274,15 @@ type ChatEventRant struct { } type ChatEventMessage struct { - Blocks []ChatEventBlock `json:"blocks"` - ChannelID *int64 `json:"channel_id"` - ID string `json:"id"` - Rant *ChatEventRant `json:"rant"` - Text string `json:"text"` - Time string `json:"time"` - UserID string `json:"user_id"` + Blocks []ChatEventBlock `json:"blocks"` + ChannelID *int64 `json:"channel_id"` + ID string `json:"id"` + Notification *ChatEventNotification `json:"notification"` + RaidNotification *ChatEventRaidNotification `json:"raid_notification"` + Rant *ChatEventRant `json:"rant"` + Text string `json:"text"` + Time string `json:"time"` + UserID string `json:"user_id"` } type ChatEventUser struct { @@ -392,7 +403,9 @@ type ChatView struct { ImageUrl string Init bool IsFollower bool + Raid bool Rant int + Sub bool Text string Time time.Time Type string @@ -456,9 +469,17 @@ func parseMessages(eventType string, messages []ChatEventMessage, users map[stri view.Color = user.Color view.ImageUrl = user.Image1 view.IsFollower = user.IsFollower + if message.RaidNotification != nil { + view.Raid = true + } if message.Rant != nil { view.Rant = message.Rant.PriceCents } + if message.Notification != nil { + if message.Notification.Badge == ChatBadgeRecurringSubscription { + view.Sub = true + } + } view.Text = message.Text t, err := time.Parse(time.RFC3339, message.Time) if err != nil { diff --git a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/client.go b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/client.go index 71b25d3..c5652f2 100644 --- a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/client.go +++ b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/client.go @@ -273,31 +273,37 @@ func (c *Client) userLogout() error { return nil } +type LoggedInResponseData struct { + Username string `json:"username"` +} + type LoggedInResponseUser struct { - LoggedIn bool `json:"logged_in"` + ID string `json:"id"` + LoggedIn bool `json:"logged_in"` } type LoggedInResponse struct { + Data LoggedInResponseData `json:"data"` User LoggedInResponseUser `json:"user"` } -func (c *Client) LoggedIn() (bool, error) { +func (c *Client) LoggedIn() (*LoggedInResponse, error) { resp, err := c.httpClient.Get(urlUserLogin) if err != nil { - return false, pkgErr("error getting login service", err) + return nil, pkgErr("error getting login service", err) } defer resp.Body.Close() bodyB, err := io.ReadAll(resp.Body) if err != nil { - return false, pkgErr("error reading body bytes", err) + return nil, pkgErr("error reading body bytes", err) } var lir LoggedInResponse err = json.NewDecoder(strings.NewReader(string(bodyB))).Decode(&lir) if err != nil { - return false, pkgErr("error un-marshaling response body", err) + return nil, pkgErr("error un-marshaling response body", err) } - return lir.User.LoggedIn, nil + return &lir, nil } diff --git a/v1/vendor/modules.txt b/v1/vendor/modules.txt index f4f7fed..bf1a8e9 100644 --- a/v1/vendor/modules.txt +++ b/v1/vendor/modules.txt @@ -77,7 +77,7 @@ github.com/tkrajina/go-reflector/reflector # github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 ## explicit; go 1.16 github.com/tylertravisty/go-utils/random -# github.com/tylertravisty/rumble-livestream-lib-go v0.7.2 +# github.com/tylertravisty/rumble-livestream-lib-go v0.9.0 ## explicit; go 1.19 github.com/tylertravisty/rumble-livestream-lib-go # github.com/valyala/bytebufferpool v1.0.0