Implemented chat bot; changed appicon; fixed issues

This commit is contained in:
tyler 2024-01-05 16:24:54 -05:00
parent 7d6b136662
commit ab3bcbb753
20 changed files with 1648 additions and 85 deletions

View file

@ -1,5 +1,11 @@
# Doing # Doing
New chat message modal:
- submit button in modal component (check for button on click and button text)
- primary button (save)
- secondary button (delete)
- on close: reset values (in stream chat message component)
Create loading indicator before API is called Create loading indicator before API is called
If api query returns error: If api query returns error:

198
app.go
View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/tylertravisty/rum-goggles/internal/api" "github.com/tylertravisty/rum-goggles/internal/api"
"github.com/tylertravisty/rum-goggles/internal/chatbot"
"github.com/tylertravisty/rum-goggles/internal/config" "github.com/tylertravisty/rum-goggles/internal/config"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go" rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
) )
@ -27,6 +28,8 @@ type App struct {
cfgMu sync.Mutex cfgMu sync.Mutex
api *api.Api api *api.Api
apiMu sync.Mutex apiMu sync.Mutex
cb *chatbot.ChatBot
cbMu sync.Mutex
logError *log.Logger logError *log.Logger
logInfo *log.Logger logInfo *log.Logger
} }
@ -126,6 +129,193 @@ func (a *App) AddChannel(url string) (*config.App, error) {
return a.cfg, nil return a.cfg, nil
} }
func (a *App) ChatBotMessages(cid string) (map[string]config.ChatMessage, error) {
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
channel, exists := a.cfg.Channels[cid]
if !exists {
a.logError.Println("channel does not exist:", cid)
return nil, fmt.Errorf("Cannot find channel. Try reloading.")
}
return channel.ChatBot.Messages, nil
}
func (a *App) AddChatMessage(cid string, asChannel bool, interval time.Duration, message string) (map[string]config.ChatMessage, error) {
var err error
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
_, err = a.cfg.NewChatMessage(cid, asChannel, interval, message)
if err != nil {
a.logError.Println("error creating new chat:", err)
return nil, fmt.Errorf("Error creating new chat message. Try again.")
}
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return nil, fmt.Errorf("Error saving chat message information. Try again.")
}
a.updateChatBotConfig(a.cfg.Channels[cid].ChatBot)
return a.cfg.Channels[cid].ChatBot.Messages, nil
}
func (a *App) DeleteChatMessage(mid string, cid string) (map[string]config.ChatMessage, error) {
a.cbMu.Lock()
if a.cb != nil {
err := a.cb.StopMessage(mid)
if err != nil {
a.logError.Println("error stopping chat bot message:", err)
return nil, fmt.Errorf("Error stopping message. Try Again.")
}
}
a.cbMu.Unlock()
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
err := a.cfg.DeleteChatMessage(mid, cid)
if err != nil {
a.logError.Println("error deleting chat message:", err)
return nil, fmt.Errorf("Error deleting chat message. Try again.")
}
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return nil, fmt.Errorf("Error saving chat message information. Try again.")
}
a.updateChatBotConfig(a.cfg.Channels[cid].ChatBot)
return a.cfg.Channels[cid].ChatBot.Messages, nil
}
func (a *App) UpdateChatMessage(id string, cid string, asChannel bool, interval time.Duration, message string) (map[string]config.ChatMessage, error) {
var err error
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
_, err = a.cfg.UpdateChatMessage(id, cid, asChannel, interval, message)
if err != nil {
a.logError.Println("error updating chat message:", err)
return nil, fmt.Errorf("Error updating chat message. Try again.")
}
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return nil, fmt.Errorf("Error saving chat message information. Try again.")
}
a.updateChatBotConfig(a.cfg.Channels[cid].ChatBot)
return a.cfg.Channels[cid].ChatBot.Messages, nil
}
func (a *App) NewChatBot(cid string, username string, password string, streamUrl string) error {
a.cbMu.Lock()
defer a.cbMu.Unlock()
if a.cb != nil {
err := a.resetChatBot()
if err != nil {
a.logError.Println("error resetting chat bot:", err)
return fmt.Errorf("Error creating chat bot. Try Again.")
}
}
channel, exists := a.cfg.Channels[cid]
if !exists {
a.logError.Println("channel does not exist:", cid)
return fmt.Errorf("Channel does not exist.")
}
var err error
a.cb, err = chatbot.NewChatBot(a.ctx, streamUrl, channel.ChatBot, a.logError)
if err != nil {
a.logError.Println("error creating new chat bot:", err)
return fmt.Errorf("Error creating new chat bot. Try Again.")
}
err = a.cb.Login(username, password)
if err != nil {
a.logError.Println("error logging into chat bot:", err)
return fmt.Errorf("Error logging in. Try Again.")
}
// a.cb = cb
return nil
}
func (a *App) ResetChatBot() error {
a.cbMu.Lock()
defer a.cbMu.Unlock()
err := a.resetChatBot()
if err != nil {
a.logError.Println("error resetting chat bot:", err)
return fmt.Errorf("Error resetting chat bot. Try Again.")
}
return nil
}
func (a *App) resetChatBot() error {
if a.cb == nil {
// return fmt.Errorf("chat bot is nil")
return nil
}
err := a.cb.StopAllMessages()
if err != nil {
return fmt.Errorf("error stopping all chat bot messages: %v", err)
}
err = a.cb.Logout()
if err != nil {
return fmt.Errorf("error logging out of chat bot: %v", err)
}
a.cb = nil
return nil
}
func (a *App) StartChatBotMessage(mid string) error {
a.cbMu.Lock()
defer a.cbMu.Unlock()
if a.cb == nil {
return fmt.Errorf("Chat bot not initialized.")
}
err := a.cb.StartMessage(mid)
if err != nil {
a.logError.Println("error starting chat bot message:", err)
return fmt.Errorf("Error starting message. Try Again.")
}
return nil
}
func (a *App) StopChatBotMessage(mid string) error {
a.cbMu.Lock()
defer a.cbMu.Unlock()
// If chat bot not initialized, then stop does nothing
if a.cb == nil {
return nil
}
err := a.cb.StopMessage(mid)
if err != nil {
a.logError.Println("error stopping chat bot message:", err)
return fmt.Errorf("Error stopping message. Try Again.")
}
return nil
}
func (a *App) StartApi(cid string) error { func (a *App) StartApi(cid string) error {
channel, found := a.cfg.Channels[cid] channel, found := a.cfg.Channels[cid]
if !found { if !found {
@ -145,3 +335,11 @@ func (a *App) StartApi(cid string) error {
func (a *App) StopApi() { func (a *App) StopApi() {
a.api.Stop() a.api.Stop()
} }
func (a *App) updateChatBotConfig(cfg config.ChatBot) {
a.cbMu.Lock()
defer a.cbMu.Unlock()
if a.cb != nil {
a.cb.Cfg = cfg
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -1,6 +1,7 @@
import eye from './eye.png'; import eye from './eye.png';
import eye_slash from './eye-slash.png'; import eye_slash from './eye-slash.png';
import gear from './gear.png'; import gear from './gear.png';
import gear_fill from './gear-fill.png';
import heart from './heart-fill.png'; import heart from './heart-fill.png';
import house from './house.png'; import house from './house.png';
import pause from './pause-fill.png'; import pause from './pause-fill.png';
@ -9,10 +10,12 @@ import plus_circle from './plus-circle-fill.png';
import star from './star-fill.png'; import star from './star-fill.png';
import thumbs_down from './hand-thumbs-down.png'; import thumbs_down from './hand-thumbs-down.png';
import thumbs_up from './hand-thumbs-up.png'; import thumbs_up from './hand-thumbs-up.png';
import x_lg from './x-lg.png';
export const Eye = eye; export const Eye = eye;
export const EyeSlash = eye_slash; export const EyeSlash = eye_slash;
export const Gear = gear; export const Gear = gear;
export const GearFill = gear_fill;
export const Heart = heart; export const Heart = heart;
export const House = house; export const House = house;
export const Pause = pause; export const Pause = pause;
@ -21,3 +24,4 @@ export const PlusCircle = plus_circle;
export const Star = star; export const Star = star;
export const ThumbsDown = thumbs_down; export const ThumbsDown = thumbs_down;
export const ThumbsUp = thumbs_up; export const ThumbsUp = thumbs_up;
export const XLg = x_lg;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,47 @@
.chat-bot-error {
border: 1px solid red;
box-sizing: border-box;
color: red;
font-family: monospace;
font-size: 16px;
padding: 5px;
text-align: center;
width: 100%;
}
.chat-bot-modal {
align-items: left;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
width: 100%;
}
.chat-bot-setting {
align-items: start;
display: flex;
flex-direction: column;
padding-top: 10px;
width: 100%;
}
.chat-bot-setting-label {
color: white;
font-family: sans-serif;
font-size: 20px;
padding-bottom: 5px;
width: 100%;
}
.chat-bot-setting-input {
border: none;
border-radius: 5px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}

View file

@ -0,0 +1,129 @@
import { useEffect, useState } from 'react';
import { Modal } from './Modal';
import { NewChatBot } from '../../wailsjs/go/main/App';
import './ChatBot.css';
export function ChatBotModal(props) {
const [error, setError] = useState('');
const [password, setPassword] = useState('');
const [saving, setSaving] = useState(false);
const updatePassword = (event) => setPassword(event.target.value);
const [url, setUrl] = useState('');
const updateUrl = (event) => setUrl(event.target.value);
const [username, setUsername] = useState('');
const updateUsername = (event) => setUsername(event.target.value);
useEffect(() => {
if (saving) {
// let user = username;
// let p = password;
// let u = url;
// props.onSubmit(user, p, u);
NewChatBot(props.cid, username, password, url)
.then(() => {
reset();
props.onClose();
})
.catch((error) => {
setSaving(false);
setError(error);
console.log('Error creating new chat bot:', error);
});
}
}, [saving]);
const reset = () => {
setError('');
setPassword('');
setSaving(false);
setUrl('');
setUsername('');
};
const close = () => {
reset();
props.onClose();
};
const submit = () => {
if (username === '') {
setError('Add username');
return;
}
if (password === '') {
setError('Add password');
return;
}
if (url === '') {
setError('Add stream URL');
return;
}
setSaving(true);
// let user = username;
// let p = password;
// let u = url;
// reset();
// props.onSubmit(user, p, u);
};
return (
<Modal
onClose={close}
show={props.show}
style={{ minWidth: '300px', maxWidth: '400px' }}
cancelButton={'Cancel'}
onCancel={close}
submitButton={saving ? 'Saving' : 'Save'}
onSubmit={
saving
? () => {
console.log('Saving');
}
: submit
}
title={'Chat Bot'}
>
<div className='chat-bot-modal'>
{error && <span className='chat-bot-error'>{error}</span>}
<div className='chat-bot-setting'>
<span className='chat-bot-setting-label'>Username</span>
<input
className='chat-bot-setting-input'
onChange={updateUsername}
placeholder='Username'
type='text'
value={username}
/>
</div>
<div className='chat-bot-setting'>
<span className='chat-bot-setting-label'>Password</span>
<input
className='chat-bot-setting-input'
onChange={updatePassword}
placeholder='Password'
type='password'
value={password}
/>
</div>
<div className='chat-bot-setting'>
<span className='chat-bot-setting-label'>Stream URL</span>
<input
className='chat-bot-setting-input'
onChange={updateUrl}
placeholder='https://'
type='text'
value={url}
/>
</div>
</div>
</Modal>
);
}
export function StreamChatMessageItem() {}

View file

@ -0,0 +1,176 @@
.modal-background {
align-items: center;
background-color: transparent;
display: flex;
height: 100vh;
justify-content: center;
left: 0;
position: absolute;
top: 0;
width: 100vw;
}
.modal-body {
align-items: center;
display: flex;
height: 80%;
justify-content: center;
width: 100%;
}
.modal-button {
background-color: #85c742;
border: none;
border-radius: 5px;
color: #061726;
cursor: pointer;
font-size: 18px;
font-weight: bold;
text-decoration: none;
/* width: 20%; */
width: 70px;
}
.modal-button-cancel {
background-color: transparent;
border: 1px solid #495a6a;
border-radius: 5px;
color: #495a6a;
cursor: pointer;
font-size: 18px;
font-weight: bold;
text-decoration: none;
/* width: 20%; */
width: 70px;
}
.modal-button-delete {
background-color: transparent;
border: 1px solid red;
border-radius: 5px;
color: red;
cursor: pointer;
font-size: 18px;
font-weight: bold;
text-decoration: none;
/* width: 20%; */
width: 70px;
}
.modal-close {
align-items: center;
background-color: transparent;
border: none;
display: flex;
flex-direction: center;
padding: 0px;
}
.modal-close:hover {
cursor: pointer;
}
.modal-close-icon {
height: 24px;
padding: 0px;
width: 24px;
}
.modal-container {
align-items: center;
background-color: rgba(6,23,38,1);
border: 1px solid #495a6a;
border-radius: 15px;
color: black;
display: flex;
flex-direction: column;
height: 50%;
justify-content: space-between;
opacity: 1;
padding: 10px 20px;
width: 50%;
}
.modal-footer {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 10%;
width: 100%;
}
.modal-header {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 10%;
width: 100%;
}
.modal-title {
color: white;
font-family: sans-serif;
font-size: 24px;
}
.small-modal-button-delete {
background-color: red;
border: none;
border-radius: 5px;
color: white;
cursor: pointer;
font-size: 18px;
font-weight: bold;
text-decoration: none;
/* width: 20%; */
width: 70px;
}
.small-modal-container {
align-items: center;
/* background-color: rgba(6,23,38,1); */
background-color: white;
border: 1px solid #495a6a;
/* border: 1px solid black; */
border-radius: 15px;
color: black;
display: flex;
flex-direction: column;
height: 50%;
justify-content: space-between;
opacity: 1;
padding: 10px 20px;
width: 50%;
}
.small-modal-header {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 10%;
width: 100%;
}
.small-modal-footer {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 20%;
width: 100%;
}
.small-modal-message {
font-family: sans-serif;
font-size: 18px;
}
.small-modal-title {
color: black;
font-family: sans-serif;
font-size: 24px;
}

View file

@ -0,0 +1,86 @@
import { XLg } from '../assets/icons';
import './Modal.css';
export function Modal(props) {
return (
<div
className='modal-background'
onClick={props.onClose}
style={{ zIndex: props.show ? 10 : -10 }}
>
<div
className='modal-container'
onClick={(event) => event.stopPropagation()}
style={props.style}
>
<div className='modal-header'>
<span className='modal-title'>{props.title}</span>
<button className='modal-close' onClick={props.onClose}>
<img className='modal-close-icon' src={XLg} />
</button>
</div>
<div className='modal-body'>{props.children}</div>
<div className='modal-footer'>
{props.cancelButton && (
<button className='modal-button-cancel' onClick={props.onCancel}>
{props.cancelButton}
</button>
)}
{props.deleteButton && (
<button className='modal-button-delete' onClick={props.onDelete}>
{props.deleteButton}
</button>
)}
{props.submitButton && (
<button className='modal-button' onClick={props.onSubmit}>
{props.submitButton}
</button>
)}
</div>
</div>
</div>
);
}
export function SmallModal(props) {
return (
<div
className='modal-background'
onClick={props.onClose}
style={{ zIndex: props.show ? 10 : -10 }}
>
<div
className='small-modal-container'
onClick={(event) => event.stopPropagation()}
style={props.style}
>
<div className='small-modal-header'>
<span className='small-modal-title'>{props.title}</span>
<button className='modal-close' onClick={props.onClose}>
<img className='modal-close-icon' src={XLg} />
</button>
</div>
<div className='modal-body'>
<span className='small-modal-message'>{props.message}</span>
</div>
<div className='small-modal-footer'>
{props.cancelButton && (
<button className='modal-button-cancel' onClick={props.onCancel}>
{props.cancelButton}
</button>
)}
{props.deleteButton && (
<button className='small-modal-button-delete' onClick={props.onDelete}>
{props.deleteButton}
</button>
)}
{props.submitButton && (
<button className='modal-button' onClick={props.onSubmit}>
{props.submitButton}
</button>
)}
</div>
</div>
</div>
);
}

View file

@ -3,24 +3,43 @@
height: 100%; height: 100%;
} }
.stream-chat-add-button { .stream-chat-button {
align-items: center; align-items: center;
background-color: rgba(6,23,38,1);
border: none; border: none;
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 0px; padding: 0px;
} }
.stream-chat-add-button:hover { .stream-chat-button:hover {
cursor: pointer; cursor: pointer;
} }
.stream-chat-add-icon { .stream-chat-button-title {
background-color: rgba(6,23,38,1);
}
.stream-chat-button-chat {
align-items: center;
background-color: #000312;
display: flex;
justify-content: center;
width: 10%;
}
.stream-chat-icon {
height: 24px; height: 24px;
width: 24px; width: 24px;
} }
.stream-chat-controls {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 55px;
}
.stream-chat-header { .stream-chat-header {
align-items: center; align-items: center;
background-color: rgba(6,23,38,1); background-color: rgba(6,23,38,1);
@ -33,6 +52,50 @@
text-align: left; text-align: left;
} }
.stream-chat-item {
border-bottom: 1px solid #82b1ff;
box-sizing: border-box;
color: white;
display: flex;
flex-direction: row;
font-family: sans-serif;
justify-content: space-between;
padding: 10px 20px;
width: 100%;
}
.stream-chat-item-sender {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: left;
padding-left: 10px;
width: 20%;
}
.stream-chat-item-interval {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: left;
padding-left: 10px;
width: 20%;
}
.stream-chat-item-message {
align-items: center;
display: flex;
justify-content: left;
overflow: hidden;
white-space: nowrap;
width: 50%;
}
.stream-chat-list {
overflow-y: auto;
height: calc(100vh - 84px - 40px - 179px);
}
.stream-chat-title { .stream-chat-title {
color: white; color: white;
font-family: sans-serif; font-family: sans-serif;

View file

@ -1,20 +1,182 @@
import { PlusCircle } from '../assets/icons'; import { useEffect, useState } from 'react';
import { StartChatBotMessage, StopChatBotMessage } from '../../wailsjs/go/main/App';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { GearFill, Pause, Play, PlusCircle } from '../assets/icons';
import './StreamChat.css'; import './StreamChat.css';
import { SmallModal } from './Modal';
function StreamChat(props) { function StreamChat(props) {
const sortChatsAlpha = () => {
let keys = Object.keys(props.chats);
let sorted = [...keys].sort((a, b) =>
props.chats[a].text.toLowerCase() > props.chats[b].text.toLowerCase() ? 1 : -1
);
return sorted;
};
return ( return (
<div className='stream-chat'> <div className='stream-chat'>
<div className='stream-chat-header'> <div className='stream-chat-header'>
<span className='stream-chat-title'>{props.title}</span> <span className='stream-chat-title'>{props.title}</span>
<div className='stream-chat-controls'>
<button <button
onClick={() => console.log('Add chat bot')} className='stream-chat-button stream-chat-button-title'
className='stream-chat-add-button' onClick={props.onAdd}
> >
<img className='stream-chat-add-icon' src={PlusCircle} /> <img className='stream-chat-icon' src={PlusCircle} />
</button> </button>
<button
className='stream-chat-button stream-chat-button-title'
onClick={props.onSettings}
>
<img className='stream-chat-icon' src={GearFill} />
</button>
</div>
</div>
<div className='stream-chat-list'>
{sortChatsAlpha().map((chat, index) => (
<StreamChatItem chat={props.chats[chat]} onItemClick={props.onEdit} />
))}
</div> </div>
</div> </div>
); );
} }
export default StreamChat; export default StreamChat;
function StreamChatItem(props) {
const [active, setActive] = useState(props.chat.active);
const [error, setError] = useState('');
const changeActive = (bool) => {
console.log('ChangeActive:', bool);
props.chat.active = bool;
setActive(bool);
};
useEffect(() => {
EventsOn('ChatBotMessageActive-' + props.chat.id, (mid) => {
console.log('ChatBotMessageActive', props.chat.id, mid);
if (mid === props.chat.id) {
changeActive(true);
}
});
EventsOn('ChatBotMessageError-' + props.chat.id, (mid) => {
console.log('ChatBotMessageError', props.chat.id, mid);
if (mid === props.chat.id) {
changeActive(false);
}
});
}, []);
const prependZero = (value) => {
if (value < 10) {
return '0' + value;
}
return '' + value;
};
const printInterval = (interval) => {
let hours = Math.floor(interval / 3600);
let minutes = Math.floor(interval / 60 - hours * 60);
let seconds = Math.floor(interval - hours * 3600 - minutes * 60);
// hours = prependZero(hours);
// minutes = prependZero(minutes);
// seconds = prependZero(seconds);
// return hours + ':' + minutes + ':' + seconds;
return hours + 'h ' + minutes + 'm ' + seconds + 's';
};
const intervalToTimer = (interval) => {
let hours = Math.floor(interval / 3600);
let minutes = Math.floor(interval / 60 - hours * 60);
let seconds = Math.floor(interval - hours * 3600 - minutes * 60);
if (minutes !== 0) {
seconds = prependZero(seconds);
}
if (hours !== 0) {
minutes = prependZero(minutes);
}
if (hours === 0) {
hours = '';
if (minutes === 0) {
minutes = '';
if (seconds === 0) {
seconds = '';
}
}
}
return hours + minutes + seconds;
};
const openChat = () => {
props.onItemClick(
props.chat.id,
props.chat.as_channel,
intervalToTimer(props.chat.interval),
props.chat.text
);
};
const startMessage = () => {
StartChatBotMessage(props.chat.id)
.then(() => {
changeActive(true);
})
.catch((error) => {
setError(error);
});
};
const stopMessage = () => {
StopChatBotMessage(props.chat.id).then(() => {
changeActive(false);
});
};
return (
<>
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
<div className='stream-chat-item' onClick={() => openChat()}>
<span className='stream-chat-item-message'>{props.chat.text}</span>
<span className='stream-chat-item-interval'>
{printInterval(props.chat.interval)}
</span>
<span className='stream-chat-item-sender'>
{props.chat.as_channel ? 'Channel' : 'User'}
</span>
<button
className='stream-chat-button stream-chat-button-chat'
onClick={(e) => {
e.stopPropagation();
console.log('message ID:', props.chat.id);
if (active) {
console.log('Stop message');
stopMessage();
} else {
console.log('Start message');
startMessage();
}
}}
>
<img className='stream-chat-icon' src={active ? Pause : Play} />
</button>
</div>
</>
);
}

View file

@ -1,4 +1,4 @@
.modal-chat { /* .modal-chat {
align-items: center; align-items: center;
background-color: red; background-color: red;
color: black; color: black;
@ -18,4 +18,170 @@
position: absolute; position: absolute;
top: 0; top: 0;
width: 100vw; width: 100vw;
} */
.chat-as-channel {
align-items: center;
display: flex;
justify-content: space-between;
padding-top: 10px;
width: 100%;
}
.chat-as-channel-label {
color: white;
font-family: sans-serif;
padding-right: 10px;
}
.chat-interval {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
padding-top: 10px;
width: 100%;
}
.chat-interval-input {
border: none;
border-radius: 34px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 5px 10px 5px 10px;
text-align: right;
}
.chat-interval-input-zero::placeholder {
text-align: center;
}
.chat-interval-input-value::placeholder {
color: black;
opacity: 1;
text-align: center;
}
.chat-interval-label {
color: white;
font-family: sans-serif;
padding-right: 10px;
}
.chat-options {
display: flex;
flex-direction: column;
width: 100%;
}
.stream-chat-message {
align-items: center;
color: white;
display: flex;
flex-direction: column;
font-family: sans-serif;
justify-content: start;
width: 100%;
}
.stream-chat-message-error {
border: 1px solid red;
box-sizing: border-box;
color: red;
font-family: monospace;
font-size: 16px;
padding: 5px;
text-align: center;
width: 100%;
}
.stream-chat-message-label {
padding: 5px 0px;
/* width: 50%; */
}
.stream-chat-message-modal {
align-items: left;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
width: 100%;
}
.stream-chat-message-textarea {
border: none;
border-radius: 5px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.stream-chat-message-title {
align-items: center;
display: flex;
flex-direction: row;
justify-content: start;
width: 100%;
}
.chat-as-channel-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.chat-as-channel-switch input {
opacity: 0;
width: 0;
height: 0;
}
.chat-as-channel-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #495a6a;
-webkit-transition: .4s;
transition: .4s;
}
.chat-as-channel-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .chat-as-channel-slider {
background-color: #85c742;
}
input:checked + .chat-as-channel-slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.chat-as-channel-slider.round {
border-radius: 34px;
}
.chat-as-channel-slider.round:before {
border-radius: 50%;
} }

View file

@ -1,12 +1,194 @@
import { useEffect, useState } from 'react';
import { Modal, SmallModal } from './Modal';
import './StreamChatMessage.css'; import './StreamChatMessage.css';
export function StreamChatMessageModal() { export function StreamChatMessageModal(props) {
const [asChannel, setAsChannel] = useState(props.asChannel);
const [openDelete, setOpenDelete] = useState(false);
const [error, setError] = useState('');
const [message, setMessage] = useState(props.message);
const updateMessage = (event) => setMessage(event.target.value);
const [timer, setTimer] = useState(props.interval);
useEffect(() => {
console.log('update chat');
setAsChannel(props.asChannel);
setError('');
setMessage(props.message);
setTimer(props.interval);
}, []);
const reset = () => {
setAsChannel(false);
setError('');
setMessage('');
setTimer('');
};
const close = () => {
reset();
props.onClose();
};
const submit = () => {
if (message === '') {
setError('Add message');
return;
}
if (timer === '') {
setError('Set timer');
return;
}
let ac = asChannel;
let msg = message;
let int = timerToInterval();
reset();
props.onSubmit(props.chatID, ac, int, msg);
};
const deleteMessage = () => {
if (props.chatID === '') {
close();
return;
}
setOpenDelete(true);
};
const confirmDelete = () => {
reset();
setOpenDelete(false);
props.onDelete(props.chatID);
};
const updateTimerBackspace = (e) => {
if (timer.length === 0) {
return;
}
if (e.keyCode === 8) {
setTimer(timer.substring(0, timer.length - 1));
}
};
const updateTimer = (e) => {
let nums = '0123456789';
let digit = e.target.value;
if (!nums.includes(digit)) {
return;
}
if (timer.length === 6) {
return;
}
if (timer.length === 0 && digit === '0') {
return;
}
setTimer(timer + digit);
};
const timerToInterval = () => {
let prefix = '0'.repeat(6 - timer.length);
let t = prefix + timer;
let hours = parseInt(t.substring(0, 2));
let minutes = parseInt(t.substring(2, 4));
let seconds = parseInt(t.substring(4, 6));
return hours * 3600 + minutes * 60 + seconds;
};
const printTimer = () => {
if (timer === '') {
return '00:00:00';
}
let prefix = '0'.repeat(6 - timer.length);
let t = prefix + timer;
return t.substring(0, 2) + ':' + t.substring(2, 4) + ':' + t.substring(4, 6);
};
const checkToggle = (e) => {
setAsChannel(e.target.checked);
};
return ( return (
<div className='modal-container'> <>
<div className='modal-chat'> <Modal
<span>hello world</span> onClose={close}
show={props.show}
style={{ minWidth: '300px', maxWidth: '400px' }}
cancelButton={props.chatID === '' ? 'Cancel' : ''}
onCancel={deleteMessage}
deleteButton={props.chatID === '' ? '' : 'Delete'}
onDelete={deleteMessage}
submitButton={'Save'}
onSubmit={submit}
title={'Chat Message'}
>
<div className='stream-chat-message-modal'>
<div className='stream-chat-message'>
{error && <span className='stream-chat-message-error'>{error}</span>}
<div className='stream-chat-message-title'>
<span className='stream-chat-message-label'>Message</span>
</div>
<textarea
className='stream-chat-message-textarea'
cols='25'
onChange={updateMessage}
rows='4'
value={message}
/>
</div>
<div className='chat-options'>
<div className='chat-interval'>
<span className='chat-interval-label'>Chat interval</span>
<input
className={
timer === ''
? 'chat-interval-input chat-interval-input-zero'
: 'chat-interval-input chat-interval-input-value'
}
onKeyDown={updateTimerBackspace}
onInput={updateTimer}
placeholder={printTimer()}
size='8'
type='text'
value={''}
/>
</div>
<div className='chat-as-channel'>
<span className='chat-as-channel-label'>Chat as channel</span>
<label className='chat-as-channel-switch'>
<input onChange={checkToggle} type='checkbox' checked={asChannel} />
<span className='chat-as-channel-slider round'></span>
</label>
</div> </div>
</div> </div>
</div>
</Modal>
<SmallModal
onClose={() => setOpenDelete(false)}
show={openDelete}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
cancelButton={'Cancel'}
onCancel={() => setOpenDelete(false)}
deleteButton={'Delete'}
message={
'Are you sure you want to delete this message? You cannot undo this action.'
}
onDelete={confirmDelete}
title={'Delete Message'}
/>
</>
); );
} }

View file

@ -1,10 +1,21 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Navigate, useLocation, useNavigate } from 'react-router-dom'; import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import { StartApi, StopApi } from '../../wailsjs/go/main/App'; import {
AddChatMessage,
ChatBotMessages,
DeleteChatMessage,
NewChatBot,
ResetChatBot,
StartApi,
StopApi,
StopChatBotMessage,
UpdateChatMessage,
} from '../../wailsjs/go/main/App';
import './Dashboard.css'; import './Dashboard.css';
import { EventsEmit, EventsOn } from '../../wailsjs/runtime/runtime'; import { EventsEmit, EventsOn } from '../../wailsjs/runtime/runtime';
import { Heart, Star } from '../assets/icons'; import { Heart, Star } from '../assets/icons';
import { ChatBotModal } from '../components/ChatBot';
import Highlight from '../components/Highlight'; import Highlight from '../components/Highlight';
import StreamEvent from '../components/StreamEvent'; import StreamEvent from '../components/StreamEvent';
import StreamActivity from '../components/StreamActivity'; import StreamActivity from '../components/StreamActivity';
@ -18,6 +29,13 @@ function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const [openChatBot, setOpenChatBot] = useState(false);
const [chatBotMessages, setChatBotMessages] = useState({});
const [chatAsChannel, setChatAsChannel] = useState(false);
const [chatID, setChatID] = useState('');
const [chatInterval, setChatInterval] = useState('');
const [chatMessage, setChatMessage] = useState('');
const [openChat, setOpenChat] = useState(false);
const [cid, setCID] = useState(location.state.cid); const [cid, setCID] = useState(location.state.cid);
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [channelName, setChannelName] = useState(''); const [channelName, setChannelName] = useState('');
@ -38,13 +56,17 @@ function Dashboard() {
const [streamTitle, setStreamTitle] = useState(''); const [streamTitle, setStreamTitle] = useState('');
const [watchingNow, setWatchingNow] = useState(0); const [watchingNow, setWatchingNow] = useState(0);
const [createdOn, setCreatedOn] = useState(''); const [createdOn, setCreatedOn] = useState('');
const [modalZ, setModalZ] = useState(false);
useEffect(() => { useEffect(() => {
console.log('use effect start'); console.log('use effect start');
// TODO: catch error
StartApi(cid); StartApi(cid);
setActive(true); setActive(true);
ChatBotMessages(cid).then((messages) => {
setChatBotMessages(messages);
});
EventsOn('QueryResponse', (response) => { EventsOn('QueryResponse', (response) => {
console.log('query response received'); console.log('query response received');
setRefresh(!refresh); setRefresh(!refresh);
@ -80,11 +102,14 @@ function Dashboard() {
const home = () => { const home = () => {
StopApi() StopApi()
.then(() => setActive(false)) .then(() => setActive(false))
.then(() => {
ResetChatBot();
})
.then(() => { .then(() => {
navigate(NavSignIn); navigate(NavSignIn);
}) })
.catch((err) => { .catch((error) => {
console.log('Stop error:', err); console.log('Stop error:', error);
}); });
}; };
@ -94,8 +119,8 @@ function Dashboard() {
.then(() => { .then(() => {
setActive(true); setActive(true);
}) })
.catch((err) => { .catch((error) => {
console.log('Start error:', err); console.log('Start error:', error);
}); });
}; };
@ -122,21 +147,93 @@ function Dashboard() {
return sorted; return sorted;
}; };
const openModal = () => { const newChat = () => {
setModalZ(true); setChatAsChannel(false);
setChatID('');
setChatInterval('');
setChatMessage('');
setOpenChat(true);
}; };
const closeModal = () => { const editChat = (id, asChannel, interval, message) => {
setModalZ(false); setChatAsChannel(asChannel);
setChatInterval(interval);
setChatMessage(message);
setChatID(id);
setOpenChat(true);
};
const deleteChat = (id) => {
setOpenChat(false);
if (id === '') {
return;
}
StopChatBotMessage(id, cid)
.then(() => {
DeleteChatMessage(id, cid)
.then((messages) => {
setChatBotMessages(messages);
})
.catch((error) => {
console.log('Error deleting message:', error);
});
})
.catch((error) => {
console.log('Error stopping message:', error);
});
};
const saveChat = (id, asChannel, interval, message) => {
setOpenChat(false);
if (id === '') {
AddChatMessage(cid, asChannel, interval, message)
.then((messages) => {
setChatBotMessages(messages);
})
.catch((error) => console.log('Error saving chat:', error));
return;
}
UpdateChatMessage(id, cid, asChannel, interval, message)
.then((messages) => {
console.log(messages);
setChatBotMessages(messages);
})
.catch((error) => console.log('Error saving chat:', error));
};
const saveChatBot = (username, password, url) => {
NewChatBot(cid, username, password, url)
.then(() => {
setOpenChatBot(false);
})
.catch((error) => console.log('Error creating new chat bot:', error));
}; };
return ( return (
<> <>
<StreamChatMessageModal /> {openChat && (
<div className='modal' style={{ zIndex: modalZ ? 10 : -10 }}> <StreamChatMessageModal
<span>show this instead</span> chatID={chatID}
<button onClick={closeModal}>close</button> asChannel={chatAsChannel}
</div> interval={chatInterval}
message={chatMessage}
onClose={() => setOpenChat(false)}
onDelete={deleteChat}
onSubmit={saveChat}
show={openChat}
/>
)}
{openChatBot && (
<ChatBotModal
cid={cid}
onClose={() => setOpenChatBot(false)}
onSubmit={saveChatBot}
show={openChatBot}
/>
)}
<div id='Dashboard'> <div id='Dashboard'>
<div className='header'> <div className='header'>
<div className='header-left'></div> <div className='header-left'></div>
@ -161,7 +258,14 @@ function Dashboard() {
<StreamActivity title={'Stream Activity'} events={activityEvents()} /> <StreamActivity title={'Stream Activity'} events={activityEvents()} />
</div> </div>
<div className='main-right'> <div className='main-right'>
<StreamChat title={'Stream Chat'} /> <StreamChat
chats={chatBotMessages}
onAdd={newChat}
onEdit={editChat}
onRefresh={() => setRefresh(!refresh)}
onSettings={() => setOpenChatBot(true)}
title={'Stream Chat'}
/>
</div> </div>
<div></div> <div></div>
</div> </div>
@ -176,7 +280,7 @@ function Dashboard() {
home={home} home={home}
play={startQuery} play={startQuery}
pause={stopQuery} pause={stopQuery}
settings={openModal} // settings={openModal}
/> />
</div> </div>
</> </>

View file

@ -5,8 +5,10 @@ import { AddChannel, Config } from '../../wailsjs/go/main/App';
import { Eye, EyeSlash } from '../assets/icons'; import { Eye, EyeSlash } from '../assets/icons';
import './SignIn.css'; import './SignIn.css';
import ChannelList from '../components/ChannelList'; import ChannelList from '../components/ChannelList';
import { SmallModal } from '../components/Modal';
function SignIn() { function SignIn() {
const [error, setError] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const [config, setConfig] = useState({ channels: {} }); const [config, setConfig] = useState({ channels: {} });
const [addChannelError, setAddChannelError] = useState(''); const [addChannelError, setAddChannelError] = useState('');
@ -20,9 +22,10 @@ function SignIn() {
.then((response) => { .then((response) => {
setConfig(response); setConfig(response);
}) })
.catch((err) => { .catch((error) => {
// TODO: display error to user // TODO: display error to user
console.log('error getting config', err); setError('Error loading config: ' + error);
console.log('error getting config', error);
}); });
}, []); }, []);
@ -33,9 +36,9 @@ function SignIn() {
setConfig(response); setConfig(response);
setStreamKey(''); setStreamKey('');
}) })
.catch((err) => { .catch((error) => {
console.log('error adding channel', err); console.log('error adding channel', error);
setAddChannelError(err); setAddChannelError(error);
}); });
}; };
@ -44,13 +47,28 @@ function SignIn() {
}; };
return ( return (
<>
{error !== '' && (
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
)}
<div id='SignIn'> <div id='SignIn'>
<div className='signin-header'> <div className='signin-header'>
<span className='signin-title-text'>Rum Goggles</span> <span className='signin-title-text'>Rum Goggles</span>
<span className='signin-title-subtext'>Rumble Stream Dashboard</span> <span className='signin-title-subtext'>Rumble Stream Dashboard</span>
</div> </div>
<div className='signin-center'> <div className='signin-center'>
<ChannelList channels={config.channels} openStreamDashboard={openStreamDashboard} /> <ChannelList
channels={config.channels}
openStreamDashboard={openStreamDashboard}
/>
</div> </div>
<div className='signin-input-box'> <div className='signin-input-box'>
<label className='signin-label'>Add Channel</label> <label className='signin-label'>Add Channel</label>
@ -82,6 +100,7 @@ function SignIn() {
</div> </div>
<div className='signin-footer'></div> <div className='signin-footer'></div>
</div> </div>
</>
); );
} }

4
go.mod
View file

@ -4,7 +4,7 @@ go 1.19
require ( require (
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909
github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231218182551-5ac1d6c01910 github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20240105170050-474340b082fc
github.com/wailsapp/wails/v2 v2.7.1 github.com/wailsapp/wails/v2 v2.7.1
) )
@ -25,7 +25,7 @@ require (
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/robertkrimen/otto v0.2.1 // indirect github.com/robertkrimen/otto v0.3.0 // indirect
github.com/samber/lo v1.38.1 // indirect github.com/samber/lo v1.38.1 // indirect
github.com/tkrajina/go-reflector v0.5.6 // indirect github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect

8
go.sum
View file

@ -44,8 +44,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= github.com/robertkrimen/otto v0.3.0 h1:5RI+8860NSxvXywDY9ddF5HcPw0puRsd8EgbXV0oqRE=
github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= github.com/robertkrimen/otto v0.3.0/go.mod h1:uW9yN1CYflmUQYvAMS0m+ZiNo3dMzRUDQJX0jWbzgxw=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -55,8 +55,8 @@ github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQ
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g= github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g=
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk= github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk=
github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231218182551-5ac1d6c01910 h1:pu5jBae9XZDF/G8YkCN7D5TMxOCsCO5+Jn1/lNPsOUY= github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20240105170050-474340b082fc h1:JaoanQiZrYIbDx0UAYTNpyjhsgx7eWlTD7KJRqnrC8A=
github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231218182551-5ac1d6c01910/go.mod h1:YrfW5N6xVozOzubzfNNsy+v0MIL2GPi9Kx3mTZ/Q9zI= github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20240105170050-474340b082fc/go.mod h1:YrfW5N6xVozOzubzfNNsy+v0MIL2GPi9Kx3mTZ/Q9zI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=

167
internal/chatbot/chatbot.go Normal file
View file

@ -0,0 +1,167 @@
package chatbot
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/tylertravisty/rum-goggles/internal/config"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type ChatBot struct {
ctx context.Context
client *rumblelivestreamlib.Client
Cfg config.ChatBot
logError *log.Logger
messages map[string]*message
messagesMu sync.Mutex
}
type message struct {
cancel context.CancelFunc
cancelMu sync.Mutex
asChannel bool
id string
interval time.Duration
text string
}
func NewChatBot(ctx context.Context, streamUrl string, cfg config.ChatBot, logError *log.Logger) (*ChatBot, error) {
client, err := rumblelivestreamlib.NewClient("", streamUrl)
if err != nil {
return nil, fmt.Errorf("chatbot: error creating new client: %v", err)
}
return &ChatBot{ctx: ctx, client: client, Cfg: cfg, logError: logError, messages: map[string]*message{}}, nil
}
func (cb *ChatBot) StartMessage(id string) error {
msg, exists := cb.Cfg.Messages[id]
if !exists {
return fmt.Errorf("chatbot: message does not exist")
}
cb.messagesMu.Lock()
defer cb.messagesMu.Unlock()
m, exists := cb.messages[id]
if exists {
m.stop()
delete(cb.messages, id)
}
m = &message{
asChannel: msg.AsChannel,
id: msg.ID,
interval: msg.Interval,
text: msg.Text,
}
ctx, cancel := context.WithCancel(context.Background())
m.cancelMu.Lock()
m.cancel = cancel
m.cancelMu.Unlock()
go cb.startMessage(ctx, m)
cb.messages[id] = m
return nil
}
func (cb *ChatBot) startMessage(ctx context.Context, m *message) {
for {
// TODO: if error, emit error to user, stop loop?
err := cb.chat(m)
if err != nil {
cb.logError.Println("error sending chat:", err)
cb.StopMessage(m.id)
runtime.EventsEmit(cb.ctx, "ChatBotMessageError-"+m.id, m.id)
// TODO: stop this loop?
} else {
runtime.EventsEmit(cb.ctx, "ChatBotMessageActive-"+m.id, m.id)
}
timer := time.NewTimer(m.interval * time.Second)
select {
case <-ctx.Done():
timer.Stop()
return
case <-timer.C:
}
}
}
func (cb *ChatBot) chat(m *message) error {
if cb.client == nil {
return fmt.Errorf("client is nil")
}
err := cb.client.Chat(m.asChannel, m.text)
if err != nil {
return fmt.Errorf("error sending chat: %v", err)
}
return nil
}
func (cb *ChatBot) StopAllMessages() error {
cb.messagesMu.Lock()
defer cb.messagesMu.Unlock()
for id, m := range cb.messages {
m.stop()
delete(cb.messages, id)
}
return nil
}
func (cb *ChatBot) StopMessage(id string) error {
cb.messagesMu.Lock()
defer cb.messagesMu.Unlock()
m, exists := cb.messages[id]
if exists {
fmt.Println("IT EXISTS!!")
m.stop()
delete(cb.messages, id)
}
return nil
}
func (m *message) stop() {
m.cancelMu.Lock()
if m.cancel != nil {
m.cancel()
}
m.cancelMu.Unlock()
}
func (cb *ChatBot) Login(username string, password string) error {
if cb.client == nil {
return fmt.Errorf("chatbot: client is nil")
}
err := cb.client.Login(username, password)
if err != nil {
return fmt.Errorf("chatbot: error logging in: %v", err)
}
return nil
}
func (cb *ChatBot) Logout() error {
if cb.client == nil {
return fmt.Errorf("chatbot: client is nil")
}
err := cb.client.Logout()
if err != nil {
return fmt.Errorf("chatbot: error logging out: %v", err)
}
return nil
}

View file

@ -73,13 +73,14 @@ func userDir() (string, error) {
} }
type ChatMessage struct { type ChatMessage struct {
ID string `json:"id"`
AsChannel bool `json:"as_channel"` AsChannel bool `json:"as_channel"`
Text string `json:"text"`
Interval time.Duration `json:"interval"` Interval time.Duration `json:"interval"`
Text string `json:"text"`
} }
type ChatBot struct { type ChatBot struct {
Messages []ChatMessage `json:"messages"` Messages map[string]ChatMessage `json:"messages"`
// Commands []ChatCommand // Commands []ChatCommand
} }
@ -99,12 +100,65 @@ func (a *App) NewChannel(url string, name string) (string, error) {
} }
if _, exists := a.Channels[id]; !exists { if _, exists := a.Channels[id]; !exists {
a.Channels[id] = Channel{id, url, name, DefaultInterval, ChatBot{[]ChatMessage{}}} a.Channels[id] = Channel{id, url, name, DefaultInterval, ChatBot{map[string]ChatMessage{}}}
return id, nil return id, nil
} }
} }
} }
func (a *App) DeleteChatMessage(id string, cid string) error {
channel, exists := a.Channels[cid]
if !exists {
return fmt.Errorf("config: channel does not exist")
}
_, exists = channel.ChatBot.Messages[id]
if !exists {
return fmt.Errorf("config: message does not exist")
}
delete(channel.ChatBot.Messages, id)
return nil
}
func (a *App) NewChatMessage(cid string, asChannel bool, interval time.Duration, message string) (string, error) {
if _, exists := a.Channels[cid]; !exists {
return "", fmt.Errorf("config: channel does not exist")
}
for {
id, err := random.String(CIDLen)
if err != nil {
return "", fmt.Errorf("config: error generating ID: %v", err)
}
if _, exists := a.Channels[cid].ChatBot.Messages[id]; !exists {
a.Channels[cid].ChatBot.Messages[id] = ChatMessage{id, asChannel, interval, message}
return id, nil
}
}
}
func (a *App) UpdateChatMessage(id string, cid string, asChannel bool, interval time.Duration, text string) (string, error) {
channel, exists := a.Channels[cid]
if !exists {
return "", fmt.Errorf("config: channel does not exist")
}
message, exists := channel.ChatBot.Messages[id]
if !exists {
return "", fmt.Errorf("config: message does not exist")
}
message.AsChannel = asChannel
message.Interval = interval
message.Text = text
channel.ChatBot.Messages[id] = message
return id, nil
}
type App struct { type App struct {
Channels map[string]Channel `json:"channels"` Channels map[string]Channel `json:"channels"`
} }