Timer
@@ -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 (
+
+
+
Configure Event
+
+
+
+
+
+
+
+
+
+
+
+ {source !== '' && (
+
+ )}
+
+
+
+
+
+
+ {event === 'Rant' && (
+
+ )}
+ {event === 'Follow' && (
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+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 (
+
+
+ {sortPages().map((option, index) => (
+
+
+
+ ))}
+
+
+ );
+}
+
+function EventOptionsRant(props) {
+ const [minAmount, setMinAmount] = useState(
+ isNaN(props.options.min_amount) ? 0 : props.options.min_amount
+ );
+ const updateMinAmount = (event) => {
+ let amount = parseInt(event.target.value);
+ if (isNaN(amount)) {
+ amount = 0;
+ }
+
+ if (maxAmount !== 0 && amount > maxAmount) {
+ setValidMaxAmount(false);
+ } else {
+ setValidMaxAmount(true);
+ }
+
+ setMinAmount(event.target.value);
+ props.setOptions({ min_amount: amount, max_amount: maxAmount });
+ };
+ const [maxAmount, setMaxAmount] = useState(
+ isNaN(props.options.max_amount) ? 0 : props.options.max_amount
+ );
+ const updateMaxAmount = (event) => {
+ let amount = parseInt(event.target.value);
+ if (isNaN(amount)) {
+ amount = 0;
+ }
+
+ if (amount !== 0) {
+ if (amount < minAmount) {
+ setValidMaxAmount(false);
+ } else {
+ setValidMaxAmount(true);
+ }
+ } else {
+ setValidMaxAmount(true);
+ }
+
+ setMaxAmount(amount);
+ props.setOptions({ min_amount: minAmount, max_amount: amount });
+ };
+ const [validMaxAmount, setValidMaxAmount] = useState(true);
+
+ return (
+ <>
+
+
+
+ $
+
+
+
+
+
+
+ $
+
+
+
+ >
+ );
+}
+
function ModalRuleTriggerTimer(props) {
const prependZero = (value) => {
if (value < 10) {
diff --git a/v1/frontend/src/components/DropDown.css b/v1/frontend/src/components/DropDown.css
new file mode 100644
index 0000000..7b4e797
--- /dev/null
+++ b/v1/frontend/src/components/DropDown.css
@@ -0,0 +1,80 @@
+.dropdown {
+ width: 100%;
+}
+
+.dropdown-menu {
+ align-items: center;
+ background-color: white;
+ border: 1px solid #061726;
+ border-radius: 5px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 5px;
+ position: fixed;
+ z-index: 10;
+}
+
+.dropdown-menu-container {
+ width: 100%;
+}
+
+.dropdown-menu-background {
+ align-items: center;
+ display: flex;
+ height: 100vh;
+ justify-content: center;
+ left: 0;
+ opacity: 0;
+ position: absolute;
+ top: 0;
+ width: 100vw;
+ z-index: 8;
+}
+
+.dropdown-menu-option {
+ background-color: white;
+ border: none;
+ border-radius: 5px;
+ box-sizing: border-box;
+ color: #061726;
+ font-family: sans-serif;
+ font-size: 16px;
+ font-weight: bold;
+ padding: 5px;
+ width: 100%;
+}
+
+.dropdown-menu-option-selected {
+ background-color: #77b23b;
+}
+
+.dropdown-menu-option:hover {
+ background-color: #77b23b;
+ cursor: pointer;
+}
+
+.dropdown-toggle {
+ align-items: center;
+ background-color: white;
+ border: 1px solid #061726;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 5px;
+ width: 100%;
+}
+
+.dropdown-toggle-text {
+ color: #061726;
+ font-family: sans-serif;
+ font-size: 16px;
+ font-weight: bold;
+}
+
+.dropdown-toggle-icon {
+ height: 20px;
+ width: 20px;
+}
\ No newline at end of file
diff --git a/v1/frontend/src/components/DropDown.jsx b/v1/frontend/src/components/DropDown.jsx
new file mode 100644
index 0000000..41a792a
--- /dev/null
+++ b/v1/frontend/src/components/DropDown.jsx
@@ -0,0 +1,97 @@
+import { useEffect, useRef, useState } from 'react';
+
+import { ChevronDown } from '../assets';
+import './DropDown.css';
+
+export function DropDown(props) {
+ const [options, setOptions] = useState(props.options !== undefined ? props.options : []);
+ const [selected, setSelected] = useState(props.selected !== undefined ? props.selected : '');
+ const [toggled, setToggled] = useState(false);
+ const toggle = () => {
+ setToggled(!toggled);
+ };
+
+ useEffect(() => {
+ setSelected(props.selected !== undefined ? props.selected : '');
+ }, [props.selected]);
+
+ useEffect(() => {
+ setOptions(props.options !== undefined ? props.options : []);
+ }, [props.options]);
+
+ const select = (option) => {
+ props.select(option);
+ setSelected(option);
+ toggle();
+ };
+
+ return (
+
+
+ {toggled && (
+
+ )}
+
+ );
+}
+
+function DropDownMenu(props) {
+ const menuRef = useRef();
+ const { width } = menuWidth(menuRef);
+
+ return (
+
+ {width !== undefined && (
+
+ {props.options.map((option, index) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+export const menuWidth = (menuRef) => {
+ const [width, setWidth] = useState(0);
+
+ useEffect(() => {
+ const getWidth = () => ({ width: menuRef.current.offsetWidth });
+
+ const handleResize = () => {
+ setWidth(getWidth());
+ };
+
+ if (menuRef.current) {
+ setWidth(getWidth());
+ }
+
+ window.addEventListener('resize', handleResize);
+
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ };
+ }, [menuRef]);
+
+ return width;
+};
diff --git a/v1/frontend/src/components/PageSideBar.jsx b/v1/frontend/src/components/PageSideBar.jsx
index 48ceb23..023fd07 100644
--- a/v1/frontend/src/components/PageSideBar.jsx
+++ b/v1/frontend/src/components/PageSideBar.jsx
@@ -190,6 +190,10 @@ function AccountIcon(props) {
setUsername(props.account.username);
}, [props.account.username]);
+ useEffect(() => {
+ setLoggedIn(props.account.cookies !== null);
+ }, [props.account.cookies]);
+
useEffect(() => {
if (username !== '') {
PageStatus(pageName(username));
diff --git a/v1/go.mod b/v1/go.mod
index fee5275..acaea6b 100644
--- a/v1/go.mod
+++ b/v1/go.mod
@@ -6,7 +6,7 @@ toolchain go1.22.0
require (
github.com/mattn/go-sqlite3 v1.14.22
- github.com/tylertravisty/rumble-livestream-lib-go v0.7.2
+ github.com/tylertravisty/rumble-livestream-lib-go v0.9.0
github.com/wailsapp/wails/v2 v2.8.1
)
diff --git a/v1/go.sum b/v1/go.sum
index e0f7763..992b3f7 100644
--- a/v1/go.sum
+++ b/v1/go.sum
@@ -60,8 +60,8 @@ github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQ
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g=
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk=
-github.com/tylertravisty/rumble-livestream-lib-go v0.7.2 h1:TRGTKhxB+uK0gnIC+rXbRxfFjMJxPHhjZzbsjDSpK+o=
-github.com/tylertravisty/rumble-livestream-lib-go v0.7.2/go.mod h1:Odkqvsn+2eoWV3ePcj257Ga0bdOqV4JBTfOJcQ+Sqf8=
+github.com/tylertravisty/rumble-livestream-lib-go v0.9.0 h1:G1b/uac43dq7BG7NzcLeRLPOfOu8GyjViE9s48qhwhw=
+github.com/tylertravisty/rumble-livestream-lib-go v0.9.0/go.mod h1:Odkqvsn+2eoWV3ePcj257Ga0bdOqV4JBTfOJcQ+Sqf8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
diff --git a/v1/internal/chatbot/chatbot.go b/v1/internal/chatbot/chatbot.go
index 073e563..c80551a 100644
--- a/v1/internal/chatbot/chatbot.go
+++ b/v1/internal/chatbot/chatbot.go
@@ -43,12 +43,32 @@ func (c clients) byUsernameLivestream(username string, url string) *rumblelivest
return user.byLivestream(url)
}
+type followReceiver struct {
+ apiCh chan events.ApiFollower
+ latest time.Time
+}
+
type receiver struct {
onCommand map[string]map[int64]chan events.Chat
onCommandMu sync.Mutex
- //onFollow []chan ???
- //onRant []chan events.Chat
- //onSubscribe []chan events.Chat
+ onFollow map[int64]*followReceiver
+ onFollowMu sync.Mutex
+ onRaid map[int64]chan events.Chat
+ onRaidMu sync.Mutex
+ onRant map[int64]chan events.Chat
+ onRantMu sync.Mutex
+ onSub map[int64]chan events.Chat
+ onSubMu sync.Mutex
+}
+
+func newReceiver() *receiver {
+ return &receiver{
+ onCommand: map[string]map[int64]chan events.Chat{},
+ onFollow: map[int64]*followReceiver{},
+ onRaid: map[int64]chan events.Chat{},
+ onRant: map[int64]chan events.Chat{},
+ onSub: map[int64]chan events.Chat{},
+ }
}
type Bot struct {
@@ -107,6 +127,10 @@ func (cb *Chatbot) addClient(username string, livestreamUrl string) (*rumblelive
return nil, fmt.Errorf("error querying account by username: %v", err)
}
+ if account.Cookies == nil {
+ return nil, fmt.Errorf("account cookies are nil")
+ }
+
var cookies []*http.Cookie
err = json.Unmarshal([]byte(*account.Cookies), &cookies)
if err != nil {
@@ -154,10 +178,17 @@ func (cb *Chatbot) Run(rule *Rule, url string) error {
}
}
+ page := ""
+ rulePage := rule.Page()
+ if rulePage != nil {
+ page = rulePage.Prefix + strings.ReplaceAll(rulePage.Name, " ", "")
+ }
+
ctx, cancel := context.WithCancel(context.Background())
runner := &Runner{
cancel: cancel,
client: client,
+ page: page,
rule: *rule,
wails: cb.wails,
}
@@ -187,13 +218,18 @@ func (cb *Chatbot) initRunner(runner *Runner) error {
runner.channelIDMu.Unlock()
switch {
- case runner.rule.Parameters.Trigger.OnTimer != nil:
- runner.run = runner.runOnTimer
case runner.rule.Parameters.Trigger.OnCommand != nil:
err = cb.initRunnerCommand(runner)
if err != nil {
return fmt.Errorf("error initializing command: %v", err)
}
+ case runner.rule.Parameters.Trigger.OnEvent != nil:
+ err = cb.initRunnerEvent(runner)
+ if err != nil {
+ return fmt.Errorf("error initializing event: %v", err)
+ }
+ case runner.rule.Parameters.Trigger.OnTimer != nil:
+ runner.run = runner.runOnTimer
}
// cb.runnersMu.Lock()
@@ -233,12 +269,12 @@ func (cb *Chatbot) initRunnerCommand(runner *Runner) error {
defer cb.receiversMu.Unlock()
rcvr, exists := cb.receivers[runner.client.LiveStreamUrl]
if !exists {
- rcvr = &receiver{
- onCommand: map[string]map[int64]chan events.Chat{},
- }
+ rcvr = newReceiver()
cb.receivers[runner.client.LiveStreamUrl] = rcvr
}
+ rcvr.onCommandMu.Lock()
+ defer rcvr.onCommandMu.Unlock()
chans, exists := rcvr.onCommand[cmd]
if !exists {
chans = map[int64]chan events.Chat{}
@@ -249,6 +285,164 @@ func (cb *Chatbot) initRunnerCommand(runner *Runner) error {
return nil
}
+func (cb *Chatbot) initRunnerEvent(runner *Runner) error {
+ event := runner.rule.Parameters.Trigger.OnEvent
+ switch {
+ case event.FromAccount != nil:
+ return cb.initRunnerEventFromAccount(runner)
+ case event.FromChannel != nil:
+ return cb.initRunnerEventFromChannel(runner)
+ case event.FromLiveStream != nil:
+ return cb.initRunnerEventFromLiveStream(runner)
+ }
+
+ return fmt.Errorf("runner event not supported")
+}
+
+func (cb *Chatbot) initRunnerEventFromAccount(runner *Runner) error {
+ fromAccount := runner.rule.Parameters.Trigger.OnEvent.FromAccount
+ switch {
+ case fromAccount.OnFollow != nil:
+ return cb.initRunnerEventFromAccountOnFollow(runner)
+ }
+
+ return fmt.Errorf("runner event not supported")
+}
+
+func (cb *Chatbot) initRunnerEventFromAccountOnFollow(runner *Runner) error {
+ runner.run = runner.runOnEventFromAccountOnFollow
+
+ apiCh := make(chan events.ApiFollower, 10)
+ runner.apiCh = apiCh
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+ rcvr, exists := cb.receivers[runner.page]
+ if !exists {
+ rcvr = newReceiver()
+ cb.receivers[runner.page] = rcvr
+ }
+
+ // TODO: should I check if channel already exists, if so delete it?
+ rcvr.onFollowMu.Lock()
+ defer rcvr.onFollowMu.Unlock()
+ rcvr.onFollow[*runner.rule.ID] = &followReceiver{apiCh, time.Now()}
+
+ return nil
+}
+
+func (cb *Chatbot) initRunnerEventFromChannel(runner *Runner) error {
+ fromChannel := runner.rule.Parameters.Trigger.OnEvent.FromChannel
+ switch {
+ case fromChannel.OnFollow != nil:
+ return cb.initRunnerEventFromChannelOnFollow(runner)
+ }
+
+ return fmt.Errorf("runner event not supported")
+}
+
+func (cb *Chatbot) initRunnerEventFromChannelOnFollow(runner *Runner) error {
+ runner.run = runner.runOnEventFromChannelOnFollow
+
+ apiCh := make(chan events.ApiFollower, 10)
+ runner.apiCh = apiCh
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+ rcvr, exists := cb.receivers[runner.page]
+ if !exists {
+ rcvr = newReceiver()
+ cb.receivers[runner.page] = rcvr
+ }
+
+ // TODO: should I check if channel already exists, if so delete it?
+ rcvr.onFollowMu.Lock()
+ defer rcvr.onFollowMu.Unlock()
+ rcvr.onFollow[*runner.rule.ID] = &followReceiver{apiCh, time.Now()}
+
+ return nil
+}
+
+func (cb *Chatbot) initRunnerEventFromLiveStream(runner *Runner) error {
+ fromLiveStream := runner.rule.Parameters.Trigger.OnEvent.FromLiveStream
+ switch {
+ case fromLiveStream.OnRaid != nil:
+ return cb.initRunnerEventFromLiveStreamOnRaid(runner)
+ case fromLiveStream.OnRant != nil:
+ return cb.initRunnerEventFromLiveStreamOnRant(runner)
+ case fromLiveStream.OnSub != nil:
+ return cb.initRunnerEventFromLiveStreamOnSub(runner)
+ }
+
+ return fmt.Errorf("runner event not supported")
+}
+
+func (cb *Chatbot) initRunnerEventFromLiveStreamOnRaid(runner *Runner) error {
+ runner.run = runner.runOnEventFromLiveStreamOnRaid
+
+ chatCh := make(chan events.Chat, 10)
+ runner.chatCh = chatCh
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+ rcvr, exists := cb.receivers[runner.client.LiveStreamUrl]
+ if !exists {
+ rcvr = newReceiver()
+ cb.receivers[runner.client.LiveStreamUrl] = rcvr
+ }
+
+ // TODO: should I check if channel already exists, if so delete it?
+ rcvr.onRaidMu.Lock()
+ defer rcvr.onRaidMu.Unlock()
+ rcvr.onRaid[*runner.rule.ID] = chatCh
+
+ return nil
+}
+
+func (cb *Chatbot) initRunnerEventFromLiveStreamOnRant(runner *Runner) error {
+ runner.run = runner.runOnEventFromLiveStreamOnRant
+
+ chatCh := make(chan events.Chat, 10)
+ runner.chatCh = chatCh
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+ rcvr, exists := cb.receivers[runner.client.LiveStreamUrl]
+ if !exists {
+ rcvr = newReceiver()
+ cb.receivers[runner.client.LiveStreamUrl] = rcvr
+ }
+
+ // TODO: should I check if channel already exists, if so delete it?
+ rcvr.onRantMu.Lock()
+ defer rcvr.onRantMu.Unlock()
+ rcvr.onRant[*runner.rule.ID] = chatCh
+
+ return nil
+}
+
+func (cb *Chatbot) initRunnerEventFromLiveStreamOnSub(runner *Runner) error {
+ runner.run = runner.runOnEventFromLiveStreamOnSub
+
+ chatCh := make(chan events.Chat, 10)
+ runner.chatCh = chatCh
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+ rcvr, exists := cb.receivers[runner.client.LiveStreamUrl]
+ if !exists {
+ rcvr = newReceiver()
+ cb.receivers[runner.client.LiveStreamUrl] = rcvr
+ }
+
+ // TODO: should I check if channel already exists, if so delete it?
+ rcvr.onSubMu.Lock()
+ defer rcvr.onSubMu.Unlock()
+ rcvr.onSub[*runner.rule.ID] = chatCh
+
+ return nil
+}
+
func (cb *Chatbot) run(ctx context.Context, runner *Runner) {
if runner == nil || runner.rule.ID == nil || runner.run == nil {
cb.logError.Println("invalid runner")
@@ -312,12 +506,6 @@ func (cb *Chatbot) stop(rule *Rule) error {
}
func (cb *Chatbot) stopRunner(chatbotID int64, ruleID int64) bool {
- // cb.runnersMu.Lock()
- // defer cb.runnersMu.Unlock()
- // runner, exists := cb.runners[id]
- // if !exists {
- // return
- // }
cb.botsMu.Lock()
defer cb.botsMu.Unlock()
bot, exists := cb.bots[chatbotID]
@@ -334,7 +522,6 @@ func (cb *Chatbot) stopRunner(chatbotID int64, ruleID int64) bool {
stopped := true
runner.stop()
- // delete(cb.runners, id)
delete(bot.runners, ruleID)
switch {
@@ -343,6 +530,11 @@ func (cb *Chatbot) stopRunner(chatbotID int64, ruleID int64) bool {
if err != nil {
cb.logError.Println("error closing runner command:", err)
}
+ case runner.rule.Parameters.Trigger.OnEvent != nil:
+ err := cb.closeRunnerEvent(runner)
+ if err != nil {
+ cb.logError.Println("error closing runner event:", err)
+ }
}
return stopped
@@ -361,6 +553,9 @@ func (cb *Chatbot) closeRunnerCommand(runner *Runner) error {
return fmt.Errorf("receiver for runner does not exist")
}
+ rcvr.onCommandMu.Lock()
+ defer rcvr.onCommandMu.Unlock()
+
cmd := runner.rule.Parameters.Trigger.OnCommand.Command
chans, exists := rcvr.onCommand[cmd]
if !exists {
@@ -378,8 +573,193 @@ func (cb *Chatbot) closeRunnerCommand(runner *Runner) error {
return nil
}
-func (cb *Chatbot) HandleChat(event events.Chat) {
+func (cb *Chatbot) closeRunnerEvent(runner *Runner) error {
+ if runner == nil || runner.rule.ID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil || runner.rule.Parameters.Trigger.OnEvent == nil {
+ return fmt.Errorf("invalid runner event")
+ }
+ switch {
+ case runner.rule.Parameters.Trigger.OnEvent.FromAccount != nil:
+ return cb.closeRunnerEventFromAccount(runner)
+ case runner.rule.Parameters.Trigger.OnEvent.FromChannel != nil:
+ return cb.closeRunnerEventFromChannel(runner)
+ case runner.rule.Parameters.Trigger.OnEvent.FromLiveStream != nil:
+ return cb.closeRunnerEventFromLiveStream(runner)
+ }
+
+ return fmt.Errorf("runner event not supported")
+}
+
+func (cb *Chatbot) closeRunnerEventFromAccount(runner *Runner) error {
+ if runner == nil || runner.rule.ID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil || runner.rule.Parameters.Trigger.OnEvent == nil || runner.rule.Parameters.Trigger.OnEvent.FromAccount == nil {
+ return fmt.Errorf("invalid runner event")
+ }
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+
+ rcvr, exists := cb.receivers[runner.page]
+ if !exists {
+ return fmt.Errorf("receiver for runner does not exist")
+ }
+
+ fromAccount := runner.rule.Parameters.Trigger.OnEvent.FromAccount
+ switch {
+ case fromAccount.OnFollow != nil:
+ rcvr.onFollowMu.Lock()
+ defer rcvr.onFollowMu.Unlock()
+ followR, exists := rcvr.onFollow[*runner.rule.ID]
+ if !exists {
+ return fmt.Errorf("channel for runner does not exist")
+ }
+ close(followR.apiCh)
+ delete(rcvr.onFollow, *runner.rule.ID)
+ }
+
+ return nil
+}
+
+func (cb *Chatbot) closeRunnerEventFromChannel(runner *Runner) error {
+ if runner == nil || runner.rule.ID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil || runner.rule.Parameters.Trigger.OnEvent == nil || runner.rule.Parameters.Trigger.OnEvent.FromChannel == nil {
+ return fmt.Errorf("invalid runner event")
+ }
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+
+ rcvr, exists := cb.receivers[runner.page]
+ if !exists {
+ return fmt.Errorf("receiver for runner does not exist")
+ }
+
+ fromChannel := runner.rule.Parameters.Trigger.OnEvent.FromChannel
+ switch {
+ case fromChannel.OnFollow != nil:
+ rcvr.onFollowMu.Lock()
+ defer rcvr.onFollowMu.Unlock()
+ followR, exists := rcvr.onFollow[*runner.rule.ID]
+ if !exists {
+ return fmt.Errorf("channel for runner does not exist")
+ }
+ close(followR.apiCh)
+ delete(rcvr.onFollow, *runner.rule.ID)
+ }
+
+ return nil
+}
+
+func (cb *Chatbot) closeRunnerEventFromLiveStream(runner *Runner) error {
+ if runner == nil || runner.rule.ID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil || runner.rule.Parameters.Trigger.OnEvent == nil || runner.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil {
+ return fmt.Errorf("invalid runner event")
+ }
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+
+ rcvr, exists := cb.receivers[runner.client.LiveStreamUrl]
+ if !exists {
+ return fmt.Errorf("receiver for runner does not exist")
+ }
+
+ fromLiveStream := runner.rule.Parameters.Trigger.OnEvent.FromLiveStream
+ switch {
+ case fromLiveStream.OnRaid != nil:
+ rcvr.onRaidMu.Lock()
+ defer rcvr.onRaidMu.Unlock()
+ ch, exists := rcvr.onRaid[*runner.rule.ID]
+ if !exists {
+ return fmt.Errorf("channel for runner does not exist")
+ }
+ close(ch)
+ delete(rcvr.onRaid, *runner.rule.ID)
+ case fromLiveStream.OnRant != nil:
+ rcvr.onRantMu.Lock()
+ defer rcvr.onRantMu.Unlock()
+ ch, exists := rcvr.onRant[*runner.rule.ID]
+ if !exists {
+ return fmt.Errorf("channel for runner does not exist")
+ }
+ close(ch)
+ delete(rcvr.onRant, *runner.rule.ID)
+ case fromLiveStream.OnSub != nil:
+ rcvr.onSubMu.Lock()
+ defer rcvr.onSubMu.Unlock()
+ ch, exists := rcvr.onSub[*runner.rule.ID]
+ if !exists {
+ return fmt.Errorf("channel for runner does not exist")
+ }
+ close(ch)
+ delete(rcvr.onSub, *runner.rule.ID)
+ }
+
+ return nil
+}
+
+func (cb *Chatbot) HandleApi(event events.Api) {
+ errs := cb.runApiFuncs(
+ event,
+ cb.handleApiFollow,
+ )
+
+ for _, err := range errs {
+ cb.logError.Println("chatbot: error handling api event:", err)
+ }
+}
+
+type apiFunc func(api events.Api) error
+
+func (cb *Chatbot) runApiFuncs(api events.Api, fns ...apiFunc) []error {
+ // TODO: validate api response?
+
+ errs := []error{}
+ for _, fn := range fns {
+ err := fn(api)
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+
+ return errs
+}
+
+func (cb *Chatbot) handleApiFollow(api events.Api) error {
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+
+ rcvr, exists := cb.receivers[api.Name]
+ if !exists {
+ return nil
+ }
+ if rcvr == nil {
+ return fmt.Errorf("receiver is nil for API: %s", api.Name)
+ }
+
+ rcvr.onFollowMu.Lock()
+ defer rcvr.onFollowMu.Unlock()
+
+ for _, runner := range rcvr.onFollow {
+ latest := runner.latest
+ for _, follower := range api.Resp.Followers.RecentFollowers {
+ followedOn, err := time.Parse(time.RFC3339, follower.FollowedOn)
+ // TODO: fix this in the API, not in the code
+ followedOn = followedOn.Add(-4 * time.Hour)
+ if err != nil {
+ return fmt.Errorf("error parsing followed_on time: %v", err)
+ }
+ if followedOn.After(runner.latest) {
+ if followedOn.After(latest) {
+ latest = followedOn
+ }
+ runner.apiCh <- events.ApiFollower{Username: follower.Username}
+ }
+ }
+ runner.latest = latest
+ }
+
+ return nil
+}
+
+func (cb *Chatbot) HandleChat(event events.Chat) {
switch event.Message.Type {
case rumblelivestreamlib.ChatTypeMessages:
cb.handleMessage(event)
@@ -390,6 +770,9 @@ func (cb *Chatbot) handleMessage(event events.Chat) {
errs := cb.runMessageFuncs(
event,
cb.handleMessageCommand,
+ cb.handleMessageEventRaid,
+ cb.handleMessageEventRant,
+ cb.handleMessageEventSub,
)
for _, err := range errs {
@@ -397,12 +780,12 @@ func (cb *Chatbot) handleMessage(event events.Chat) {
}
}
-func (cb *Chatbot) runMessageFuncs(event events.Chat, fns ...messageFunc) []error {
+func (cb *Chatbot) runMessageFuncs(chat events.Chat, fns ...messageFunc) []error {
// TODO: validate message
errs := []error{}
for _, fn := range fns {
- err := fn(event)
+ err := fn(chat)
if err != nil {
errs = append(errs, err)
}
@@ -411,25 +794,25 @@ func (cb *Chatbot) runMessageFuncs(event events.Chat, fns ...messageFunc) []erro
return errs
}
-type messageFunc func(event events.Chat) error
+type messageFunc func(chat events.Chat) error
-func (cb *Chatbot) handleMessageCommand(event events.Chat) error {
- if strings.Index(event.Message.Text, "!") != 0 {
+func (cb *Chatbot) handleMessageCommand(chat events.Chat) error {
+ if strings.Index(chat.Message.Text, "!") != 0 {
return nil
}
- words := strings.Split(event.Message.Text, " ")
+ words := strings.Split(chat.Message.Text, " ")
cmd := words[0]
cb.receiversMu.Lock()
defer cb.receiversMu.Unlock()
- receiver, exists := cb.receivers[event.Livestream]
+ receiver, exists := cb.receivers[chat.Livestream]
if !exists {
return nil
}
if receiver == nil {
- return fmt.Errorf("receiver is nil for livestream: %s", event.Livestream)
+ return fmt.Errorf("receiver is nil for livestream: %s", chat.Livestream)
}
receiver.onCommandMu.Lock()
@@ -440,7 +823,85 @@ func (cb *Chatbot) handleMessageCommand(event events.Chat) error {
}
for _, runner := range runners {
- runner <- event
+ runner <- chat
+ }
+
+ return nil
+}
+
+func (cb *Chatbot) handleMessageEventRaid(chat events.Chat) error {
+ if !chat.Message.Raid {
+ return nil
+ }
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+
+ receiver, exists := cb.receivers[chat.Livestream]
+ if !exists {
+ return nil
+ }
+ if receiver == nil {
+ return fmt.Errorf("receiver is nil for livestream: %s", chat.Livestream)
+ }
+
+ receiver.onRaidMu.Lock()
+ defer receiver.onRaidMu.Unlock()
+
+ for _, runner := range receiver.onRaid {
+ runner <- chat
+ }
+
+ return nil
+}
+
+func (cb *Chatbot) handleMessageEventRant(chat events.Chat) error {
+ if chat.Message.Rant == 0 {
+ return nil
+ }
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+
+ receiver, exists := cb.receivers[chat.Livestream]
+ if !exists {
+ return nil
+ }
+ if receiver == nil {
+ return fmt.Errorf("receiver is nil for livestream: %s", chat.Livestream)
+ }
+
+ receiver.onRantMu.Lock()
+ defer receiver.onRantMu.Unlock()
+
+ for _, runner := range receiver.onRant {
+ runner <- chat
+ }
+
+ return nil
+}
+
+func (cb *Chatbot) handleMessageEventSub(chat events.Chat) error {
+ if !chat.Message.Sub {
+ return nil
+ }
+
+ cb.receiversMu.Lock()
+ defer cb.receiversMu.Unlock()
+
+ receiver, exists := cb.receivers[chat.Livestream]
+ if !exists {
+ return nil
+ }
+ if receiver == nil {
+ return fmt.Errorf("receiver is nil for livestream: %s", chat.Livestream)
+ }
+
+ receiver.onSubMu.Lock()
+ defer receiver.onSubMu.Unlock()
+
+ for _, runner := range receiver.onSub {
+ runner <- chat
}
return nil
diff --git a/v1/internal/chatbot/rule.go b/v1/internal/chatbot/rule.go
index b80928d..eeb4b3a 100644
--- a/v1/internal/chatbot/rule.go
+++ b/v1/internal/chatbot/rule.go
@@ -16,6 +16,11 @@ import (
"github.com/tylertravisty/rum-goggles/v1/internal/models"
)
+const (
+ PrefixAccount = "/user/"
+ PrefixChannel = "/c/"
+)
+
func SortRules(rules []Rule) {
slices.SortFunc(rules, func(a, b Rule) int {
return cmp.Compare(strings.ToLower(a.Display), strings.ToLower(b.Display))
@@ -30,12 +35,33 @@ type Rule struct {
Running bool `json:"running"`
}
+type Page struct {
+ Name string
+ Prefix string
+}
+
+func (r *Rule) Page() *Page {
+ if r.Parameters != nil {
+ return r.Parameters.Page()
+ }
+
+ return nil
+}
+
type RuleParameters struct {
Message *RuleMessage `json:"message"`
SendAs *RuleSender `json:"send_as"`
Trigger *RuleTrigger `json:"trigger"`
}
+func (rp *RuleParameters) Page() *Page {
+ if rp.Trigger != nil {
+ return rp.Trigger.Page()
+ }
+
+ return nil
+}
+
type RuleMessage struct {
FromFile *RuleMessageFile `json:"from_file"`
FromText string `json:"from_text"`
@@ -131,6 +157,14 @@ type RuleTrigger struct {
OnTimer *time.Duration `json:"on_timer"`
}
+func (rt *RuleTrigger) Page() *Page {
+ if rt.OnEvent != nil {
+ return rt.OnEvent.Page()
+ }
+
+ return nil
+}
+
type RuleTriggerCommand struct {
Command string `json:"command"`
Restrict *RuleTriggerCommandRestriction `json:"restrict"`
@@ -154,12 +188,49 @@ type RuleTriggerCommandRestrictionBypass struct {
}
type RuleTriggerEvent struct {
- OnFollow bool `json:"on_follow"`
- OnSubscribe bool `json:"on_subscribe"`
- OnRaid bool `json:"on_raid"`
- OnRant int `json:"on_rant"`
+ FromAccount *RuleTriggerEventAccount `json:"from_account"`
+ FromChannel *RuleTriggerEventChannel `json:"from_channel"`
+ FromLiveStream *RuleTriggerEventLiveStream `json:"from_live_stream"`
}
+func (rte *RuleTriggerEvent) Page() *Page {
+ switch {
+ case rte.FromAccount != nil:
+ return &Page{rte.FromAccount.Name, PrefixAccount}
+ case rte.FromChannel != nil:
+ return &Page{rte.FromChannel.Name, PrefixChannel}
+ default:
+ return nil
+ }
+}
+
+type RuleTriggerEventAccount struct {
+ Name string `json:"name"`
+ OnFollow *RuleTriggerEventAccountFollow `json:"on_follow"`
+}
+
+type RuleTriggerEventAccountFollow struct{}
+
+type RuleTriggerEventChannel struct {
+ Name string `json:"name"`
+ OnFollow *RuleTriggerEventChannelFollow `json:"on_follow"`
+}
+
+type RuleTriggerEventChannelFollow struct{}
+
+type RuleTriggerEventLiveStream struct {
+ OnRaid *RuleTriggerEventLiveStreamRaid `json:"on_raid"`
+ OnRant *RuleTriggerEventLiveStreamRant `json:"on_rant"`
+ OnSub *RuleTriggerEventLiveStreamSub `json:"on_sub"`
+}
+
+type RuleTriggerEventLiveStreamRaid struct{}
+type RuleTriggerEventLiveStreamRant struct {
+ MinAmount int `json:"min_amount"`
+ MaxAmount int `json:"max_amount"`
+}
+type RuleTriggerEventLiveStreamSub struct{}
+
func (rule *Rule) ToModelsChatbotRule() (*models.ChatbotRule, error) {
modelsRule := &models.ChatbotRule{
ID: rule.ID,
diff --git a/v1/internal/chatbot/runner.go b/v1/internal/chatbot/runner.go
index 6611003..0c79d8d 100644
--- a/v1/internal/chatbot/runner.go
+++ b/v1/internal/chatbot/runner.go
@@ -14,13 +14,14 @@ import (
)
type Runner struct {
- apiCh chan events.Api
+ apiCh chan events.ApiFollower
cancel context.CancelFunc
cancelMu sync.Mutex
channelID *int
channelIDMu sync.Mutex
chatCh chan events.Chat
client *rumblelivestreamlib.Client
+ page string
rule Rule
run runFunc
wails context.Context
@@ -61,29 +62,31 @@ func (r *Runner) chat(fields *chatFields) error {
return nil
}
-func (r *Runner) init() error {
- if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil {
- return fmt.Errorf("invalid rule")
- }
+// func (r *Runner) init() error {
+// if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil {
+// return fmt.Errorf("invalid rule")
+// }
- channelID, err := r.rule.Parameters.SendAs.ChannelIDInt()
- if err != nil {
- return fmt.Errorf("error converting channel ID to int: %v", err)
- }
+// channelID, err := r.rule.Parameters.SendAs.ChannelIDInt()
+// if err != nil {
+// return fmt.Errorf("error converting channel ID to int: %v", err)
+// }
- r.channelIDMu.Lock()
- r.channelID = channelID
- r.channelIDMu.Unlock()
+// r.channelIDMu.Lock()
+// r.channelID = channelID
+// r.channelIDMu.Unlock()
- switch {
- case r.rule.Parameters.Trigger.OnTimer != nil:
- r.run = r.runOnTimer
- case r.rule.Parameters.Trigger.OnCommand != nil:
- r.run = r.runOnCommand
- }
+// switch {
+// case r.rule.Parameters.Trigger.OnTimer != nil:
+// r.run = r.runOnTimer
+// case r.rule.Parameters.Trigger.OnEvent != nil:
+// r.run = r.runOnEvent
+// case r.rule.Parameters.Trigger.OnCommand != nil:
+// r.run = r.runOnCommand
+// }
- return nil
-}
+// return nil
+// }
type runFunc func(ctx context.Context) error
@@ -102,18 +105,18 @@ func (r *Runner) runOnCommand(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
- case event := <-r.chatCh:
+ case chat := <-r.chatCh:
now := time.Now()
if now.Sub(prev) < r.rule.Parameters.Trigger.OnCommand.Timeout*time.Second {
break
}
- if block := r.blockCommand(event); block {
- // if bypass := r.bypassCommand(event); !bypass {break}
+ if block := r.blockCommand(chat); block {
+ // if bypass := r.bypassCommand(chat); !bypass {break}
break
}
- err := r.handleCommand(event)
+ err := r.handleCommand(chat)
if err != nil {
return fmt.Errorf("error handling command: %v", err)
}
@@ -122,18 +125,18 @@ func (r *Runner) runOnCommand(ctx context.Context) error {
}
}
-func (r *Runner) blockCommand(event events.Chat) bool {
+func (r *Runner) blockCommand(chat events.Chat) bool {
if r.rule.Parameters.Trigger.OnCommand.Restrict == nil {
return false
}
if r.rule.Parameters.Trigger.OnCommand.Restrict.ToFollower &&
- !event.Message.IsFollower {
+ !chat.Message.IsFollower {
return true
}
subscriber := false
- for _, badge := range event.Message.Badges {
+ for _, badge := range chat.Message.Badges {
if badge == rumblelivestreamlib.ChatBadgeLocalsSupporter || badge == rumblelivestreamlib.ChatBadgeRecurringSubscription {
subscriber = true
}
@@ -144,24 +147,238 @@ func (r *Runner) blockCommand(event events.Chat) bool {
return true
}
- if event.Message.Rant < r.rule.Parameters.Trigger.OnCommand.Restrict.ToRant*100 {
+ if chat.Message.Rant < r.rule.Parameters.Trigger.OnCommand.Restrict.ToRant*100 {
return true
}
return false
}
-func (r *Runner) handleCommand(event events.Chat) error {
- displayName := event.Message.Username
- if event.Message.ChannelName != "" {
- displayName = event.Message.ChannelName
+func (r *Runner) handleCommand(chat events.Chat) error {
+ displayName := chat.Message.Username
+ if chat.Message.ChannelName != "" {
+ displayName = chat.Message.ChannelName
}
fields := &chatFields{
- ChannelName: event.Message.ChannelName,
+ ChannelName: chat.Message.ChannelName,
DisplayName: displayName,
- Username: event.Message.Username,
- Rant: event.Message.Rant / 100,
+ Username: chat.Message.Username,
+ Rant: chat.Message.Rant / 100,
+ }
+
+ err := r.chat(fields)
+ if err != nil {
+ return fmt.Errorf("error sending chat: %v", err)
+ }
+
+ return nil
+}
+
+func (r *Runner) runOnEventFromAccountOnFollow(ctx context.Context) error {
+ if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil {
+ return fmt.Errorf("invalid rule")
+ }
+ if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromAccount == nil || r.rule.Parameters.Trigger.OnEvent.FromAccount.OnFollow == nil {
+ return fmt.Errorf("event is nil")
+ }
+
+ for {
+ runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true)
+
+ select {
+ case <-ctx.Done():
+ return nil
+ case api := <-r.apiCh:
+ err := r.handleEventOnFollow(api)
+ if err != nil {
+ return fmt.Errorf("error handling event: %v", err)
+ }
+ }
+ }
+}
+
+func (r *Runner) runOnEventFromChannelOnFollow(ctx context.Context) error {
+ if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil {
+ return fmt.Errorf("invalid rule")
+ }
+ if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromChannel == nil || r.rule.Parameters.Trigger.OnEvent.FromChannel.OnFollow == nil {
+ return fmt.Errorf("event is nil")
+ }
+
+ for {
+ runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true)
+
+ select {
+ case <-ctx.Done():
+ return nil
+ case api := <-r.apiCh:
+ err := r.handleEventOnFollow(api)
+ if err != nil {
+ return fmt.Errorf("error handling event: %v", err)
+ }
+ }
+ }
+}
+
+func (r *Runner) handleEventOnFollow(follower events.ApiFollower) error {
+ fields := &chatFields{
+ DisplayName: follower.Username,
+ Username: follower.Username,
+ }
+
+ err := r.chat(fields)
+ if err != nil {
+ return fmt.Errorf("error sending chat: %v", err)
+ }
+
+ return nil
+}
+
+func (r *Runner) runOnEventFromLiveStreamOnRaid(ctx context.Context) error {
+ if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil {
+ return fmt.Errorf("invalid rule")
+ }
+ if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRaid == nil {
+ return fmt.Errorf("event is nil")
+ }
+
+ for {
+ runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true)
+
+ select {
+ case <-ctx.Done():
+ return nil
+ case chat := <-r.chatCh:
+ err := r.handleEventFromLiveStreamOnRaid(chat)
+ if err != nil {
+ return fmt.Errorf("error handling event: %v", err)
+ }
+ }
+ }
+}
+
+func (r *Runner) handleEventFromLiveStreamOnRaid(chat events.Chat) error {
+ if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil || r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRaid == nil {
+ return fmt.Errorf("invalid rule")
+ }
+
+ displayName := chat.Message.Username
+ if chat.Message.ChannelName != "" {
+ displayName = chat.Message.ChannelName
+ }
+
+ fields := &chatFields{
+ ChannelName: chat.Message.ChannelName,
+ DisplayName: displayName,
+ Username: chat.Message.Username,
+ Rant: chat.Message.Rant / 100,
+ }
+
+ err := r.chat(fields)
+ if err != nil {
+ return fmt.Errorf("error sending chat: %v", err)
+ }
+
+ return nil
+}
+
+func (r *Runner) runOnEventFromLiveStreamOnRant(ctx context.Context) error {
+ if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil {
+ return fmt.Errorf("invalid rule")
+ }
+ if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRant == nil {
+ return fmt.Errorf("event is nil")
+ }
+
+ for {
+ runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true)
+
+ select {
+ case <-ctx.Done():
+ return nil
+ case chat := <-r.chatCh:
+ err := r.handleEventFromLiveStreamOnRant(chat)
+ if err != nil {
+ return fmt.Errorf("error handling event: %v", err)
+ }
+ }
+ }
+}
+
+func (r *Runner) handleEventFromLiveStreamOnRant(chat events.Chat) error {
+ if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil || r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRant == nil {
+ return fmt.Errorf("invalid rule")
+ }
+
+ rant := chat.Message.Rant / 100
+ minAmount := r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRant.MinAmount
+ maxAmount := r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnRant.MaxAmount
+ if minAmount != 0 && rant < minAmount {
+ return nil
+ }
+ if maxAmount != 0 && rant > maxAmount {
+ return nil
+ }
+
+ displayName := chat.Message.Username
+ if chat.Message.ChannelName != "" {
+ displayName = chat.Message.ChannelName
+ }
+
+ fields := &chatFields{
+ ChannelName: chat.Message.ChannelName,
+ DisplayName: displayName,
+ Username: chat.Message.Username,
+ Rant: chat.Message.Rant / 100,
+ }
+
+ err := r.chat(fields)
+ if err != nil {
+ return fmt.Errorf("error sending chat: %v", err)
+ }
+
+ return nil
+}
+
+func (r *Runner) runOnEventFromLiveStreamOnSub(ctx context.Context) error {
+ if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil {
+ return fmt.Errorf("invalid rule")
+ }
+ if r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnSub == nil {
+ return fmt.Errorf("event is nil")
+ }
+
+ for {
+ runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true)
+
+ select {
+ case <-ctx.Done():
+ return nil
+ case chat := <-r.chatCh:
+ err := r.handleEventFromLiveStreamOnSub(chat)
+ if err != nil {
+ return fmt.Errorf("error handling event: %v", err)
+ }
+ }
+ }
+}
+
+func (r *Runner) handleEventFromLiveStreamOnSub(chat events.Chat) error {
+ if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil || r.rule.Parameters.Trigger.OnEvent == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream == nil || r.rule.Parameters.Trigger.OnEvent.FromLiveStream.OnSub == nil {
+ return fmt.Errorf("invalid rule")
+ }
+
+ displayName := chat.Message.Username
+ if chat.Message.ChannelName != "" {
+ displayName = chat.Message.ChannelName
+ }
+
+ fields := &chatFields{
+ ChannelName: chat.Message.ChannelName,
+ DisplayName: displayName,
+ Username: chat.Message.Username,
+ Rant: chat.Message.Rant / 100,
}
err := r.chat(fields)
diff --git a/v1/internal/events/api.go b/v1/internal/events/api.go
index 94702c7..1fbefa8 100644
--- a/v1/internal/events/api.go
+++ b/v1/internal/events/api.go
@@ -16,6 +16,10 @@ type Api struct {
Stop bool
}
+type ApiFollower struct {
+ Username string
+}
+
type apiProducer struct {
cancel context.CancelFunc
cancelMu sync.Mutex
diff --git a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go
index c839a48..b05e050 100644
--- a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go
+++ b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go
@@ -258,6 +258,15 @@ type ChatEventBlock struct {
Type string `json:"type"`
}
+type ChatEventNotification struct {
+ Badge string `json:"badge"`
+ Text string `json:"text"`
+}
+
+type ChatEventRaidNotification struct {
+ StartTs int64 `json:"start_ts"`
+}
+
type ChatEventRant struct {
Duration int `json:"duration"`
ExpiresOn string `json:"expires_on"`
@@ -265,13 +274,15 @@ type ChatEventRant struct {
}
type ChatEventMessage struct {
- Blocks []ChatEventBlock `json:"blocks"`
- ChannelID *int64 `json:"channel_id"`
- ID string `json:"id"`
- Rant *ChatEventRant `json:"rant"`
- Text string `json:"text"`
- Time string `json:"time"`
- UserID string `json:"user_id"`
+ Blocks []ChatEventBlock `json:"blocks"`
+ ChannelID *int64 `json:"channel_id"`
+ ID string `json:"id"`
+ Notification *ChatEventNotification `json:"notification"`
+ RaidNotification *ChatEventRaidNotification `json:"raid_notification"`
+ Rant *ChatEventRant `json:"rant"`
+ Text string `json:"text"`
+ Time string `json:"time"`
+ UserID string `json:"user_id"`
}
type ChatEventUser struct {
@@ -392,7 +403,9 @@ type ChatView struct {
ImageUrl string
Init bool
IsFollower bool
+ Raid bool
Rant int
+ Sub bool
Text string
Time time.Time
Type string
@@ -456,9 +469,17 @@ func parseMessages(eventType string, messages []ChatEventMessage, users map[stri
view.Color = user.Color
view.ImageUrl = user.Image1
view.IsFollower = user.IsFollower
+ if message.RaidNotification != nil {
+ view.Raid = true
+ }
if message.Rant != nil {
view.Rant = message.Rant.PriceCents
}
+ if message.Notification != nil {
+ if message.Notification.Badge == ChatBadgeRecurringSubscription {
+ view.Sub = true
+ }
+ }
view.Text = message.Text
t, err := time.Parse(time.RFC3339, message.Time)
if err != nil {
diff --git a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/client.go b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/client.go
index 71b25d3..c5652f2 100644
--- a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/client.go
+++ b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/client.go
@@ -273,31 +273,37 @@ func (c *Client) userLogout() error {
return nil
}
+type LoggedInResponseData struct {
+ Username string `json:"username"`
+}
+
type LoggedInResponseUser struct {
- LoggedIn bool `json:"logged_in"`
+ ID string `json:"id"`
+ LoggedIn bool `json:"logged_in"`
}
type LoggedInResponse struct {
+ Data LoggedInResponseData `json:"data"`
User LoggedInResponseUser `json:"user"`
}
-func (c *Client) LoggedIn() (bool, error) {
+func (c *Client) LoggedIn() (*LoggedInResponse, error) {
resp, err := c.httpClient.Get(urlUserLogin)
if err != nil {
- return false, pkgErr("error getting login service", err)
+ return nil, pkgErr("error getting login service", err)
}
defer resp.Body.Close()
bodyB, err := io.ReadAll(resp.Body)
if err != nil {
- return false, pkgErr("error reading body bytes", err)
+ return nil, pkgErr("error reading body bytes", err)
}
var lir LoggedInResponse
err = json.NewDecoder(strings.NewReader(string(bodyB))).Decode(&lir)
if err != nil {
- return false, pkgErr("error un-marshaling response body", err)
+ return nil, pkgErr("error un-marshaling response body", err)
}
- return lir.User.LoggedIn, nil
+ return &lir, nil
}
diff --git a/v1/vendor/modules.txt b/v1/vendor/modules.txt
index f4f7fed..bf1a8e9 100644
--- a/v1/vendor/modules.txt
+++ b/v1/vendor/modules.txt
@@ -77,7 +77,7 @@ github.com/tkrajina/go-reflector/reflector
# github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909
## explicit; go 1.16
github.com/tylertravisty/go-utils/random
-# github.com/tylertravisty/rumble-livestream-lib-go v0.7.2
+# github.com/tylertravisty/rumble-livestream-lib-go v0.9.0
## explicit; go 1.19
github.com/tylertravisty/rumble-livestream-lib-go
# github.com/valyala/bytebufferpool v1.0.0