diff --git a/app.go b/app.go
index 3675453..48f8b7d 100644
--- a/app.go
+++ b/app.go
@@ -14,22 +14,34 @@ import (
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
)
-const (
- configFilepath = "./config.json"
-)
+type chat struct {
+ username string
+ password string
+ url string
+}
// App struct
type App struct {
- ctx context.Context
- cfg *config.App
- cfgMu sync.Mutex
- api *api.Api
- apiMu sync.Mutex
+ ctx context.Context
+ cfg *config.App
+ cfgMu sync.Mutex
+ api *api.Api
+ apiMu sync.Mutex
+ logError *log.Logger
+ logInfo *log.Logger
}
// NewApp creates a new App application struct
func NewApp() *App {
- return &App{api: api.NewApi()}
+ app := &App{}
+ err := app.initLog()
+ if err != nil {
+ log.Fatal("error initializing log")
+ }
+
+ app.api = api.NewApi(app.logError, app.logInfo)
+
+ return app
}
// startup is called when the app starts. The context is saved
@@ -37,15 +49,31 @@ func NewApp() *App {
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.api.Startup(ctx)
+
err := a.loadConfig()
if err != nil {
- // TODO: handle error better on startup
- log.Fatal("error loading config: ", err)
+ a.logError.Fatal("error loading config: ", err)
}
}
+func (a *App) initLog() error {
+ fp, err := config.LogFile()
+ if err != nil {
+ return fmt.Errorf("error getting filepath for log file")
+ }
+
+ f, err := os.OpenFile(fp, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
+ if err != nil {
+ return fmt.Errorf("error opening log file")
+ }
+
+ a.logInfo = log.New(f, "[info]", log.LstdFlags|log.Lshortfile)
+ a.logError = log.New(f, "[error]", log.LstdFlags|log.Lshortfile)
+ return nil
+}
+
func (a *App) loadConfig() error {
- cfg, err := config.Load(configFilepath)
+ cfg, err := config.Load()
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("error loading config: %v", err)
@@ -60,7 +88,7 @@ func (a *App) loadConfig() error {
func (a *App) newConfig() error {
cfg := &config.App{Channels: map[string]config.Channel{}}
- err := cfg.Save(configFilepath)
+ err := cfg.Save()
if err != nil {
return fmt.Errorf("error saving new config: %v", err)
}
@@ -73,23 +101,12 @@ func (a *App) Config() *config.App {
return a.cfg
}
-func (a *App) SaveConfig() error {
- err := a.cfg.Save(configFilepath)
- if err != nil {
- // TODO: log error; return user error
- return fmt.Errorf("Error saving config")
- }
-
- return nil
-}
-
func (a *App) AddChannel(url string) (*config.App, error) {
client := rumblelivestreamlib.Client{StreamKey: url}
resp, err := client.Request()
if err != nil {
- // TODO: log error
- fmt.Println("error requesting api:", err)
- return nil, fmt.Errorf("error querying API")
+ a.logError.Println("error executing api request:", err)
+ return nil, fmt.Errorf("Error querying API. Verify key and try again.")
}
name := resp.Username
@@ -101,16 +118,14 @@ func (a *App) AddChannel(url string) (*config.App, error) {
defer a.cfgMu.Unlock()
_, err = a.cfg.NewChannel(url, name)
if err != nil {
- // TODO: log error
- fmt.Println("error creating new channel:", err)
- return nil, fmt.Errorf("error creating new channel")
+ a.logError.Println("error creating new channel:", err)
+ return nil, fmt.Errorf("Error creating new channel. Try again.")
}
- err = a.cfg.Save(configFilepath)
+ err = a.cfg.Save()
if err != nil {
- // TODO: log error
- fmt.Println("error saving config:", err)
- return nil, fmt.Errorf("error saving new channel")
+ a.logError.Println("error saving config:", err)
+ return nil, fmt.Errorf("Error saving channel information. Try again.")
}
return a.cfg, nil
@@ -119,15 +134,13 @@ func (a *App) AddChannel(url string) (*config.App, error) {
func (a *App) StartApi(cid string) error {
channel, found := a.cfg.Channels[cid]
if !found {
- // TODO: log error
- fmt.Println("could not find channel CID:", cid)
+ a.logError.Println("could not find channel CID:", cid)
return fmt.Errorf("channel CID not found")
}
err := a.api.Start(channel.ApiUrl, channel.Interval*time.Second)
if err != nil {
- // TODO: log error
- fmt.Println("error starting api:", err)
+ a.logError.Println("error starting api:", err)
return fmt.Errorf("error starting API")
}
diff --git a/frontend/src/components/StreamChat.css b/frontend/src/components/StreamChat.css
index b70cb73..c8fea5b 100644
--- a/frontend/src/components/StreamChat.css
+++ b/frontend/src/components/StreamChat.css
@@ -3,12 +3,34 @@
height: 100%;
}
+.stream-chat-add-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 {
+ cursor: pointer;
+}
+
+.stream-chat-add-icon {
+ height: 24px;
+ width: 24px;
+}
+
.stream-chat-header {
- text-align: left;
+ 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-chat-title {
diff --git a/frontend/src/components/StreamChat.jsx b/frontend/src/components/StreamChat.jsx
index bb9e3c1..49de3bc 100644
--- a/frontend/src/components/StreamChat.jsx
+++ b/frontend/src/components/StreamChat.jsx
@@ -1,3 +1,4 @@
+import { PlusCircle } from '../assets/icons';
import './StreamChat.css';
function StreamChat(props) {
@@ -5,6 +6,12 @@ function StreamChat(props) {
{props.title}
+
);
diff --git a/frontend/src/components/StreamChatMessage.css b/frontend/src/components/StreamChatMessage.css
new file mode 100644
index 0000000..6187703
--- /dev/null
+++ b/frontend/src/components/StreamChatMessage.css
@@ -0,0 +1,21 @@
+.modal-chat {
+ align-items: center;
+ background-color: red;
+ color: black;
+ display: flex;
+ height: 50%;
+ justify-content: center;
+ opacity: 1;
+ width: 50%;
+}
+
+.modal-container {
+ align-items: center;
+ display: flex;
+ height: 100vh;
+ justify-content: center;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100vw;
+}
\ No newline at end of file
diff --git a/frontend/src/components/StreamChatMessage.jsx b/frontend/src/components/StreamChatMessage.jsx
new file mode 100644
index 0000000..88a6f37
--- /dev/null
+++ b/frontend/src/components/StreamChatMessage.jsx
@@ -0,0 +1,13 @@
+import './StreamChatMessage.css';
+
+export function StreamChatMessageModal() {
+ return (
+
+ );
+}
+
+export function StreamChatMessageItem() {}
diff --git a/frontend/src/screens/Dashboard.jsx b/frontend/src/screens/Dashboard.jsx
index 643a54a..6bba160 100644
--- a/frontend/src/screens/Dashboard.jsx
+++ b/frontend/src/screens/Dashboard.jsx
@@ -11,6 +11,7 @@ import StreamActivity from '../components/StreamActivity';
import StreamChat from '../components/StreamChat';
import StreamInfo from '../components/StreamInfo';
import { NavSignIn } from './Navigation';
+import { StreamChatMessageItem, StreamChatMessageModal } from '../components/StreamChatMessage';
function Dashboard() {
const location = useLocation();
@@ -131,6 +132,7 @@ function Dashboard() {
return (
<>
+
show this instead
diff --git a/frontend/src/screens/SignIn.css b/frontend/src/screens/SignIn.css
index 3e08e32..0d08ebe 100644
--- a/frontend/src/screens/SignIn.css
+++ b/frontend/src/screens/SignIn.css
@@ -7,6 +7,19 @@
height: 100vh;
}
+.add-channel-description {
+ font-family: sans-serif;
+ font-size: 12px;
+ padding-bottom: 5px;
+}
+
+.add-channel-error {
+ color: red;
+ font-family: sans-serif;
+ font-size: 12px;
+ padding-top: 5px;
+}
+
.signin-input-box {
display: flex;
flex-direction: column;
diff --git a/frontend/src/screens/SignIn.jsx b/frontend/src/screens/SignIn.jsx
index 39538c6..20a19c3 100644
--- a/frontend/src/screens/SignIn.jsx
+++ b/frontend/src/screens/SignIn.jsx
@@ -9,6 +9,7 @@ import ChannelList from '../components/ChannelList';
function SignIn() {
const navigate = useNavigate();
const [config, setConfig] = useState({ channels: {} });
+ const [addChannelError, setAddChannelError] = useState('');
const [streamKey, setStreamKey] = useState('');
const updateStreamKey = (event) => setStreamKey(event.target.value);
const [showStreamKey, setShowStreamKey] = useState(false);
@@ -34,6 +35,7 @@ function SignIn() {
})
.catch((err) => {
console.log('error adding channel', err);
+ setAddChannelError(err);
});
};
@@ -52,6 +54,9 @@ function SignIn() {
+
+ Copy your API key from your Rumble account
+
+
+ {addChannelError ? addChannelError : '\u00A0'}
+
diff --git a/internal/api/api.go b/internal/api/api.go
index 77e0483..f98395c 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -3,6 +3,7 @@ package api
import (
"context"
"fmt"
+ "log"
"sync"
"time"
@@ -14,12 +15,14 @@ type Api struct {
ctx context.Context
cancel context.CancelFunc
cancelMu sync.Mutex
+ logError *log.Logger
+ logInfo *log.Logger
querying bool
queryingMu sync.Mutex
}
-func NewApi() *Api {
- return &Api{}
+func NewApi(logError *log.Logger, logInfo *log.Logger) *Api {
+ return &Api{logError: logError, logInfo: logInfo}
}
func (a *Api) Startup(ctx context.Context) {
@@ -27,7 +30,7 @@ func (a *Api) Startup(ctx context.Context) {
}
func (a *Api) Start(url string, interval time.Duration) error {
- fmt.Println("Api.Start")
+ a.logInfo.Println("Api.Start")
if url == "" {
return fmt.Errorf("empty stream key")
}
@@ -38,21 +41,21 @@ func (a *Api) Start(url string, interval time.Duration) error {
a.queryingMu.Unlock()
if start {
- fmt.Println("Starting querying")
+ a.logInfo.Println("Start querying")
ctx, cancel := context.WithCancel(context.Background())
a.cancelMu.Lock()
a.cancel = cancel
a.cancelMu.Unlock()
go a.start(ctx, url, interval)
} else {
- fmt.Println("Querying already started")
+ a.logInfo.Println("Querying already started")
}
return nil
}
func (a *Api) Stop() {
- fmt.Println("stop querying")
+ a.logInfo.Println("Stop querying")
a.cancelMu.Lock()
if a.cancel != nil {
a.cancel()
@@ -77,12 +80,12 @@ func (a *Api) start(ctx context.Context, url string, interval time.Duration) {
}
func (a *Api) query(url string) {
- fmt.Println("QueryAPI")
+ a.logInfo.Println("QueryAPI")
client := rumblelivestreamlib.Client{StreamKey: url}
resp, err := client.Request()
if err != nil {
// TODO: log error
- fmt.Println("client.Request err:", err)
+ a.logError.Println("api: error executing client request:", err)
a.Stop()
runtime.EventsEmit(a.ctx, "QueryResponseError", "Failed to query API")
return
diff --git a/internal/config/config.go b/internal/config/config.go
index d82d435..24085be 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"os"
+ "path/filepath"
+ "runtime"
"time"
"github.com/tylertravisty/go-utils/random"
@@ -12,13 +14,69 @@ import (
const (
CIDLen = 8
DefaultInterval = 10
+
+ configDir = ".rum-goggles"
+ configDirWin = "RumGoggles"
+ configFile = "config.json"
+ logFile = "logs.txt"
)
+func LogFile() (string, error) {
+ dir, err := buildConfigDir()
+ if err != nil {
+ return "", fmt.Errorf("config: error getting config directory: %v", err)
+ }
+
+ return filepath.Join(dir, logFile), nil
+}
+
+func buildConfigDir() (string, error) {
+ userDir, err := userDir()
+ if err != nil {
+ return "", fmt.Errorf("error getting user directory: %v", err)
+ }
+
+ var dir string
+ switch runtime.GOOS {
+ case "windows":
+ dir = filepath.Join(userDir, configDirWin)
+ default:
+ dir = filepath.Join(userDir, configDir)
+ }
+
+ return dir, nil
+}
+
+func userDir() (string, error) {
+ var dir string
+ var err error
+ switch runtime.GOOS {
+ case "windows":
+ dir, err = os.UserCacheDir()
+ default:
+ dir, err = os.UserHomeDir()
+ }
+
+ return dir, err
+}
+
+type ChatMessage struct {
+ AsChannel bool `json:"as_channel"`
+ Text string `json:"text"`
+ Interval time.Duration `json:"interval"`
+}
+
+type ChatBot struct {
+ Messages []ChatMessage `json:"messages"`
+ // Commands []ChatCommand
+}
+
type Channel struct {
ID string `json:"id"`
ApiUrl string `json:"api_url"`
Name string `json:"name"`
Interval time.Duration `json:"interval"`
+ ChatBot ChatBot `json:"chat_bot"`
}
func (a *App) NewChannel(url string, name string) (string, error) {
@@ -29,7 +87,7 @@ func (a *App) NewChannel(url string, name string) (string, error) {
}
if _, exists := a.Channels[id]; !exists {
- a.Channels[id] = Channel{id, url, name, DefaultInterval}
+ a.Channels[id] = Channel{id, url, name, DefaultInterval, ChatBot{[]ChatMessage{}}}
return id, nil
}
}
@@ -39,31 +97,66 @@ type App struct {
Channels map[string]Channel `json:"channels"`
}
-func Load(filepath string) (*App, error) {
+func Load() (*App, error) {
+ dir, err := buildConfigDir()
+ if err != nil {
+ return nil, fmt.Errorf("config: error getting config directory: %v", err)
+ }
+
+ fp := filepath.Join(dir, configFile)
+ app, err := load(fp)
+ if err != nil {
+ return nil, fmt.Errorf("config: error loading config: %w", err)
+ }
+
+ return app, nil
+}
+
+func load(filepath string) (*App, error) {
f, err := os.Open(filepath)
if err != nil {
- return nil, fmt.Errorf("config: error opening file: %w", err)
+ return nil, fmt.Errorf("error opening file: %w", err)
}
var app App
decoder := json.NewDecoder(f)
err = decoder.Decode(&app)
if err != nil {
- return nil, fmt.Errorf("config: error decoding file into json: %v", err)
+ return nil, fmt.Errorf("error decoding file into json: %v", err)
}
return &app, nil
}
-func (app *App) Save(filepath string) error {
- b, err := json.MarshalIndent(app, "", "\t")
+func (a *App) Save() error {
+ dir, err := buildConfigDir()
if err != nil {
- return fmt.Errorf("config: error encoding config into json: %v", err)
+ return fmt.Errorf("config: error getting config directory: %v", err)
}
- err = os.WriteFile(filepath, b, 0666)
+ err = os.MkdirAll(dir, 0750)
if err != nil {
- return fmt.Errorf("config: error writing config file: %v", err)
+ return fmt.Errorf("config: error making config directory: %v", err)
+ }
+
+ fp := filepath.Join(dir, configFile)
+ err = a.save(fp)
+ if err != nil {
+ return fmt.Errorf("config: error saving config: %v", err)
+ }
+
+ return nil
+}
+
+func (app *App) save(filepath string) error {
+ b, err := json.MarshalIndent(app, "", "\t")
+ if err != nil {
+ return fmt.Errorf("error encoding config into json: %v", err)
+ }
+
+ err = os.WriteFile(filepath, b, 0666)
+ if err != nil {
+ return fmt.Errorf("error writing config file: %v", err)
}
return nil