diff --git a/NOTES.md b/NOTES.md
index 9b49b98..fe1d68f 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -1,5 +1,11 @@
# Doing
+New chat message modal:
+- submit button in modal component (check for button on click and button text)
+ - primary button (save)
+ - secondary button (delete)
+- on close: reset values (in stream chat message component)
+
Create loading indicator before API is called
If api query returns error:
diff --git a/app.go b/app.go
index 464e85e..b7ae59e 100644
--- a/app.go
+++ b/app.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/tylertravisty/rum-goggles/internal/api"
+ "github.com/tylertravisty/rum-goggles/internal/chatbot"
"github.com/tylertravisty/rum-goggles/internal/config"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
)
@@ -27,6 +28,8 @@ type App struct {
cfgMu sync.Mutex
api *api.Api
apiMu sync.Mutex
+ cb *chatbot.ChatBot
+ cbMu sync.Mutex
logError *log.Logger
logInfo *log.Logger
}
@@ -126,6 +129,193 @@ func (a *App) AddChannel(url string) (*config.App, error) {
return a.cfg, nil
}
+func (a *App) ChatBotMessages(cid string) (map[string]config.ChatMessage, error) {
+ a.cfgMu.Lock()
+ defer a.cfgMu.Unlock()
+ channel, exists := a.cfg.Channels[cid]
+ if !exists {
+ a.logError.Println("channel does not exist:", cid)
+ return nil, fmt.Errorf("Cannot find channel. Try reloading.")
+ }
+
+ return channel.ChatBot.Messages, nil
+}
+
+func (a *App) AddChatMessage(cid string, asChannel bool, interval time.Duration, message string) (map[string]config.ChatMessage, error) {
+ var err error
+ a.cfgMu.Lock()
+ defer a.cfgMu.Unlock()
+ _, err = a.cfg.NewChatMessage(cid, asChannel, interval, message)
+ if err != nil {
+ a.logError.Println("error creating new chat:", err)
+ return nil, fmt.Errorf("Error creating new chat message. Try again.")
+ }
+
+ err = a.cfg.Save()
+ if err != nil {
+ a.logError.Println("error saving config:", err)
+ return nil, fmt.Errorf("Error saving chat message information. Try again.")
+ }
+
+ a.updateChatBotConfig(a.cfg.Channels[cid].ChatBot)
+
+ return a.cfg.Channels[cid].ChatBot.Messages, nil
+}
+
+func (a *App) DeleteChatMessage(mid string, cid string) (map[string]config.ChatMessage, error) {
+ a.cbMu.Lock()
+ if a.cb != nil {
+ err := a.cb.StopMessage(mid)
+ if err != nil {
+ a.logError.Println("error stopping chat bot message:", err)
+ return nil, fmt.Errorf("Error stopping message. Try Again.")
+ }
+ }
+ a.cbMu.Unlock()
+
+ a.cfgMu.Lock()
+ defer a.cfgMu.Unlock()
+ err := a.cfg.DeleteChatMessage(mid, cid)
+ if err != nil {
+ a.logError.Println("error deleting chat message:", err)
+ return nil, fmt.Errorf("Error deleting chat message. Try again.")
+ }
+
+ err = a.cfg.Save()
+ if err != nil {
+ a.logError.Println("error saving config:", err)
+ return nil, fmt.Errorf("Error saving chat message information. Try again.")
+ }
+
+ a.updateChatBotConfig(a.cfg.Channels[cid].ChatBot)
+
+ return a.cfg.Channels[cid].ChatBot.Messages, nil
+}
+
+func (a *App) UpdateChatMessage(id string, cid string, asChannel bool, interval time.Duration, message string) (map[string]config.ChatMessage, error) {
+ var err error
+ a.cfgMu.Lock()
+ defer a.cfgMu.Unlock()
+ _, err = a.cfg.UpdateChatMessage(id, cid, asChannel, interval, message)
+ if err != nil {
+ a.logError.Println("error updating chat message:", err)
+ return nil, fmt.Errorf("Error updating chat message. Try again.")
+ }
+
+ err = a.cfg.Save()
+ if err != nil {
+ a.logError.Println("error saving config:", err)
+ return nil, fmt.Errorf("Error saving chat message information. Try again.")
+ }
+
+ a.updateChatBotConfig(a.cfg.Channels[cid].ChatBot)
+
+ return a.cfg.Channels[cid].ChatBot.Messages, nil
+}
+
+func (a *App) NewChatBot(cid string, username string, password string, streamUrl string) error {
+ a.cbMu.Lock()
+ defer a.cbMu.Unlock()
+
+ if a.cb != nil {
+ err := a.resetChatBot()
+ if err != nil {
+ a.logError.Println("error resetting chat bot:", err)
+ return fmt.Errorf("Error creating chat bot. Try Again.")
+ }
+ }
+ channel, exists := a.cfg.Channels[cid]
+ if !exists {
+ a.logError.Println("channel does not exist:", cid)
+ return fmt.Errorf("Channel does not exist.")
+ }
+
+ var err error
+ a.cb, err = chatbot.NewChatBot(a.ctx, streamUrl, channel.ChatBot, a.logError)
+ if err != nil {
+ a.logError.Println("error creating new chat bot:", err)
+ return fmt.Errorf("Error creating new chat bot. Try Again.")
+ }
+
+ err = a.cb.Login(username, password)
+ if err != nil {
+ a.logError.Println("error logging into chat bot:", err)
+ return fmt.Errorf("Error logging in. Try Again.")
+ }
+
+ // a.cb = cb
+ return nil
+}
+
+func (a *App) ResetChatBot() error {
+ a.cbMu.Lock()
+ defer a.cbMu.Unlock()
+
+ err := a.resetChatBot()
+ if err != nil {
+ a.logError.Println("error resetting chat bot:", err)
+ return fmt.Errorf("Error resetting chat bot. Try Again.")
+ }
+
+ return nil
+}
+
+func (a *App) resetChatBot() error {
+ if a.cb == nil {
+ // return fmt.Errorf("chat bot is nil")
+ return nil
+ }
+
+ err := a.cb.StopAllMessages()
+ if err != nil {
+ return fmt.Errorf("error stopping all chat bot messages: %v", err)
+ }
+
+ err = a.cb.Logout()
+ if err != nil {
+ return fmt.Errorf("error logging out of chat bot: %v", err)
+ }
+
+ a.cb = nil
+
+ return nil
+}
+
+func (a *App) StartChatBotMessage(mid string) error {
+ a.cbMu.Lock()
+ defer a.cbMu.Unlock()
+
+ if a.cb == nil {
+ return fmt.Errorf("Chat bot not initialized.")
+ }
+
+ err := a.cb.StartMessage(mid)
+ if err != nil {
+ a.logError.Println("error starting chat bot message:", err)
+ return fmt.Errorf("Error starting message. Try Again.")
+ }
+
+ return nil
+}
+
+func (a *App) StopChatBotMessage(mid string) error {
+ a.cbMu.Lock()
+ defer a.cbMu.Unlock()
+
+ // If chat bot not initialized, then stop does nothing
+ if a.cb == nil {
+ return nil
+ }
+
+ err := a.cb.StopMessage(mid)
+ if err != nil {
+ a.logError.Println("error stopping chat bot message:", err)
+ return fmt.Errorf("Error stopping message. Try Again.")
+ }
+
+ return nil
+}
+
func (a *App) StartApi(cid string) error {
channel, found := a.cfg.Channels[cid]
if !found {
@@ -145,3 +335,11 @@ func (a *App) StartApi(cid string) error {
func (a *App) StopApi() {
a.api.Stop()
}
+
+func (a *App) updateChatBotConfig(cfg config.ChatBot) {
+ a.cbMu.Lock()
+ defer a.cbMu.Unlock()
+ if a.cb != nil {
+ a.cb.Cfg = cfg
+ }
+}
diff --git a/build/appicon.png b/build/appicon.png
index 63617fe..a2b4d08 100644
Binary files a/build/appicon.png and b/build/appicon.png differ
diff --git a/frontend/src/assets/icons/gear-fill.png b/frontend/src/assets/icons/gear-fill.png
new file mode 100644
index 0000000..e0bf42b
Binary files /dev/null and b/frontend/src/assets/icons/gear-fill.png differ
diff --git a/frontend/src/assets/icons/index.jsx b/frontend/src/assets/icons/index.jsx
index 6e2b0b5..2c394db 100644
--- a/frontend/src/assets/icons/index.jsx
+++ b/frontend/src/assets/icons/index.jsx
@@ -1,6 +1,7 @@
import eye from './eye.png';
import eye_slash from './eye-slash.png';
import gear from './gear.png';
+import gear_fill from './gear-fill.png';
import heart from './heart-fill.png';
import house from './house.png';
import pause from './pause-fill.png';
@@ -9,10 +10,12 @@ import plus_circle from './plus-circle-fill.png';
import star from './star-fill.png';
import thumbs_down from './hand-thumbs-down.png';
import thumbs_up from './hand-thumbs-up.png';
+import x_lg from './x-lg.png';
export const Eye = eye;
export const EyeSlash = eye_slash;
export const Gear = gear;
+export const GearFill = gear_fill;
export const Heart = heart;
export const House = house;
export const Pause = pause;
@@ -21,3 +24,4 @@ export const PlusCircle = plus_circle;
export const Star = star;
export const ThumbsDown = thumbs_down;
export const ThumbsUp = thumbs_up;
+export const XLg = x_lg;
diff --git a/frontend/src/assets/icons/x-lg.png b/frontend/src/assets/icons/x-lg.png
new file mode 100644
index 0000000..88a3d2b
Binary files /dev/null and b/frontend/src/assets/icons/x-lg.png differ
diff --git a/frontend/src/components/ChatBot.css b/frontend/src/components/ChatBot.css
new file mode 100644
index 0000000..332cb42
--- /dev/null
+++ b/frontend/src/components/ChatBot.css
@@ -0,0 +1,47 @@
+.chat-bot-error {
+ border: 1px solid red;
+ box-sizing: border-box;
+ color: red;
+ font-family: monospace;
+ font-size: 16px;
+ padding: 5px;
+ text-align: center;
+ width: 100%;
+}
+
+.chat-bot-modal {
+ align-items: left;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ width: 100%;
+}
+
+.chat-bot-setting {
+ align-items: start;
+ display: flex;
+ flex-direction: column;
+ padding-top: 10px;
+ width: 100%;
+}
+
+.chat-bot-setting-label {
+ color: white;
+ font-family: sans-serif;
+ font-size: 20px;
+ padding-bottom: 5px;
+ width: 100%;
+}
+
+.chat-bot-setting-input {
+ border: none;
+ border-radius: 5px;
+ box-sizing: border-box;
+ font-family: monospace;
+ font-size: 16px;
+ outline: none;
+ padding: 10px;
+ resize: none;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/frontend/src/components/ChatBot.jsx b/frontend/src/components/ChatBot.jsx
new file mode 100644
index 0000000..eef6f57
--- /dev/null
+++ b/frontend/src/components/ChatBot.jsx
@@ -0,0 +1,129 @@
+import { useEffect, useState } from 'react';
+
+import { Modal } from './Modal';
+
+import { NewChatBot } from '../../wailsjs/go/main/App';
+
+import './ChatBot.css';
+
+export function ChatBotModal(props) {
+ const [error, setError] = useState('');
+ const [password, setPassword] = useState('');
+ const [saving, setSaving] = useState(false);
+ const updatePassword = (event) => setPassword(event.target.value);
+ const [url, setUrl] = useState('');
+ const updateUrl = (event) => setUrl(event.target.value);
+ const [username, setUsername] = useState('');
+ const updateUsername = (event) => setUsername(event.target.value);
+
+ useEffect(() => {
+ if (saving) {
+ // let user = username;
+ // let p = password;
+ // let u = url;
+ // props.onSubmit(user, p, u);
+ NewChatBot(props.cid, username, password, url)
+ .then(() => {
+ reset();
+ props.onClose();
+ })
+ .catch((error) => {
+ setSaving(false);
+ setError(error);
+ console.log('Error creating new chat bot:', error);
+ });
+ }
+ }, [saving]);
+
+ const reset = () => {
+ setError('');
+ setPassword('');
+ setSaving(false);
+ setUrl('');
+ setUsername('');
+ };
+
+ const close = () => {
+ reset();
+ props.onClose();
+ };
+
+ const submit = () => {
+ if (username === '') {
+ setError('Add username');
+ return;
+ }
+
+ if (password === '') {
+ setError('Add password');
+ return;
+ }
+
+ if (url === '') {
+ setError('Add stream URL');
+ return;
+ }
+
+ setSaving(true);
+ // let user = username;
+ // let p = password;
+ // let u = url;
+ // reset();
+ // props.onSubmit(user, p, u);
+ };
+
+ return (
+ {
+ console.log('Saving');
+ }
+ : submit
+ }
+ title={'Chat Bot'}
+ >
+
+
+ );
+}
+
+export function StreamChatMessageItem() {}
diff --git a/frontend/src/components/Modal.css b/frontend/src/components/Modal.css
new file mode 100644
index 0000000..d4da5b4
--- /dev/null
+++ b/frontend/src/components/Modal.css
@@ -0,0 +1,176 @@
+
+.modal-background {
+ align-items: center;
+ background-color: transparent;
+ display: flex;
+ height: 100vh;
+ justify-content: center;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100vw;
+}
+
+.modal-body {
+ align-items: center;
+ display: flex;
+ height: 80%;
+ justify-content: center;
+ width: 100%;
+}
+
+.modal-button {
+ background-color: #85c742;
+ border: none;
+ border-radius: 5px;
+ color: #061726;
+ cursor: pointer;
+ font-size: 18px;
+ font-weight: bold;
+ text-decoration: none;
+ /* width: 20%; */
+ width: 70px;
+}
+
+.modal-button-cancel {
+ background-color: transparent;
+ border: 1px solid #495a6a;
+ border-radius: 5px;
+ color: #495a6a;
+ cursor: pointer;
+ font-size: 18px;
+ font-weight: bold;
+ text-decoration: none;
+ /* width: 20%; */
+ width: 70px;
+}
+
+.modal-button-delete {
+ background-color: transparent;
+ border: 1px solid red;
+ border-radius: 5px;
+ color: red;
+ cursor: pointer;
+ font-size: 18px;
+ font-weight: bold;
+ text-decoration: none;
+ /* width: 20%; */
+ width: 70px;
+}
+
+.modal-close {
+ align-items: center;
+ background-color: transparent;
+ border: none;
+ display: flex;
+ flex-direction: center;
+ padding: 0px;
+}
+
+.modal-close:hover {
+ cursor: pointer;
+}
+
+.modal-close-icon {
+ height: 24px;
+ padding: 0px;
+ width: 24px;
+}
+
+.modal-container {
+ align-items: center;
+ background-color: rgba(6,23,38,1);
+ border: 1px solid #495a6a;
+ border-radius: 15px;
+ color: black;
+ display: flex;
+ flex-direction: column;
+ height: 50%;
+ justify-content: space-between;
+ opacity: 1;
+ padding: 10px 20px;
+ width: 50%;
+}
+
+.modal-footer {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ height: 10%;
+ width: 100%;
+}
+
+.modal-header {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ height: 10%;
+ width: 100%;
+}
+
+.modal-title {
+ color: white;
+ font-family: sans-serif;
+ font-size: 24px;
+}
+
+.small-modal-button-delete {
+ background-color: red;
+ border: none;
+ border-radius: 5px;
+ color: white;
+ cursor: pointer;
+ font-size: 18px;
+ font-weight: bold;
+ text-decoration: none;
+ /* width: 20%; */
+ width: 70px;
+}
+
+.small-modal-container {
+ align-items: center;
+ /* background-color: rgba(6,23,38,1); */
+ background-color: white;
+ border: 1px solid #495a6a;
+ /* border: 1px solid black; */
+ border-radius: 15px;
+ color: black;
+ display: flex;
+ flex-direction: column;
+ height: 50%;
+ justify-content: space-between;
+ opacity: 1;
+ padding: 10px 20px;
+ width: 50%;
+}
+
+.small-modal-header {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ height: 10%;
+ width: 100%;
+}
+
+.small-modal-footer {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ height: 20%;
+ width: 100%;
+}
+
+.small-modal-message {
+ font-family: sans-serif;
+ font-size: 18px;
+}
+
+.small-modal-title {
+ color: black;
+ font-family: sans-serif;
+ font-size: 24px;
+}
\ No newline at end of file
diff --git a/frontend/src/components/Modal.jsx b/frontend/src/components/Modal.jsx
new file mode 100644
index 0000000..1be4fe8
--- /dev/null
+++ b/frontend/src/components/Modal.jsx
@@ -0,0 +1,86 @@
+import { XLg } from '../assets/icons';
+import './Modal.css';
+
+export function Modal(props) {
+ return (
+
+
event.stopPropagation()}
+ style={props.style}
+ >
+
+
{props.title}
+
+
+
{props.children}
+
+ {props.cancelButton && (
+
+ )}
+ {props.deleteButton && (
+
+ )}
+ {props.submitButton && (
+
+ )}
+
+
+
+ );
+}
+
+export function SmallModal(props) {
+ return (
+
+
event.stopPropagation()}
+ style={props.style}
+ >
+
+
{props.title}
+
+
+
+ {props.message}
+
+
+ {props.cancelButton && (
+
+ )}
+ {props.deleteButton && (
+
+ )}
+ {props.submitButton && (
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/StreamChat.css b/frontend/src/components/StreamChat.css
index c8fea5b..0560425 100644
--- a/frontend/src/components/StreamChat.css
+++ b/frontend/src/components/StreamChat.css
@@ -3,24 +3,43 @@
height: 100%;
}
-.stream-chat-add-button {
+.stream-chat-button {
align-items: center;
- background-color: rgba(6,23,38,1);
border: none;
display: flex;
justify-content: center;
padding: 0px;
}
-.stream-chat-add-button:hover {
+.stream-chat-button:hover {
cursor: pointer;
}
-.stream-chat-add-icon {
+.stream-chat-button-title {
+ background-color: rgba(6,23,38,1);
+}
+
+.stream-chat-button-chat {
+ align-items: center;
+ background-color: #000312;
+ display: flex;
+ justify-content: center;
+ width: 10%;
+}
+
+.stream-chat-icon {
height: 24px;
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);
@@ -33,6 +52,50 @@
text-align: left;
}
+.stream-chat-item {
+ border-bottom: 1px solid #82b1ff;
+ box-sizing: border-box;
+ color: white;
+ display: flex;
+ flex-direction: row;
+ font-family: sans-serif;
+ justify-content: space-between;
+ padding: 10px 20px;
+ width: 100%;
+}
+
+.stream-chat-item-sender {
+ align-items: center;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: left;
+ padding-left: 10px;
+ width: 20%;
+}
+
+.stream-chat-item-interval {
+ align-items: center;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: left;
+ padding-left: 10px;
+ width: 20%;
+}
+
+.stream-chat-item-message {
+ align-items: center;
+ display: flex;
+ justify-content: left;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 50%;
+}
+
+.stream-chat-list {
+ overflow-y: auto;
+ height: calc(100vh - 84px - 40px - 179px);
+}
+
.stream-chat-title {
color: white;
font-family: sans-serif;
diff --git a/frontend/src/components/StreamChat.jsx b/frontend/src/components/StreamChat.jsx
index 49de3bc..6394850 100644
--- a/frontend/src/components/StreamChat.jsx
+++ b/frontend/src/components/StreamChat.jsx
@@ -1,20 +1,182 @@
-import { PlusCircle } from '../assets/icons';
+import { useEffect, useState } from 'react';
+import { StartChatBotMessage, StopChatBotMessage } from '../../wailsjs/go/main/App';
+import { EventsOn } from '../../wailsjs/runtime/runtime';
+import { GearFill, Pause, Play, PlusCircle } from '../assets/icons';
import './StreamChat.css';
+import { SmallModal } from './Modal';
function StreamChat(props) {
+ const sortChatsAlpha = () => {
+ let keys = Object.keys(props.chats);
+
+ let sorted = [...keys].sort((a, b) =>
+ props.chats[a].text.toLowerCase() > props.chats[b].text.toLowerCase() ? 1 : -1
+ );
+ return sorted;
+ };
+
return (
{props.title}
-
+
+
+
+
+
+
+ {sortChatsAlpha().map((chat, index) => (
+
+ ))}
);
}
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/StreamChatMessage.css b/frontend/src/components/StreamChatMessage.css
index 6187703..143efaf 100644
--- a/frontend/src/components/StreamChatMessage.css
+++ b/frontend/src/components/StreamChatMessage.css
@@ -1,4 +1,4 @@
-.modal-chat {
+/* .modal-chat {
align-items: center;
background-color: red;
color: black;
@@ -18,4 +18,170 @@
position: absolute;
top: 0;
width: 100vw;
+} */
+
+.chat-as-channel {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ padding-top: 10px;
+ width: 100%;
+}
+
+.chat-as-channel-label {
+ color: white;
+ font-family: sans-serif;
+ padding-right: 10px;
+}
+
+.chat-interval {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding-top: 10px;
+ width: 100%;
+}
+
+.chat-interval-input {
+ border: none;
+ border-radius: 34px;
+ box-sizing: border-box;
+ font-family: monospace;
+ font-size: 16px;
+ outline: none;
+ padding: 5px 10px 5px 10px;
+ text-align: right;
+}
+
+.chat-interval-input-zero::placeholder {
+ text-align: center;
+}
+
+.chat-interval-input-value::placeholder {
+ color: black;
+ opacity: 1;
+ text-align: center;
+}
+
+.chat-interval-label {
+ color: white;
+ font-family: sans-serif;
+ padding-right: 10px;
+}
+
+.chat-options {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.stream-chat-message {
+ align-items: center;
+ color: white;
+ display: flex;
+ flex-direction: column;
+ font-family: sans-serif;
+ justify-content: start;
+ width: 100%;
+}
+
+.stream-chat-message-error {
+ border: 1px solid red;
+ box-sizing: border-box;
+ color: red;
+ font-family: monospace;
+ font-size: 16px;
+ padding: 5px;
+ text-align: center;
+ width: 100%;
+}
+
+.stream-chat-message-label {
+ padding: 5px 0px;
+ /* width: 50%; */
+}
+
+.stream-chat-message-modal {
+ align-items: left;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ width: 100%;
+}
+
+.stream-chat-message-textarea {
+ border: none;
+ border-radius: 5px;
+ box-sizing: border-box;
+ font-family: monospace;
+ font-size: 16px;
+ outline: none;
+ padding: 10px;
+ resize: none;
+ width: 100%;
+}
+
+.stream-chat-message-title {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: start;
+ width: 100%;
+}
+
+.chat-as-channel-switch {
+ position: relative;
+ display: inline-block;
+ width: 50px;
+ height: 24px;
+}
+
+.chat-as-channel-switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.chat-as-channel-slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #495a6a;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+.chat-as-channel-slider:before {
+ position: absolute;
+ content: "";
+ height: 16px;
+ width: 16px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+input:checked + .chat-as-channel-slider {
+ background-color: #85c742;
+}
+
+input:checked + .chat-as-channel-slider:before {
+ -webkit-transform: translateX(26px);
+ -ms-transform: translateX(26px);
+ transform: translateX(26px);
+}
+/* Rounded sliders */
+.chat-as-channel-slider.round {
+ border-radius: 34px;
+}
+
+.chat-as-channel-slider.round:before {
+ border-radius: 50%;
}
\ No newline at end of file
diff --git a/frontend/src/components/StreamChatMessage.jsx b/frontend/src/components/StreamChatMessage.jsx
index 88a6f37..6a29869 100644
--- a/frontend/src/components/StreamChatMessage.jsx
+++ b/frontend/src/components/StreamChatMessage.jsx
@@ -1,12 +1,194 @@
+import { useEffect, useState } from 'react';
+
+import { Modal, SmallModal } from './Modal';
+
import './StreamChatMessage.css';
-export function StreamChatMessageModal() {
+export function StreamChatMessageModal(props) {
+ const [asChannel, setAsChannel] = useState(props.asChannel);
+ const [openDelete, setOpenDelete] = useState(false);
+ const [error, setError] = useState('');
+ const [message, setMessage] = useState(props.message);
+ const updateMessage = (event) => setMessage(event.target.value);
+ const [timer, setTimer] = useState(props.interval);
+
+ useEffect(() => {
+ console.log('update chat');
+ setAsChannel(props.asChannel);
+ setError('');
+ setMessage(props.message);
+ setTimer(props.interval);
+ }, []);
+
+ const reset = () => {
+ setAsChannel(false);
+ setError('');
+ setMessage('');
+ setTimer('');
+ };
+
+ const close = () => {
+ reset();
+ props.onClose();
+ };
+
+ const submit = () => {
+ if (message === '') {
+ setError('Add message');
+ return;
+ }
+
+ if (timer === '') {
+ setError('Set timer');
+ return;
+ }
+
+ let ac = asChannel;
+ let msg = message;
+ let int = timerToInterval();
+ reset();
+ props.onSubmit(props.chatID, ac, int, msg);
+ };
+
+ const deleteMessage = () => {
+ if (props.chatID === '') {
+ close();
+ return;
+ }
+
+ setOpenDelete(true);
+ };
+
+ const confirmDelete = () => {
+ reset();
+ setOpenDelete(false);
+ props.onDelete(props.chatID);
+ };
+
+ const updateTimerBackspace = (e) => {
+ if (timer.length === 0) {
+ return;
+ }
+
+ if (e.keyCode === 8) {
+ setTimer(timer.substring(0, timer.length - 1));
+ }
+ };
+
+ const updateTimer = (e) => {
+ let nums = '0123456789';
+ let digit = e.target.value;
+
+ if (!nums.includes(digit)) {
+ return;
+ }
+
+ if (timer.length === 6) {
+ return;
+ }
+
+ if (timer.length === 0 && digit === '0') {
+ return;
+ }
+
+ setTimer(timer + digit);
+ };
+
+ const timerToInterval = () => {
+ let prefix = '0'.repeat(6 - timer.length);
+ let t = prefix + timer;
+
+ let hours = parseInt(t.substring(0, 2));
+ let minutes = parseInt(t.substring(2, 4));
+ let seconds = parseInt(t.substring(4, 6));
+
+ return hours * 3600 + minutes * 60 + seconds;
+ };
+
+ const printTimer = () => {
+ if (timer === '') {
+ return '00:00:00';
+ }
+
+ let prefix = '0'.repeat(6 - timer.length);
+ let t = prefix + timer;
+
+ return t.substring(0, 2) + ':' + t.substring(2, 4) + ':' + t.substring(4, 6);
+ };
+
+ const checkToggle = (e) => {
+ setAsChannel(e.target.checked);
+ };
+
return (
-
+ <>
+
+
+
+ {error &&
{error}}
+
+ Message
+
+
+
+
+
+
+ setOpenDelete(false)}
+ show={openDelete}
+ style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
+ cancelButton={'Cancel'}
+ onCancel={() => setOpenDelete(false)}
+ deleteButton={'Delete'}
+ message={
+ 'Are you sure you want to delete this message? You cannot undo this action.'
+ }
+ onDelete={confirmDelete}
+ title={'Delete Message'}
+ />
+ >
);
}
diff --git a/frontend/src/screens/Dashboard.jsx b/frontend/src/screens/Dashboard.jsx
index 6bba160..b679cdd 100644
--- a/frontend/src/screens/Dashboard.jsx
+++ b/frontend/src/screens/Dashboard.jsx
@@ -1,10 +1,21 @@
import { useEffect, useState } from 'react';
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
-import { StartApi, StopApi } from '../../wailsjs/go/main/App';
+import {
+ AddChatMessage,
+ ChatBotMessages,
+ DeleteChatMessage,
+ NewChatBot,
+ ResetChatBot,
+ StartApi,
+ StopApi,
+ StopChatBotMessage,
+ UpdateChatMessage,
+} from '../../wailsjs/go/main/App';
import './Dashboard.css';
import { EventsEmit, EventsOn } from '../../wailsjs/runtime/runtime';
import { Heart, Star } from '../assets/icons';
+import { ChatBotModal } from '../components/ChatBot';
import Highlight from '../components/Highlight';
import StreamEvent from '../components/StreamEvent';
import StreamActivity from '../components/StreamActivity';
@@ -18,6 +29,13 @@ function Dashboard() {
const navigate = useNavigate();
const [refresh, setRefresh] = useState(false);
const [active, setActive] = useState(false);
+ const [openChatBot, setOpenChatBot] = useState(false);
+ const [chatBotMessages, setChatBotMessages] = useState({});
+ const [chatAsChannel, setChatAsChannel] = useState(false);
+ const [chatID, setChatID] = useState('');
+ const [chatInterval, setChatInterval] = useState('');
+ const [chatMessage, setChatMessage] = useState('');
+ const [openChat, setOpenChat] = useState(false);
const [cid, setCID] = useState(location.state.cid);
const [username, setUsername] = useState('');
const [channelName, setChannelName] = useState('');
@@ -38,13 +56,17 @@ function Dashboard() {
const [streamTitle, setStreamTitle] = useState('');
const [watchingNow, setWatchingNow] = useState(0);
const [createdOn, setCreatedOn] = useState('');
- const [modalZ, setModalZ] = useState(false);
useEffect(() => {
console.log('use effect start');
+ // TODO: catch error
StartApi(cid);
setActive(true);
+ ChatBotMessages(cid).then((messages) => {
+ setChatBotMessages(messages);
+ });
+
EventsOn('QueryResponse', (response) => {
console.log('query response received');
setRefresh(!refresh);
@@ -80,11 +102,14 @@ function Dashboard() {
const home = () => {
StopApi()
.then(() => setActive(false))
+ .then(() => {
+ ResetChatBot();
+ })
.then(() => {
navigate(NavSignIn);
})
- .catch((err) => {
- console.log('Stop error:', err);
+ .catch((error) => {
+ console.log('Stop error:', error);
});
};
@@ -94,8 +119,8 @@ function Dashboard() {
.then(() => {
setActive(true);
})
- .catch((err) => {
- console.log('Start error:', err);
+ .catch((error) => {
+ console.log('Start error:', error);
});
};
@@ -122,21 +147,93 @@ function Dashboard() {
return sorted;
};
- const openModal = () => {
- setModalZ(true);
+ const newChat = () => {
+ setChatAsChannel(false);
+ setChatID('');
+ setChatInterval('');
+ setChatMessage('');
+ setOpenChat(true);
};
- const closeModal = () => {
- setModalZ(false);
+ const editChat = (id, asChannel, interval, message) => {
+ setChatAsChannel(asChannel);
+ setChatInterval(interval);
+ setChatMessage(message);
+ setChatID(id);
+ setOpenChat(true);
+ };
+
+ const deleteChat = (id) => {
+ setOpenChat(false);
+ if (id === '') {
+ return;
+ }
+
+ StopChatBotMessage(id, cid)
+ .then(() => {
+ DeleteChatMessage(id, cid)
+ .then((messages) => {
+ setChatBotMessages(messages);
+ })
+ .catch((error) => {
+ console.log('Error deleting message:', error);
+ });
+ })
+ .catch((error) => {
+ console.log('Error stopping message:', error);
+ });
+ };
+
+ const saveChat = (id, asChannel, interval, message) => {
+ setOpenChat(false);
+ if (id === '') {
+ AddChatMessage(cid, asChannel, interval, message)
+ .then((messages) => {
+ setChatBotMessages(messages);
+ })
+ .catch((error) => console.log('Error saving chat:', error));
+
+ return;
+ }
+
+ UpdateChatMessage(id, cid, asChannel, interval, message)
+ .then((messages) => {
+ console.log(messages);
+ setChatBotMessages(messages);
+ })
+ .catch((error) => console.log('Error saving chat:', error));
+ };
+
+ const saveChatBot = (username, password, url) => {
+ NewChatBot(cid, username, password, url)
+ .then(() => {
+ setOpenChatBot(false);
+ })
+ .catch((error) => console.log('Error creating new chat bot:', error));
};
return (
<>
-
-
- show this instead
-
-
+ {openChat && (
+ setOpenChat(false)}
+ onDelete={deleteChat}
+ onSubmit={saveChat}
+ show={openChat}
+ />
+ )}
+ {openChatBot && (
+ setOpenChatBot(false)}
+ onSubmit={saveChatBot}
+ show={openChatBot}
+ />
+ )}
@@ -161,7 +258,14 @@ function Dashboard() {
-
+ setRefresh(!refresh)}
+ onSettings={() => setOpenChatBot(true)}
+ title={'Stream Chat'}
+ />
@@ -176,7 +280,7 @@ function Dashboard() {
home={home}
play={startQuery}
pause={stopQuery}
- settings={openModal}
+ // settings={openModal}
/>
>
diff --git a/frontend/src/screens/SignIn.jsx b/frontend/src/screens/SignIn.jsx
index 20a19c3..4b78575 100644
--- a/frontend/src/screens/SignIn.jsx
+++ b/frontend/src/screens/SignIn.jsx
@@ -5,8 +5,10 @@ import { AddChannel, Config } from '../../wailsjs/go/main/App';
import { Eye, EyeSlash } from '../assets/icons';
import './SignIn.css';
import ChannelList from '../components/ChannelList';
+import { SmallModal } from '../components/Modal';
function SignIn() {
+ const [error, setError] = useState('');
const navigate = useNavigate();
const [config, setConfig] = useState({ channels: {} });
const [addChannelError, setAddChannelError] = useState('');
@@ -20,9 +22,10 @@ function SignIn() {
.then((response) => {
setConfig(response);
})
- .catch((err) => {
+ .catch((error) => {
// TODO: display error to user
- console.log('error getting config', err);
+ setError('Error loading config: ' + error);
+ console.log('error getting config', error);
});
}, []);
@@ -33,9 +36,9 @@ function SignIn() {
setConfig(response);
setStreamKey('');
})
- .catch((err) => {
- console.log('error adding channel', err);
- setAddChannelError(err);
+ .catch((error) => {
+ console.log('error adding channel', error);
+ setAddChannelError(error);
});
};
@@ -44,44 +47,60 @@ function SignIn() {
};
return (
-
-
- Rum Goggles
- Rumble Stream Dashboard
-
-
-
-
-
-
-
- Copy your API key from your Rumble account
-
-
-
-
-
+ <>
+ {error !== '' && (
+
setError('')}
+ show={error !== ''}
+ style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
+ title={'Error'}
+ message={error}
+ submitButton={'OK'}
+ onSubmit={() => setError('')}
+ />
+ )}
+
+
+ Rum Goggles
+ Rumble Stream Dashboard
-
- {addChannelError ? addChannelError : '\u00A0'}
-
+
+
+
+
+
+
+ Copy your API key from your Rumble account
+
+
+
+
+
+
+
+ {addChannelError ? addChannelError : '\u00A0'}
+
+
+
-
-
+ >
);
}
diff --git a/go.mod b/go.mod
index 5a1e8b7..e319a77 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.0.0-20231218182551-5ac1d6c01910
+ github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20240105170050-474340b082fc
github.com/wailsapp/wails/v2 v2.7.1
)
@@ -25,7 +25,7 @@ require (
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
- github.com/robertkrimen/otto v0.2.1 // indirect
+ github.com/robertkrimen/otto v0.3.0 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index adf327d..664db9b 100644
--- a/go.sum
+++ b/go.sum
@@ -44,8 +44,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
-github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
+github.com/robertkrimen/otto v0.3.0 h1:5RI+8860NSxvXywDY9ddF5HcPw0puRsd8EgbXV0oqRE=
+github.com/robertkrimen/otto v0.3.0/go.mod h1:uW9yN1CYflmUQYvAMS0m+ZiNo3dMzRUDQJX0jWbzgxw=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -55,8 +55,8 @@ github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQ
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/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.0.0-20231218182551-5ac1d6c01910 h1:pu5jBae9XZDF/G8YkCN7D5TMxOCsCO5+Jn1/lNPsOUY=
-github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231218182551-5ac1d6c01910/go.mod h1:YrfW5N6xVozOzubzfNNsy+v0MIL2GPi9Kx3mTZ/Q9zI=
+github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20240105170050-474340b082fc h1:JaoanQiZrYIbDx0UAYTNpyjhsgx7eWlTD7KJRqnrC8A=
+github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20240105170050-474340b082fc/go.mod h1:YrfW5N6xVozOzubzfNNsy+v0MIL2GPi9Kx3mTZ/Q9zI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
diff --git a/internal/chatbot/chatbot.go b/internal/chatbot/chatbot.go
new file mode 100644
index 0000000..521b5b7
--- /dev/null
+++ b/internal/chatbot/chatbot.go
@@ -0,0 +1,167 @@
+package chatbot
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "sync"
+ "time"
+
+ "github.com/tylertravisty/rum-goggles/internal/config"
+ rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
+ "github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+type ChatBot struct {
+ ctx context.Context
+ client *rumblelivestreamlib.Client
+ Cfg config.ChatBot
+ logError *log.Logger
+ messages map[string]*message
+ messagesMu sync.Mutex
+}
+
+type message struct {
+ cancel context.CancelFunc
+ cancelMu sync.Mutex
+ asChannel bool
+ id string
+ interval time.Duration
+ text string
+}
+
+func NewChatBot(ctx context.Context, streamUrl string, cfg config.ChatBot, logError *log.Logger) (*ChatBot, error) {
+ client, err := rumblelivestreamlib.NewClient("", streamUrl)
+ if err != nil {
+ return nil, fmt.Errorf("chatbot: error creating new client: %v", err)
+ }
+
+ return &ChatBot{ctx: ctx, client: client, Cfg: cfg, logError: logError, messages: map[string]*message{}}, nil
+}
+
+func (cb *ChatBot) StartMessage(id string) error {
+ msg, exists := cb.Cfg.Messages[id]
+ if !exists {
+ return fmt.Errorf("chatbot: message does not exist")
+ }
+
+ cb.messagesMu.Lock()
+ defer cb.messagesMu.Unlock()
+ m, exists := cb.messages[id]
+ if exists {
+ m.stop()
+ delete(cb.messages, id)
+ }
+
+ m = &message{
+ asChannel: msg.AsChannel,
+ id: msg.ID,
+ interval: msg.Interval,
+ text: msg.Text,
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ m.cancelMu.Lock()
+ m.cancel = cancel
+ m.cancelMu.Unlock()
+ go cb.startMessage(ctx, m)
+
+ cb.messages[id] = m
+
+ return nil
+}
+
+func (cb *ChatBot) startMessage(ctx context.Context, m *message) {
+ for {
+ // TODO: if error, emit error to user, stop loop?
+ err := cb.chat(m)
+ if err != nil {
+ cb.logError.Println("error sending chat:", err)
+ cb.StopMessage(m.id)
+ runtime.EventsEmit(cb.ctx, "ChatBotMessageError-"+m.id, m.id)
+ // TODO: stop this loop?
+ } else {
+ runtime.EventsEmit(cb.ctx, "ChatBotMessageActive-"+m.id, m.id)
+ }
+
+ timer := time.NewTimer(m.interval * time.Second)
+ select {
+ case <-ctx.Done():
+ timer.Stop()
+ return
+ case <-timer.C:
+ }
+ }
+}
+
+func (cb *ChatBot) chat(m *message) error {
+ if cb.client == nil {
+ return fmt.Errorf("client is nil")
+ }
+
+ err := cb.client.Chat(m.asChannel, m.text)
+ if err != nil {
+ return fmt.Errorf("error sending chat: %v", err)
+ }
+
+ return nil
+}
+
+func (cb *ChatBot) StopAllMessages() error {
+ cb.messagesMu.Lock()
+ defer cb.messagesMu.Unlock()
+
+ for id, m := range cb.messages {
+ m.stop()
+ delete(cb.messages, id)
+ }
+
+ return nil
+}
+
+func (cb *ChatBot) StopMessage(id string) error {
+ cb.messagesMu.Lock()
+ defer cb.messagesMu.Unlock()
+ m, exists := cb.messages[id]
+ if exists {
+ fmt.Println("IT EXISTS!!")
+ m.stop()
+ delete(cb.messages, id)
+ }
+
+ return nil
+}
+
+func (m *message) stop() {
+ m.cancelMu.Lock()
+ if m.cancel != nil {
+ m.cancel()
+ }
+ m.cancelMu.Unlock()
+}
+
+func (cb *ChatBot) Login(username string, password string) error {
+ if cb.client == nil {
+ return fmt.Errorf("chatbot: client is nil")
+ }
+
+ err := cb.client.Login(username, password)
+ if err != nil {
+ return fmt.Errorf("chatbot: error logging in: %v", err)
+ }
+
+ return nil
+}
+
+func (cb *ChatBot) Logout() error {
+ if cb.client == nil {
+ return fmt.Errorf("chatbot: client is nil")
+ }
+
+ err := cb.client.Logout()
+ if err != nil {
+ return fmt.Errorf("chatbot: error logging out: %v", err)
+ }
+
+ return nil
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 76175a1..4a5a3dd 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -73,13 +73,14 @@ func userDir() (string, error) {
}
type ChatMessage struct {
+ ID string `json:"id"`
AsChannel bool `json:"as_channel"`
- Text string `json:"text"`
Interval time.Duration `json:"interval"`
+ Text string `json:"text"`
}
type ChatBot struct {
- Messages []ChatMessage `json:"messages"`
+ Messages map[string]ChatMessage `json:"messages"`
// Commands []ChatCommand
}
@@ -99,12 +100,65 @@ func (a *App) NewChannel(url string, name string) (string, error) {
}
if _, exists := a.Channels[id]; !exists {
- a.Channels[id] = Channel{id, url, name, DefaultInterval, ChatBot{[]ChatMessage{}}}
+ a.Channels[id] = Channel{id, url, name, DefaultInterval, ChatBot{map[string]ChatMessage{}}}
return id, nil
}
}
}
+func (a *App) DeleteChatMessage(id string, cid string) error {
+ channel, exists := a.Channels[cid]
+ if !exists {
+ return fmt.Errorf("config: channel does not exist")
+ }
+
+ _, exists = channel.ChatBot.Messages[id]
+ if !exists {
+ return fmt.Errorf("config: message does not exist")
+ }
+
+ delete(channel.ChatBot.Messages, id)
+
+ return nil
+}
+
+func (a *App) NewChatMessage(cid string, asChannel bool, interval time.Duration, message string) (string, error) {
+ if _, exists := a.Channels[cid]; !exists {
+ return "", fmt.Errorf("config: channel does not exist")
+ }
+
+ for {
+ id, err := random.String(CIDLen)
+ if err != nil {
+ return "", fmt.Errorf("config: error generating ID: %v", err)
+ }
+
+ if _, exists := a.Channels[cid].ChatBot.Messages[id]; !exists {
+ a.Channels[cid].ChatBot.Messages[id] = ChatMessage{id, asChannel, interval, message}
+ return id, nil
+ }
+ }
+}
+
+func (a *App) UpdateChatMessage(id string, cid string, asChannel bool, interval time.Duration, text string) (string, error) {
+ channel, exists := a.Channels[cid]
+ if !exists {
+ return "", fmt.Errorf("config: channel does not exist")
+ }
+
+ message, exists := channel.ChatBot.Messages[id]
+ if !exists {
+ return "", fmt.Errorf("config: message does not exist")
+ }
+
+ message.AsChannel = asChannel
+ message.Interval = interval
+ message.Text = text
+ channel.ChatBot.Messages[id] = message
+
+ return id, nil
+}
+
type App struct {
Channels map[string]Channel `json:"channels"`
}