diff --git a/NOTES.md b/NOTES.md
index fe1d68f..ea5a2c9 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -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
diff --git a/app.go b/app.go
index b7ae59e..ac052b4 100644
--- a/app.go
+++ b/app.go
@@ -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)
+}
diff --git a/frontend/src/components/ChatMessage.css b/frontend/src/components/ChatMessage.css
new file mode 100644
index 0000000..5e5a270
--- /dev/null
+++ b/frontend/src/components/ChatMessage.css
@@ -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;
+}
\ No newline at end of file
diff --git a/frontend/src/components/ChatMessage.jsx b/frontend/src/components/ChatMessage.jsx
new file mode 100644
index 0000000..ca18b71
--- /dev/null
+++ b/frontend/src/components/ChatMessage.jsx
@@ -0,0 +1,28 @@
+import './ChatMessage.css';
+
+function ChatMessage(props) {
+ const upperInitial = () => {
+ return props.message.username[0].toUpperCase();
+ };
+
+ return (
+
+ {props.message.image === '' || props.message.image === undefined ? (
+
{upperInitial()}
+ ) : (
+
+ )}
+
+
+ {props.message.username}
+
+ {props.message.text}
+
+
+ );
+}
+
+export default ChatMessage;
diff --git a/frontend/src/components/Highlight.jsx b/frontend/src/components/Highlight.jsx
index 5d355ca..3d00a63 100644
--- a/frontend/src/components/Highlight.jsx
+++ b/frontend/src/components/Highlight.jsx
@@ -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;
diff --git a/frontend/src/components/StreamChat.css b/frontend/src/components/StreamChat.css
index 0560425..e75b20c 100644
--- a/frontend/src/components/StreamChat.css
+++ b/frontend/src/components/StreamChat.css
@@ -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);
diff --git a/frontend/src/components/StreamChat.jsx b/frontend/src/components/StreamChat.jsx
index 6394850..3414bb7 100644
--- a/frontend/src/components/StreamChat.jsx
+++ b/frontend/src/components/StreamChat.jsx
@@ -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 (
{props.title}
-
-
-
-
- {sortChatsAlpha().map((chat, index) => (
-
+ {messages.map((message, index) => (
+
))}
@@ -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 (
- <>
- setError('')}
- show={error !== ''}
- style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
- title={'Error'}
- message={error}
- submitButton={'OK'}
- onSubmit={() => setError('')}
- />
- openChat()}>
-
{props.chat.text}
-
- {printInterval(props.chat.interval)}
-
-
- {props.chat.as_channel ? 'Channel' : 'User'}
-
-
-
- >
- );
-}
diff --git a/frontend/src/components/StreamChatBot.css b/frontend/src/components/StreamChatBot.css
new file mode 100644
index 0000000..3526c82
--- /dev/null
+++ b/frontend/src/components/StreamChatBot.css
@@ -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;
+}
\ No newline at end of file
diff --git a/frontend/src/components/StreamChatBot.jsx b/frontend/src/components/StreamChatBot.jsx
new file mode 100644
index 0000000..012c4ba
--- /dev/null
+++ b/frontend/src/components/StreamChatBot.jsx
@@ -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 (
+
+
+
{props.title}
+
+
+
+
+
+
+ {sortChatsAlpha().map((chat, index) => (
+
+ ))}
+
+
+ );
+}
+
+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 (
+ <>
+ setError('')}
+ show={error !== ''}
+ style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
+ title={'Error'}
+ message={error}
+ submitButton={'OK'}
+ onSubmit={() => setError('')}
+ />
+ openChat()}>
+
+ {props.chat.text_file !== '' ? filename : props.chat.text}
+
+
+ {props.chat.on_command
+ ? props.chat.command
+ : printInterval(props.chat.interval)}
+
+
+ {props.chat.as_channel ? 'Channel' : 'User'}
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/components/StreamChatMessage.css b/frontend/src/components/StreamChatMessage.css
index 143efaf..02038e9 100644
--- a/frontend/src/components/StreamChatMessage.css
+++ b/frontend/src/components/StreamChatMessage.css
@@ -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%;
-}
\ No newline at end of file
+}
+
+.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;
+}
+
diff --git a/frontend/src/components/StreamChatMessage.jsx b/frontend/src/components/StreamChatMessage.jsx
index 6a29869..770d9af 100644
--- a/frontend/src/components/StreamChatMessage.jsx
+++ b/frontend/src/components/StreamChatMessage.jsx
@@ -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 (
<>
- {error &&
{error}}
+ {/* {error &&
{error}} */}
-
+ {readFromFile ? (
+
+
+
+
+
{textFile}
+
+ ) : (
+
+ )}
@@ -188,6 +309,15 @@ export function StreamChatMessageModal(props) {
onDelete={confirmDelete}
title={'Delete Message'}
/>
+
setError('')}
+ show={error !== ''}
+ style={{ minWidth: '300px', maxWidth: '300px', maxHeight: '100px' }}
+ title={'Error'}
+ message={error}
+ submitButton={'OK'}
+ onSubmit={() => setError('')}
+ />
>
);
}
diff --git a/frontend/src/screens/Dashboard.css b/frontend/src/screens/Dashboard.css
index 41750f6..d2ce342 100644
--- a/frontend/src/screens/Dashboard.css
+++ b/frontend/src/screens/Dashboard.css
@@ -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%;
}
diff --git a/frontend/src/screens/Dashboard.jsx b/frontend/src/screens/Dashboard.jsx
index 7e7036d..6ce791a 100644
--- a/frontend/src/screens/Dashboard.jsx
+++ b/frontend/src/screens/Dashboard.jsx
@@ -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() {
setOpenChat(false)}
onDelete={deleteChat}
onSubmit={saveChat}
show={openChat}
+ text={chatText}
+ textFile={chatTextFile}
/>
)}
{openChatBot && (
@@ -270,16 +285,18 @@ function Dashboard() {
+ {/*
+
+
*/}
- setOpenChatBot(true)}
- title={'Stream Chat'}
+ title={'Chat Bot'}
/>
-
setError('')}
show={error !== ''}
- style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
+ style={{ minWidth: '300px', maxWidth: '300px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
diff --git a/go.mod b/go.mod
index 9241309..4d8e1b5 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index 2aeca6d..6e3c9e9 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/internal/api/api.go b/internal/api/api.go
index 188c115..74c12a0 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -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 {
diff --git a/internal/chatbot/chatbot.go b/internal/chatbot/chatbot.go
index 6312462..c7ff7f4 100644
--- a/internal/chatbot/chatbot.go
+++ b/internal/chatbot/chatbot.go
@@ -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)
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 4a5a3dd..920386d 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -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
}
diff --git a/licenses/github.com/twbs/icons/LICENSE b/licenses/github.com/twbs/icons/LICENSE
new file mode 100644
index 0000000..47f06b6
--- /dev/null
+++ b/licenses/github.com/twbs/icons/LICENSE
@@ -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.