Moved config to its own directory

This commit is contained in:
tyler 2023-12-24 16:18:42 -05:00
parent d329c36653
commit 5ddf50a152
10 changed files with 250 additions and 55 deletions

87
app.go
View file

@ -14,22 +14,34 @@ import (
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go" rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
) )
const ( type chat struct {
configFilepath = "./config.json" username string
) password string
url string
}
// App struct // App struct
type App struct { type App struct {
ctx context.Context ctx context.Context
cfg *config.App cfg *config.App
cfgMu sync.Mutex cfgMu sync.Mutex
api *api.Api api *api.Api
apiMu sync.Mutex apiMu sync.Mutex
logError *log.Logger
logInfo *log.Logger
} }
// NewApp creates a new App application struct // NewApp creates a new App application struct
func NewApp() *App { 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 // 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) { func (a *App) startup(ctx context.Context) {
a.ctx = ctx a.ctx = ctx
a.api.Startup(ctx) a.api.Startup(ctx)
err := a.loadConfig() err := a.loadConfig()
if err != nil { if err != nil {
// TODO: handle error better on startup a.logError.Fatal("error loading config: ", err)
log.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 { func (a *App) loadConfig() error {
cfg, err := config.Load(configFilepath) cfg, err := config.Load()
if err != nil { if err != nil {
if !errors.Is(err, os.ErrNotExist) { if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("error loading config: %v", err) return fmt.Errorf("error loading config: %v", err)
@ -60,7 +88,7 @@ func (a *App) loadConfig() error {
func (a *App) newConfig() error { func (a *App) newConfig() error {
cfg := &config.App{Channels: map[string]config.Channel{}} cfg := &config.App{Channels: map[string]config.Channel{}}
err := cfg.Save(configFilepath) err := cfg.Save()
if err != nil { if err != nil {
return fmt.Errorf("error saving new config: %v", err) return fmt.Errorf("error saving new config: %v", err)
} }
@ -73,23 +101,12 @@ func (a *App) Config() *config.App {
return a.cfg 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) { func (a *App) AddChannel(url string) (*config.App, error) {
client := rumblelivestreamlib.Client{StreamKey: url} client := rumblelivestreamlib.Client{StreamKey: url}
resp, err := client.Request() resp, err := client.Request()
if err != nil { if err != nil {
// TODO: log error a.logError.Println("error executing api request:", err)
fmt.Println("error requesting api:", err) return nil, fmt.Errorf("Error querying API. Verify key and try again.")
return nil, fmt.Errorf("error querying API")
} }
name := resp.Username name := resp.Username
@ -101,16 +118,14 @@ func (a *App) AddChannel(url string) (*config.App, error) {
defer a.cfgMu.Unlock() defer a.cfgMu.Unlock()
_, err = a.cfg.NewChannel(url, name) _, err = a.cfg.NewChannel(url, name)
if err != nil { if err != nil {
// TODO: log error a.logError.Println("error creating new channel:", err)
fmt.Println("error creating new channel:", err) return nil, fmt.Errorf("Error creating new channel. Try again.")
return nil, fmt.Errorf("error creating new channel")
} }
err = a.cfg.Save(configFilepath) err = a.cfg.Save()
if err != nil { if err != nil {
// TODO: log error a.logError.Println("error saving config:", err)
fmt.Println("error saving config:", err) return nil, fmt.Errorf("Error saving channel information. Try again.")
return nil, fmt.Errorf("error saving new channel")
} }
return a.cfg, nil return a.cfg, nil
@ -119,15 +134,13 @@ func (a *App) AddChannel(url string) (*config.App, error) {
func (a *App) StartApi(cid string) error { func (a *App) StartApi(cid string) error {
channel, found := a.cfg.Channels[cid] channel, found := a.cfg.Channels[cid]
if !found { if !found {
// TODO: log error a.logError.Println("could not find channel CID:", cid)
fmt.Println("could not find channel CID:", cid)
return fmt.Errorf("channel CID not found") return fmt.Errorf("channel CID not found")
} }
err := a.api.Start(channel.ApiUrl, channel.Interval*time.Second) err := a.api.Start(channel.ApiUrl, channel.Interval*time.Second)
if err != nil { if err != nil {
// TODO: log error a.logError.Println("error starting api:", err)
fmt.Println("error starting api:", err)
return fmt.Errorf("error starting API") return fmt.Errorf("error starting API")
} }

View file

@ -3,12 +3,34 @@
height: 100%; 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 { .stream-chat-header {
text-align: left; align-items: center;
background-color: rgba(6,23,38,1); background-color: rgba(6,23,38,1);
border-bottom: 1px solid #495a6a; border-bottom: 1px solid #495a6a;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 19px; height: 19px;
padding: 10px 20px; padding: 10px 20px;
text-align: left;
} }
.stream-chat-title { .stream-chat-title {

View file

@ -1,3 +1,4 @@
import { PlusCircle } from '../assets/icons';
import './StreamChat.css'; import './StreamChat.css';
function StreamChat(props) { function StreamChat(props) {
@ -5,6 +6,12 @@ function StreamChat(props) {
<div className='stream-chat'> <div className='stream-chat'>
<div className='stream-chat-header'> <div className='stream-chat-header'>
<span className='stream-chat-title'>{props.title}</span> <span className='stream-chat-title'>{props.title}</span>
<button
onClick={() => console.log('Add chat bot')}
className='stream-chat-add-button'
>
<img className='stream-chat-add-icon' src={PlusCircle} />
</button>
</div> </div>
</div> </div>
); );

View file

@ -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;
}

View file

@ -0,0 +1,13 @@
import './StreamChatMessage.css';
export function StreamChatMessageModal() {
return (
<div className='modal-container'>
<div className='modal-chat'>
<span>hello world</span>
</div>
</div>
);
}
export function StreamChatMessageItem() {}

View file

@ -11,6 +11,7 @@ import StreamActivity from '../components/StreamActivity';
import StreamChat from '../components/StreamChat'; import StreamChat from '../components/StreamChat';
import StreamInfo from '../components/StreamInfo'; import StreamInfo from '../components/StreamInfo';
import { NavSignIn } from './Navigation'; import { NavSignIn } from './Navigation';
import { StreamChatMessageItem, StreamChatMessageModal } from '../components/StreamChatMessage';
function Dashboard() { function Dashboard() {
const location = useLocation(); const location = useLocation();
@ -131,6 +132,7 @@ function Dashboard() {
return ( return (
<> <>
<StreamChatMessageModal />
<div className='modal' style={{ zIndex: modalZ ? 10 : -10 }}> <div className='modal' style={{ zIndex: modalZ ? 10 : -10 }}>
<span>show this instead</span> <span>show this instead</span>
<button onClick={closeModal}>close</button> <button onClick={closeModal}>close</button>

View file

@ -7,6 +7,19 @@
height: 100vh; 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 { .signin-input-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -9,6 +9,7 @@ import ChannelList from '../components/ChannelList';
function SignIn() { function SignIn() {
const navigate = useNavigate(); const navigate = useNavigate();
const [config, setConfig] = useState({ channels: {} }); const [config, setConfig] = useState({ channels: {} });
const [addChannelError, setAddChannelError] = useState('');
const [streamKey, setStreamKey] = useState(''); const [streamKey, setStreamKey] = useState('');
const updateStreamKey = (event) => setStreamKey(event.target.value); const updateStreamKey = (event) => setStreamKey(event.target.value);
const [showStreamKey, setShowStreamKey] = useState(false); const [showStreamKey, setShowStreamKey] = useState(false);
@ -34,6 +35,7 @@ function SignIn() {
}) })
.catch((err) => { .catch((err) => {
console.log('error adding channel', err); console.log('error adding channel', err);
setAddChannelError(err);
}); });
}; };
@ -52,6 +54,9 @@ function SignIn() {
</div> </div>
<div className='signin-input-box'> <div className='signin-input-box'>
<label className='signin-label'>Add Channel</label> <label className='signin-label'>Add Channel</label>
<span className='add-channel-description'>
Copy your API key from your Rumble account
</span>
<div className='signin-input-button'> <div className='signin-input-button'>
<input <input
id='StreamKey' id='StreamKey'
@ -71,6 +76,9 @@ function SignIn() {
Save Save
</button> </button>
</div> </div>
<span className='add-channel-error'>
{addChannelError ? addChannelError : '\u00A0'}
</span>
</div> </div>
<div className='signin-footer'></div> <div className='signin-footer'></div>
</div> </div>

View file

@ -3,6 +3,7 @@ package api
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"sync" "sync"
"time" "time"
@ -14,12 +15,14 @@ type Api struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
cancelMu sync.Mutex cancelMu sync.Mutex
logError *log.Logger
logInfo *log.Logger
querying bool querying bool
queryingMu sync.Mutex queryingMu sync.Mutex
} }
func NewApi() *Api { func NewApi(logError *log.Logger, logInfo *log.Logger) *Api {
return &Api{} return &Api{logError: logError, logInfo: logInfo}
} }
func (a *Api) Startup(ctx context.Context) { 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 { func (a *Api) Start(url string, interval time.Duration) error {
fmt.Println("Api.Start") a.logInfo.Println("Api.Start")
if url == "" { if url == "" {
return fmt.Errorf("empty stream key") return fmt.Errorf("empty stream key")
} }
@ -38,21 +41,21 @@ func (a *Api) Start(url string, interval time.Duration) error {
a.queryingMu.Unlock() a.queryingMu.Unlock()
if start { if start {
fmt.Println("Starting querying") a.logInfo.Println("Start querying")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
a.cancelMu.Lock() a.cancelMu.Lock()
a.cancel = cancel a.cancel = cancel
a.cancelMu.Unlock() a.cancelMu.Unlock()
go a.start(ctx, url, interval) go a.start(ctx, url, interval)
} else { } else {
fmt.Println("Querying already started") a.logInfo.Println("Querying already started")
} }
return nil return nil
} }
func (a *Api) Stop() { func (a *Api) Stop() {
fmt.Println("stop querying") a.logInfo.Println("Stop querying")
a.cancelMu.Lock() a.cancelMu.Lock()
if a.cancel != nil { if a.cancel != nil {
a.cancel() a.cancel()
@ -77,12 +80,12 @@ func (a *Api) start(ctx context.Context, url string, interval time.Duration) {
} }
func (a *Api) query(url string) { func (a *Api) query(url string) {
fmt.Println("QueryAPI") a.logInfo.Println("QueryAPI")
client := rumblelivestreamlib.Client{StreamKey: url} client := rumblelivestreamlib.Client{StreamKey: url}
resp, err := client.Request() resp, err := client.Request()
if err != nil { if err != nil {
// TODO: log error // TODO: log error
fmt.Println("client.Request err:", err) a.logError.Println("api: error executing client request:", err)
a.Stop() a.Stop()
runtime.EventsEmit(a.ctx, "QueryResponseError", "Failed to query API") runtime.EventsEmit(a.ctx, "QueryResponseError", "Failed to query API")
return return

View file

@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath"
"runtime"
"time" "time"
"github.com/tylertravisty/go-utils/random" "github.com/tylertravisty/go-utils/random"
@ -12,13 +14,69 @@ import (
const ( const (
CIDLen = 8 CIDLen = 8
DefaultInterval = 10 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 { type Channel struct {
ID string `json:"id"` ID string `json:"id"`
ApiUrl string `json:"api_url"` ApiUrl string `json:"api_url"`
Name string `json:"name"` Name string `json:"name"`
Interval time.Duration `json:"interval"` Interval time.Duration `json:"interval"`
ChatBot ChatBot `json:"chat_bot"`
} }
func (a *App) NewChannel(url string, name string) (string, error) { 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 { 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 return id, nil
} }
} }
@ -39,31 +97,66 @@ type App struct {
Channels map[string]Channel `json:"channels"` 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) f, err := os.Open(filepath)
if err != nil { 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 var app App
decoder := json.NewDecoder(f) decoder := json.NewDecoder(f)
err = decoder.Decode(&app) err = decoder.Decode(&app)
if err != nil { 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 return &app, nil
} }
func (app *App) Save(filepath string) error { func (a *App) Save() error {
b, err := json.MarshalIndent(app, "", "\t") dir, err := buildConfigDir()
if err != nil { 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 { 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 return nil