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"
"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)

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

View file

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

View file

@ -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' && (
<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' && (
<ModalRuleTrigger
onClose={props.onClose}
@ -632,6 +719,16 @@ function ModalRule(props) {
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' && (
<ModalRuleTriggerTimer
onBack={goBack}
@ -642,39 +739,6 @@ function ModalRule(props) {
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');
};
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}
/>
</button>
{/* <button
className='modal-add-account-channel-button'
onClick={() => next('trigger-stream_event')}
>
<button className='modal-add-account-channel-button' onClick={triggerOnEvent}>
<div className='modal-add-account-channel-button-left'>
<span>Stream Event</span>
<span>Event</span>
</div>
<img
className='modal-add-account-channel-button-right-icon'
src={ChevronRight}
/>
</button> */}
</button>
<button className='modal-add-account-channel-button' onClick={triggerOnTimer}>
<div className='modal-add-account-channel-button-left'>
<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) {
const prependZero = (value) => {
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);
}, [props.account.username]);
useEffect(() => {
setLoggedIn(props.account.cookies !== null);
}, [props.account.cookies]);
useEffect(() => {
if (username !== '') {
PageStatus(pageName(username));

View file

@ -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
)

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/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=

View file

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

View file

@ -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,

View file

@ -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)

View file

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

View file

@ -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 {

View file

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

View file

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