Added chat commands

This commit is contained in:
tyler 2024-01-30 12:24:07 -05:00
parent f40ba29179
commit 60b8dd7bab
20 changed files with 1039 additions and 336 deletions

View file

@ -1,10 +1,16 @@
# 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)
Show error when choosing file "chooseFile"
Show filename in chat bot list
Add styling to choose file button
Commands
- specify for follower/subscriber/locals only/rants
- check badges for subscriber and locals
Update
- github.com/rhysd/go-github-selfupdate
- github.com/inconshreveable/go-update
Create loading indicator before API is called

42
app.go
View file

@ -6,6 +6,7 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"sync"
"time"
@ -13,6 +14,7 @@ import (
"github.com/tylertravisty/rum-goggles/internal/chatbot"
"github.com/tylertravisty/rum-goggles/internal/config"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type chat struct {
@ -141,11 +143,11 @@ func (a *App) ChatBotMessages(cid string) (map[string]config.ChatMessage, error)
return channel.ChatBot.Messages, nil
}
func (a *App) AddChatMessage(cid string, asChannel bool, interval time.Duration, message string) (map[string]config.ChatMessage, error) {
func (a *App) AddChatMessage(cid string, asChannel bool, command string, interval time.Duration, onCommand bool, text string, textFile string) (map[string]config.ChatMessage, error) {
var err error
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
_, err = a.cfg.NewChatMessage(cid, asChannel, interval, message)
_, err = a.cfg.NewChatMessage(cid, asChannel, command, interval, onCommand, text, textFile)
if err != nil {
a.logError.Println("error creating new chat:", err)
return nil, fmt.Errorf("Error creating new chat message. Try again.")
@ -192,11 +194,11 @@ func (a *App) DeleteChatMessage(mid string, cid string) (map[string]config.ChatM
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) {
func (a *App) UpdateChatMessage(id string, cid string, asChannel bool, command string, interval time.Duration, onCommand bool, text string, textFile 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)
_, err = a.cfg.UpdateChatMessage(id, cid, asChannel, command, interval, onCommand, text, textFile)
if err != nil {
a.logError.Println("error updating chat message:", err)
return nil, fmt.Errorf("Error updating chat message. Try again.")
@ -240,7 +242,13 @@ func (a *App) NewChatBot(cid string, username string, password string, streamUrl
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.")
return fmt.Errorf("Error logging in. Try again.")
}
err = a.cb.StartChatStream()
if err != nil {
a.logError.Println("error starting chat stream:", err)
return fmt.Errorf("Error connecting to chat. Try again.")
}
// a.cb = cb
@ -271,6 +279,11 @@ func (a *App) resetChatBot() error {
return fmt.Errorf("error stopping all chat bot messages: %v", err)
}
err = a.cb.StopChatStream()
if err != nil {
return fmt.Errorf("error stopping chat stream: %v", err)
}
err = a.cb.Logout()
if err != nil {
return fmt.Errorf("error logging out of chat bot: %v", err)
@ -343,3 +356,22 @@ func (a *App) updateChatBotConfig(cfg config.ChatBot) {
a.cb.Cfg = cfg
}
}
func (a *App) OpenFileDialog() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
a.logError.Println("error getting home directory:", err)
return "", fmt.Errorf("Error opening file explorer. Try again.")
}
filepath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{DefaultDirectory: home})
if err != nil {
a.logError.Println("error opening file dialog:", err)
return "", fmt.Errorf("Error opening file explorer. Try again.")
}
return filepath, err
}
func (a *App) FilepathBase(path string) string {
return filepath.Base(path)
}

View file

@ -0,0 +1,44 @@
.chat-message {
align-items: start;
background-color: rgba(6,23,38,1);
padding: 10px;
display: flex;
flex-direction: row;
}
.chat-message-user-image {
border-radius: 50%;
height: 22px;
margin-right: 8px;
width: 22px;
}
.chat-message-user-initial {
align-items: center;
background-color: #37c;
border: 1px solid #eee;
border-radius: 50%;
color: #eee;
display: flex;
font-family: sans-serif;
font-size: 12px;
font-weight: bold;
height: 22px;
justify-content: center;
margin-right: 8px;
width: 22px;
}
.chat-message-username {
color: white;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-right: 3px;
}
.chat-message-text {
color: white;
font-family: sans-serif;
font-size: 14px;
}

View file

@ -0,0 +1,28 @@
import './ChatMessage.css';
function ChatMessage(props) {
const upperInitial = () => {
return props.message.username[0].toUpperCase();
};
return (
<div className='chat-message'>
{props.message.image === '' || props.message.image === undefined ? (
<span className='chat-message-user-initial'>{upperInitial()}</span>
) : (
<img className='chat-message-user-image' src={props.message.image} />
)}
<div>
<span
className='chat-message-username'
style={props.message.color && { color: props.message.color }}
>
{props.message.username}
</span>
<span className='chat-message-text'>{props.message.text}</span>
</div>
</div>
);
}
export default ChatMessage;

View file

@ -17,13 +17,11 @@ function Highlight(props) {
};
const stopwatchString = () => {
console.log(props.value);
if (isNaN(Date.parse(props.value))) {
return '--:--';
}
let now = new Date();
let date = new Date(props.value);
console.log(date);
let diff = now - date;
let msMinute = 1000 * 60;

View file

@ -3,43 +3,6 @@
height: 100%;
}
.stream-chat-button {
align-items: center;
border: none;
display: flex;
justify-content: center;
padding: 0px;
}
.stream-chat-button:hover {
cursor: pointer;
}
.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;
width: 24px;
}
.stream-chat-controls {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 55px;
}
.stream-chat-header {
align-items: center;
background-color: rgba(6,23,38,1);
@ -52,45 +15,6 @@
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);

View file

@ -1,42 +1,34 @@
import { useEffect, useState } from 'react';
import { StartChatBotMessage, StopChatBotMessage } from '../../wailsjs/go/main/App';
import { useState } from 'react';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { GearFill, Pause, Play, PlusCircle } from '../assets/icons';
import ChatMessage from './ChatMessage';
import './StreamChat.css';
import { SmallModal } from './Modal';
function StreamChat(props) {
const sortChatsAlpha = () => {
let keys = Object.keys(props.chats);
const [messages, setMessages] = useState([
{
color: '#ec131f',
image: 'https://ak2.rmbl.ws/z0/V/m/v/E/VmvEe.asF.4-18osof-s35kf7.jpeg',
username: 'tylertravisty',
text: 'Hello, world this is si s a a sdf asd f',
},
{
username: 'tylertravisty',
text: 'Another chat message',
},
]);
let sorted = [...keys].sort((a, b) =>
props.chats[a].text.toLowerCase() > props.chats[b].text.toLowerCase() ? 1 : -1
);
return sorted;
};
EventsOn('ChatMessage', (msg) => {
setMessages(...messages, msg);
});
return (
<div className='stream-chat'>
<div className='stream-chat-header'>
<span className='stream-chat-title'>{props.title}</span>
<div className='stream-chat-controls'>
<button
className='stream-chat-button stream-chat-button-title'
onClick={props.onAdd}
>
<img className='stream-chat-icon' src={PlusCircle} />
</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} />
{messages.map((message, index) => (
<ChatMessage message={message} />
))}
</div>
</div>
@ -44,139 +36,3 @@ function StreamChat(props) {
}
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

@ -0,0 +1,104 @@
.stream-chatbot {
width: 100%;
height: 100%;
}
.stream-chatbot-button {
align-items: center;
border: none;
display: flex;
justify-content: center;
padding: 0px;
}
.stream-chatbot-button:hover {
cursor: pointer;
}
.stream-chatbot-button-title {
background-color: rgba(6,23,38,1);
}
.stream-chatbot-button-chat {
align-items: center;
background-color: #000312;
display: flex;
justify-content: center;
width: 10%;
}
.stream-chatbot-icon {
height: 24px;
width: 24px;
}
.stream-chatbot-controls {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 55px;
}
.stream-chatbot-header {
align-items: center;
background-color: rgba(6,23,38,1);
border-bottom: 1px solid #495a6a;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 19px;
padding: 10px 20px;
text-align: left;
}
.stream-chatbot-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-chatbot-item-sender {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: left;
padding-left: 10px;
width: 20%;
}
.stream-chatbot-item-interval {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: left;
padding-left: 10px;
width: 20%;
}
.stream-chatbot-item-message {
align-items: center;
display: flex;
justify-content: left;
overflow: hidden;
white-space: nowrap;
width: 50%;
}
.stream-chatbot-list {
overflow-y: auto;
height: calc(100vh - 84px - 40px - 179px);
}
.stream-chatbot-title {
color: white;
font-family: sans-serif;
font-size: 12px;
font-weight: bold;
}

View file

@ -0,0 +1,212 @@
import { useEffect, useState } from 'react';
import { FilepathBase, StartChatBotMessage, StopChatBotMessage } from '../../wailsjs/go/main/App';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { GearFill, Pause, Play, PlusCircle } from '../assets/icons';
import './StreamChatBot.css';
import { SmallModal } from './Modal';
function StreamChatBot(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 (
<div className='stream-chatbot'>
<div className='stream-chatbot-header'>
<span className='stream-chatbot-title'>{props.title}</span>
<div className='stream-chatbot-controls'>
<button
className='stream-chatbot-button stream-chatbot-button-title'
onClick={props.onAdd}
>
<img className='stream-chatbot-icon' src={PlusCircle} />
</button>
<button
className='stream-chatbot-button stream-chatbot-button-title'
onClick={props.onSettings}
>
<img className='stream-chatbot-icon' src={GearFill} />
</button>
</div>
</div>
<div className='stream-chatbot-list'>
{sortChatsAlpha().map((chat, index) => (
<StreamChatItem chat={props.chats[chat]} onItemClick={props.onEdit} />
))}
</div>
</div>
);
}
export default StreamChatBot;
function StreamChatItem(props) {
const [active, setActive] = useState(props.chat.active);
const [error, setError] = useState('');
const [filename, setFilename] = useState('');
useEffect(() => {
if (props.chat.text_file !== '') {
FilepathBase(props.chat.text_file).then((name) => {
setFilename(name);
});
}
}, []);
const changeActive = (bool) => {
console.log('ChangeActive:', bool);
props.chat.active = bool;
setActive(bool);
};
useEffect(() => {
EventsOn('ChatBotCommandActive-' + props.chat.id, (mid) => {
console.log('ChatBotCommandActive', props.chat.id, mid);
if (mid === props.chat.id) {
changeActive(true);
}
});
EventsOn('ChatBotCommandError-' + props.chat.id, (mid) => {
console.log('ChatBotCommandError', props.chat.id, mid);
if (mid === props.chat.id) {
changeActive(false);
}
});
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,
props.chat.command,
intervalToTimer(props.chat.interval),
props.chat.on_command,
props.chat.text,
props.chat.text_file
);
};
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-chatbot-item' onClick={() => openChat()}>
<span className='stream-chatbot-item-message'>
{props.chat.text_file !== '' ? filename : props.chat.text}
</span>
<span className='stream-chatbot-item-interval'>
{props.chat.on_command
? props.chat.command
: printInterval(props.chat.interval)}
</span>
<span className='stream-chatbot-item-sender'>
{props.chat.as_channel ? 'Channel' : 'User'}
</span>
<button
className='stream-chatbot-button stream-chatbot-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-chatbot-icon' src={active ? Pause : Play} />
</button>
</div>
</>
);
}

View file

@ -20,7 +20,7 @@
width: 100vw;
} */
.chat-as-channel {
.chat-toggle {
align-items: center;
display: flex;
justify-content: space-between;
@ -28,12 +28,49 @@
width: 100%;
}
.chat-as-channel-label {
.chat-toggle-label {
color: white;
font-family: sans-serif;
padding-right: 10px;
}
.chat-command {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
padding-top: 10px;
width: 100%;
}
.chat-command-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;
width: 100%;
}
.chat-command-label {
color: white;
height: 29px;
}
.chat-command-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: center;
}
.chat-interval {
align-items: center;
display: flex;
@ -127,24 +164,31 @@
align-items: center;
display: flex;
flex-direction: row;
justify-content: start;
justify-content: space-between;
width: 100%;
}
.chat-as-channel-switch {
.stream-chat-message-title-right {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
}
.chat-toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.chat-as-channel-switch input {
.chat-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.chat-as-channel-slider {
.chat-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
@ -156,7 +200,7 @@
transition: .4s;
}
.chat-as-channel-slider:before {
.chat-toggle-slider:before {
position: absolute;
content: "";
height: 16px;
@ -168,20 +212,117 @@
transition: .4s;
}
input:checked + .chat-as-channel-slider {
input:checked + .chat-toggle-slider {
background-color: #85c742;
}
input:checked + .chat-as-channel-slider:before {
input:checked + .chat-toggle-slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.chat-as-channel-slider.round {
.chat-toggle-slider.round {
border-radius: 34px;
}
.chat-as-channel-slider.round:before {
.chat-toggle-slider.round:before {
border-radius: 50%;
}
}
.chat-toggle-check-container {
display: block;
position: relative;
padding-left: 16px;
margin-bottom: 15px;
cursor: pointer;
font-size: 15px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.chat-toggle-check-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.chat-toggle-check {
border-radius: 3px;
position: absolute;
top: 0;
left: 0;
height: 15px;
width: 15px;
background-color: #495a6a;
}
.chat-toggle-check-container:hover input ~ .chat-toggle-check {
background-color: #495a6a;
}
.chat-toggle-check-container input:checked ~ .chat-toggle-check {
background-color: #85c742;
}
.chat-toggle-check:after {
content: "";
position: absolute;
display: none;
}
.chat-toggle-check-container input:checked ~ .chat-toggle-check:after {
display: block;
}
.chat-toggle-check-container .chat-toggle-check:after {
left: 4px;
top: 1px;
width: 4px;
height: 8px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.chat-toggle-check-label {
padding-right: 5px;
}
.choose-file {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.choose-file-button-box {
min-width: 100px;
width: 100px;
}
.choose-file-button {
background-color: #85c742;
border: none;
border-radius: 5px;
color: #061726;
cursor: pointer;
font-size: 16px;
text-decoration: none;
/* width: 200px; */
width: 100%;
}
.choose-file-path {
overflow: scroll;
margin-left: 5px;
white-space: nowrap;
}

View file

@ -2,28 +2,41 @@ import { useEffect, useState } from 'react';
import { Modal, SmallModal } from './Modal';
import { OpenFileDialog } from '../../wailsjs/go/main/App';
import './StreamChatMessage.css';
export function StreamChatMessageModal(props) {
const [asChannel, setAsChannel] = useState(props.asChannel);
const [openDelete, setOpenDelete] = useState(false);
const [chatCommand, setChatCommand] = useState(props.chatCommand);
const [error, setError] = useState('');
const [message, setMessage] = useState(props.message);
const updateMessage = (event) => setMessage(event.target.value);
const [onCommand, setOnCommand] = useState(props.onCommand);
const [openDelete, setOpenDelete] = useState(false);
const [readFromFile, setReadFromFile] = useState(false);
const [text, setText] = useState(props.text);
const [textFile, setTextFile] = useState(props.textFile);
const updateText = (event) => setText(event.target.value);
const [timer, setTimer] = useState(props.interval);
useEffect(() => {
console.log('update chat');
setAsChannel(props.asChannel);
setOnCommand(props.onCommand);
setError('');
setMessage(props.message);
setReadFromFile(props.textFile !== '');
setText(props.text);
setTextFile(props.textFile);
setTimer(props.interval);
}, []);
const reset = () => {
setAsChannel(false);
setChatCommand(false);
setError('');
setMessage('');
setReadFromFile(false);
setText('');
setTextFile('');
setOnCommand(false);
setTimer('');
};
@ -33,21 +46,34 @@ export function StreamChatMessageModal(props) {
};
const submit = () => {
if (message === '') {
if (!readFromFile && text === '') {
setError('Add message');
return;
}
if (readFromFile && textFile === '') {
setError('Select file containing messages');
return;
}
if (timer === '') {
setError('Set timer');
return;
}
if (onCommand && chatCommand === '') {
setError('Add command');
return;
}
let ac = asChannel;
let msg = message;
let oc = onCommand;
let cmd = chatCommand;
let int = timerToInterval();
let txt = text;
let txtfile = textFile;
reset();
props.onSubmit(props.chatID, ac, int, msg);
props.onSubmit(props.chatID, ac, cmd, int, oc, txt, txtfile);
};
const deleteMessage = () => {
@ -65,6 +91,24 @@ export function StreamChatMessageModal(props) {
props.onDelete(props.chatID);
};
const updateChatCommand = (e) => {
let command = e.target.value;
if (command.length === 1) {
if (command !== '!') {
command = '!' + command;
}
}
command = command.toLowerCase();
let postfix = command.replace('!', '');
if (postfix !== '' && !/^[a-z0-9]+$/gi.test(postfix)) {
return;
}
setChatCommand(command);
};
const updateTimerBackspace = (e) => {
if (timer.length === 0) {
return;
@ -116,10 +160,31 @@ export function StreamChatMessageModal(props) {
return t.substring(0, 2) + ':' + t.substring(2, 4) + ':' + t.substring(4, 6);
};
const checkToggle = (e) => {
const checkChannelToggle = (e) => {
setAsChannel(e.target.checked);
};
const checkCommandToggle = (e) => {
setOnCommand(e.target.checked);
};
const checkReadFromFile = (e) => {
setReadFromFile(e.target.checked);
if (!e.target.checked) {
setTextFile('');
}
};
const chooseFile = () => {
OpenFileDialog()
.then((filepath) => {
if (filepath !== '') {
setTextFile(filepath);
}
})
.catch((error) => setError(error));
};
return (
<>
<Modal
@ -130,27 +195,52 @@ export function StreamChatMessageModal(props) {
onCancel={deleteMessage}
deleteButton={props.chatID === '' ? '' : 'Delete'}
onDelete={deleteMessage}
style={{ minHeight: '450px', maxWidth: '400px' }}
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>}
{/* {error && <span className='stream-chat-message-error'>{error}</span>} */}
<div className='stream-chat-message-title'>
<span className='stream-chat-message-label'>Message</span>
<div className='stream-chat-message-title-right'>
<span className='chat-toggle-check-label'>Read from file</span>
<label className='chat-toggle-check-container'>
<input
checked={readFromFile}
onChange={checkReadFromFile}
type='checkbox'
/>
<span className='chat-toggle-check'></span>
</label>
</div>
</div>
<textarea
className='stream-chat-message-textarea'
cols='25'
onChange={updateMessage}
rows='4'
value={message}
/>
{readFromFile ? (
<div className='choose-file'>
<div className='choose-file-button-box'>
<button className='choose-file-button' onClick={chooseFile}>
Choose file
</button>
</div>
<span className='choose-file-path'>{textFile}</span>
</div>
) : (
<textarea
className='stream-chat-message-textarea'
cols='25'
onChange={updateText}
rows='4'
value={text}
/>
)}
</div>
<div className='chat-options'>
<div className='chat-interval'>
<span className='chat-interval-label'>Chat interval</span>
<span className='chat-interval-label'>
{onCommand ? 'Command timeout' : 'Chat interval'}
</span>
<input
className={
timer === ''
@ -165,13 +255,44 @@ export function StreamChatMessageModal(props) {
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>
<div className='chat-toggle'>
<span className='chat-toggle-label'>Chat as channel</span>
<label className='chat-toggle-switch'>
<input
onChange={checkChannelToggle}
type='checkbox'
checked={asChannel}
/>
<span className='chat-toggle-slider round'></span>
</label>
</div>
<div className='chat-toggle'>
<span className='chat-toggle-label'>Chat on command</span>
<label className='chat-toggle-switch'>
<input
onChange={checkCommandToggle}
type='checkbox'
checked={onCommand}
/>
<span className='chat-toggle-slider round'></span>
</label>
</div>
{onCommand ? (
<div className='chat-command'>
<input
className='chat-command-input'
onInput={updateChatCommand}
placeholder={'!command'}
size='8'
type='text'
value={chatCommand}
/>
</div>
) : (
<div className='chat-command'>
<span className='chat-command-label'>{'\u00A0'}</span>
</div>
)}
</div>
</div>
</Modal>
@ -188,6 +309,15 @@ export function StreamChatMessageModal(props) {
onDelete={confirmDelete}
title={'Delete Message'}
/>
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '300px', maxHeight: '100px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
</>
);
}

View file

@ -37,12 +37,18 @@
.main-left {
border-right: 1px solid #495a6a;
width: 30%;
width: 33%;
height: 100%;
}
.main-middle {
border-right: 1px solid #495a6a;
width: 33%;
height: 100%;
}
.main-right {
width: 70%;
width: 67%;
height: 100%;
}

View file

@ -21,9 +21,10 @@ import { SmallModal } from '../components/Modal';
import StreamEvent from '../components/StreamEvent';
import StreamActivity from '../components/StreamActivity';
import StreamChat from '../components/StreamChat';
import StreamChatBot from '../components/StreamChatBot';
import StreamInfo from '../components/StreamInfo';
import { NavSignIn } from './Navigation';
import { StreamChatMessageItem, StreamChatMessageModal } from '../components/StreamChatMessage';
import { StreamChatMessageModal } from '../components/StreamChatMessage';
function Dashboard() {
const location = useLocation();
@ -34,9 +35,12 @@ function Dashboard() {
const [openChatBot, setOpenChatBot] = useState(false);
const [chatBotMessages, setChatBotMessages] = useState({});
const [chatAsChannel, setChatAsChannel] = useState(false);
const [chatCommand, setChatCommand] = useState('');
const [chatOnCommand, setChatOnCommand] = useState(false);
const [chatID, setChatID] = useState('');
const [chatInterval, setChatInterval] = useState('');
const [chatMessage, setChatMessage] = useState('');
const [chatText, setChatText] = useState('');
const [chatTextFile, setChatTextFile] = useState('');
const [openChat, setOpenChat] = useState(false);
const [cid, setCID] = useState(location.state.cid);
const [username, setUsername] = useState('');
@ -66,6 +70,7 @@ function Dashboard() {
setActive(true);
ChatBotMessages(cid).then((messages) => {
console.log(messages);
setChatBotMessages(messages);
});
@ -154,17 +159,23 @@ function Dashboard() {
const newChat = () => {
setChatAsChannel(false);
setChatCommand('');
setChatID('');
setChatInterval('');
setChatMessage('');
setChatText('');
setChatTextFile('');
setChatOnCommand(false);
setOpenChat(true);
};
const editChat = (id, asChannel, interval, message) => {
const editChat = (id, asChannel, command, interval, onCommand, text, textFile) => {
setChatAsChannel(asChannel);
setChatInterval(interval);
setChatMessage(message);
setChatCommand(command);
setChatID(id);
setChatInterval(interval);
setChatOnCommand(onCommand);
setChatText(text);
setChatTextFile(textFile);
setOpenChat(true);
};
@ -191,10 +202,11 @@ function Dashboard() {
});
};
const saveChat = (id, asChannel, interval, message) => {
const saveChat = (id, asChannel, command, interval, onCommand, text, textFile) => {
console.log('save chat textfile:', textFile);
setOpenChat(false);
if (id === '') {
AddChatMessage(cid, asChannel, interval, message)
AddChatMessage(cid, asChannel, command, interval, onCommand, text, textFile)
.then((messages) => {
setChatBotMessages(messages);
})
@ -206,7 +218,7 @@ function Dashboard() {
return;
}
UpdateChatMessage(id, cid, asChannel, interval, message)
UpdateChatMessage(id, cid, asChannel, command, interval, onCommand, text, textFile)
.then((messages) => {
console.log(messages);
setChatBotMessages(messages);
@ -231,12 +243,15 @@ function Dashboard() {
<StreamChatMessageModal
chatID={chatID}
asChannel={chatAsChannel}
chatCommand={chatCommand}
onCommand={chatOnCommand}
interval={chatInterval}
message={chatMessage}
onClose={() => setOpenChat(false)}
onDelete={deleteChat}
onSubmit={saveChat}
show={openChat}
text={chatText}
textFile={chatTextFile}
/>
)}
{openChatBot && (
@ -270,16 +285,18 @@ function Dashboard() {
<div className='main-left'>
<StreamActivity title={'Stream Activity'} events={activityEvents()} />
</div>
{/* <div className='main-middle'>
<StreamChat title={'Stream Chat'} />
</div> */}
<div className='main-right'>
<StreamChat
<StreamChatBot
chats={chatBotMessages}
onAdd={newChat}
onEdit={editChat}
onSettings={() => setOpenChatBot(true)}
title={'Stream Chat'}
title={'Chat Bot'}
/>
</div>
<div></div>
</div>
<StreamInfo
active={active}

View file

@ -52,7 +52,7 @@ function SignIn() {
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
style={{ minWidth: '300px', maxWidth: '300px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}

10
go.mod
View file

@ -4,7 +4,7 @@ go 1.19
require (
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909
github.com/tylertravisty/rumble-livestream-lib-go v0.1.0
github.com/tylertravisty/rumble-livestream-lib-go v0.2.0
github.com/wailsapp/wails/v2 v2.7.1
)
@ -24,6 +24,7 @@ require (
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/r3labs/sse/v2 v2.10.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/robertkrimen/otto v0.3.0 // indirect
github.com/samber/lo v1.38.1 // indirect
@ -32,10 +33,11 @@ require (
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.10 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
)

24
go.sum
View file

@ -41,6 +41,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0=
github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I=
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/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@ -55,8 +57,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.1.0 h1:HQxuRkkA0iN2XyfK3KfoeBFonWCHZt8cy4GVKAiIIeM=
github.com/tylertravisty/rumble-livestream-lib-go v0.1.0/go.mod h1:YrfW5N6xVozOzubzfNNsy+v0MIL2GPi9Kx3mTZ/Q9zI=
github.com/tylertravisty/rumble-livestream-lib-go v0.2.0 h1:sOXTZKBeB9PN3xfVSVYiILhMQdBQ2OiLMq+k70x5yb0=
github.com/tylertravisty/rumble-livestream-lib-go v0.2.0/go.mod h1:CACpHQV9xQqBKB7C13tUkL7O8Neb35+dJzRV1N211s4=
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=
@ -68,13 +70,16 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.7.1 h1:HAzp2c5ODOzsLC6ZMDVtNOB72ozM7/SJecJPB2Ur+UU=
github.com/wailsapp/wails/v2 v2.7.1/go.mod h1:oIJVwwso5fdOgprBYWXBBqtx6PaSvxg8/KTQHNGkadc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -85,13 +90,16 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=

View file

@ -80,7 +80,7 @@ func (a *Api) start(ctx context.Context, url string, interval time.Duration) {
}
func (a *Api) query(url string) {
a.logInfo.Println("QueryAPI")
// a.logInfo.Println("QueryAPI")
client := rumblelivestreamlib.Client{StreamKey: url}
resp, err := client.Request()
if err != nil {

View file

@ -1,9 +1,14 @@
package chatbot
import (
"bufio"
"context"
"crypto/rand"
"fmt"
"log"
"math/big"
"os"
"strings"
"sync"
"time"
@ -15,6 +20,8 @@ import (
type ChatBot struct {
ctx context.Context
client *rumblelivestreamlib.Client
commands map[string]chan rumblelivestreamlib.ChatView
commandsMu sync.Mutex
Cfg config.ChatBot
logError *log.Logger
messages map[string]*message
@ -22,12 +29,15 @@ type ChatBot struct {
}
type message struct {
cancel context.CancelFunc
cancelMu sync.Mutex
asChannel bool
id string
interval time.Duration
text string
cancel context.CancelFunc
cancelMu sync.Mutex
asChannel bool
command string
id string
interval time.Duration
onCommand bool
text string
textFromFile []string
}
func NewChatBot(ctx context.Context, streamUrl string, cfg config.ChatBot, logError *log.Logger) (*ChatBot, error) {
@ -36,7 +46,7 @@ func NewChatBot(ctx context.Context, streamUrl string, cfg config.ChatBot, logEr
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
return &ChatBot{ctx: ctx, client: client, Cfg: cfg, commands: map[string]chan rumblelivestreamlib.ChatView{}, logError: logError, messages: map[string]*message{}}, nil
}
func (cb *ChatBot) StartMessage(id string) error {
@ -53,24 +63,82 @@ func (cb *ChatBot) StartMessage(id string) error {
delete(cb.messages, id)
}
textFromFile := []string{}
if msg.TextFile != "" {
file, err := os.Open(msg.TextFile)
if err != nil {
return fmt.Errorf("chatbot: error opening file with responses: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
textFromFile = append(textFromFile, line)
}
}
m = &message{
asChannel: msg.AsChannel,
id: msg.ID,
interval: msg.Interval,
text: msg.Text,
asChannel: msg.AsChannel,
command: msg.Command,
id: msg.ID,
interval: msg.Interval,
onCommand: msg.OnCommand,
text: msg.Text,
textFromFile: textFromFile,
}
ctx, cancel := context.WithCancel(context.Background())
m.cancelMu.Lock()
m.cancel = cancel
m.cancelMu.Unlock()
go cb.startMessage(ctx, m)
if msg.OnCommand {
go cb.startCommand(ctx, m)
} else {
go cb.startMessage(ctx, m)
}
cb.messages[id] = m
return nil
}
// TODO: lock commands map, update commands map with channel, unlock commands map
func (cb *ChatBot) startCommand(ctx context.Context, m *message) {
cb.commandsMu.Lock()
ch := make(chan rumblelivestreamlib.ChatView)
cb.commands[m.command] = ch
cb.commandsMu.Unlock()
var prev time.Time
for {
// TODO: if error, emit error to user, stop loop?
select {
case <-ctx.Done():
return
case <-ch:
// TODO: parse !command
now := time.Now()
if now.Sub(prev) < m.interval*time.Second {
break
}
err := cb.chat(m)
if err != nil {
cb.logError.Println("error sending chat:", err)
cb.StopMessage(m.id)
runtime.EventsEmit(cb.ctx, "ChatBotCommandError-"+m.id, m.id)
} else {
prev = now
runtime.EventsEmit(cb.ctx, "ChatBotCommandActive-"+m.id, m.id)
}
}
}
}
func (cb *ChatBot) startMessage(ctx context.Context, m *message) {
for {
// TODO: if error, emit error to user, stop loop?
@ -99,7 +167,17 @@ func (cb *ChatBot) chat(m *message) error {
return fmt.Errorf("client is nil")
}
err := cb.client.Chat(m.asChannel, m.text)
text := m.text
if len(m.textFromFile) > 0 {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(m.textFromFile))))
if err != nil {
return fmt.Errorf("error generating random number: %v", err)
}
text = m.textFromFile[n.Int64()]
}
err := cb.client.Chat(m.asChannel, text)
if err != nil {
return fmt.Errorf("error sending chat: %v", err)
}
@ -114,6 +192,16 @@ func (cb *ChatBot) StopAllMessages() error {
for id, m := range cb.messages {
m.stop()
delete(cb.messages, id)
if m.command != "" && m.onCommand {
cb.commandsMu.Lock()
defer cb.commandsMu.Unlock()
ch, exists := cb.commands[m.command]
if exists {
close(ch)
delete(cb.commands, m.command)
}
}
}
return nil
@ -122,10 +210,21 @@ func (cb *ChatBot) StopAllMessages() error {
func (cb *ChatBot) StopMessage(id string) error {
cb.messagesMu.Lock()
defer cb.messagesMu.Unlock()
m, exists := cb.messages[id]
if exists {
m.stop()
delete(cb.messages, id)
if m.command != "" && m.onCommand {
cb.commandsMu.Lock()
defer cb.commandsMu.Unlock()
ch, exists := cb.commands[m.command]
if exists {
close(ch)
delete(cb.commands, m.command)
}
}
}
return nil
@ -164,3 +263,64 @@ func (cb *ChatBot) Logout() error {
return nil
}
func (cb *ChatBot) StartChatStream() error {
if cb.client == nil {
return fmt.Errorf("chatbot: client is nil")
}
err := cb.client.ChatInfo()
if err != nil {
return fmt.Errorf("chatbot: error getting chat info: %v", err)
}
err = cb.client.StartChatStream(cb.handleChat, cb.handleError)
if err != nil {
return fmt.Errorf("chatbot: error starting chat stream: %v", err)
}
return nil
}
func (cb *ChatBot) StopChatStream() error {
if cb.client == nil {
return fmt.Errorf("chatbot: client is nil")
}
// TODO: should a panic be caught here?
cb.client.StopChatStream()
return nil
}
func (cb *ChatBot) handleChat(cv rumblelivestreamlib.ChatView) {
// runtime.EventsEmit(cb.ctx, "ChatMessageReceived", cv)
if cv.Type != "init" {
cb.handleCommand(cv)
}
}
func (cb *ChatBot) handleCommand(cv rumblelivestreamlib.ChatView) {
cb.commandsMu.Lock()
defer cb.commandsMu.Unlock()
words := strings.Split(cv.Text, " ")
first := words[0]
cmd, exists := cb.commands[first]
if !exists {
return
}
select {
case cmd <- cv:
return
default:
return
}
}
func (cb *ChatBot) handleError(err error) {
cb.logError.Println("error handling chat message:", err)
// runtime.EventsEmit(cb.ctx, "ChatError", err)
}

View file

@ -75,8 +75,11 @@ func userDir() (string, error) {
type ChatMessage struct {
ID string `json:"id"`
AsChannel bool `json:"as_channel"`
Command string `json:"command"`
Interval time.Duration `json:"interval"`
OnCommand bool `json:"on_command"`
Text string `json:"text"`
TextFile string `json:"text_file"`
}
type ChatBot struct {
@ -122,7 +125,7 @@ func (a *App) DeleteChatMessage(id string, cid string) error {
return nil
}
func (a *App) NewChatMessage(cid string, asChannel bool, interval time.Duration, message string) (string, error) {
func (a *App) NewChatMessage(cid string, asChannel bool, command string, interval time.Duration, onCommand bool, text string, textFile string) (string, error) {
if _, exists := a.Channels[cid]; !exists {
return "", fmt.Errorf("config: channel does not exist")
}
@ -134,27 +137,38 @@ func (a *App) NewChatMessage(cid string, asChannel bool, interval time.Duration,
}
if _, exists := a.Channels[cid].ChatBot.Messages[id]; !exists {
a.Channels[cid].ChatBot.Messages[id] = ChatMessage{id, asChannel, interval, message}
a.Channels[cid].ChatBot.Messages[id] = ChatMessage{
ID: id,
AsChannel: asChannel,
Command: command,
Interval: interval,
OnCommand: onCommand,
Text: text,
TextFile: textFile,
}
return id, nil
}
}
}
func (a *App) UpdateChatMessage(id string, cid string, asChannel bool, interval time.Duration, text string) (string, error) {
func (a *App) UpdateChatMessage(id string, cid string, asChannel bool, command string, interval time.Duration, onCommand bool, text string, textFile string) (string, error) {
channel, exists := a.Channels[cid]
if !exists {
return "", fmt.Errorf("config: channel does not exist")
}
message, exists := channel.ChatBot.Messages[id]
cm, 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
cm.AsChannel = asChannel
cm.Command = command
cm.Interval = interval
cm.OnCommand = onCommand
cm.Text = text
cm.TextFile = textFile
channel.ChatBot.Messages[id] = cm
return id, nil
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019-2021 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.