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 ( +
+
+ hello world +
+
+ ); +} + +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