Implemented chatbot event triggers on follow, raid, rant, and subscribe

This commit is contained in:
tyler 2024-05-28 16:44:31 -04:00
parent 1e87346086
commit 562b90ebf7
17 changed files with 1791 additions and 124 deletions

View file

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"time" "time"
@ -190,7 +191,7 @@ func (a *App) processChat(event events.Chat) {
// TODO: implement this // TODO: implement this
func (a *App) chatbotApiProcessor(event events.Api) { func (a *App) chatbotApiProcessor(event events.Api) {
return a.chatbot.HandleApi(event)
} }
func (a *App) chatbotChatProcessor(event events.Chat) { func (a *App) chatbotChatProcessor(event events.Chat) {
@ -356,9 +357,9 @@ func (a *App) verifyAccounts() (int, error) {
} }
loggedIn, err := client.LoggedIn() loggedIn, err := client.LoggedIn()
if err != nil { 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 a.clients[*account.Username] = client
} else { } else {
account.Cookies = nil account.Cookies = nil
@ -493,7 +494,24 @@ func (a *App) Login(username string, password string) error {
return fmt.Errorf("Error logging in. Try again.") return fmt.Errorf("Error logging in. Try again.")
} }
if acct == nil { 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) id, err := a.services.AccountS.Create(acct)
if err != nil { if err != nil {
a.logError.Println("error creating account:", err) a.logError.Println("error creating account:", err)
@ -789,6 +807,27 @@ func (a *App) ActivateChannel(id int64) error {
return nil 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 inactivate, activate.
// If page is active, deactivate. // If page is active, deactivate.
func (a *App) activatePage(pi PageInfo) error { 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) a.logError.Println("error starting chat producer:", err)
// TODO: send error to UI that chatbot URL could not be started // TODO: send error to UI that chatbot URL could not be started
//runtime.EventsEmit("Ch") //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) err = a.chatbot.Run(rule, *mChatbot.Url)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,4 +1,5 @@
import chess_rook from './icons/Font-Awesome/chess-rook.png'; 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_left from './icons/twbs/chevron-left.png';
import chevron_right from './icons/twbs/chevron-right.png'; import chevron_right from './icons/twbs/chevron-right.png';
import circle_green_background from './icons/twbs/circle-green-background.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 ChessRook = chess_rook;
export const ChevronLeft = chevron_left; export const ChevronLeft = chevron_left;
export const ChevronDown = chevron_down;
export const ChevronRight = chevron_right; export const ChevronRight = chevron_right;
export const CircleGreenBackground = circle_green_background; export const CircleGreenBackground = circle_green_background;
export const CircleRedBackground = circle_red_background; export const CircleRedBackground = circle_red_background;

View file

@ -184,6 +184,100 @@
width: 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 { .chatbot-modal-pages {
background-color: white; background-color: white;
/* border: 1px solid #D6E0EA; */ /* border: 1px solid #D6E0EA; */
@ -248,6 +342,13 @@
font-size: 16px; font-size: 16px;
} }
.chatbot-modal-setting-description-warning {
color: #f23160;
font-family: sans-serif;
font-size: 16px;
font-style: italic;
}
.chatbot-modal-textarea { .chatbot-modal-textarea {
border: none; border: none;
border-radius: 5px; border-radius: 5px;
@ -461,6 +562,10 @@ input:checked + .chatbot-modal-toggle-slider:before {
padding-right: 1px; padding-right: 1px;
} }
.dropdown-option-hide {
display: none;
}
.timer-input { .timer-input {
border: none; border: none;
border-radius: 34px; border-radius: 34px;

View file

@ -30,6 +30,7 @@ import {
StopBigRed, StopBigRed,
} from '../assets'; } from '../assets';
import './ChatBot.css'; import './ChatBot.css';
import { DropDown } from './DropDown';
function ChatBot(props) { function ChatBot(props) {
const [chatbots, setChatbots] = useState([]); const [chatbots, setChatbots] = useState([]);
@ -326,12 +327,52 @@ function ChatbotRule(props) {
return hours + 'h ' + minutes + 'm ' + seconds + 's'; 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 = () => { const printTrigger = () => {
let trigger = props.rule.parameters.trigger; let trigger = props.rule.parameters.trigger;
switch (true) { switch (true) {
case trigger.on_command !== undefined && trigger.on_command !== null: case trigger.on_command !== undefined && trigger.on_command !== null:
return trigger.on_command.command; 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: case trigger.on_timer !== undefined && trigger.on_timer !== null:
return prettyTimer(props.rule.parameters.trigger.on_timer); return prettyTimer(props.rule.parameters.trigger.on_timer);
} }
@ -372,6 +413,8 @@ function ChatbotRule(props) {
switch (true) { switch (true) {
case trigger.on_command !== undefined && trigger.on_command !== null: case trigger.on_command !== undefined && trigger.on_command !== null:
return 'on_command'; return 'on_command';
case trigger.on_event !== undefined && trigger.on_event !== null:
return 'on_event';
case trigger.on_timer !== undefined && trigger.on_timer !== null: case trigger.on_timer !== undefined && trigger.on_timer !== null:
return 'on_timer'; return 'on_timer';
} }
@ -600,6 +643,7 @@ function ModalRule(props) {
}); });
}; };
console.log('back:', back);
return ( return (
<> <>
{error !== '' && ( {error !== '' && (
@ -613,6 +657,49 @@ function ModalRule(props) {
onSubmit={() => setError('')} onSubmit={() => setError('')}
/> />
)} )}
{stage === 'event-from_stream' && (
<ModalRuleEventStream
onBack={goBack}
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'message' && (
<ModalRuleMessage
onBack={goBack}
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'review' && (
<ModalRuleReview
edit={edit}
setEdit={setEdit}
new={props.new}
onBack={goBack}
onClose={props.onClose}
onDelete={props.onDelete}
onSubmit={submit}
rule={rule}
show={props.show}
/>
)}
{stage === 'sender' && (
<ModalRuleSender
onBack={goBack}
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'trigger' && ( {stage === 'trigger' && (
<ModalRuleTrigger <ModalRuleTrigger
onClose={props.onClose} onClose={props.onClose}
@ -632,6 +719,16 @@ function ModalRule(props) {
show={props.show} show={props.show}
/> />
)} )}
{stage === 'trigger-on_event' && (
<ModalRuleTriggerEvent
onBack={goBack}
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'trigger-on_timer' && ( {stage === 'trigger-on_timer' && (
<ModalRuleTriggerTimer <ModalRuleTriggerTimer
onBack={goBack} onBack={goBack}
@ -642,39 +739,6 @@ function ModalRule(props) {
show={props.show} show={props.show}
/> />
)} )}
{stage === 'message' && (
<ModalRuleMessage
onBack={goBack}
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'sender' && (
<ModalRuleSender
onBack={goBack}
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'review' && (
<ModalRuleReview
edit={edit}
setEdit={setEdit}
new={props.new}
onBack={goBack}
onClose={props.onClose}
onDelete={props.onDelete}
onSubmit={submit}
rule={rule}
show={props.show}
/>
)}
</> </>
); );
} }
@ -707,6 +771,20 @@ function ModalRuleTrigger(props) {
next('trigger-on_command'); 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 triggerOnTimer = () => {
const rule = props.rule; const rule = props.rule;
if (rule.trigger == undefined || rule.trigger == null) { if (rule.trigger == undefined || rule.trigger == null) {
@ -739,18 +817,15 @@ function ModalRuleTrigger(props) {
src={ChevronRight} src={ChevronRight}
/> />
</button> </button>
{/* <button <button className='modal-add-account-channel-button' onClick={triggerOnEvent}>
className='modal-add-account-channel-button'
onClick={() => next('trigger-stream_event')}
>
<div className='modal-add-account-channel-button-left'> <div className='modal-add-account-channel-button-left'>
<span>Stream Event</span> <span>Event</span>
</div> </div>
<img <img
className='modal-add-account-channel-button-right-icon' className='modal-add-account-channel-button-right-icon'
src={ChevronRight} src={ChevronRight}
/> />
</button> */} </button>
<button className='modal-add-account-channel-button' onClick={triggerOnTimer}> <button className='modal-add-account-channel-button' onClick={triggerOnTimer}>
<div className='modal-add-account-channel-button-left'> <div className='modal-add-account-channel-button-left'>
<span>Timer</span> <span>Timer</span>
@ -958,6 +1033,456 @@ function ModalRuleTriggerCommand(props) {
); );
} }
function ModalRuleTriggerEvent(props) {
const [event, setEvent] = useState('');
const [validEvent, setValidEvent] = useState(true);
const updateEvent = (e) => {
setEvent(e);
if (e !== event) {
setOptions({});
setValidOptions(true);
switch (e) {
case 'Rant':
setOptions({ min_amount: 0, max_amount: 0 });
break;
default:
setOptions({});
}
}
setValidEvent(true);
};
const [options, setOptions] = useState({});
const [validOptions, setValidOptions] = useState(true);
const [source, setSource] = useState('');
const [validSource, setValidSource] = useState(true);
const updateSource = (s) => {
setSource(s);
if (s !== source) {
setEvent('');
setValidOptions(true);
}
setValidSource(true);
};
const [parameters, setParameters] = useState({
Account: { events: ['Follow'] },
Channel: { events: ['Follow'] },
'Live Stream': { events: ['Raid', 'Rant', 'Sub'] },
});
useEffect(() => {
if (props.rule.trigger.on_event === undefined || props.rule.trigger.on_event === null) {
return;
}
const onEvent = props.rule.trigger.on_event;
switch (true) {
case onEvent.from_account !== undefined && onEvent.from_account !== null:
setSource('Account');
const fromAccount = props.rule.trigger.on_event.from_account;
switch (true) {
case fromAccount.on_follow !== undefined && fromAccount.on_follow !== null:
setEvent('Follow');
break;
}
break;
case onEvent.from_channel !== undefined && onEvent.from_channel !== null:
setSource('Channel');
const fromChannel = props.rule.trigger.on_event.from_channel;
switch (true) {
case fromChannel.on_follow !== undefined && fromChannel.on_follow !== null:
setEvent('Follow');
break;
}
break;
case onEvent.from_live_stream !== undefined && onEvent.from_live_stream !== null:
setSource('Live Stream');
const fromLiveStream = props.rule.trigger.on_event.from_live_stream;
switch (true) {
case fromLiveStream.on_raid !== undefined && fromLiveStream.on_raid !== null:
setEvent('Raid');
break;
case fromLiveStream.on_rant !== undefined && fromLiveStream.on_rant !== null:
setEvent('Rant');
setOptions(props.rule.trigger.on_event.from_live_stream.on_rant);
break;
case fromLiveStream.on_sub !== undefined && fromLiveStream.on_sub !== null:
setEvent('Sub');
break;
}
break;
default:
return;
}
}, []);
const validRantOptions = () => {
if (isNaN(options.min_amount) || isNaN(options.max_amount)) {
setValidOptions(false);
return false;
}
if (options.max_amount !== 0 && options.min_amount > options.max_amount) {
setValidOptions(false);
return false;
}
return true;
};
const fromAccount = () => {
let from_account = {};
switch (event) {
case 'Follow':
from_account.name = options.page;
from_account.on_follow = {};
break;
default:
setValidEvent(false);
return;
}
const rule = props.rule;
if (rule.trigger.on_event == undefined || rule.trigger.on_event == null) {
rule.trigger.on_event = {};
}
rule.trigger.on_event.from_account = from_account;
rule.trigger.on_event.from_channel = null;
rule.trigger.on_event.from_live_stream = null;
props.setRule(rule);
next('message');
};
const fromChannel = () => {
let from_channel = {};
switch (event) {
case 'Follow':
from_channel.name = options.page;
from_channel.on_follow = {};
break;
default:
setValidEvent(false);
return;
}
const rule = props.rule;
if (rule.trigger.on_event == undefined || rule.trigger.on_event == null) {
rule.trigger.on_event = {};
}
rule.trigger.on_event.from_account = null;
rule.trigger.on_event.from_channel = from_channel;
rule.trigger.on_event.from_live_stream = null;
props.setRule(rule);
next('message');
};
const fromLiveStream = () => {
let from_live_stream = {};
switch (event) {
case 'Raid':
from_live_stream.on_raid = {};
break;
case 'Rant':
if (!validRantOptions()) {
return;
}
from_live_stream.on_rant = options;
break;
case 'Sub':
from_live_stream.on_sub = {};
break;
default:
setValidEvent(false);
return;
}
const rule = props.rule;
if (rule.trigger.on_event == undefined || rule.trigger.on_event == null) {
rule.trigger.on_event = {};
}
rule.trigger.on_event.from_account = null;
rule.trigger.on_event.from_channel = null;
rule.trigger.on_event.from_live_stream = from_live_stream;
props.setRule(rule);
next('message');
};
const back = () => {
props.onBack();
};
const next = (stage) => {
props.setStage(stage);
};
const submit = () => {
switch (source) {
case 'Account':
fromAccount();
break;
case 'Channel':
fromChannel();
break;
case 'Live Stream':
fromLiveStream();
break;
default:
setValidSource(false);
}
};
return (
<Modal
cancelButton={'Back'}
onCancel={back}
onClose={props.onClose}
show={props.show}
submitButton={'Next'}
onSubmit={submit}
style={{ height: '480px', minHeight: '480px', width: '360px', minWidth: '360px' }}
>
<div className='modal-add-account-channel'>
<span className='modal-add-account-channel-title'>Configure Event</span>
<div className='chatbot-modal-event-body'>
<div className='chatbot-modal-event-body-top'>
<div className='chatbot-modal-event-setting'>
<label
className={
validSource
? 'chatbot-modal-option-label'
: 'chatbot-modal-option-label-warning'
}
>
Source{!validSource && '*'}
</label>
<div style={{ width: '250px' }}>
<DropDown
options={Object.keys(parameters)}
select={updateSource}
selected={source}
/>
</div>
</div>
<div className='chatbot-modal-event-setting'>
<label
className={
validEvent
? 'chatbot-modal-option-label'
: 'chatbot-modal-option-label-warning'
}
>
Event{!validEvent && '*'}
</label>
<div style={{ width: '250px' }}>
{source !== '' && (
<DropDown
options={parameters[source].events}
select={updateEvent}
selected={event}
/>
)}
</div>
</div>
</div>
<div className='chatbot-modal-event-body-bottom'>
<label
className={
validOptions
? 'chatbot-modal-event-options-label'
: 'chatbot-modal-event-options-label-warning'
}
>
{validOptions ? 'Options' : 'Verify Options'}
</label>
<div className='chatbot-modal-event-options'>
{event === 'Rant' && (
<EventOptionsRant options={options} setOptions={setOptions} />
)}
{event === 'Follow' && (
<EventOptionsFollow
options={options}
setOptions={setOptions}
source={source}
/>
)}
</div>
</div>
</div>
<div></div>
</div>
</Modal>
);
}
function EventOptionsFollow(props) {
const [accounts, setAccounts] = useState({});
const [page, setPage] = useState(props.options.page === undefined ? '' : props.options.page);
const updatePage = (name) => {
setPage(name);
props.setOptions({ page: name });
};
useEffect(() => {
AccountList()
.then((response) => {
setAccounts(response);
})
.catch((error) => {
setError(error);
});
}, []);
const sortChannels = (channels) => {
let sorted = [...channels].sort((a, b) =>
a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
);
return sorted;
};
const sortAccounts = () => {
let keys = Object.keys(accounts);
let sorted = [...keys].sort((a, b) =>
accounts[a].account.username.toLowerCase() > accounts[b].account.username.toLowerCase()
? 1
: -1
);
return sorted;
};
const sortPages = () => {
let pages = [];
const keys = sortAccounts();
keys.forEach((key, i) => {
const account = accounts[key];
if (props.source === 'Account') {
pages.push(account.account.username);
}
if (props.source === 'Channel') {
const channels = sortChannels(account.channels);
channels.forEach((channel, j) => {
pages.push(channel.name);
});
}
});
return pages;
};
return (
<div className='modal-add-account-channel-body' style={{ height: '90%' }}>
<div className='chatbot-modal-pages'>
{sortPages().map((option, index) => (
<div className={'chatbot-modal-page'} key={index}>
<button
className='chatbot-modal-page-button'
onClick={() => updatePage(option)}
style={{
backgroundColor: page === option ? '#85c742' : '',
}}
>
{option}
</button>
</div>
))}
</div>
</div>
);
}
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 (
<>
<div className='chatbot-modal-setting' style={{ paddingTop: '0px' }}>
<label className='chatbot-modal-setting-description'>Min rant amount</label>
<div>
<span className='command-rant-amount-symbol'>$</span>
<input
className='command-rant-amount'
onChange={updateMinAmount}
placeholder='0'
size='4'
type='text'
value={minAmount === 0 ? '' : minAmount}
/>
</div>
</div>
<div className='chatbot-modal-setting' style={{ paddingTop: '0px' }}>
<label
className={
validMaxAmount
? 'chatbot-modal-setting-description'
: 'chatbot-modal-setting-description-warning'
}
>
Max rant amount{!validMaxAmount && ' (>= min)'}
</label>
<div>
<span className='command-rant-amount-symbol'>$</span>
<input
className='command-rant-amount'
onChange={updateMaxAmount}
placeholder='0'
size='4'
type='text'
value={maxAmount === 0 ? '' : maxAmount}
/>
</div>
</div>
</>
);
}
function ModalRuleTriggerTimer(props) { function ModalRuleTriggerTimer(props) {
const prependZero = (value) => { const prependZero = (value) => {
if (value < 10) { if (value < 10) {

View file

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

View file

@ -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 (
<div className='dropdown'>
<button className='dropdown-toggle' onClick={toggle}>
<div style={{ width: '20px' }}></div>
<span className='dropdown-toggle-text'>{selected}</span>
<img className='dropdown-toggle-icon' src={ChevronDown} />
</button>
{toggled && (
<DropDownMenu
options={options}
select={select}
selected={selected}
toggle={toggle}
/>
)}
</div>
);
}
function DropDownMenu(props) {
const menuRef = useRef();
const { width } = menuWidth(menuRef);
return (
<div className='dropdown-menu-container' ref={menuRef}>
{width !== undefined && (
<div className='dropdown-menu' style={{ width: width + 'px' }}>
{props.options.map((option, index) => (
<button
className={
props.selected === option
? 'dropdown-menu-option dropdown-menu-option-selected'
: 'dropdown-menu-option'
}
key={index}
onClick={() => props.select(option)}
>
{option}
</button>
))}
</div>
)}
<div className='dropdown-menu-background' onClick={props.toggle}></div>
</div>
);
}
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;
};

View file

@ -190,6 +190,10 @@ function AccountIcon(props) {
setUsername(props.account.username); setUsername(props.account.username);
}, [props.account.username]); }, [props.account.username]);
useEffect(() => {
setLoggedIn(props.account.cookies !== null);
}, [props.account.cookies]);
useEffect(() => { useEffect(() => {
if (username !== '') { if (username !== '') {
PageStatus(pageName(username)); PageStatus(pageName(username));

View file

@ -6,7 +6,7 @@ toolchain go1.22.0
require ( require (
github.com/mattn/go-sqlite3 v1.14.22 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 github.com/wailsapp/wails/v2 v2.8.1
) )

View file

@ -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/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 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g=
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk= 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.9.0 h1:G1b/uac43dq7BG7NzcLeRLPOfOu8GyjViE9s48qhwhw=
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/go.mod h1:Odkqvsn+2eoWV3ePcj257Ga0bdOqV4JBTfOJcQ+Sqf8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=

View file

@ -43,12 +43,32 @@ func (c clients) byUsernameLivestream(username string, url string) *rumblelivest
return user.byLivestream(url) return user.byLivestream(url)
} }
type followReceiver struct {
apiCh chan events.ApiFollower
latest time.Time
}
type receiver struct { type receiver struct {
onCommand map[string]map[int64]chan events.Chat onCommand map[string]map[int64]chan events.Chat
onCommandMu sync.Mutex onCommandMu sync.Mutex
//onFollow []chan ??? onFollow map[int64]*followReceiver
//onRant []chan events.Chat onFollowMu sync.Mutex
//onSubscribe []chan events.Chat 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 { 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) 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 var cookies []*http.Cookie
err = json.Unmarshal([]byte(*account.Cookies), &cookies) err = json.Unmarshal([]byte(*account.Cookies), &cookies)
if err != nil { 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()) ctx, cancel := context.WithCancel(context.Background())
runner := &Runner{ runner := &Runner{
cancel: cancel, cancel: cancel,
client: client, client: client,
page: page,
rule: *rule, rule: *rule,
wails: cb.wails, wails: cb.wails,
} }
@ -187,13 +218,18 @@ func (cb *Chatbot) initRunner(runner *Runner) error {
runner.channelIDMu.Unlock() runner.channelIDMu.Unlock()
switch { switch {
case runner.rule.Parameters.Trigger.OnTimer != nil:
runner.run = runner.runOnTimer
case runner.rule.Parameters.Trigger.OnCommand != nil: case runner.rule.Parameters.Trigger.OnCommand != nil:
err = cb.initRunnerCommand(runner) err = cb.initRunnerCommand(runner)
if err != nil { if err != nil {
return fmt.Errorf("error initializing command: %v", err) 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() // cb.runnersMu.Lock()
@ -233,12 +269,12 @@ func (cb *Chatbot) initRunnerCommand(runner *Runner) error {
defer cb.receiversMu.Unlock() defer cb.receiversMu.Unlock()
rcvr, exists := cb.receivers[runner.client.LiveStreamUrl] rcvr, exists := cb.receivers[runner.client.LiveStreamUrl]
if !exists { if !exists {
rcvr = &receiver{ rcvr = newReceiver()
onCommand: map[string]map[int64]chan events.Chat{},
}
cb.receivers[runner.client.LiveStreamUrl] = rcvr cb.receivers[runner.client.LiveStreamUrl] = rcvr
} }
rcvr.onCommandMu.Lock()
defer rcvr.onCommandMu.Unlock()
chans, exists := rcvr.onCommand[cmd] chans, exists := rcvr.onCommand[cmd]
if !exists { if !exists {
chans = map[int64]chan events.Chat{} chans = map[int64]chan events.Chat{}
@ -249,6 +285,164 @@ func (cb *Chatbot) initRunnerCommand(runner *Runner) error {
return nil 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) { func (cb *Chatbot) run(ctx context.Context, runner *Runner) {
if runner == nil || runner.rule.ID == nil || runner.run == nil { if runner == nil || runner.rule.ID == nil || runner.run == nil {
cb.logError.Println("invalid runner") 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 { 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() cb.botsMu.Lock()
defer cb.botsMu.Unlock() defer cb.botsMu.Unlock()
bot, exists := cb.bots[chatbotID] bot, exists := cb.bots[chatbotID]
@ -334,7 +522,6 @@ func (cb *Chatbot) stopRunner(chatbotID int64, ruleID int64) bool {
stopped := true stopped := true
runner.stop() runner.stop()
// delete(cb.runners, id)
delete(bot.runners, ruleID) delete(bot.runners, ruleID)
switch { switch {
@ -343,6 +530,11 @@ func (cb *Chatbot) stopRunner(chatbotID int64, ruleID int64) bool {
if err != nil { if err != nil {
cb.logError.Println("error closing runner command:", err) 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 return stopped
@ -361,6 +553,9 @@ func (cb *Chatbot) closeRunnerCommand(runner *Runner) error {
return fmt.Errorf("receiver for runner does not exist") return fmt.Errorf("receiver for runner does not exist")
} }
rcvr.onCommandMu.Lock()
defer rcvr.onCommandMu.Unlock()
cmd := runner.rule.Parameters.Trigger.OnCommand.Command cmd := runner.rule.Parameters.Trigger.OnCommand.Command
chans, exists := rcvr.onCommand[cmd] chans, exists := rcvr.onCommand[cmd]
if !exists { if !exists {
@ -378,8 +573,193 @@ func (cb *Chatbot) closeRunnerCommand(runner *Runner) error {
return nil 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 { switch event.Message.Type {
case rumblelivestreamlib.ChatTypeMessages: case rumblelivestreamlib.ChatTypeMessages:
cb.handleMessage(event) cb.handleMessage(event)
@ -390,6 +770,9 @@ func (cb *Chatbot) handleMessage(event events.Chat) {
errs := cb.runMessageFuncs( errs := cb.runMessageFuncs(
event, event,
cb.handleMessageCommand, cb.handleMessageCommand,
cb.handleMessageEventRaid,
cb.handleMessageEventRant,
cb.handleMessageEventSub,
) )
for _, err := range errs { 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 // TODO: validate message
errs := []error{} errs := []error{}
for _, fn := range fns { for _, fn := range fns {
err := fn(event) err := fn(chat)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
@ -411,25 +794,25 @@ func (cb *Chatbot) runMessageFuncs(event events.Chat, fns ...messageFunc) []erro
return errs return errs
} }
type messageFunc func(event events.Chat) error type messageFunc func(chat events.Chat) error
func (cb *Chatbot) handleMessageCommand(event events.Chat) error { func (cb *Chatbot) handleMessageCommand(chat events.Chat) error {
if strings.Index(event.Message.Text, "!") != 0 { if strings.Index(chat.Message.Text, "!") != 0 {
return nil return nil
} }
words := strings.Split(event.Message.Text, " ") words := strings.Split(chat.Message.Text, " ")
cmd := words[0] cmd := words[0]
cb.receiversMu.Lock() cb.receiversMu.Lock()
defer cb.receiversMu.Unlock() defer cb.receiversMu.Unlock()
receiver, exists := cb.receivers[event.Livestream] receiver, exists := cb.receivers[chat.Livestream]
if !exists { if !exists {
return nil return nil
} }
if receiver == 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() receiver.onCommandMu.Lock()
@ -440,7 +823,85 @@ func (cb *Chatbot) handleMessageCommand(event events.Chat) error {
} }
for _, runner := range runners { 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 return nil

View file

@ -16,6 +16,11 @@ import (
"github.com/tylertravisty/rum-goggles/v1/internal/models" "github.com/tylertravisty/rum-goggles/v1/internal/models"
) )
const (
PrefixAccount = "/user/"
PrefixChannel = "/c/"
)
func SortRules(rules []Rule) { func SortRules(rules []Rule) {
slices.SortFunc(rules, func(a, b Rule) int { slices.SortFunc(rules, func(a, b Rule) int {
return cmp.Compare(strings.ToLower(a.Display), strings.ToLower(b.Display)) return cmp.Compare(strings.ToLower(a.Display), strings.ToLower(b.Display))
@ -30,12 +35,33 @@ type Rule struct {
Running bool `json:"running"` 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 { type RuleParameters struct {
Message *RuleMessage `json:"message"` Message *RuleMessage `json:"message"`
SendAs *RuleSender `json:"send_as"` SendAs *RuleSender `json:"send_as"`
Trigger *RuleTrigger `json:"trigger"` Trigger *RuleTrigger `json:"trigger"`
} }
func (rp *RuleParameters) Page() *Page {
if rp.Trigger != nil {
return rp.Trigger.Page()
}
return nil
}
type RuleMessage struct { type RuleMessage struct {
FromFile *RuleMessageFile `json:"from_file"` FromFile *RuleMessageFile `json:"from_file"`
FromText string `json:"from_text"` FromText string `json:"from_text"`
@ -131,6 +157,14 @@ type RuleTrigger struct {
OnTimer *time.Duration `json:"on_timer"` OnTimer *time.Duration `json:"on_timer"`
} }
func (rt *RuleTrigger) Page() *Page {
if rt.OnEvent != nil {
return rt.OnEvent.Page()
}
return nil
}
type RuleTriggerCommand struct { type RuleTriggerCommand struct {
Command string `json:"command"` Command string `json:"command"`
Restrict *RuleTriggerCommandRestriction `json:"restrict"` Restrict *RuleTriggerCommandRestriction `json:"restrict"`
@ -154,12 +188,49 @@ type RuleTriggerCommandRestrictionBypass struct {
} }
type RuleTriggerEvent struct { type RuleTriggerEvent struct {
OnFollow bool `json:"on_follow"` FromAccount *RuleTriggerEventAccount `json:"from_account"`
OnSubscribe bool `json:"on_subscribe"` FromChannel *RuleTriggerEventChannel `json:"from_channel"`
OnRaid bool `json:"on_raid"` FromLiveStream *RuleTriggerEventLiveStream `json:"from_live_stream"`
OnRant int `json:"on_rant"`
} }
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) { func (rule *Rule) ToModelsChatbotRule() (*models.ChatbotRule, error) {
modelsRule := &models.ChatbotRule{ modelsRule := &models.ChatbotRule{
ID: rule.ID, ID: rule.ID,

View file

@ -14,13 +14,14 @@ import (
) )
type Runner struct { type Runner struct {
apiCh chan events.Api apiCh chan events.ApiFollower
cancel context.CancelFunc cancel context.CancelFunc
cancelMu sync.Mutex cancelMu sync.Mutex
channelID *int channelID *int
channelIDMu sync.Mutex channelIDMu sync.Mutex
chatCh chan events.Chat chatCh chan events.Chat
client *rumblelivestreamlib.Client client *rumblelivestreamlib.Client
page string
rule Rule rule Rule
run runFunc run runFunc
wails context.Context wails context.Context
@ -61,29 +62,31 @@ func (r *Runner) chat(fields *chatFields) error {
return nil return nil
} }
func (r *Runner) init() error { // func (r *Runner) init() error {
if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { // if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil {
return fmt.Errorf("invalid rule") // return fmt.Errorf("invalid rule")
} // }
channelID, err := r.rule.Parameters.SendAs.ChannelIDInt() // channelID, err := r.rule.Parameters.SendAs.ChannelIDInt()
if err != nil { // if err != nil {
return fmt.Errorf("error converting channel ID to int: %v", err) // return fmt.Errorf("error converting channel ID to int: %v", err)
} // }
r.channelIDMu.Lock() // r.channelIDMu.Lock()
r.channelID = channelID // r.channelID = channelID
r.channelIDMu.Unlock() // r.channelIDMu.Unlock()
switch { // switch {
case r.rule.Parameters.Trigger.OnTimer != nil: // case r.rule.Parameters.Trigger.OnTimer != nil:
r.run = r.runOnTimer // r.run = r.runOnTimer
case r.rule.Parameters.Trigger.OnCommand != nil: // case r.rule.Parameters.Trigger.OnEvent != nil:
r.run = r.runOnCommand // 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 type runFunc func(ctx context.Context) error
@ -102,18 +105,18 @@ func (r *Runner) runOnCommand(ctx context.Context) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
case event := <-r.chatCh: case chat := <-r.chatCh:
now := time.Now() now := time.Now()
if now.Sub(prev) < r.rule.Parameters.Trigger.OnCommand.Timeout*time.Second { if now.Sub(prev) < r.rule.Parameters.Trigger.OnCommand.Timeout*time.Second {
break break
} }
if block := r.blockCommand(event); block { if block := r.blockCommand(chat); block {
// if bypass := r.bypassCommand(event); !bypass {break} // if bypass := r.bypassCommand(chat); !bypass {break}
break break
} }
err := r.handleCommand(event) err := r.handleCommand(chat)
if err != nil { if err != nil {
return fmt.Errorf("error handling command: %v", err) 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 { if r.rule.Parameters.Trigger.OnCommand.Restrict == nil {
return false return false
} }
if r.rule.Parameters.Trigger.OnCommand.Restrict.ToFollower && if r.rule.Parameters.Trigger.OnCommand.Restrict.ToFollower &&
!event.Message.IsFollower { !chat.Message.IsFollower {
return true return true
} }
subscriber := false subscriber := false
for _, badge := range event.Message.Badges { for _, badge := range chat.Message.Badges {
if badge == rumblelivestreamlib.ChatBadgeLocalsSupporter || badge == rumblelivestreamlib.ChatBadgeRecurringSubscription { if badge == rumblelivestreamlib.ChatBadgeLocalsSupporter || badge == rumblelivestreamlib.ChatBadgeRecurringSubscription {
subscriber = true subscriber = true
} }
@ -144,24 +147,238 @@ func (r *Runner) blockCommand(event events.Chat) bool {
return true 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 true
} }
return false return false
} }
func (r *Runner) handleCommand(event events.Chat) error { func (r *Runner) handleCommand(chat events.Chat) error {
displayName := event.Message.Username displayName := chat.Message.Username
if event.Message.ChannelName != "" { if chat.Message.ChannelName != "" {
displayName = event.Message.ChannelName displayName = chat.Message.ChannelName
} }
fields := &chatFields{ fields := &chatFields{
ChannelName: event.Message.ChannelName, ChannelName: chat.Message.ChannelName,
DisplayName: displayName, DisplayName: displayName,
Username: event.Message.Username, Username: chat.Message.Username,
Rant: event.Message.Rant / 100, 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) err := r.chat(fields)

View file

@ -16,6 +16,10 @@ type Api struct {
Stop bool Stop bool
} }
type ApiFollower struct {
Username string
}
type apiProducer struct { type apiProducer struct {
cancel context.CancelFunc cancel context.CancelFunc
cancelMu sync.Mutex cancelMu sync.Mutex

View file

@ -258,6 +258,15 @@ type ChatEventBlock struct {
Type string `json:"type"` 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 { type ChatEventRant struct {
Duration int `json:"duration"` Duration int `json:"duration"`
ExpiresOn string `json:"expires_on"` ExpiresOn string `json:"expires_on"`
@ -268,6 +277,8 @@ type ChatEventMessage struct {
Blocks []ChatEventBlock `json:"blocks"` Blocks []ChatEventBlock `json:"blocks"`
ChannelID *int64 `json:"channel_id"` ChannelID *int64 `json:"channel_id"`
ID string `json:"id"` ID string `json:"id"`
Notification *ChatEventNotification `json:"notification"`
RaidNotification *ChatEventRaidNotification `json:"raid_notification"`
Rant *ChatEventRant `json:"rant"` Rant *ChatEventRant `json:"rant"`
Text string `json:"text"` Text string `json:"text"`
Time string `json:"time"` Time string `json:"time"`
@ -392,7 +403,9 @@ type ChatView struct {
ImageUrl string ImageUrl string
Init bool Init bool
IsFollower bool IsFollower bool
Raid bool
Rant int Rant int
Sub bool
Text string Text string
Time time.Time Time time.Time
Type string Type string
@ -456,9 +469,17 @@ func parseMessages(eventType string, messages []ChatEventMessage, users map[stri
view.Color = user.Color view.Color = user.Color
view.ImageUrl = user.Image1 view.ImageUrl = user.Image1
view.IsFollower = user.IsFollower view.IsFollower = user.IsFollower
if message.RaidNotification != nil {
view.Raid = true
}
if message.Rant != nil { if message.Rant != nil {
view.Rant = message.Rant.PriceCents view.Rant = message.Rant.PriceCents
} }
if message.Notification != nil {
if message.Notification.Badge == ChatBadgeRecurringSubscription {
view.Sub = true
}
}
view.Text = message.Text view.Text = message.Text
t, err := time.Parse(time.RFC3339, message.Time) t, err := time.Parse(time.RFC3339, message.Time)
if err != nil { if err != nil {

View file

@ -273,31 +273,37 @@ func (c *Client) userLogout() error {
return nil return nil
} }
type LoggedInResponseData struct {
Username string `json:"username"`
}
type LoggedInResponseUser struct { type LoggedInResponseUser struct {
ID string `json:"id"`
LoggedIn bool `json:"logged_in"` LoggedIn bool `json:"logged_in"`
} }
type LoggedInResponse struct { type LoggedInResponse struct {
Data LoggedInResponseData `json:"data"`
User LoggedInResponseUser `json:"user"` User LoggedInResponseUser `json:"user"`
} }
func (c *Client) LoggedIn() (bool, error) { func (c *Client) LoggedIn() (*LoggedInResponse, error) {
resp, err := c.httpClient.Get(urlUserLogin) resp, err := c.httpClient.Get(urlUserLogin)
if err != nil { if err != nil {
return false, pkgErr("error getting login service", err) return nil, pkgErr("error getting login service", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
bodyB, err := io.ReadAll(resp.Body) bodyB, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return false, pkgErr("error reading body bytes", err) return nil, pkgErr("error reading body bytes", err)
} }
var lir LoggedInResponse var lir LoggedInResponse
err = json.NewDecoder(strings.NewReader(string(bodyB))).Decode(&lir) err = json.NewDecoder(strings.NewReader(string(bodyB))).Decode(&lir)
if err != nil { 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
} }

View file

@ -77,7 +77,7 @@ github.com/tkrajina/go-reflector/reflector
# github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 # github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909
## explicit; go 1.16 ## explicit; go 1.16
github.com/tylertravisty/go-utils/random 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 ## explicit; go 1.19
github.com/tylertravisty/rumble-livestream-lib-go github.com/tylertravisty/rumble-livestream-lib-go
# github.com/valyala/bytebufferpool v1.0.0 # github.com/valyala/bytebufferpool v1.0.0