From 14ef632e6c1639bbd45fa30f3aec4555974755f6 Mon Sep 17 00:00:00 2001 From: tyler Date: Wed, 22 May 2024 12:51:46 -0400 Subject: [PATCH] Feature parity with v0 --- v1/app.go | 363 ++++++-- .../src/assets/icons/twbs/gear-fill-white.png | Bin 0 -> 4515 bytes .../src/assets/icons/twbs/pause-fill.png | Bin 1867 -> 2680 bytes .../src/assets/icons/twbs/play-fill-green.png | Bin 0 -> 3203 bytes .../src/assets/icons/twbs/play-fill.png | Bin 2343 -> 1460 bytes .../src/assets/icons/twbs/stop-fill-red.png | Bin 0 -> 2302 bytes v1/frontend/src/assets/index.js | 10 + v1/frontend/src/components/ChatBot.css | 131 ++- v1/frontend/src/components/ChatBot.jsx | 819 +++++++++++++++--- v1/frontend/src/components/Modal.css | 1 + v1/go.mod | 4 +- v1/go.sum | 8 +- v1/internal/chatbot/chatbot.go | 447 ++++++++++ v1/internal/chatbot/error.go | 14 + v1/internal/chatbot/rule.go | 180 ++++ v1/internal/chatbot/runner.go | 206 +++++ v1/internal/events/chat.go | 49 +- v1/internal/models/chatbotrule.go | 65 +- v1/internal/models/error.go | 4 +- v1/internal/models/services.go | 10 + .../rumble-livestream-lib-go/chat.go | 67 +- .../wails/v2/internal/binding/reflect.go | 2 +- .../frontend/desktop/darwin/Application.h | 2 +- .../frontend/desktop/darwin/Application.m | 4 +- .../frontend/desktop/darwin/WailsContext.h | 2 +- .../frontend/desktop/darwin/WailsContext.m | 7 +- .../frontend/desktop/darwin/browser.go | 4 +- .../internal/frontend/desktop/darwin/main.m | 3 +- .../frontend/desktop/darwin/window.go | 6 +- .../frontend/desktop/linux/browser.go | 4 +- .../internal/frontend/desktop/linux/window.c | 4 +- .../frontend/desktop/windows/browser.go | 23 +- .../frontend/desktop/windows/winc/app.go | 2 +- .../frontend/desktop/windows/winc/combobox.go | 2 +- .../frontend/desktop/windows/winc/listview.go | 2 +- .../frontend/desktop/windows/winc/treeview.go | 2 +- .../frontend/runtime/ipc_websocket.js | 8 +- .../frontend/runtime/package-lock.json | 64 +- .../wails/v2/internal/goversion/min.go | 2 +- .../wails/v2/pkg/assetserver/assethandler.go | 2 +- .../wails/v2/pkg/assetserver/assetserver.go | 5 +- .../v2/pkg/assetserver/assetserver_webview.go | 2 +- .../wailsapp/wails/v2/pkg/options/mac/mac.go | 1 + .../wailsapp/wails/v2/pkg/runtime/screen.go | 2 +- v1/vendor/modules.txt | 4 +- 45 files changed, 2264 insertions(+), 273 deletions(-) create mode 100644 v1/frontend/src/assets/icons/twbs/gear-fill-white.png create mode 100644 v1/frontend/src/assets/icons/twbs/play-fill-green.png create mode 100644 v1/frontend/src/assets/icons/twbs/stop-fill-red.png create mode 100644 v1/internal/chatbot/chatbot.go create mode 100644 v1/internal/chatbot/error.go create mode 100644 v1/internal/chatbot/rule.go create mode 100644 v1/internal/chatbot/runner.go diff --git a/v1/app.go b/v1/app.go index 89414e4..f892fec 100644 --- a/v1/app.go +++ b/v1/app.go @@ -7,9 +7,11 @@ import ( "log" "net/http" "os" + "path/filepath" "sync" "time" + "github.com/tylertravisty/rum-goggles/v1/internal/chatbot" "github.com/tylertravisty/rum-goggles/v1/internal/config" "github.com/tylertravisty/rum-goggles/v1/internal/events" "github.com/tylertravisty/rum-goggles/v1/internal/models" @@ -47,6 +49,7 @@ func (p *Page) staticLiveStreamUrl() string { // App struct type App struct { cancelProc context.CancelFunc + chatbot *chatbot.Chatbot clients map[string]*rumblelivestreamlib.Client clientsMu sync.Mutex displaying string @@ -101,24 +104,34 @@ func (a *App) process(ctx context.Context) { for { select { case apiE := <-a.producers.ApiP.Ch: - err := a.processApi(apiE) - if err != nil { - a.logError.Println("error handling API event:", err) - } + a.processApi(apiE) case chatE := <-a.producers.ChatP.Ch: - err := a.processChat(chatE) - if err != nil { - a.logError.Println("error handling chat event:", err) - } + a.processChat(chatE) case <-ctx.Done(): return } } } -func (a *App) processApi(event events.Api) error { +type apiProcessor func(event events.Api) + +func (a *App) runApiProcessors(event events.Api, procs ...apiProcessor) { + for _, proc := range procs { + proc(event) + } +} + +func (a *App) processApi(event events.Api) { + a.runApiProcessors( + event, + a.pageApiProcessor, + a.chatbotApiProcessor, + ) +} + +func (a *App) pageApiProcessor(event events.Api) { if event.Name == "" { - return fmt.Errorf("event name is empty") + a.logError.Println("page cannot process API: event name is empty") } a.pagesMu.Lock() @@ -138,7 +151,7 @@ func (a *App) processApi(event events.Api) error { if event.Stop { runtime.EventsEmit(a.wails, "ApiActive-"+page.name, false) - return nil + return } runtime.EventsEmit(a.wails, "ApiActive-"+page.name, true) @@ -153,12 +166,39 @@ func (a *App) processApi(event events.Api) error { page.apiSt.respMu.Unlock() a.updatePage(page) - - return nil } -func (a *App) processChat(event events.Chat) error { - return nil +type chatProcessor func(event events.Chat) + +func (a *App) runChatProcessors(event events.Chat, procs ...chatProcessor) { + for _, proc := range procs { + proc(event) + } +} + +func (a *App) processChat(event events.Chat) { + if event.Stop { + runtime.EventsEmit(a.wails, "ChatStreamActive-"+event.Url, false) + return + } + + a.runChatProcessors( + event, + a.chatbotChatProcessor, + ) +} + +// TODO: implement this +func (a *App) chatbotApiProcessor(event events.Api) { + return +} + +func (a *App) chatbotChatProcessor(event events.Chat) { + if event.Message.Type == rumblelivestreamlib.ChatTypeInit { + return + } + + a.chatbot.HandleChat(event) } func (a *App) shutdown(ctx context.Context) { @@ -212,6 +252,14 @@ func (a *App) Start() (bool, error) { } runtime.EventsEmit(a.wails, "StartupMessage", "Initializing event producers complete.") + runtime.EventsEmit(a.wails, "StartupMessage", "Initializing chat bot...") + err = a.initChatbot() + if err != nil { + a.logError.Println("error initializing chat bot:", err) + return false, fmt.Errorf("Error starting Rum Goggles. Try restarting.") + } + runtime.EventsEmit(a.wails, "StartupMessage", "Initializing chat bot complete.") + // TODO: check for update - if available, pop up window // runtime.EventsEmit(a.ctx, "StartupMessage", "Checking for updates...") // update, err = a.checkForUpdate() @@ -229,6 +277,13 @@ func (a *App) Start() (bool, error) { return signin, nil } +func (a *App) initChatbot() error { + cb := chatbot.New(a.services.AccountS, a.services.ChatbotS, a.logError, a.wails) + a.chatbot = cb + + return nil +} + func (a *App) initProducers() error { producers, err := events.NewProducers( events.WithLoggers(a.logError, a.logInfo), @@ -261,6 +316,7 @@ func (a *App) initServices() error { models.WithChannelService(), models.WithAccountChannelService(), models.WithChatbotService(), + models.WithChatbotRuleService(), ) if err != nil { return fmt.Errorf("error initializing services: %v", err) @@ -1076,6 +1132,12 @@ func (a *App) DeleteChatbot(chatbot *models.Chatbot) error { return fmt.Errorf("Invalid chatbot. Try again.") } + err := a.StopChatbotRules(chatbot.ID) + if err != nil { + a.logError.Println("error stopping chatbot rules before deleting chatbot") + return fmt.Errorf("Error deleting chatbot. Could not stop running rules. Try Again.") + } + cb, err := a.services.ChatbotS.ByID(*chatbot.ID) if err != nil { a.logError.Println("error getting chatbot by ID:", err) @@ -1085,6 +1147,20 @@ func (a *App) DeleteChatbot(chatbot *models.Chatbot) error { return fmt.Errorf("Chatbot does not exist.") } + rules, err := a.services.ChatbotRuleS.ByChatbotID(*chatbot.ID) + if err != nil { + a.logError.Println("error getting chatbot rules by chatbot ID:", err) + return fmt.Errorf("Error deleting chatbot. Try again.") + } + + for _, rule := range rules { + err = a.services.ChatbotRuleS.Delete(&rule) + if err != nil { + a.logError.Println("error deleting chatbot rule:", err) + return fmt.Errorf("Error deleting chatbot. Try again.") + } + } + err = a.services.ChatbotS.Delete(chatbot) if err != nil { a.logError.Println("error deleting chatbot:", err) @@ -1189,60 +1265,241 @@ func (a *App) chatbotList() ([]models.Chatbot, error) { return list, err } -type ChatbotRule struct { - Message *ChatbotRuleMessage `json:"message"` - SendAs *ChatbotRuleSender `json:"send_as"` - Trigger *ChatbotRuleTrigger `json:"trigger"` +func (a *App) ChatbotRules(chatbot *models.Chatbot) ([]chatbot.Rule, error) { + if chatbot == nil || chatbot.ID == nil { + return nil, fmt.Errorf("Invalid chatbot. Try again.") + } + + rules, err := a.chatbotRules(*chatbot.ID) + if err != nil { + a.logError.Println("error getting chatbot rules:", err) + return nil, fmt.Errorf("Error getting chatbot rules. Try again.") + } + + return rules, nil } -type ChatbotRuleMessage struct { - FromFile *ChatbotRuleMessageFile `json:"from_file"` - FromText string `json:"from_text"` +func (a *App) chatbotRules(chatbotID int64) ([]chatbot.Rule, error) { + modelsRules, err := a.services.ChatbotRuleS.ByChatbotID(chatbotID) + if err != nil { + return nil, fmt.Errorf("error querying chatbot rules: %v", err) + } + + rules := []chatbot.Rule{} + for _, modelsRule := range modelsRules { + rule := chatbot.Rule{ + ID: modelsRule.ID, + ChatbotID: modelsRule.ChatbotID, + } + + if modelsRule.Parameters != nil { + var params chatbot.RuleParameters + err = json.Unmarshal([]byte(*modelsRule.Parameters), ¶ms) + if err != nil { + return nil, fmt.Errorf("error un-marshaling chatbot rule parameters from json: %v", err) + } + + rule.Parameters = ¶ms + } + + rule.Running = a.chatbot.Running(*rule.ChatbotID, *rule.ID) + + rule.Display = rule.Parameters.Message.FromText + if rule.Parameters.Message.FromFile != nil { + rule.Display = filepath.Base(rule.Parameters.Message.FromFile.Filepath) + } + + rules = append(rules, rule) + } + + chatbot.SortRules(rules) + + return rules, err } -type ChatbotRuleMessageFile struct { - Filepath string `json:"filepath"` - RandomRead bool `json:"random_read"` +func (a *App) DeleteChatbotRule(rule *chatbot.Rule) error { + if rule == nil || rule.ID == nil || rule.ChatbotID == nil { + return fmt.Errorf("Invalid chatbot rule. Try again.") + } + + mRule, err := rule.ToModelsChatbotRule() + if err != nil { + a.logError.Println("error converting chatbot.Rule into models.ChatbotRule:", err) + return fmt.Errorf("Error deleting chatbot rule. Try again.") + } + + err = a.chatbot.Stop(rule) + if err != nil { + a.logError.Println("error stopping chatbot rule:", err) + return fmt.Errorf("Error deleting chatbot rule. Try again.") + } + + err = a.services.ChatbotRuleS.Delete(mRule) + if err != nil { + a.logError.Println("error deleting chatbot rule:", err) + return fmt.Errorf("Error deleting chatbot rule. Try again.") + } + + rules, err := a.chatbotRules(*rule.ChatbotID) + if err != nil { + a.logError.Println("error getting chatbot rules:", err) + return fmt.Errorf("Error deleting chatbot rule. Try again.") + } + runtime.EventsEmit(a.wails, "ChatbotRules", rules) + + return nil } -type ChatbotRuleSender struct { - Username string `json:"username"` - ChannelID *int `json:"channel_id"` +func (a *App) NewChatbotRule(rule *chatbot.Rule) error { + if rule == nil || rule.ChatbotID == nil || rule.Parameters == nil { + return fmt.Errorf("Invalid chatbot rule. Try again.") + } + + mRule, err := rule.ToModelsChatbotRule() + if err != nil { + a.logError.Println("error converting chatbot.Rule into models.ChatbotRule:", err) + return fmt.Errorf("Error creating chatbot rule. Try again.") + } + + _, err = a.services.ChatbotRuleS.Create(mRule) + if err != nil { + a.logError.Println("error creating chatbot rule:", err) + return fmt.Errorf("Error creating chatbot rule. Try again.") + } + + rules, err := a.chatbotRules(*rule.ChatbotID) + if err != nil { + a.logError.Println("error getting chatbot rules:", err) + return fmt.Errorf("Error creating chatbot rule. Try again.") + } + runtime.EventsEmit(a.wails, "ChatbotRules", rules) + + return nil } -type ChatbotRuleTrigger struct { - OnCommand *ChatbotRuleTriggerCommand `json:"on_command"` - OnEvent *ChatbotRuleTriggerEvent `json:"on_event"` - OnTimer *time.Duration `json:"on_timer"` +func (a *App) RunChatbotRule(rule *chatbot.Rule) error { + if rule == nil || rule.ChatbotID == nil { + return fmt.Errorf("Invalid chatbot rule. Try again.") + } + + mChatbot, err := a.services.ChatbotS.ByID(*rule.ChatbotID) + if err != nil { + a.logError.Println("error getting chatbot by ID:", err) + return fmt.Errorf("Error running chatbot rule. Try again.") + } + if mChatbot == nil { + return fmt.Errorf("Chatbot does not exist. Try again.") + } + if mChatbot.Url == nil { + a.logError.Println("chatbot url is nil") + return fmt.Errorf("Chatbot url is not set. Update url and try again.") + } + + _, err = a.producers.ChatP.Start(*mChatbot.Url) + if err != nil { + a.logError.Println("error starting chat producer:", err) + // TODO: send error to UI that chatbot URL could not be started + //runtime.EventsEmit("Ch") + } + + err = a.chatbot.Run(rule, *mChatbot.Url) + if err != nil { + a.logError.Println("error running chat bot rule:", err) + return fmt.Errorf("Error running chatbot rule. Try again.") + } + + return nil } -type ChatbotRuleTriggerCommand struct { - Command string `json:"command"` - Restrict *ChatbotRuleTriggerCommandRestriction `json:"restrict"` - Timeout time.Duration `json:"timeout"` +func (a *App) StopChatbotRule(rule *chatbot.Rule) error { + err := a.chatbot.Stop(rule) + if err != nil { + a.logError.Println("error stopping chat bot rule:", err) + return fmt.Errorf("Error stopping chatbot rule. Try again.") + } + + return nil } -type ChatbotRuleTriggerCommandRestriction struct { - Bypass *ChatbotRuleTriggerCommandRestrictionBypass `json:"bypass"` - ToAdmin bool `json:"to_admin"` - ToFollower bool `json:"to_follower"` - ToMod bool `json:"to_mod"` - ToStreamer bool `json:"to_streamer"` - ToSubscriber bool `json:"to_subscriber"` - ToRant int `json:"to_rant"` +func (a *App) UpdateChatbotRule(rule *chatbot.Rule) error { + if rule == nil || rule.ID == nil || rule.ChatbotID == nil { + return fmt.Errorf("Invalid chatbot rule. Try again.") + } + + mRule, err := rule.ToModelsChatbotRule() + if err != nil { + a.logError.Println("error converting chatbot.Rule into models.ChatbotRule:", err) + return fmt.Errorf("Error updating chatbot rule. Try again.") + } + + err = a.chatbot.Stop(rule) + if err != nil { + a.logError.Println("error stopping chatbot rule:", err) + return fmt.Errorf("Error updating chatbot rule. Try again.") + } + + err = a.services.ChatbotRuleS.Update(mRule) + if err != nil { + a.logError.Println("error updating chatbot rule:", err) + return fmt.Errorf("Error updating chatbot rule. Try again.") + } + + rules, err := a.chatbotRules(*rule.ChatbotID) + if err != nil { + a.logError.Println("error getting chatbot rules:", err) + return fmt.Errorf("Error updating chatbot rule. Try again.") + } + runtime.EventsEmit(a.wails, "ChatbotRules", rules) + + return nil } -type ChatbotRuleTriggerCommandRestrictionBypass struct { - IfAdmin bool `json:"if_admin"` - IfMod bool `json:"if_mod"` - IfStreamer bool `json:"if_streamer"` +func (a *App) RunChatbotRules(chatbotID *int64) error { + if chatbotID == nil { + return fmt.Errorf("Invalid chatbot. Try again.") + } + + rules, err := a.chatbotRules(*chatbotID) + if err != nil { + a.logError.Println("error getting chatbot rules:", err) + return fmt.Errorf("Error running chatbot rules. Try again.") + } + + var errored bool + for _, rule := range rules { + if err = a.RunChatbotRule(&rule); err != nil { + errored = true + } + } + if errored { + return fmt.Errorf("An error occurred while running rules. Check error log for details.") + } + + return nil } -type ChatbotRuleTriggerEvent struct { - OnFollow bool `json:"on_follow"` - OnSubscribe bool `json:"on_subscribe"` - OnRaid bool `json:"on_raid"` - OnRant int `json:"on_rant"` +func (a *App) StopChatbotRules(chatbotID *int64) error { + if chatbotID == nil { + return fmt.Errorf("Invalid chatbot. Try again.") + } + + rules, err := a.chatbotRules(*chatbotID) + if err != nil { + a.logError.Println("error getting chatbot rules:", err) + return fmt.Errorf("Error stopping chatbot rules. Try again.") + } + + var errored bool + for _, rule := range rules { + if err = a.StopChatbotRule(&rule); err != nil { + errored = true + } + } + if errored { + return fmt.Errorf("An error occurred while stopping rules. Check error log for details.") + } + + return nil } func (a *App) OpenFileDialog() (string, error) { diff --git a/v1/frontend/src/assets/icons/twbs/gear-fill-white.png b/v1/frontend/src/assets/icons/twbs/gear-fill-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3f8c3fc7e32168fcff9d43443d9e10d2eb6d003a GIT binary patch literal 4515 zcmV;U5nS$xP)005u}1^@s6i_d2*00009a7bBm000kR z000kR0jNKxX#fBbRY^oaRCr$Poqdd6MHR+(0YyQrSRe%imL{a2MI?YCG>D`YD-{y{ zKpRuP_yZC}iHRs*8niYPG-|L+&dpBkcL{~p z>*piVJEd!-tA~e&|6Qo!{7^<!n47XDKr?qni;J7y%JziS&h{tefOH z$}Sn$fLySf2#7S>OIKDP-o&ERwS70y%wCs5KtvgqUS&wWURBgNT*{dIaQ6@pNtQa0 zuUF;L?jf2zE{uR!GEch6k$}`audqQKcrXtMA;@8Pyj%Y~*g5X^sQyDVO|#j#omN!8 zT`R1t$H)_FARnGUaN5cu1Tq1)(csh?8Bu3jHM&*4sXPP=GY!9Q)sN?-JCuiTpc1dD z)NUGv|1nVMgbL*za7!?ctC4)P2hkTK#G`>-$pqX2-SGn>IC#M(+8o?kr;?o?f;If| zNBE3%S0{}7Rm+e+OT+M{n%j-4x!eN|4KXiQH~ApJZqm-r31$Kg4V}_pm(XO}4Z6Lp z=}r&98h%Fv#Y589Hx!Nks@*vnhDRG}zwu_f-UD9LAo&PB#K(p#T|+><-(tr#-?CLN z?i&6*e`_Ct6&dEhj+HX^#@6tCQR!XMHPTyU1C!+Gi9py{dbae*g5ASL zJ-14FlqDLf?+9s?UCsr~FZ~!c9pT{XO{y0xiwLnfi&$CO(Rf{QXp!{sA_(+(<_7)$ z1FAZrI!=*ps9=Y8QEaRHWeir*>nj*btQJ+pyYcdJg5AZ-D*!RR=&Sk%TiY2qCw)GU?{W(iCe-n={(XgXfpo`y#_D%7 zq$f)m#yvNgI=DDiB{pL_KJbp*59b{N7GMH4)_}XD8)M@`&^xY2QCYBm6Ja$B9cUT?i=h@&0g= zk@LBlI1u=VNcc-@Q_Kk{^6>@5>cU>u#^(AOMc@gw&Z^BzmR5MIgnT}e&oV^O)J2E# z1#4?edqDMmOfo+z-NV|@TwfFjJS8jkv7D^7?g3fh$mENHD=tP5TViY`)&yjEElb)6 zn8w(efQP1yefCZWuzqhQ)&v|evn*E>D*|jDF%xS7zS+#OTv4nD^j(!1#9(Uzvcoq^ z+6b_1&P=Qc$bP;oX(M17V`~CFkT&+&J0ZY^VKcEN;B97><%(iOfZgh5V!aZ=UcJYp z*2Fe*I9F5&0S?jcYk5?{lqfRnNf~%^B^Ptym=UN&e6~Nq;=)niy}e^8^Mn3 zxgsFDvQ5Rj2MoO+8&WSbwKP}OBmzJ3AfHKJV0yN~ z+}JG@-Y+a%W9Lf1kXDR$rI?3dzSO@bT~^oKZNi1S>CTp(A*F|mRagU_DO}v%Gd+kt(NS-5d@538+1dMbDf`2L`B{uh_*3{;C1Wx)l*#A6Dg8OJh4*O)|`P z2`{$SnTux+_^k-Z#~j|#v5}9c$|jC7aE$^NC&nanAm8>f$c2FVYyPwhc71mk01(K> zT^xTJplmHgf^8pAW7ry#kdM1^_WsEnG9Mmg=zG-AIw`9Iu?WL1Jzuos8xNE)S^s&4cIQl zhZyBr88uS%4`85oKs8Ly$^6x47~Z5!-M2fH|EzE@$=ujHU)A7HURb|FKkkkgW2TNc zfOfmEF8#_-n1EsVh%ejYr3}3tP4Z@YLY-3v~^H_}U+ta~954cQv-o)PU|9Zgi#dwt-Jg24P z1BmNVBVXtRSsh`cY#W7+3{G7+aaLmD5d3{T^szR2IL>BvGN&qS_l+47IJQ-`4zXe; zTgHFf^2l+9V^{TnrMA(x#b?Ec;g_$?n3m`Uf$tNF(byl;SPv-2SiN9ZaNDKy8ICqN zte`Qsd25?&?qlnB1LwBxR%UlDpZaYVPOqgd>k@E~BdlM-k6|&9MP|;qwq=fKD*TUB zFOhV1R>kW`0brV?v>`$>SGy-eFsm9VkPjSPcI`G4pnlDoUX#$EZ*XoQ1Z0IH1$kDz z-;{^OQ*cr~azc#{krZS;1T@{>)gUUoMs!8EI64J63z8=+5(Om^nJjHP6<_a!fb3jNL3Yg`)+jO51(=nF0gH zwn{#h*%GR1_?;zF|0YfMSSGRH+@|zg{U_Nv?`cVV#+%7|5?3LlCiv7w^1&f;GoCwH zhJ>7&qXjfo@5j;`sX5wkw+M(9PR&txS4Bupv_O^jk`r<1O3ac{a-#1DXzmn=S-9M% zmz?NVMN^{`Cg6C_$E9Z@Z!ibhV;mY%Nzca0#~-CJPA`FRrSxo-|Ds4zC?#28jMDZ5 zl{*O3-^U1Si1|om0$EicgBD9)iV&>xt$eJz9$~pGLOh9aJSlo749$njxECS=ZKLer zy|q;$n@^N(iVRC)Yr5V(&^zE%k(7Z)==Y?Lh-ueu+pcLiRQQqQVdsecYU0190kHJ- z!{vLYM+6+EuupJ}{x}Yd%&ym%BOCeXxFs_3Vdv=~wx$r8_kca?1mqh1H`6vT2|4zT z6Od8f86wQMX9e=bw&9d%6_=Z@JPDXDD=hJ+$2*@MzcRv}ubr0_>Mw>oElf#n;`djjw>SA-sJ0K14P^5= zOX@ZnRfNkmhOSNvVAd|#?AmxwGer=X%_lkMvZBjJ6$9E!2+D>JHW?iUuWhoHQNU;utN zc9esC?l1!dadeaKOe(w8^~8n?CR+L+m_3Y{e&zvp)ZO! zoSSA+m;?OxO_zgE&y(zlUgNz|_Ed6)BgT)5BK?AU?CKz3-)owRAkgqL^i(GW9WzA) zV=j?ggq-YRb%0)yRUe4ASNPem*=EhV$F2?nrjq`x7gcU7Lc1XDv8&@9aHI>7O4LX9 z*tvcM^8&8Gkkgo-S=xsH_wLQmdoMp-#JdM1Q$Z%5OXzB;l-RjuoA4&!$7_KpH{ywa zYYg3nU=6?Iu;V0G?g2gN;a@ky?`#djF1HYP_JCi@e)^3^hfa?9;E(6)|;X!O`-*bKC5!j?6ds}`HTN8zI4c2v*hbfov zLV(zwU|G2}S$O};k~RWtF*6fu0=8l2vl)0Dt;qCjwKW0B;+Z9F1WaRWO~A@q*wc<( z65g4n;%hD=q0+Ns@OWFvS z#@Ko#ghlH|r5wP{k{$w2$)$a?62k5n6EW=pgJGALSe7e^5rK;>$%licJs=foJ(xq- z>>J~ZY?gEoctWns5dmMabjh>_9Aoqr15dLwGS?La0;gG%&!q?KL8YFMO{r%(L&tdq zc3zK4x$l@BeOKxJ(gUQa?8qid)q_&)F+MYG94hnCTm}hQk@LrU|B?n&Dq4t4De|xs(oyib4*r~Gmo_YT*$(UG>OAW zmHV{x0qJeh8!fkfPw0`k?Pr~Ir@DZirkxG?{W0>F9E9c~%sDE-Zgn;cvuBenRqR}1 z^9bwrETOW(ajW$_pSd%sRH&eEw_Pj}=iO_m1Uptg#J(7A`6P93XQlQbI3}b;z!BNQ zs*UL;%Mw2F+QI6=Xo)gOJ!-xZ(k665;JI+QDY?4KQ2IW;y*Js^j@J1d><0RsV=xYLe1*g8$VJOpp+cUCpf1vSxT z18z>;Wao$AFbs1`5<_Ag*H+LZJBoL!>u$Q}SU1j~&JV$05s1KzNOj5DOuj8pX98}c z+{(Jxr*`UWt46oVHcFb7i=xmnO}LLh;rmch$!1j+1hR;)RoEs zsmP<3ka;4_66y05SV-9=2|;ne;{7>pQl^;8BeEGGd6$z^GXv}1?3?r|u zZT5r)L3!yUSR2@K?&!6OWsM2y+SwnMqV-o&(qbdNtS((0_=8!*CnHo&hz07Y%IeNV{q$8Pqoc4l+$TPNQaeA zvg&slDS||iI)u0>n(($&LnGo0WuykR*5F-D4)@1;kCk2FW#m~dzM5Q1em2Ata5 zei>GbQvI|+L?)~_+d^aVyRA%}8v8i~rhZq&x^N1kbCk7zDDf4PpPOeMm@HPppq;N@ zjb?TsESkHxS^6@#_YmabiQ73XzMVHbDZJ9JIm=w71F=2}GfG&P4VV}y2M~;uwk7-B z(iQ7hO|?*)^iJu@O=?xhWuQaFBwE0TY_(ejUPDp4X;pUs-r)LUH|lET6nXUdMBCk) zLF%t8`72Q&mWun>j0$^kWhQa1Z+J_JLRlYr2-u*@S=F3XAY)Tg|ActfdJWkuR!VLW z47cQvS!3eLYrrB#Ek27HU6kC6YX>DwJq-Str|Ung2f5W95!muMO167Vd`_P(QTU~r z@7y6DgJ76HCx=pP4LoCj5%;<-c&oz0TO6Zn;by@5(`RmOrG z(DyjygRtUoumf_fgrCkFX1t9)S87_OrYAA8UXbaGDf%Tf`$9IQT|d21F^{p zwHlgZ*D)RL=xn0wGbMTK{jtvRvCz)7qDt>~+xdEVUqxXdAy(`^#(&J4jNC7SxJdRU z={%eF7Q&+(?T?I91QcU4pkk# z_(}*&UjbT&py;BR9|t2SzRoc2Lx~!eT+rS25JEmUqxt~aD(~_anh-FioZG(_{`;i= zx25p_7P%e7-`kH?%gSqPehQDSc#{NeMae&LtFFA@<#Un6a;p)Uj;Pi_Rk}J0Pvlos zS9$>VCd!}KJ(N+%>_gf#-aOVA=VC*#N9G25NqwuZN97s)qiYP}hpveZKXm=a=ucFk z(KGhi+vp4`nz2`YUu96}wXRlzXhm>-wib}1vfzf=Ss4$FKM^NdkOOLUs9@9lI#$@Ih{?6t##8oOR6`@x7v}hd5DNU+b1F(0MODq~)gYi4M#x2Z9N%Q$SM;r{6buIDS!1n%TM zc0kz+*YJ5GE!J?l^Zv=ffENSuOHkJlQ+s{pxCTjuJ_Ly#D7)?%Zh+9JYEQ#m(^da; zzQ5_DNM6`Cfq>LXcRJgHYYYCEY@GWZ`BBQiH+k7pc6Z(LPMbskKAB&I9kl)F9vpxH zB`705xZaa4j%+#NKc!toyK-1@nmjN3{@_r!pL!GdD*-prka#p#GamfvX{P}{q0Kjp z_xU{L-E`}Af9x`W{2#aSF2u>XvBd^N4@`}dqHYS~SuwKyTFj6`J4<}^2neTbuXzTq z7d`C+9U->7l$NhQ{MBQ|dMV9$YM-qP!9#E{ixV5h!+1;dxSow;>9l)_N|SW^^QAK& z_J+R<`P=L@b78btJ#@E-u59|72wnU4|K*C?t(W$GDOZh=;%jrr1>?~y^Ni-OBB#&H zU&Vn3jd0R+$Jp84=i(`|8Bx##mGgxUtUk$Q#{KIax@vz`-utGJCaGvT-kvaVNkBJyjUgA&DDy86$0%M@e^qa?uI#>u+w ISAXJv0aAtCJpcdz literal 1867 zcmeH{`#;kQ7{|Y3#42V=URoguFU(0aVy3eel52E9TG2FX%o&j#wn`y~Mde;nsAM=N zw`MbiZYnXxnaf;WaWG6~t6`2h{R{nee)v4k^V{=!J@3!+N%i)0*$B~t006Mj&Gmrq zTEF*AYN~5Bts~4506=5{2IEZ#2>}4p%$Q7$dyBK~%c8nNSaX#aXw++OZ1L4{gN%S= z1ojXU*JJ!_7xPBRNY;)ve{z(mT?!&`qhcKkQVF?yPi;rUY@(&_DAl7FD{w%}F-&Ca z0o1xLNA~RzRodZlH9B@`cZElIK$?t_K>0^@W@0v=Rn?uV(ndV9az6>XQ|V{7(Q+7vffp9H(8n_%hJ z{Km&QlkMZ%-tAvno>|$|?W6T$qSVbYuWc^3}CuOqk7-E?7uu8+; z0tM`ojm#sy_Axf&l30U$k=K1oc{qHFZem?1qPG;VZ-@blL0TdpQ+)t5FJY zX#HgBEWO1DS13(JDu8{(=ZqU_gz;+#)XA-g%** zsHk7w$0r5qYb3WY+zYAM_K404uWf6$N=umaqy*CRz_F?s^g`%sEb88*1;r63Sy8w) z5_;Q)SM)w^Z=GhU;PXO=uX?1BNKrYnfWm0pGZkO;q({|`WYZ2OHj-Gv$ubVw$gd}B zh*AsvVEwA>bl;f;@f#V@HIkoC$hPNyW~C%}a?Sm{>cUsVLiMmbqs`Y(I$pC+II|*Q zxR^iwN}J*7|II$fAT%-haM|`*duclhHi{M}AS2T`!lG?b&Or`7&uE-7Z-R5Mm1D-O z%vGveq({UjTAhL zd~fa2eO41B63$ev=>0j7a@QRZD0&v{5j!Nr082EB<4d+@I8aXjKsQFT z5kR2BX;a5w&}LZ9Xbl}G!!DYutKm}JqXL`(-hKIPFu1z4)Xvg?PViYdq@;9w)VELv z+FbJ}@&7eHj@G)hz)~8-DjQT0 zQwaAD&!?bVBtiKX{=)}aKAvxS#PPce$_CGLloACefk=1lv)9p~f`LivcDPSIrly~c ze>FBAMO-RnK2#P6d;eM;l8DH5%dWG?cDUQkq3Ka{QsHGW0`-L^q2_3u5)!lRyJRDy zUd>Kq2h`mfv=fsIk@DuS>)T~ElEajpB!mL&cxO8b8ku!v)x8S&;02NgHy=oDAGm)f z2}w$iAFQxl^LfRmnB7Y?TVs3pOCWU-s*0}{{r$NfC3qsOF2bb2Pp;0Hj9XBG{OtJb z+zZN8{c<_lvqkZ=ml(W|Kz(ohil0#+5~8=&FV^X{Zf7g@tlV70vhjIihPjP#8=l(q zO&ZQUIVxi@b4aMj>4_`TigTqj69G4SN=P2ai2G0mpg5d9jwzfE(8HK;^Hv%l`rrt1of@ diff --git a/v1/frontend/src/assets/icons/twbs/play-fill-green.png b/v1/frontend/src/assets/icons/twbs/play-fill-green.png new file mode 100644 index 0000000000000000000000000000000000000000..6b7294a2cae72062266861ccd5b1e6ed3b36b3c5 GIT binary patch literal 3203 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 UG%f6A{%rxq`WAWwluOM20p&3KssI20 literal 0 HcmV?d00001 diff --git a/v1/frontend/src/assets/icons/twbs/play-fill.png b/v1/frontend/src/assets/icons/twbs/play-fill.png index 2cbb56cc299b04e1b5d5fd7405f6d91051908540..98a0c0603562f70213d6154f306090dbb750003c 100644 GIT binary patch literal 1460 zcmV;l1xxygP)^M-<0*jR*OlC@5$kBp`x#Ac7*ONf2T_iGtq* zKLtcT#QR!NlxRd<7Ew_aR6IZtB?*WcMZp8a13VH<#8pJ_j7La3BI>H^@98u!U?DCC z_*9^`XhgKG0>0-UB0VM|fXXf^7ZPof^J2FmM@VLQb=WMdNg zM4=Z5Li3@LSY52mM@I zTbo4vkv7>j*iK_lurVFre?;2)m>-^FH+d2O$Y|H@ok+cH0HH^6N?8nZ-gq6LhcN&4G3AV#`EE`t)*;$8n5MJKd4nFO$9lmD^T}An8ZAxPpaf3OGoo5Iu+fXlCX=lZ=i}^n1!%t`p zq2-lL;IjzZ;d_IPS!kavxBgj%?Le1)x%AJapPp#SO3e0xIGTf(h~;Zhw7L zg{QxW61>80rF)yrd1!Z3T}6dm>dGhCH4Wew!}jvL#%wQ+BRP1USiTYU2otz^wl~cu z9R3ll4`Y-_VVk+`Rm^4r{E(TsyOo9!GkWr)Q2BA_KGer7OR1H@_D60E=6?ZPW@2>l zm$jxw!-!vEt@aPtT!6NljuI*C;IN&X=L%iL)0K)+*p&#GAK!}URJ{EEa#<9%IZPAF zQy7gdciXQQNE3wSHp4Mpr=D#NEDXybc=!3X1*vC;fwr`a$!^6e>}X(*aw}FZkkPDMIHs@zv-6#Cqr&#hEKr86TpZB_m@1y%!I!$>Q=du&cbBccGUVdNud znJd|VRsvb(P)3&W9!7qJ>>MDwK7S6IdR}4w2}qg1bTn+BUghO2X8#L}AJE{qLFXxE z%j+>3(GnobT*yRH9>R!(3YkuQIT6A|m1a4N$VG2F`ql=}Sq&pHmvK2-4P=?)Xidsm z7}*Kg`9QvF4dJ2_$qH<_wU(}&i~(Ak(jK#As>SzcnC+m`9JA%km21&5AV0O%mD`+H z8pFtb+Atf)*U|#nInOk~mZ2!=FIP43Gs!O~4KZ87guGa5V46YO9ual7E$>C O0000tvzB^ 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 WV6`b5zShTY6<};&s$Z?=68#^V=?Gl_ literal 0 HcmV?d00001 diff --git a/v1/frontend/src/assets/index.js b/v1/frontend/src/assets/index.js index 7faea08..f4e317b 100644 --- a/v1/frontend/src/assets/index.js +++ b/v1/frontend/src/assets/index.js @@ -7,12 +7,17 @@ import eye from './icons/twbs/eye.png'; import eye_red from './icons/twbs/eye-red.png'; import eye_slash from './icons/twbs/eye-slash.png'; import gear_fill from './icons/twbs/gear-fill.png'; +import gear_fill_white from './icons/twbs/gear-fill-white.png'; import heart from './icons/twbs/heart-fill.png'; import pause from './icons/twbs/pause-circle-green.png'; +import pause_big from './icons/twbs/pause-fill.png'; import play from './icons/twbs/play-circle-green.png'; +import play_big from './icons/twbs/play-fill.png'; +import play_big_green from './icons/twbs/play-fill-green.png'; import plus_circle from './icons/twbs/plus-circle-fill.png'; import robot from './icons/Font-Awesome/robot.png'; import star from './icons/twbs/star-fill.png'; +import stop_big_red from './icons/twbs/stop-fill-red.png'; import thumbs_down from './icons/twbs/hand-thumbs-down-fill.png'; import thumbs_up from './icons/twbs/hand-thumbs-up-fill.png'; import x_lg from './icons/twbs/x-lg.png'; @@ -27,13 +32,18 @@ export const Eye = eye; export const EyeRed = eye_red; export const EyeSlash = eye_slash; export const Gear = gear_fill; +export const GearWhite = gear_fill_white; export const Heart = heart; export const Logo = logo; export const Pause = pause; +export const PauseBig = pause_big; export const Play = play; +export const PlayBig = play_big; +export const PlayBigGreen = play_big_green; export const PlusCircle = plus_circle; export const Robot = robot; export const Star = star; +export const StopBigRed = stop_big_red; export const ThumbsDown = thumbs_down; export const ThumbsUp = thumbs_up; export const XLg = x_lg; \ No newline at end of file diff --git a/v1/frontend/src/components/ChatBot.css b/v1/frontend/src/components/ChatBot.css index 4542233..1fab05b 100644 --- a/v1/frontend/src/components/ChatBot.css +++ b/v1/frontend/src/components/ChatBot.css @@ -84,10 +84,10 @@ flex-direction: column; height: 100%; overflow-y: auto; - padding: 0px 10px; + padding: 0px; } -.chatbot-list-button { +.chatbot-list-item-button { align-items: center; background-color: #344453; border: none; @@ -98,7 +98,7 @@ width: 100%; } -.chatbot-list-button:hover { +.chatbot-list-item-button:hover { background-color: #415568; cursor: pointer; } @@ -114,6 +114,7 @@ font-weight: bold; max-width: 300px; overflow: hidden; + padding: 0px 10px; text-overflow: ellipsis; white-space: nowrap; /* width: 100%; */ @@ -221,6 +222,16 @@ cursor: pointer; } +.chatbot-modal-review { + color: #eee; + font-family: sans-serif; + font-size: 16px; + height: 350px; + overflow-x: scroll; + overflow-y: scroll; + width: 100%; +} + .chatbot-modal-setting { align-items: center; box-sizing: border-box; @@ -286,6 +297,88 @@ transition: .4s; } +.chatbot-rules { + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; + padding: 0px; +} +.chatbot-rule { + border-bottom: 1px solid #1f2e3c; + box-sizing: border-box; + color: white; + display: flex; + flex-direction: row; + font-family: sans-serif; + justify-content: space-between; + padding: 10px 20px; +} + +.chatbot-rule-header { + font-weight: bold; +} + +.chatbot-rule-output { + align-items: center; + display: flex; + justify-content: left; + overflow: hidden; + overflow-x: scroll; + white-space: nowrap; + width: 50%; +} + +.chatbot-rule-buttons { + align-items: center; + box-sizing: border-box; + display: flex; + flex-direction: center; + justify-content: space-evenly; + padding-left: 10px; + width: 75px; +} + +.chatbot-rule-button { + align-items: center; + background-color: #344453; + border: none; + display: flex; + justify-content: center; + padding: 0px; +} + +.chatbot-rule-button:hover { + cursor: pointer; +} + +.chatbot-rule-button-icon { + height: 16px; + width: 16px; +} + +.chatbot-rule-sender { + align-items: center; + box-sizing: border-box; + display: flex; + justify-content: left; + overflow-x: scroll; + padding-left: 10px; + white-space: nowrap; + width: 25%; +} + +.chatbot-rule-trigger { + align-items: center; + box-sizing: border-box; + display: flex; + justify-content: left; + overflow-x: scroll; + padding-left: 10px; + white-space: nowrap; + width: 25%; +} + .choose-file { align-items: center; display: flex; @@ -338,12 +431,42 @@ input:checked + .chatbot-modal-toggle-slider:before { border-radius: 50%; } +.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; + width: 100%; +} + +.command-rant-amount { + border: none; + border-radius: 5px; + box-sizing: border-box; + font-family: monospace; + font-size: 16px; + outline: none; + padding: 5px; + text-align: center; +} + +.command-rant-amount-symbol { + color: #eee; + font-family: sans-serif; + font-size: 20px; + padding-right: 1px; +} + .timer-input { border: none; border-radius: 34px; box-sizing: border-box; font-family: monospace; - font-size: 24px; + font-size: 16px; outline: none; padding: 5px 10px 5px 10px; text-align: right; diff --git a/v1/frontend/src/components/ChatBot.jsx b/v1/frontend/src/components/ChatBot.jsx index b5a6fa0..82f0b13 100644 --- a/v1/frontend/src/components/ChatBot.jsx +++ b/v1/frontend/src/components/ChatBot.jsx @@ -3,13 +3,32 @@ import { Modal, SmallModal } from './Modal'; import { AccountList, ChatbotList, + ChatbotRules, DeleteChatbot, + DeleteChatbotRule, NewChatbot, + NewChatbotRule, OpenFileDialog, + RunChatbotRule, + RunChatbotRules, + StopChatbotRule, + StopChatbotRules, UpdateChatbot, + UpdateChatbotRule, } from '../../wailsjs/go/main/App'; -import { EventsOn } from '../../wailsjs/runtime/runtime'; -import { ChevronLeft, ChevronRight, Gear, PlusCircle, Robot } from '../assets'; +import { EventsOff, EventsOn } from '../../wailsjs/runtime/runtime'; +import { + ChevronLeft, + ChevronRight, + Gear, + GearWhite, + PauseBig, + PlayBig, + PlayBigGreen, + PlusCircle, + Robot, + StopBigRed, +} from '../assets'; import './ChatBot.css'; function ChatBot(props) { @@ -20,6 +39,7 @@ function ChatBot(props) { const [openChatbot, setOpenChatbot] = useState(null); const [openNewChatbot, setOpenNewChatbot] = useState(false); const [openNewRule, setOpenNewRule] = useState(false); + const [chatbotRules, setChatbotRules] = useState([]); const [chatbotSettings, setChatbotSettings] = useState(true); useEffect(() => { @@ -33,6 +53,10 @@ function ChatBot(props) { } } }); + + EventsOn('ChatbotRules', (event) => { + setChatbotRules(event); + }); }, []); useEffect(() => { @@ -46,7 +70,14 @@ function ChatBot(props) { }, []); const open = (chatbot) => { - setOpenChatbot(chatbot); + ChatbotRules(chatbot) + .then((response) => { + setChatbotRules(response); + setOpenChatbot(chatbot); + }) + .catch((error) => { + setError(error); + }); }; const closeEdit = () => { @@ -82,6 +113,22 @@ function ChatBot(props) { }); }; + const deleteChatbotRule = (rule) => { + DeleteChatbotRule(rule).catch((error) => setError(error)); + }; + + const startAll = () => { + RunChatbotRules(openChatbot.id).catch((error) => { + setError(error); + }); + }; + + const stopAll = () => { + StopChatbotRules(openChatbot.id).catch((error) => { + setError(error); + }); + }; + return ( <> {error !== '' && ( @@ -135,7 +182,12 @@ function ChatBot(props) { /> )} {openNewRule && ( - setOpenNewRule(false)} show={openNewRule} /> + setOpenNewRule(false)} + new={true} + show={openNewRule} + /> )}
{openChatbot === null ? ( @@ -158,30 +210,57 @@ function ChatBot(props) {
) : ( -
-
- setOpenChatbot(null)} - src={ChevronLeft} - /> + <> +
+
+ setOpenChatbot(null)} + src={ChevronLeft} + /> +
+ {openChatbot.name} +
+ + +
- {openChatbot.name} -
- - +
+ Output + + Trigger + + Sender +
+ + +
-
+
+ {chatbotRules.map((rule, index) => ( + + ))} +
+ )}
@@ -193,13 +272,148 @@ export default ChatBot; function ChatbotListItem(props) { return (
-
); } +function ChatbotRule(props) { + const [ruleActive, setRuleActive] = useState(props.rule.running); + const updateRuleActive = (active) => { + props.rule.running = active; + setRuleActive(active); + }; + const [ruleError, setRuleError] = useState(''); + const [ruleID, setRuleID] = useState(0); + const [updateRule, setUpdateRule] = useState(false); + + useEffect(() => { + if (ruleID !== props.rule.id) { + EventsOff('ChatbotRuleActive-' + props.rule.id); + EventsOff('ChatbotRuleError-' + props.rule.id); + } + + EventsOn('ChatbotRuleActive-' + props.rule.id, (event) => { + updateRuleActive(event); + }); + + EventsOn('ChatbotRuleError-' + props.rule.id, (event) => { + setRuleError(event); + }); + + setRuleID(props.rule.id); + }, [props.rule.id]); + + useEffect(() => { + setRuleActive(props.rule.running); + }, [props.rule.running]); + + const deleteRule = () => { + setUpdateRule(false); + props.deleteRule(props.rule); + }; + + const prettyTimer = (timer) => { + let hours = Math.floor(timer / 3600); + let minutes = Math.floor(timer / 60 - hours * 60); + let seconds = Math.floor(timer - hours * 3600 - minutes * 60); + + return hours + 'h ' + minutes + 'm ' + seconds + 's'; + }; + + const printTrigger = () => { + let trigger = props.rule.parameters.trigger; + + switch (true) { + case trigger.on_command !== undefined && trigger.on_command !== null: + return trigger.on_command.command; + case trigger.on_timer !== undefined && trigger.on_timer !== null: + return prettyTimer(props.rule.parameters.trigger.on_timer); + } + }; + + const startRule = () => { + setRuleActive(true); + RunChatbotRule(props.rule) + .then(() => { + updateRuleActive(true); + }) + .catch((error) => { + // TODO: format error in rule with exclamation point indicator + // Replace play/pause button with exclamation point + // User must clear error before reactivating + setRuleActive(false); + }); + }; + + const stopRule = () => { + let active = ruleActive; + setRuleActive(false); + StopChatbotRule(props.rule) + .then(() => { + updateRuleActive(false); + }) + .catch((error) => { + setRuleActive(active); + // TODO: format error in rule with exclamation point indicator + // Replace play/pause button with exclamation point + // User must clear error before reactivating + }); + }; + + const triggerKey = () => { + const trigger = props.rule.parameters.trigger; + + switch (true) { + case trigger.on_command !== undefined && trigger.on_command !== null: + return 'on_command'; + case trigger.on_timer !== undefined && trigger.on_timer !== null: + return 'on_timer'; + } + }; + + return ( + <> + {updateRule && ( + setUpdateRule(false)} + onDelete={deleteRule} + new={false} + rule={JSON.parse(JSON.stringify(props.rule.parameters))} + ruleID={props.rule.id} + show={updateRule} + trigger={triggerKey()} + /> + )} +
+ {props.rule.display} + {printTrigger()} + {props.rule.parameters.send_as.display} +
+ + +
+
+ + ); +} + function ModalChatbot(props) { const [error, setError] = useState(''); const [id, setId] = useState(props.chatbot === undefined ? null : props.chatbot.id); @@ -314,12 +528,23 @@ function ModalChatbot(props) { ); } -function ModalNewRule(props) { - const [back, setBack] = useState([]); - const [rule, setRule] = useState({}); - const [stage, setStage] = useState('trigger'); - const updateStage = (next, reverse) => { - setBack([...back, { stage: stage, reverse: reverse }]); +function ModalRule(props) { + const [back, setBack] = useState( + props.new + ? [] + : [ + { stage: 'trigger' }, + { stage: 'trigger-' + props.trigger }, + { stage: 'message' }, + { stage: 'sender' }, + ] + ); + const [edit, setEdit] = useState(props.new ? true : false); + const [error, setError] = useState(''); + const [rule, setRule] = useState(props.new ? {} : props.rule); + const [stage, setStage] = useState(props.new ? 'trigger' : 'review'); + const updateStage = (next) => { + setBack([...back, { stage: stage }]); setStage(next); }; @@ -336,12 +561,60 @@ function ModalNewRule(props) { setBack(back.slice(0, back.length - 1)); }; - const submit = () => {}; + const submit = () => { + if (props.new) { + submitNew(); + } + + submitUpdate(); + }; + + const submitNew = () => { + let appRule = { + chatbot_id: props.chatbot.id, + parameters: rule, + }; + + NewChatbotRule(appRule) + .then(() => { + props.onClose(); + }) + .catch((err) => { + setError(err); + }); + }; + + const submitUpdate = () => { + let appRule = { + id: props.ruleID, + chatbot_id: props.chatbot.id, + parameters: rule, + }; + + UpdateChatbotRule(appRule) + .then(() => { + props.onClose(); + }) + .catch((err) => { + setError(err); + }); + }; return ( <> + {error !== '' && ( + setError('')} + show={error !== ''} + style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }} + title={'Error'} + message={error} + submitButton={'OK'} + onSubmit={() => setError('')} + /> + )} {stage === 'trigger' && ( - )} - {stage === 'trigger-timer' && ( - + )} + {stage === 'trigger-on_timer' && ( + )} {stage === 'message' && ( - )} {stage === 'sender' && ( - )} {stage === 'review' && ( - )} ); } -function ModalNewRuleTrigger(props) { +function ModalRuleTrigger(props) { const next = (stage) => { - const rule = props.rule; - rule.trigger = {}; - props.setRule(rule); - props.setStage(stage, reverse); + props.setStage(stage); }; - const reverse = (rule) => { - rule.trigger = null; - return rule; + const triggerOnCommand = () => { + const rule = props.rule; + if (rule.trigger == undefined || rule.trigger == null) { + rule.trigger = {}; + } + if (rule.trigger.on_command == undefined || rule.trigger.on_command == null) { + rule.trigger.on_command = {}; + } + if ( + rule.trigger.on_command.restrict == undefined || + rule.trigger.on_command.restrict == null + ) { + rule.trigger.on_command.restrict = {}; + } + + rule.trigger.on_event = null; + rule.trigger.on_timer = null; + + props.setRule(rule); + + next('trigger-on_command'); + }; + + const triggerOnTimer = () => { + const rule = props.rule; + if (rule.trigger == undefined || rule.trigger == null) { + rule.trigger = {}; + } + + rule.trigger.on_command = null; + rule.trigger.on_event = null; + + props.setRule(rule); + + next('trigger-on_timer'); }; return ( @@ -414,10 +730,7 @@ function ModalNewRuleTrigger(props) {
Choose Rule Trigger
- - - */} +
@@ -517,7 +1061,7 @@ function ModalNewRuleTriggerTimer(props) { ); } -function ModalNewRuleMessage(props) { +function ModalRuleMessage(props) { const [error, setError] = useState(''); const [message, setMessage] = useState( props.rule.message !== undefined && props.rule.message !== null ? props.rule.message : {} @@ -527,9 +1071,9 @@ function ModalNewRuleMessage(props) { const [validText, setValidText] = useState(true); const back = () => { - const rule = props.rule; - rule.message = null; - props.setRule(rule); + // const rule = props.rule; + // rule.message = null; + // props.setRule(rule); props.onBack(); }; @@ -549,7 +1093,7 @@ function ModalNewRuleMessage(props) { const rule = props.rule; rule.message = message; props.setRule(rule); - props.setStage('sender', null); + props.setStage('sender'); }; const chooseFile = () => { @@ -573,6 +1117,7 @@ function ModalNewRuleMessage(props) { message.from_file = null; } else { message.from_file = {}; + message.from_text = ''; } setMessage(message); @@ -604,6 +1149,7 @@ function ModalNewRuleMessage(props) { const updateMessageText = (event) => { message.from_text = event.target.value; setMessage(message); + setRefresh(!refresh); }; const updateMessageFilepath = (filepath) => { @@ -707,7 +1253,7 @@ function ModalNewRuleMessage(props) { ); } -function ModalNewRuleSender(props) { +function ModalRuleSender(props) { const [accounts, setAccounts] = useState({}); const [error, setError] = useState(''); const [sender, setSender] = useState( @@ -726,9 +1272,9 @@ function ModalNewRuleSender(props) { }, []); const back = () => { - const rule = props.rule; - rule.send_as = null; - props.setRule(rule); + // const rule = props.rule; + // rule.send_as = null; + // props.setRule(rule); props.onBack(); }; @@ -741,7 +1287,7 @@ function ModalNewRuleSender(props) { const rule = props.rule; rule.send_as = sender; props.setRule(rule); - props.setStage('review', null); + props.setStage('review'); }; const selectSender = (sender) => { @@ -816,7 +1362,7 @@ function ModalNewRuleSender(props) { >
- Choose sender + Choose Sender
{validSender ? ( @@ -850,7 +1396,13 @@ function ModalNewRuleSender(props) { ); } -function ModalNewRuleReview(props) { +function ModalRuleReview(props) { + const [deleteRule, setDeleteRule] = useState(false); + const [edit, setEdit] = useState(props.edit); + const updateEdit = (e) => { + setEdit(e); + props.setEdit(e); + }; const [error, setError] = useState(''); const back = () => { @@ -861,33 +1413,91 @@ function ModalNewRuleReview(props) { props.onSubmit(); }; + const displayTrigger = () => { + switch (true) { + case props.rule.trigger.on_timer !== undefined || props.rule.trigger.on_timer !== null: + return 'Timer'; + default: + return 'Error'; + } + }; + + const confirmDelete = () => { + setDeleteRule(false); + props.onDelete(); + }; + return ( - -
-
- Review -
-
-
- - + <> + {deleteRule && ( + setDeleteRule(false)} + onClose={() => setDeleteRule(false)} + show={deleteRule} + style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }} + title={'Delete Rule'} + message={'Are you sure you want to delete this rule?'} + submitButton={'OK'} + onSubmit={confirmDelete} + /> + )} + setDeleteRule(true)} + show={props.show} + submitButton={edit ? 'Submit' : 'Edit'} + onSubmit={edit ? submit : () => updateEdit(true)} + style={{ height: '480px', minHeight: '480px', width: '360px', minWidth: '360px' }} + > +
+
+ Review
-
- - +
+
+
{JSON.stringify(props.rule, null, 2)}
+
+
-
-
-
+ + + ); +} + +function Command(props) { + const updateCommand = (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; + } + + props.setCommand(command); + }; + + return ( + ); } @@ -934,6 +1544,8 @@ function Timer(props) { }; const printTimer = () => { + // let timer = intervalToTimer(props.timer); + if (props.timer === '') { return '00:00:00'; } @@ -955,6 +1567,7 @@ function Timer(props) { onInput={updateTimer} placeholder={printTimer()} size='8' + style={props.style} type='text' value={''} /> diff --git a/v1/frontend/src/components/Modal.css b/v1/frontend/src/components/Modal.css index 7b93a1a..3385bfc 100644 --- a/v1/frontend/src/components/Modal.css +++ b/v1/frontend/src/components/Modal.css @@ -200,6 +200,7 @@ .small-modal-message { font-family: sans-serif; font-size: 18px; + overflow-x: scroll; } .small-modal-title { diff --git a/v1/go.mod b/v1/go.mod index 3c8823e..fee5275 100644 --- a/v1/go.mod +++ b/v1/go.mod @@ -6,8 +6,8 @@ toolchain go1.22.0 require ( github.com/mattn/go-sqlite3 v1.14.22 - github.com/tylertravisty/rumble-livestream-lib-go v0.5.1 - github.com/wailsapp/wails/v2 v2.8.0 + github.com/tylertravisty/rumble-livestream-lib-go v0.7.2 + github.com/wailsapp/wails/v2 v2.8.1 ) require ( diff --git a/v1/go.sum b/v1/go.sum index dcc020d..e0f7763 100644 --- a/v1/go.sum +++ b/v1/go.sum @@ -60,8 +60,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.5.1 h1:vq65n/8MOvvg6tHiaHFFfYf25w7yuR1viSoBCjY2DSg= -github.com/tylertravisty/rumble-livestream-lib-go v0.5.1/go.mod h1:Odkqvsn+2eoWV3ePcj257Ga0bdOqV4JBTfOJcQ+Sqf8= +github.com/tylertravisty/rumble-livestream-lib-go v0.7.2 h1:TRGTKhxB+uK0gnIC+rXbRxfFjMJxPHhjZzbsjDSpK+o= +github.com/tylertravisty/rumble-livestream-lib-go v0.7.2/go.mod h1:Odkqvsn+2eoWV3ePcj257Ga0bdOqV4JBTfOJcQ+Sqf8= 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= @@ -71,8 +71,8 @@ github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhy github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.8.0 h1:b2NNn99uGPiN6P5bDsnPwOJZWtAOUhNLv7Vl+YxMTr4= -github.com/wailsapp/wails/v2 v2.8.0/go.mod h1:EFUGWkUX3KofO4fmKR/GmsLy3HhPH7NbyOEaMt8lBF0= +github.com/wailsapp/wails/v2 v2.8.1 h1:KAudNjlFaiXnDfFEfSNoLoibJ1ovoutSrJ8poerTPW0= +github.com/wailsapp/wails/v2 v2.8.1/go.mod h1:EFUGWkUX3KofO4fmKR/GmsLy3HhPH7NbyOEaMt8lBF0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= diff --git a/v1/internal/chatbot/chatbot.go b/v1/internal/chatbot/chatbot.go new file mode 100644 index 0000000..073e563 --- /dev/null +++ b/v1/internal/chatbot/chatbot.go @@ -0,0 +1,447 @@ +package chatbot + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "sync" + "time" + + "github.com/tylertravisty/rum-goggles/v1/internal/events" + "github.com/tylertravisty/rum-goggles/v1/internal/models" + rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go" + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +type user struct { + livestreams map[string]*rumblelivestreamlib.Client + livestreamsMu sync.Mutex +} + +func (u *user) byLivestream(url string) *rumblelivestreamlib.Client { + client, _ := u.livestreams[url] + + return client +} + +type clients map[string]*user + +func (c clients) byUsername(username string) *user { + user, _ := c[username] + return user +} + +func (c clients) byUsernameLivestream(username string, url string) *rumblelivestreamlib.Client { + user := c.byUsername(username) + if user == nil { + return nil + } + + return user.byLivestream(url) +} + +type receiver struct { + onCommand map[string]map[int64]chan events.Chat + onCommandMu sync.Mutex + //onFollow []chan ??? + //onRant []chan events.Chat + //onSubscribe []chan events.Chat +} + +type Bot struct { + runners map[int64]*Runner + runnersMu sync.Mutex +} + +type Chatbot struct { + accountS models.AccountService + bots map[int64]*Bot + botsMu sync.Mutex + chatbotS models.ChatbotService + clients clients + clientsMu sync.Mutex + logError *log.Logger + receivers map[string]*receiver + receiversMu sync.Mutex + //runners map[int64]*Runner + // runnersMu sync.Mutex + wails context.Context +} + +func New(accountS models.AccountService, chatbotS models.ChatbotService, logError *log.Logger, wails context.Context) *Chatbot { + return &Chatbot{ + accountS: accountS, + bots: map[int64]*Bot{}, + chatbotS: chatbotS, + clients: map[string]*user{}, + logError: logError, + receivers: map[string]*receiver{}, + // runners: map[int64]*Runner{}, + wails: wails, + } +} + +// TODO: resetClient/updateClient +func (cb *Chatbot) addClient(username string, livestreamUrl string) (*rumblelivestreamlib.Client, error) { + cb.clientsMu.Lock() + defer cb.clientsMu.Unlock() + + u := cb.clients.byUsername(username) + if u == nil { + u = &user{ + livestreams: map[string]*rumblelivestreamlib.Client{}, + } + cb.clients[username] = u + } + + client := u.byLivestream(livestreamUrl) + if client != nil { + return client, nil + } + + account, err := cb.accountS.ByUsername(username) + if err != nil { + return nil, fmt.Errorf("error querying account by username: %v", err) + } + + var cookies []*http.Cookie + err = json.Unmarshal([]byte(*account.Cookies), &cookies) + if err != nil { + return nil, fmt.Errorf("error un-marshaling cookie string: %v", err) + } + client, err = rumblelivestreamlib.NewClient(rumblelivestreamlib.NewClientOptions{Cookies: cookies, LiveStreamUrl: livestreamUrl}) + if err != nil { + return nil, fmt.Errorf("error creating new client: %v", err) + } + + _, err = client.ChatInfo(true) + if err != nil { + return nil, fmt.Errorf("error getting chat info for client: %v", err) + } + + u.livestreamsMu.Lock() + defer u.livestreamsMu.Unlock() + u.livestreams[livestreamUrl] = client + + return client, nil +} + +func (cb *Chatbot) Run(rule *Rule, url string) error { + if rule == nil || + rule.ChatbotID == nil || + rule.ID == nil || + rule.Parameters == nil || + rule.Parameters.SendAs == nil { + return pkgErr("", fmt.Errorf("invalid rule")) + } + + stopped := cb.stopRunner(*rule.ChatbotID, *rule.ID) + if stopped { + // TODO: figure out better way to determine when running rule is cleaned up. + // If rule was stopped, wait for everything to complete before running again. + time.Sleep(1 * time.Second) + } + + var err error + client := cb.clients.byUsernameLivestream(rule.Parameters.SendAs.Username, url) + if client == nil { + client, err = cb.addClient(rule.Parameters.SendAs.Username, url) + if err != nil { + return pkgErr("error adding client", err) + } + } + + ctx, cancel := context.WithCancel(context.Background()) + runner := &Runner{ + cancel: cancel, + client: client, + rule: *rule, + wails: cb.wails, + } + + err = cb.initRunner(runner) + if err != nil { + return pkgErr("error initializing runner", err) + } + + go cb.run(ctx, runner) + + return nil +} + +func (cb *Chatbot) initRunner(runner *Runner) error { + if runner == nil || runner.rule.ID == nil || runner.rule.ChatbotID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil { + return fmt.Errorf("invalid runner") + } + + channelID, err := runner.rule.Parameters.SendAs.ChannelIDInt() + if err != nil { + return fmt.Errorf("error converting channel ID to int: %v", err) + } + + runner.channelIDMu.Lock() + runner.channelID = channelID + runner.channelIDMu.Unlock() + + switch { + case runner.rule.Parameters.Trigger.OnTimer != nil: + runner.run = runner.runOnTimer + case runner.rule.Parameters.Trigger.OnCommand != nil: + err = cb.initRunnerCommand(runner) + if err != nil { + return fmt.Errorf("error initializing command: %v", err) + } + } + + // cb.runnersMu.Lock() + // defer cb.runnersMu.Unlock() + // cb.runners[*runner.rule.ID] = runner + + cb.botsMu.Lock() + defer cb.botsMu.Unlock() + bot, exists := cb.bots[*runner.rule.ChatbotID] + if !exists { + bot = &Bot{ + runners: map[int64]*Runner{}, + } + + cb.bots[*runner.rule.ChatbotID] = bot + } + + bot.runnersMu.Lock() + defer bot.runnersMu.Unlock() + bot.runners[*runner.rule.ID] = runner + + return nil +} + +func (cb *Chatbot) initRunnerCommand(runner *Runner) error { + runner.run = runner.runOnCommand + + cmd := runner.rule.Parameters.Trigger.OnCommand.Command + if cmd == "" || cmd[0] != '!' { + return fmt.Errorf("invalid command") + } + + chatCh := make(chan events.Chat, 10) + runner.chatCh = chatCh + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + rcvr, exists := cb.receivers[runner.client.LiveStreamUrl] + if !exists { + rcvr = &receiver{ + onCommand: map[string]map[int64]chan events.Chat{}, + } + cb.receivers[runner.client.LiveStreamUrl] = rcvr + } + + chans, exists := rcvr.onCommand[cmd] + if !exists { + chans = map[int64]chan events.Chat{} + rcvr.onCommand[cmd] = chans + } + chans[*runner.rule.ID] = chatCh + + return nil +} + +func (cb *Chatbot) run(ctx context.Context, runner *Runner) { + if runner == nil || runner.rule.ID == nil || runner.run == nil { + cb.logError.Println("invalid runner") + return + } + + runtime.EventsEmit(cb.wails, fmt.Sprintf("ChatbotRuleActive-%d", *runner.rule.ID), true) + err := runner.run(ctx) + if err != nil { + prefix := fmt.Sprintf("chatbot runner for rule %d returned error:", *runner.rule.ID) + cb.logError.Println(prefix, err) + runtime.EventsEmit(cb.wails, fmt.Sprintf("ChatbotRuleError-%d", *runner.rule.ID), "Chatbot encountered an error while running this rule.") + } + + err = cb.stop(&runner.rule) + if err != nil { + prefix := fmt.Sprintf("error stopping rule %d after runner returns:", *runner.rule.ID) + cb.logError.Println(prefix, err) + return + } + + runtime.EventsEmit(cb.wails, fmt.Sprintf("ChatbotRuleActive-%d", *runner.rule.ID), false) +} + +func (cb *Chatbot) Running(chatbotID int64, ruleID int64) bool { + // cb.runnersMu.Lock() + // defer cb.runnersMu.Unlock() + // _, exists := cb.runners[id] + // return exists + + cb.botsMu.Lock() + defer cb.botsMu.Unlock() + bot, exists := cb.bots[chatbotID] + if !exists { + return false + } + + bot.runnersMu.Lock() + defer bot.runnersMu.Unlock() + _, exists = bot.runners[ruleID] + return exists +} + +func (cb *Chatbot) Stop(rule *Rule) error { + err := cb.stop(rule) + if err != nil { + return pkgErr("", err) + } + + return nil +} + +func (cb *Chatbot) stop(rule *Rule) error { + if rule == nil || rule.ID == nil || rule.ChatbotID == nil { + return fmt.Errorf("invalid rule") + } + + cb.stopRunner(*rule.ChatbotID, *rule.ID) + + return nil +} + +func (cb *Chatbot) stopRunner(chatbotID int64, ruleID int64) bool { + // cb.runnersMu.Lock() + // defer cb.runnersMu.Unlock() + // runner, exists := cb.runners[id] + // if !exists { + // return + // } + cb.botsMu.Lock() + defer cb.botsMu.Unlock() + bot, exists := cb.bots[chatbotID] + if !exists { + return false + } + + bot.runnersMu.Lock() + defer bot.runnersMu.Unlock() + runner, exists := bot.runners[ruleID] + if !exists { + return false + } + + stopped := true + runner.stop() + // delete(cb.runners, id) + delete(bot.runners, ruleID) + + switch { + case runner.rule.Parameters.Trigger.OnCommand != nil: + err := cb.closeRunnerCommand(runner) + if err != nil { + cb.logError.Println("error closing runner command:", err) + } + } + + return stopped +} + +func (cb *Chatbot) closeRunnerCommand(runner *Runner) error { + if runner == nil || runner.rule.ID == nil || runner.rule.Parameters == nil || runner.rule.Parameters.Trigger == nil || runner.rule.Parameters.Trigger.OnCommand == nil { + return fmt.Errorf("invalid runner command") + } + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + + rcvr, exists := cb.receivers[runner.client.LiveStreamUrl] + if !exists { + return fmt.Errorf("receiver for runner does not exist") + } + + cmd := runner.rule.Parameters.Trigger.OnCommand.Command + chans, exists := rcvr.onCommand[cmd] + if !exists { + return fmt.Errorf("channel map for runner does not exist") + } + + ch, exists := chans[*runner.rule.ID] + if !exists { + return fmt.Errorf("channel for runner does not exist") + } + + close(ch) + delete(chans, *runner.rule.ID) + + return nil +} + +func (cb *Chatbot) HandleChat(event events.Chat) { + + switch event.Message.Type { + case rumblelivestreamlib.ChatTypeMessages: + cb.handleMessage(event) + } +} + +func (cb *Chatbot) handleMessage(event events.Chat) { + errs := cb.runMessageFuncs( + event, + cb.handleMessageCommand, + ) + + for _, err := range errs { + cb.logError.Println("chatbot: error handling message:", err) + } +} + +func (cb *Chatbot) runMessageFuncs(event events.Chat, fns ...messageFunc) []error { + // TODO: validate message + + errs := []error{} + for _, fn := range fns { + err := fn(event) + if err != nil { + errs = append(errs, err) + } + } + + return errs +} + +type messageFunc func(event events.Chat) error + +func (cb *Chatbot) handleMessageCommand(event events.Chat) error { + if strings.Index(event.Message.Text, "!") != 0 { + return nil + } + + words := strings.Split(event.Message.Text, " ") + cmd := words[0] + + cb.receiversMu.Lock() + defer cb.receiversMu.Unlock() + + receiver, exists := cb.receivers[event.Livestream] + if !exists { + return nil + } + if receiver == nil { + return fmt.Errorf("receiver is nil for livestream: %s", event.Livestream) + } + + receiver.onCommandMu.Lock() + defer receiver.onCommandMu.Unlock() + runners, exist := receiver.onCommand[cmd] + if !exist { + return nil + } + + for _, runner := range runners { + runner <- event + } + + return nil +} diff --git a/v1/internal/chatbot/error.go b/v1/internal/chatbot/error.go new file mode 100644 index 0000000..2a0c08d --- /dev/null +++ b/v1/internal/chatbot/error.go @@ -0,0 +1,14 @@ +package chatbot + +import "fmt" + +const pkgName = "chatbot" + +func pkgErr(prefix string, err error) error { + pkgErr := pkgName + if prefix != "" { + pkgErr = fmt.Sprintf("%s: %s", pkgErr, prefix) + } + + return fmt.Errorf("%s: %v", pkgErr, err) +} diff --git a/v1/internal/chatbot/rule.go b/v1/internal/chatbot/rule.go new file mode 100644 index 0000000..b80928d --- /dev/null +++ b/v1/internal/chatbot/rule.go @@ -0,0 +1,180 @@ +package chatbot + +import ( + "bufio" + "cmp" + "crypto/rand" + "encoding/json" + "fmt" + "math/big" + "os" + "slices" + "strconv" + "strings" + "time" + + "github.com/tylertravisty/rum-goggles/v1/internal/models" +) + +func SortRules(rules []Rule) { + slices.SortFunc(rules, func(a, b Rule) int { + return cmp.Compare(strings.ToLower(a.Display), strings.ToLower(b.Display)) + }) +} + +type Rule struct { + ID *int64 `json:"id"` + ChatbotID *int64 `json:"chatbot_id"` + Display string `json:"display"` + Parameters *RuleParameters `json:"parameters"` + Running bool `json:"running"` +} + +type RuleParameters struct { + Message *RuleMessage `json:"message"` + SendAs *RuleSender `json:"send_as"` + Trigger *RuleTrigger `json:"trigger"` +} + +type RuleMessage struct { + FromFile *RuleMessageFile `json:"from_file"` + FromText string `json:"from_text"` +} + +func (rm *RuleMessage) String() (string, error) { + if rm.FromFile == nil { + return rm.FromText, nil + } + + s, err := rm.FromFile.string() + if err != nil { + return "", fmt.Errorf("error reading from file: %v", err) + } + + return s, nil +} + +func (rmf *RuleMessageFile) string() (string, error) { + if rmf.Filepath == "" { + return "", fmt.Errorf("filepath is empty") + } + + if len(rmf.lines) == 0 { + file, err := os.Open(rmf.Filepath) + if err != nil { + return "", fmt.Errorf("error opening file: %v", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + rmf.lines = append(rmf.lines, line) + } + + if len(rmf.lines) == 0 { + return "", fmt.Errorf("no lines read") + } + } + + if rmf.RandomRead { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(rmf.lines)))) + if err != nil { + return "", fmt.Errorf("error generating random line number: %v", err) + } + + return rmf.lines[n.Int64()], nil + } + + line := rmf.lines[rmf.lineNum] + rmf.lineNum = rmf.lineNum + 1 + if rmf.lineNum >= len(rmf.lines) { + rmf.lineNum = 0 + } + + return line, nil +} + +type RuleMessageFile struct { + Filepath string `json:"filepath"` + RandomRead bool `json:"random_read"` + lines []string + lineNum int +} + +type RuleSender struct { + ChannelID *string `json:"channel_id"` + Display string `json:"display"` + Username string `json:"username"` +} + +func (rs *RuleSender) ChannelIDInt() (*int, error) { + if rs.ChannelID == nil { + return nil, nil + } + + i64, err := strconv.ParseInt(*rs.ChannelID, 10, 64) + if err != nil { + return nil, pkgErr("error parsing channel ID", err) + } + i := int(i64) + + return &i, nil +} + +type RuleTrigger struct { + OnCommand *RuleTriggerCommand `json:"on_command"` + OnEvent *RuleTriggerEvent `json:"on_event"` + OnTimer *time.Duration `json:"on_timer"` +} + +type RuleTriggerCommand struct { + Command string `json:"command"` + Restrict *RuleTriggerCommandRestriction `json:"restrict"` + Timeout time.Duration `json:"timeout"` +} + +type RuleTriggerCommandRestriction struct { + Bypass *RuleTriggerCommandRestrictionBypass `json:"bypass"` + ToAdmin bool `json:"to_admin"` + ToFollower bool `json:"to_follower"` + ToMod bool `json:"to_mod"` + ToStreamer bool `json:"to_streamer"` + ToSubscriber bool `json:"to_subscriber"` + ToRant int `json:"to_rant"` +} + +type RuleTriggerCommandRestrictionBypass struct { + IfAdmin bool `json:"if_admin"` + IfMod bool `json:"if_mod"` + IfStreamer bool `json:"if_streamer"` +} + +type RuleTriggerEvent struct { + OnFollow bool `json:"on_follow"` + OnSubscribe bool `json:"on_subscribe"` + OnRaid bool `json:"on_raid"` + OnRant int `json:"on_rant"` +} + +func (rule *Rule) ToModelsChatbotRule() (*models.ChatbotRule, error) { + modelsRule := &models.ChatbotRule{ + ID: rule.ID, + ChatbotID: rule.ChatbotID, + } + + if rule.Parameters != nil { + paramsB, err := json.Marshal(rule.Parameters) + if err != nil { + return nil, fmt.Errorf("error marshaling parameters into json: %v", err) + } + + paramsS := string(paramsB) + modelsRule.Parameters = ¶msS + } + + return modelsRule, nil +} diff --git a/v1/internal/chatbot/runner.go b/v1/internal/chatbot/runner.go new file mode 100644 index 0000000..6611003 --- /dev/null +++ b/v1/internal/chatbot/runner.go @@ -0,0 +1,206 @@ +package chatbot + +import ( + "bytes" + "context" + "fmt" + "html/template" + "sync" + "time" + + "github.com/tylertravisty/rum-goggles/v1/internal/events" + rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go" + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +type Runner struct { + apiCh chan events.Api + cancel context.CancelFunc + cancelMu sync.Mutex + channelID *int + channelIDMu sync.Mutex + chatCh chan events.Chat + client *rumblelivestreamlib.Client + rule Rule + run runFunc + wails context.Context +} + +type chatFields struct { + ChannelName string + DisplayName string + Username string + Rant int +} + +func (r *Runner) chat(fields *chatFields) error { + msg, err := r.rule.Parameters.Message.String() + if err != nil { + return fmt.Errorf("error getting message string: %v", err) + } + + if fields != nil { + tmpl, err := template.New("chat").Parse(msg) + if err != nil { + return fmt.Errorf("error creating template: %v", err) + } + + var msgB bytes.Buffer + err = tmpl.Execute(&msgB, fields) + if err != nil { + return fmt.Errorf("error executing template: %v", err) + } + msg = msgB.String() + } + + err = r.client.Chat(msg, r.channelID) + if err != nil { + return fmt.Errorf("error sending chat: %v", err) + } + + return nil +} + +func (r *Runner) init() error { + if r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { + return fmt.Errorf("invalid rule") + } + + channelID, err := r.rule.Parameters.SendAs.ChannelIDInt() + if err != nil { + return fmt.Errorf("error converting channel ID to int: %v", err) + } + + r.channelIDMu.Lock() + r.channelID = channelID + r.channelIDMu.Unlock() + + switch { + case r.rule.Parameters.Trigger.OnTimer != nil: + r.run = r.runOnTimer + case r.rule.Parameters.Trigger.OnCommand != nil: + r.run = r.runOnCommand + } + + return nil +} + +type runFunc func(ctx context.Context) error + +func (r *Runner) runOnCommand(ctx context.Context) error { + if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { + return fmt.Errorf("invalid rule") + } + if r.rule.Parameters.Trigger.OnCommand == nil { + return fmt.Errorf("command is nil") + } + + var prev time.Time + for { + runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true) + + select { + case <-ctx.Done(): + return nil + case event := <-r.chatCh: + now := time.Now() + if now.Sub(prev) < r.rule.Parameters.Trigger.OnCommand.Timeout*time.Second { + break + } + + if block := r.blockCommand(event); block { + // if bypass := r.bypassCommand(event); !bypass {break} + break + } + + err := r.handleCommand(event) + if err != nil { + return fmt.Errorf("error handling command: %v", err) + } + prev = now + } + } +} + +func (r *Runner) blockCommand(event events.Chat) bool { + if r.rule.Parameters.Trigger.OnCommand.Restrict == nil { + return false + } + + if r.rule.Parameters.Trigger.OnCommand.Restrict.ToFollower && + !event.Message.IsFollower { + return true + } + + subscriber := false + for _, badge := range event.Message.Badges { + if badge == rumblelivestreamlib.ChatBadgeLocalsSupporter || badge == rumblelivestreamlib.ChatBadgeRecurringSubscription { + subscriber = true + } + } + + if r.rule.Parameters.Trigger.OnCommand.Restrict.ToSubscriber && + !subscriber { + return true + } + + if event.Message.Rant < r.rule.Parameters.Trigger.OnCommand.Restrict.ToRant*100 { + return true + } + + return false +} + +func (r *Runner) handleCommand(event events.Chat) error { + displayName := event.Message.Username + if event.Message.ChannelName != "" { + displayName = event.Message.ChannelName + } + + fields := &chatFields{ + ChannelName: event.Message.ChannelName, + DisplayName: displayName, + Username: event.Message.Username, + Rant: event.Message.Rant / 100, + } + + err := r.chat(fields) + if err != nil { + return fmt.Errorf("error sending chat: %v", err) + } + + return nil +} + +func (r *Runner) runOnTimer(ctx context.Context) error { + if r.rule.ID == nil || r.rule.Parameters == nil || r.rule.Parameters.Trigger == nil { + return fmt.Errorf("invalid rule") + } + if r.rule.Parameters.Trigger.OnTimer == nil { + return fmt.Errorf("timer is nil") + } + + for { + runtime.EventsEmit(r.wails, fmt.Sprintf("ChatbotRuleActive-%d", *r.rule.ID), true) + err := r.chat(nil) + if err != nil { + return fmt.Errorf("error sending chat: %v", err) + } + + trigger := time.NewTimer(*r.rule.Parameters.Trigger.OnTimer * time.Second) + select { + case <-ctx.Done(): + trigger.Stop() + return nil + case <-trigger.C: + } + } +} + +func (r *Runner) stop() { + r.cancelMu.Lock() + if r.cancel != nil { + r.cancel() + } + r.cancelMu.Unlock() +} diff --git a/v1/internal/events/chat.go b/v1/internal/events/chat.go index 5781429..9b8b691 100644 --- a/v1/internal/events/chat.go +++ b/v1/internal/events/chat.go @@ -11,16 +11,18 @@ import ( ) type Chat struct { - Message rumblelivestreamlib.ChatView - Stop bool - Url string + Livestream string + Message rumblelivestreamlib.ChatView + Stop bool + Url string } type chatProducer struct { - cancel context.CancelFunc - cancelMu sync.Mutex - client *rumblelivestreamlib.Client - url string + cancel context.CancelFunc + cancelMu sync.Mutex + client *rumblelivestreamlib.Client + livestream string + url string } type chatProducerValFunc func(*chatProducer) error @@ -82,6 +84,12 @@ func (cp *ChatProducer) Start(liveStreamUrl string) (string, error) { return "", pkgErr("", fmt.Errorf("url is empty")) } + cp.producersMu.Lock() + defer cp.producersMu.Unlock() + if producer, active := cp.producers[liveStreamUrl]; active { + return producer.url, nil + } + client, err := rumblelivestreamlib.NewClient(rumblelivestreamlib.NewClientOptions{LiveStreamUrl: liveStreamUrl}) if err != nil { return "", pkgErr("error creating new rumble client", err) @@ -93,19 +101,21 @@ func (cp *ChatProducer) Start(liveStreamUrl string) (string, error) { } chatStreamUrl := chatInfo.StreamUrl() - cp.producersMu.Lock() - defer cp.producersMu.Unlock() - if _, active := cp.producers[chatStreamUrl]; active { - return chatStreamUrl, nil - } + // cp.producersMu.Lock() + // defer cp.producersMu.Unlock() + // if _, active := cp.producers[chatStreamUrl]; active { + // return chatStreamUrl, nil + // } ctx, cancel := context.WithCancel(context.Background()) producer := &chatProducer{ - cancel: cancel, - client: client, - url: chatStreamUrl, + cancel: cancel, + client: client, + livestream: liveStreamUrl, + url: chatStreamUrl, } - cp.producers[chatStreamUrl] = producer + // cp.producers[chatStreamUrl] = producer + cp.producers[liveStreamUrl] = producer go cp.run(ctx, producer) return chatStreamUrl, nil @@ -138,6 +148,8 @@ func (cp *ChatProducer) run(ctx context.Context, producer *chatProducer) { return } + // TODO: handle the case when restarting stream with possibly missing messages + // Start new stream, make sure it's running, close old stream for { err = producer.client.StartChatStream(cp.handleChat(producer), cp.handleError(producer)) if err != nil { @@ -154,6 +166,7 @@ func (cp *ChatProducer) run(ctx context.Context, producer *chatProducer) { cp.stop(producer) return case <-timer.C: + producer.client.StopChatStream() } } } @@ -164,7 +177,7 @@ func (cp *ChatProducer) handleChat(p *chatProducer) func(cv rumblelivestreamlib. return } - cp.Ch <- Chat{Message: cv, Url: p.url} + cp.Ch <- Chat{Livestream: p.livestream, Message: cv, Url: p.url} } } @@ -184,7 +197,7 @@ func (cp *ChatProducer) stop(p *chatProducer) { return } - cp.Ch <- Chat{Stop: true, Url: p.url} + cp.Ch <- Chat{Livestream: p.livestream, Stop: true, Url: p.url} cp.producersMu.Lock() delete(cp.producers, p.url) diff --git a/v1/internal/models/chatbotrule.go b/v1/internal/models/chatbotrule.go index f81e0de..154cb7d 100644 --- a/v1/internal/models/chatbotrule.go +++ b/v1/internal/models/chatbotrule.go @@ -6,18 +6,18 @@ import ( ) const ( - chatbotRuleColumns = "id, chatbot_id, name, rule" + chatbotRuleColumns = "id, chatbot_id, parameters" chatbotRuleTable = "chatbot_rule" ) type ChatbotRule struct { - ID *int64 `json:"id"` - ChatbotID *int64 `json:"chatbot_id"` - Rule *string `json:"rule"` + ID *int64 `json:"id"` + ChatbotID *int64 `json:"chatbot_id"` + Parameters *string `json:"parameters"` } func (c *ChatbotRule) values() []any { - return []any{c.ID, c.ChatbotID, c.Rule} + return []any{c.ID, c.ChatbotID, c.Parameters} } func (c *ChatbotRule) valuesNoID() []any { @@ -30,26 +30,27 @@ func (c *ChatbotRule) valuesEndID() []any { } type sqlChatbotRule struct { - id sql.NullInt64 - chatbotID sql.NullInt64 - rule sql.NullString + id sql.NullInt64 + chatbotID sql.NullInt64 + parameters sql.NullString } func (sc *sqlChatbotRule) scan(r Row) error { - return r.Scan(&sc.id, &sc.chatbotID, &sc.rule) + return r.Scan(&sc.id, &sc.chatbotID, &sc.parameters) } func (sc sqlChatbotRule) toChatbotRule() *ChatbotRule { var c ChatbotRule c.ID = toInt64(sc.id) c.ChatbotID = toInt64(sc.chatbotID) - c.Rule = toString(sc.rule) + c.Parameters = toString(sc.parameters) return &c } type ChatbotRuleService interface { AutoMigrate() error + ByChatbotID(cid int64) ([]ChatbotRule, error) Create(c *ChatbotRule) (int64, error) Delete(c *ChatbotRule) error DestructiveReset() error @@ -82,7 +83,7 @@ func (cs *chatbotRuleService) createChatbotRuleTable() error { CREATE TABLE IF NOT EXISTS "%s" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, chatbot_id INTEGER NOT NULL, - rule TEXT NOT NULL + parameters TEXT NOT NULL, FOREIGN KEY (chatbot_id) REFERENCES "%s" (id) ) `, chatbotRuleTable, chatbotTable) @@ -95,10 +96,42 @@ func (cs *chatbotRuleService) createChatbotRuleTable() error { return nil } +func (cs *chatbotRuleService) ByChatbotID(cid int64) ([]ChatbotRule, error) { + selectQ := fmt.Sprintf(` + SELECT %s + FROM "%s" + WHERE chatbot_id=? + `, chatbotRuleColumns, chatbotRuleTable) + + rows, err := cs.Database.Query(selectQ, cid) + if err != nil { + return nil, pkgErr("error executing select query", err) + } + defer rows.Close() + + rules := []ChatbotRule{} + for rows.Next() { + scr := &sqlChatbotRule{} + + err = scr.scan(rows) + if err != nil { + return nil, pkgErr("error scanning row", err) + } + + rules = append(rules, *scr.toChatbotRule()) + } + err = rows.Err() + if err != nil && err != sql.ErrNoRows { + return nil, pkgErr("error iterating over rows", err) + } + + return rules, nil +} + func (cs *chatbotRuleService) Create(c *ChatbotRule) (int64, error) { err := runChatbotRuleValFuncs( c, - chatbotRuleRequireRule, + chatbotRuleRequireParameters, ) if err != nil { return -1, pkgErr("invalid chat rule", err) @@ -169,7 +202,7 @@ func (cs *chatbotRuleService) Update(c *ChatbotRule) error { err := runChatbotRuleValFuncs( c, chatbotRuleRequireID, - chatbotRuleRequireRule, + chatbotRuleRequireParameters, ) if err != nil { return pkgErr("invalid chat rule", err) @@ -215,9 +248,9 @@ func chatbotRuleRequireID(c *ChatbotRule) error { return nil } -func chatbotRuleRequireRule(c *ChatbotRule) error { - if c.Rule == nil || *c.Rule == "" { - return ErrChatbotRuleInvalidRule +func chatbotRuleRequireParameters(c *ChatbotRule) error { + if c.Parameters == nil || *c.Parameters == "" { + return ErrChatbotRuleInvalidParameters } return nil diff --git a/v1/internal/models/error.go b/v1/internal/models/error.go index aea0846..7fb7b41 100644 --- a/v1/internal/models/error.go +++ b/v1/internal/models/error.go @@ -17,8 +17,8 @@ const ( ErrChatbotInvalidID ValidatorError = "invalid chatbot id" ErrChatbotInvalidName ValidatorError = "invalid chatbot name" - ErrChatbotRuleInvalidID ValidatorError = "invalid chatbot rule id" - ErrChatbotRuleInvalidRule ValidatorError = "invalid chatbot rule rule" + ErrChatbotRuleInvalidID ValidatorError = "invalid chatbot rule id" + ErrChatbotRuleInvalidParameters ValidatorError = "invalid chatbot rule parameters" ) func pkgErr(prefix string, err error) error { diff --git a/v1/internal/models/services.go b/v1/internal/models/services.go index b75416f..0620aed 100644 --- a/v1/internal/models/services.go +++ b/v1/internal/models/services.go @@ -18,6 +18,7 @@ type Services struct { AccountChannelS AccountChannelService ChannelS ChannelService ChatbotS ChatbotService + ChatbotRuleS ChatbotRuleService Database *sql.DB tables []table } @@ -116,3 +117,12 @@ func WithChatbotService() ServicesInit { return nil } } + +func WithChatbotRuleService() ServicesInit { + return func(s *Services) error { + s.ChatbotRuleS = NewChatbotRuleService(s.Database) + s.tables = append(s.tables, table{chatbotRuleTable, s.ChatbotRuleS.AutoMigrate, s.ChatbotRuleS.DestructiveReset}) + + return nil + } +} diff --git a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go index ea47182..c839a48 100644 --- a/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go +++ b/v1/vendor/github.com/tylertravisty/rumble-livestream-lib-go/chat.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "context" - "encoding/csv" "encoding/json" "fmt" "io" @@ -19,6 +18,20 @@ import ( "gopkg.in/cenkalti/backoff.v1" ) +const ( + ChatBadgeRecurringSubscription = "recurring_subscription" + ChatBadgeLocalsSupporter = "locals_supporter" + + ChatTypeInit = "init" + ChatTypeMessages = "messages" + ChatTypeMuteUsers = "mute_users" + ChatTypeDeleteMessages = "delete_messages" + ChatTypeSubscriber = "locals_supporter" + ChatTypeRaiding = "raid_confirmed" + ChatTypePinMessage = "pin_message" + ChatTypeUnpinMessage = "unpin_message" +) + type ChatInfo struct { ChannelID int ChatID string @@ -74,21 +87,12 @@ func (c *Client) getChatInfo() (*ChatInfo, error) { if end == -1 { return nil, fmt.Errorf("error finding end of chat function in webpage") } - argsS := strings.ReplaceAll(lineS[start:start+end], ", ", ",") - argsS = strings.Replace(argsS, "[", "\"[", 1) - n := strings.LastIndex(argsS, "]") - argsS = argsS[:n] + "]\"" + argsS[n+1:] - c := csv.NewReader(strings.NewReader(argsS)) - args, err := c.ReadAll() - if err != nil { - return nil, fmt.Errorf("error parsing csv: %v", err) - } - info := args[0] - channelID, err := strconv.Atoi(info[5]) + args := parseRumbleChatArgs(lineS[start : start+end]) + channelID, err := strconv.Atoi(args[5]) if err != nil { return nil, fmt.Errorf("error converting channel ID argument string to int: %v", err) } - chatInfo = &ChatInfo{ChannelID: channelID, ChatID: info[1], UrlPrefix: info[0]} + chatInfo = &ChatInfo{ChannelID: channelID, ChatID: args[1], UrlPrefix: args[0]} } else if strings.Contains(lineS, "media-by--a") && strings.Contains(lineS, "author") { r := strings.NewReader(lineS) node, err := html.Parse(r) @@ -117,6 +121,37 @@ func (c *Client) getChatInfo() (*ChatInfo, error) { return chatInfo, nil } +func parseRumbleChatArgs(argsS string) []string { + open := 0 + + args := []string{} + arg := []rune{} + for _, c := range argsS { + if c == ',' && open == 0 { + args = append(args, trimRumbleChatArg(string(arg))) + arg = []rune{} + } else { + if c == '[' { + open = open + 1 + } + if c == ']' { + open = open - 1 + } + + arg = append(arg, c) + } + } + if len(arg) > 0 { + args = append(args, trimRumbleChatArg(string(arg))) + } + + return args +} + +func trimRumbleChatArg(arg string) string { + return strings.Trim(strings.TrimSpace(arg), "\"") +} + type ChatMessage struct { Text string `json:"text"` } @@ -359,6 +394,7 @@ type ChatView struct { IsFollower bool Rant int Text string + Time time.Time Type string Username string } @@ -424,6 +460,11 @@ func parseMessages(eventType string, messages []ChatEventMessage, users map[stri view.Rant = message.Rant.PriceCents } view.Text = message.Text + t, err := time.Parse(time.RFC3339, message.Time) + if err != nil { + return nil, fmt.Errorf("error parsing message time: %v", err) + } + view.Time = t view.Type = eventType view.Username = user.Username diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/binding/reflect.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/binding/reflect.go index 263af29..57a6335 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/binding/reflect.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/binding/reflect.go @@ -19,7 +19,7 @@ func isFunction(value interface{}) bool { return reflect.ValueOf(value).Kind() == reflect.Func } -// isStructPtr returns true if the value given is a struct +// isStruct returns true if the value given is a struct func isStruct(value interface{}) bool { return reflect.ValueOf(value).Kind() == reflect.Struct } diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/Application.h b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/Application.h index eb679f3..5c4f2fe 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/Application.h +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/Application.h @@ -17,7 +17,7 @@ #define WindowStartsMinimised 2 #define WindowStartsFullscreen 3 -WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceEnabled, const char* singleInstanceUniqueId); +WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int zoomable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceEnabled, const char* singleInstanceUniqueId); void Run(void*, const char* url); void SetTitle(void* ctx, const char *title); diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/Application.m b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/Application.m index c428b4c..f0a5a2a 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/Application.m +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/Application.m @@ -14,7 +14,7 @@ #import "WailsMenu.h" #import "WailsMenuItem.h" -WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceLockEnabled, const char* singleInstanceUniqueId) { +WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int zoomable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceLockEnabled, const char* singleInstanceUniqueId) { [NSApplication sharedApplication]; @@ -27,7 +27,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in fullscreen = 1; } - [result CreateWindow:width :height :frameless :resizable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight :fraudulentWebsiteWarningEnabled :preferences]; + [result CreateWindow:width :height :frameless :resizable :zoomable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight :fraudulentWebsiteWarningEnabled :preferences]; [result SetTitle:safeInit(title)]; [result Center]; diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/WailsContext.h b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/WailsContext.h index ab5e5c2..0e83ff0 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/WailsContext.h +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/WailsContext.h @@ -64,7 +64,7 @@ struct Preferences { bool *fullscreenEnabled; }; -- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString *)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences; +- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)zoomable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString *)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences; - (void) SetSize:(int)width :(int)height; - (void) SetPosition:(int)x :(int) y; - (void) SetMinSize:(int)minWidth :(int)minHeight; diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/WailsContext.m b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/WailsContext.m index fd15465..13af17f 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/WailsContext.m +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/WailsContext.m @@ -136,7 +136,7 @@ typedef void (^schemeTaskCaller)(id); return NO; } -- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences { +- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)zoomable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences { NSWindowStyleMask styleMask = 0; if( !frameless ) { @@ -158,7 +158,6 @@ typedef void (^schemeTaskCaller)(id); self.mainWindow = [[WailsWindow alloc] initWithContentRect:NSMakeRect(0, 0, width, height) styleMask:styleMask backing:NSBackingStoreBuffered defer:NO]; - if (!frameless && useToolbar) { id toolbar = [[NSToolbar alloc] initWithIdentifier:@"wails.toolbar"]; [toolbar autorelease]; @@ -188,6 +187,10 @@ typedef void (^schemeTaskCaller)(id); [self.mainWindow setAppearance:nsAppearance]; } + if (!zoomable && resizable) { + NSButton *button = [self.mainWindow standardWindowButton:NSWindowZoomButton]; + [button setEnabled: NO]; + } NSSize minSize = { minWidth, minHeight }; NSSize maxSize = { maxWidth, maxHeight }; diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/browser.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/browser.go index 417501c..12b2bc8 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/browser.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/browser.go @@ -10,5 +10,7 @@ import ( // BrowserOpenURL Use the default browser to open the url func (f *Frontend) BrowserOpenURL(url string) { // Specific method implementation - _ = browser.OpenURL(url) + if err := browser.OpenURL(url); err != nil { + f.logger.Error("Unable to open default system browser") + } } diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/main.m b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/main.m index 1dd8bb1..75a84dc 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/main.m +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/main.m @@ -203,6 +203,7 @@ int main(int argc, const char * argv[]) { // insert code here... int frameless = 0; int resizable = 1; + int zoomable = 0; int fullscreen = 1; int fullSizeContent = 1; int hideTitleBar = 0; @@ -219,7 +220,7 @@ int main(int argc, const char * argv[]) { int defaultContextMenuEnabled = 1; int windowStartState = 0; int startsHidden = 0; - WailsContext *result = Create("OI OI!",400,400, frameless, resizable, fullscreen, fullSizeContent, hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent, alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, windowStartState, + WailsContext *result = Create("OI OI!",400,400, frameless, resizable, zoomable, fullscreen, fullSizeContent, hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent, alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, windowStartState, startsHidden, 400, 400, 600, 600, false); SetBackgroundColour(result, 255, 0, 0, 255); void *m = NewMenu(""); diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/window.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/window.go index 236026e..458c81e 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/window.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/window.go @@ -60,7 +60,7 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window defaultContextMenuEnabled := bool2Cint(debug || frontendOptions.EnableDefaultContextMenu) singleInstanceEnabled := bool2Cint(frontendOptions.SingleInstanceLock != nil) - var fullSizeContent, hideTitleBar, hideTitle, useToolbar, webviewIsTransparent C.int + var fullSizeContent, hideTitleBar, zoomable, hideTitle, useToolbar, webviewIsTransparent C.int var titlebarAppearsTransparent, hideToolbarSeparator, windowIsTranslucent C.int var appearance, title *C.char var preferences C.struct_Preferences @@ -108,12 +108,14 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window } } + zoomable = bool2Cint(!frontendOptions.Mac.DisableZoom) + windowIsTranslucent = bool2Cint(mac.WindowIsTranslucent) webviewIsTransparent = bool2Cint(mac.WebviewIsTransparent) appearance = c.String(string(mac.Appearance)) } - var context *C.WailsContext = C.Create(title, width, height, frameless, resizable, fullscreen, fullSizeContent, + var context *C.WailsContext = C.Create(title, width, height, frameless, resizable, zoomable, fullscreen, fullSizeContent, hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent, alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings, diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/linux/browser.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/linux/browser.go index 47bf0ba..30ca962 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/linux/browser.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/linux/browser.go @@ -8,5 +8,7 @@ import "github.com/pkg/browser" // BrowserOpenURL Use the default browser to open the url func (f *Frontend) BrowserOpenURL(url string) { // Specific method implementation - _ = browser.OpenURL(url) + if err := browser.OpenURL(url); err != nil { + f.logger.Error("Unable to open default system browser") + } } diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/linux/window.c b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/linux/window.c index 7cd1c24..0000b65 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/linux/window.c +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/linux/window.c @@ -245,7 +245,7 @@ void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_wid gtk_window_set_geometry_hints(window, NULL, &size, flags); } -// function to disable the context menu but propogate the event +// function to disable the context menu but propagate the event static gboolean disableContextMenu(GtkWidget *widget, WebKitContextMenu *context_menu, GdkEvent *event, WebKitHitTestResult *hit_test_result, gpointer data) { // return true to disable the context menu @@ -254,7 +254,7 @@ static gboolean disableContextMenu(GtkWidget *widget, WebKitContextMenu *context void DisableContextMenu(void *webview) { - // Disable the context menu but propogate the event + // Disable the context menu but propagate the event g_signal_connect(WEBKIT_WEB_VIEW(webview), "context-menu", G_CALLBACK(disableContextMenu), NULL); } diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/browser.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/browser.go index f23b04d..2b058fe 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/browser.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/browser.go @@ -5,10 +5,31 @@ package windows import ( "github.com/pkg/browser" + "golang.org/x/sys/windows" ) +var fallbackBrowserPaths = []string{ + `\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`, + `\Program Files\Google\Chrome\Application\chrome.exe`, + `\Program Files (x86)\Google\Chrome\Application\chrome.exe`, + `\Program Files\Mozilla Firefox\firefox.exe`, +} + // BrowserOpenURL Use the default browser to open the url func (f *Frontend) BrowserOpenURL(url string) { // Specific method implementation - _ = browser.OpenURL(url) + err := browser.OpenURL(url) + if err == nil { + return + } + for _, fallback := range fallbackBrowserPaths { + if err := openBrowser(fallback, url); err == nil { + return + } + } + f.logger.Error("Unable to open default system browser") +} + +func openBrowser(path, url string) error { + return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(path), windows.StringToUTF16Ptr(url), nil, windows.SW_SHOWNORMAL) } diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/app.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/app.go index 52b30f5..9738804 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/app.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/app.go @@ -37,7 +37,7 @@ func init() { w32.InitCommonControlsEx(&initCtrls) } -// SetAppIconID sets recource icon ID for the apps windows. +// SetAppIcon sets resource icon ID for the apps windows. func SetAppIcon(appIconID int) { AppIconID = appIconID } diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/combobox.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/combobox.go index 3b4348a..380ea88 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/combobox.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/combobox.go @@ -54,7 +54,7 @@ func (cb *ComboBox) OnSelectedChange() *EventManager { return &cb.onSelectedChange } -// Message processer +// Message processor func (cb *ComboBox) WndProc(msg uint32, wparam, lparam uintptr) uintptr { switch msg { case w32.WM_COMMAND: diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/listview.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/listview.go index c98fc4c..8edfd1c 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/listview.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/listview.go @@ -438,7 +438,7 @@ func (lv *ListView) OnEndScroll() *EventManager { return &lv.onEndScroll } -// Message processer +// Message processor func (lv *ListView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { switch msg { /*case w32.WM_ERASEBKGND: diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/treeview.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/treeview.go index 9118f3d..2cdc0e9 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/treeview.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/treeview.go @@ -248,7 +248,7 @@ func (tv *TreeView) OnViewChange() *EventManager { return &tv.onViewChange } -// Message processer +// Message processor func (tv *TreeView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { switch msg { case w32.WM_NOTIFY: diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/runtime/ipc_websocket.js b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/runtime/ipc_websocket.js index d5dca66..a0d6b4a 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/runtime/ipc_websocket.js +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/frontend/runtime/ipc_websocket.js @@ -1,10 +1,10 @@ -(()=>{function D(t){console.log("%c wails dev %c "+t+" ","background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem","background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem")}function p(){}var A=t=>t;function N(t){return t()}function it(){return Object.create(null)}function b(t){t.forEach(N)}function w(t){return typeof t=="function"}function L(t,e){return t!=t?e==e:t!==e||t&&typeof t=="object"||typeof t=="function"}function ot(t){return Object.keys(t).length===0}function rt(t,...e){if(t==null)return p;let n=t.subscribe(...e);return n.unsubscribe?()=>n.unsubscribe():n}function st(t,e,n){t.$$.on_destroy.push(rt(e,n))}var ct=typeof window!="undefined",Ot=ct?()=>window.performance.now():()=>Date.now(),P=ct?t=>requestAnimationFrame(t):p;var x=new Set;function lt(t){x.forEach(e=>{e.c(t)||(x.delete(e),e.f())}),x.size!==0&&P(lt)}function Dt(t){let e;return x.size===0&&P(lt),{promise:new Promise(n=>{x.add(e={c:t,f:n})}),abort(){x.delete(e)}}}var ut=!1;function At(){ut=!0}function Lt(){ut=!1}function Bt(t,e){t.appendChild(e)}function at(t,e,n){let i=R(t);if(!i.getElementById(e)){let o=B("style");o.id=e,o.textContent=n,ft(i,o)}}function R(t){if(!t)return document;let e=t.getRootNode?t.getRootNode():t.ownerDocument;return e&&e.host?e:t.ownerDocument}function Tt(t){let e=B("style");return ft(R(t),e),e.sheet}function ft(t,e){return Bt(t.head||t,e),e.sheet}function W(t,e,n){t.insertBefore(e,n||null)}function S(t){t.parentNode.removeChild(t)}function B(t){return document.createElement(t)}function Jt(t){return document.createTextNode(t)}function dt(){return Jt("")}function ht(t,e,n){n==null?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function zt(t){return Array.from(t.childNodes)}function Ht(t,e,{bubbles:n=!1,cancelable:i=!1}={}){let o=document.createEvent("CustomEvent");return o.initCustomEvent(t,n,i,e),o}var T=new Map,J=0;function Gt(t){let e=5381,n=t.length;for(;n--;)e=(e<<5)-e^t.charCodeAt(n);return e>>>0}function qt(t,e){let n={stylesheet:Tt(e),rules:{}};return T.set(t,n),n}function _t(t,e,n,i,o,c,s,l=0){let f=16.666/i,r=`{ +(()=>{function D(t){console.log("%c wails dev %c "+t+" ","background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem","background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem")}function _(){}var A=t=>t;function N(t){return t()}function it(){return Object.create(null)}function b(t){t.forEach(N)}function w(t){return typeof t=="function"}function L(t,e){return t!=t?e==e:t!==e||t&&typeof t=="object"||typeof t=="function"}function ot(t){return Object.keys(t).length===0}function rt(t,...e){if(t==null)return _;let n=t.subscribe(...e);return n.unsubscribe?()=>n.unsubscribe():n}function st(t,e,n){t.$$.on_destroy.push(rt(e,n))}var ct=typeof window!="undefined",Ot=ct?()=>window.performance.now():()=>Date.now(),P=ct?t=>requestAnimationFrame(t):_;var x=new Set;function lt(t){x.forEach(e=>{e.c(t)||(x.delete(e),e.f())}),x.size!==0&&P(lt)}function Dt(t){let e;return x.size===0&&P(lt),{promise:new Promise(n=>{x.add(e={c:t,f:n})}),abort(){x.delete(e)}}}var ut=!1;function At(){ut=!0}function Lt(){ut=!1}function Bt(t,e){t.appendChild(e)}function at(t,e,n){let i=R(t);if(!i.getElementById(e)){let o=B("style");o.id=e,o.textContent=n,ft(i,o)}}function R(t){if(!t)return document;let e=t.getRootNode?t.getRootNode():t.ownerDocument;return e&&e.host?e:t.ownerDocument}function Tt(t){let e=B("style");return ft(R(t),e),e.sheet}function ft(t,e){return Bt(t.head||t,e),e.sheet}function W(t,e,n){t.insertBefore(e,n||null)}function S(t){t.parentNode.removeChild(t)}function B(t){return document.createElement(t)}function Jt(t){return document.createTextNode(t)}function dt(){return Jt("")}function ht(t,e,n){n==null?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function zt(t){return Array.from(t.childNodes)}function Ht(t,e,{bubbles:n=!1,cancelable:i=!1}={}){let o=document.createEvent("CustomEvent");return o.initCustomEvent(t,n,i,e),o}var T=new Map,J=0;function Gt(t){let e=5381,n=t.length;for(;n--;)e=(e<<5)-e^t.charCodeAt(n);return e>>>0}function qt(t,e){let n={stylesheet:Tt(e),rules:{}};return T.set(t,n),n}function pt(t,e,n,i,o,c,s,l=0){let f=16.666/i,r=`{ `;for(let g=0;g<=1;g+=f){let F=e+(n-e)*c(g);r+=g*100+`%{${s(F,1-F)}} `}let y=r+`100% {${s(n,1-n)}} -}`,a=`__svelte_${Gt(y)}_${l}`,u=R(t),{stylesheet:h,rules:_}=T.get(u)||qt(u,t);_[a]||(_[a]=!0,h.insertRule(`@keyframes ${a} ${y}`,h.cssRules.length));let v=t.style.animation||"";return t.style.animation=`${v?`${v}, `:""}${a} ${i}ms linear ${o}ms 1 both`,J+=1,a}function Kt(t,e){let n=(t.style.animation||"").split(", "),i=n.filter(e?c=>c.indexOf(e)<0:c=>c.indexOf("__svelte")===-1),o=n.length-i.length;o&&(t.style.animation=i.join(", "),J-=o,J||Nt())}function Nt(){P(()=>{J||(T.forEach(t=>{let{ownerNode:e}=t.stylesheet;e&&S(e)}),T.clear())})}var V;function C(t){V=t}var k=[];var pt=[],z=[],mt=[],Pt=Promise.resolve(),U=!1;function Rt(){U||(U=!0,Pt.then(yt))}function $(t){z.push(t)}var X=new Set,H=0;function yt(){let t=V;do{for(;H{E=null})),E}function Z(t,e,n){t.dispatchEvent(Ht(`${e?"intro":"outro"}${n}`))}var G=new Set,m;function gt(){m={r:0,c:[],p:m}}function bt(){m.r||b(m.c),m=m.p}function I(t,e){t&&t.i&&(G.delete(t),t.i(e))}function Q(t,e,n,i){if(t&&t.o){if(G.has(t))return;G.add(t),m.c.push(()=>{G.delete(t),i&&(n&&t.d(1),i())}),t.o(e)}else i&&i()}var Ut={duration:0};function Y(t,e,n,i){let o=e(t,n),c=i?0:1,s=null,l=null,f=null;function r(){f&&Kt(t,f)}function y(u,h){let _=u.b-c;return h*=Math.abs(_),{a:c,b:u.b,d:_,duration:h,start:u.start,end:u.start+h,group:u.group}}function a(u){let{delay:h=0,duration:_=300,easing:v=A,tick:g=p,css:F}=o||Ut,K={start:Ot()+h,b:u};u||(K.group=m,m.r+=1),s||l?l=K:(F&&(r(),f=_t(t,c,u,_,h,v,F)),u&&g(0,1),s=y(K,_),$(()=>Z(t,u,"start")),Dt(O=>{if(l&&O>l.start&&(s=y(l,_),l=null,Z(t,s.b,"start"),F&&(r(),f=_t(t,c,s.b,s.duration,0,v,o.css))),s){if(O>=s.end)g(c=s.b,1-c),Z(t,s.b,"end"),l||(s.b?r():--s.group.r||b(s.group.c)),s=null;else if(O>=s.start){let jt=O-s.start;c=s.a+s.d*v(jt/s.duration),g(c,1-c)}}return!!(s||l)}))}return{run(u){w(o)?Vt().then(()=>{o=o(),a(u)}):a(u)},end(){r(),s=l=null}}}var le=typeof window!="undefined"?window:typeof globalThis!="undefined"?globalThis:global;var ue=new Set(["allowfullscreen","allowpaymentrequest","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]);function Xt(t,e,n,i){let{fragment:o,after_update:c}=t.$$;o&&o.m(e,n),i||$(()=>{let s=t.$$.on_mount.map(N).filter(w);t.$$.on_destroy?t.$$.on_destroy.push(...s):b(s),t.$$.on_mount=[]}),c.forEach($)}function wt(t,e){let n=t.$$;n.fragment!==null&&(b(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function Zt(t,e){t.$$.dirty[0]===-1&&(k.push(t),Rt(),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{let _=h.length?h[0]:u;return r.ctx&&o(r.ctx[a],r.ctx[a]=_)&&(!r.skip_bound&&r.bound[a]&&r.bound[a](_),y&&Zt(t,a)),u}):[],r.update(),y=!0,b(r.before_update),r.fragment=i?i(r.ctx):!1,e.target){if(e.hydrate){At();let a=zt(e.target);r.fragment&&r.fragment.l(a),a.forEach(S)}else r.fragment&&r.fragment.c();e.intro&&I(t.$$.fragment),Xt(t,e.target,e.anchor,e.customElement),Lt(),yt()}C(f)}var Qt;typeof HTMLElement=="function"&&(Qt=class extends HTMLElement{constructor(){super();this.attachShadow({mode:"open"})}connectedCallback(){let{on_mount:t}=this.$$;this.$$.on_disconnect=t.map(N).filter(w);for(let e in this.$$.slotted)this.appendChild(this.$$.slotted[e])}attributeChangedCallback(t,e,n){this[t]=n}disconnectedCallback(){b(this.$$.on_disconnect)}$destroy(){wt(this,1),this.$destroy=p}$on(t,e){if(!w(e))return p;let n=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return n.push(e),()=>{let i=n.indexOf(e);i!==-1&&n.splice(i,1)}}$set(t){this.$$set&&!ot(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}});var tt=class{$destroy(){wt(this,1),this.$destroy=p}$on(e,n){if(!w(n))return p;let i=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return i.push(n),()=>{let o=i.indexOf(n);o!==-1&&i.splice(o,1)}}$set(e){this.$$set&&!ot(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}};var M=[];function Ft(t,e=p){let n,i=new Set;function o(l){if(L(t,l)&&(t=l,n)){let f=!M.length;for(let r of i)r[1](),M.push(r,t);if(f){for(let r=0;r{i.delete(r),i.size===0&&(n(),n=null)}}return{set:o,update:c,subscribe:s}}var q=Ft(!1);function xt(){q.set(!0)}function $t(){q.set(!1)}function et(t,{delay:e=0,duration:n=400,easing:i=A}={}){let o=+getComputedStyle(t).opacity;return{delay:e,duration:n,easing:i,css:c=>`opacity: ${c*o}`}}function Yt(t){at(t,"svelte-181h7z",`.wails-reconnect-overlay.svelte-181h7z{position:fixed;top:0;left:0;width:100%;height:100%;backdrop-filter:blur(2px) saturate(0%) contrast(50%) brightness(25%);z-index:999999 +}`,a=`__svelte_${Gt(y)}_${l}`,u=R(t),{stylesheet:h,rules:p}=T.get(u)||qt(u,t);p[a]||(p[a]=!0,h.insertRule(`@keyframes ${a} ${y}`,h.cssRules.length));let v=t.style.animation||"";return t.style.animation=`${v?`${v}, `:""}${a} ${i}ms linear ${o}ms 1 both`,J+=1,a}function Kt(t,e){let n=(t.style.animation||"").split(", "),i=n.filter(e?c=>c.indexOf(e)<0:c=>c.indexOf("__svelte")===-1),o=n.length-i.length;o&&(t.style.animation=i.join(", "),J-=o,J||Nt())}function Nt(){P(()=>{J||(T.forEach(t=>{let{ownerNode:e}=t.stylesheet;e&&S(e)}),T.clear())})}var V;function C(t){V=t}var k=[];var _t=[],z=[],mt=[],Pt=Promise.resolve(),U=!1;function Rt(){U||(U=!0,Pt.then(yt))}function $(t){z.push(t)}var X=new Set,H=0;function yt(){let t=V;do{for(;H{E=null})),E}function Z(t,e,n){t.dispatchEvent(Ht(`${e?"intro":"outro"}${n}`))}var G=new Set,m;function gt(){m={r:0,c:[],p:m}}function bt(){m.r||b(m.c),m=m.p}function I(t,e){t&&t.i&&(G.delete(t),t.i(e))}function Q(t,e,n,i){if(t&&t.o){if(G.has(t))return;G.add(t),m.c.push(()=>{G.delete(t),i&&(n&&t.d(1),i())}),t.o(e)}else i&&i()}var Ut={duration:0};function Y(t,e,n,i){let o=e(t,n),c=i?0:1,s=null,l=null,f=null;function r(){f&&Kt(t,f)}function y(u,h){let p=u.b-c;return h*=Math.abs(p),{a:c,b:u.b,d:p,duration:h,start:u.start,end:u.start+h,group:u.group}}function a(u){let{delay:h=0,duration:p=300,easing:v=A,tick:g=_,css:F}=o||Ut,K={start:Ot()+h,b:u};u||(K.group=m,m.r+=1),s||l?l=K:(F&&(r(),f=pt(t,c,u,p,h,v,F)),u&&g(0,1),s=y(K,p),$(()=>Z(t,u,"start")),Dt(O=>{if(l&&O>l.start&&(s=y(l,p),l=null,Z(t,s.b,"start"),F&&(r(),f=pt(t,c,s.b,s.duration,0,v,o.css))),s){if(O>=s.end)g(c=s.b,1-c),Z(t,s.b,"end"),l||(s.b?r():--s.group.r||b(s.group.c)),s=null;else if(O>=s.start){let jt=O-s.start;c=s.a+s.d*v(jt/s.duration),g(c,1-c)}}return!!(s||l)}))}return{run(u){w(o)?Vt().then(()=>{o=o(),a(u)}):a(u)},end(){r(),s=l=null}}}var le=typeof window!="undefined"?window:typeof globalThis!="undefined"?globalThis:global;var ue=new Set(["allowfullscreen","allowpaymentrequest","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]);function Xt(t,e,n,i){let{fragment:o,after_update:c}=t.$$;o&&o.m(e,n),i||$(()=>{let s=t.$$.on_mount.map(N).filter(w);t.$$.on_destroy?t.$$.on_destroy.push(...s):b(s),t.$$.on_mount=[]}),c.forEach($)}function wt(t,e){let n=t.$$;n.fragment!==null&&(b(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function Zt(t,e){t.$$.dirty[0]===-1&&(k.push(t),Rt(),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{let p=h.length?h[0]:u;return r.ctx&&o(r.ctx[a],r.ctx[a]=p)&&(!r.skip_bound&&r.bound[a]&&r.bound[a](p),y&&Zt(t,a)),u}):[],r.update(),y=!0,b(r.before_update),r.fragment=i?i(r.ctx):!1,e.target){if(e.hydrate){At();let a=zt(e.target);r.fragment&&r.fragment.l(a),a.forEach(S)}else r.fragment&&r.fragment.c();e.intro&&I(t.$$.fragment),Xt(t,e.target,e.anchor,e.customElement),Lt(),yt()}C(f)}var Qt;typeof HTMLElement=="function"&&(Qt=class extends HTMLElement{constructor(){super();this.attachShadow({mode:"open"})}connectedCallback(){let{on_mount:t}=this.$$;this.$$.on_disconnect=t.map(N).filter(w);for(let e in this.$$.slotted)this.appendChild(this.$$.slotted[e])}attributeChangedCallback(t,e,n){this[t]=n}disconnectedCallback(){b(this.$$.on_disconnect)}$destroy(){wt(this,1),this.$destroy=_}$on(t,e){if(!w(e))return _;let n=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return n.push(e),()=>{let i=n.indexOf(e);i!==-1&&n.splice(i,1)}}$set(t){this.$$set&&!ot(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}});var tt=class{$destroy(){wt(this,1),this.$destroy=_}$on(e,n){if(!w(n))return _;let i=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return i.push(n),()=>{let o=i.indexOf(n);o!==-1&&i.splice(o,1)}}$set(e){this.$$set&&!ot(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}};var M=[];function Ft(t,e=_){let n,i=new Set;function o(l){if(L(t,l)&&(t=l,n)){let f=!M.length;for(let r of i)r[1](),M.push(r,t);if(f){for(let r=0;r{i.delete(r),i.size===0&&(n(),n=null)}}return{set:o,update:c,subscribe:s}}var q=Ft(!1);function xt(){q.set(!0)}function $t(){q.set(!1)}function et(t,{delay:e=0,duration:n=400,easing:i=A}={}){let o=+getComputedStyle(t).opacity;return{delay:e,duration:n,easing:i,css:c=>`opacity: ${c*o}`}}function Yt(t){at(t,"svelte-181h7z",`.wails-reconnect-overlay.svelte-181h7z{position:fixed;top:0;left:0;width:100%;height:100%;backdrop-filter:blur(2px) saturate(0%) contrast(50%) brightness(25%);z-index:999999 }.wails-reconnect-overlay-content.svelte-181h7z{position:relative;top:50%;transform:translateY(-50%);margin:0;background-image:url(data:image/png;base64,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);background-repeat:no-repeat;background-position:center }.wails-reconnect-overlay-loadingspinner.svelte-181h7z{pointer-events:none;width:2.5em;height:2.5em;border:.4em solid transparent;border-color:#f00 #eee0 #f00 #eee0;border-radius:50%;animation:svelte-181h7z-loadingspin 1s linear infinite;margin:auto;padding:2.5em - }@keyframes svelte-181h7z-loadingspin{100%{transform:rotate(360deg)}}`)}function Mt(t){let e,n,i;return{c(){e=B("div"),e.innerHTML='
',ht(e,"class","wails-reconnect-overlay svelte-181h7z")},m(o,c){W(o,e,c),i=!0},i(o){i||($(()=>{n||(n=Y(e,et,{duration:300},!0)),n.run(1)}),i=!0)},o(o){n||(n=Y(e,et,{duration:300},!1)),n.run(0),i=!1},d(o){o&&S(e),o&&n&&n.end()}}}function te(t){let e,n,i=t[0]&&Mt(t);return{c(){i&&i.c(),e=dt()},m(o,c){i&&i.m(o,c),W(o,e,c),n=!0},p(o,[c]){o[0]?i?c&1&&I(i,1):(i=Mt(o),i.c(),I(i,1),i.m(e.parentNode,e)):i&&(gt(),Q(i,1,1,()=>{i=null}),bt())},i(o){n||(I(i),n=!0)},o(o){Q(i),n=!1},d(o){i&&i.d(o),o&&S(e)}}}function ee(t,e,n){let i;return st(t,q,o=>n(0,i=o)),[i]}var St=class extends tt{constructor(e){super();vt(this,e,ee,te,L,{},Yt)}},Ct=St;var ne={},nt=null,j=[];window.WailsInvoke=t=>{if(!nt){console.log("Queueing: "+t),j.push(t);return}nt(t)};window.addEventListener("DOMContentLoaded",()=>{ne.overlay=new Ct({target:document.body,anchor:document.querySelector("#wails-spinner")})});var d=null,kt;window.onbeforeunload=function(){d&&(d.onclose=function(){},d.close(),d=null)};It();function ie(){nt=t=>{d.send(t)};for(let t=0;t
',ht(e,"class","wails-reconnect-overlay svelte-181h7z")},m(o,c){W(o,e,c),i=!0},i(o){i||($(()=>{n||(n=Y(e,et,{duration:300},!0)),n.run(1)}),i=!0)},o(o){n||(n=Y(e,et,{duration:300},!1)),n.run(0),i=!1},d(o){o&&S(e),o&&n&&n.end()}}}function te(t){let e,n,i=t[0]&&Mt(t);return{c(){i&&i.c(),e=dt()},m(o,c){i&&i.m(o,c),W(o,e,c),n=!0},p(o,[c]){o[0]?i?c&1&&I(i,1):(i=Mt(o),i.c(),I(i,1),i.m(e.parentNode,e)):i&&(gt(),Q(i,1,1,()=>{i=null}),bt())},i(o){n||(I(i),n=!0)},o(o){Q(i),n=!1},d(o){i&&i.d(o),o&&S(e)}}}function ee(t,e,n){let i;return st(t,q,o=>n(0,i=o)),[i]}var St=class extends tt{constructor(e){super();vt(this,e,ee,te,L,{},Yt)}},Ct=St;var ne={},nt=null,j=[];window.WailsInvoke=t=>{if(!nt){console.log("Queueing: "+t),j.push(t);return}nt(t)};window.addEventListener("DOMContentLoaded",()=>{ne.overlay=new Ct({target:document.body,anchor:document.querySelector("#wails-spinner")})});var d=null,kt;window.onbeforeunload=function(){d&&(d.onclose=function(){},d.close(),d=null)};It();function ie(){nt=t=>{d.send(t)};for(let t=0;t= 14", "less": "*", "sass": "*", "stylus": "*", + "sugarss": "*", "terser": "^5.4.0" }, "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, "less": { "optional": true }, @@ -1900,6 +1905,9 @@ "stylus": { "optional": true }, + "sugarss": { + "optional": true + }, "terser": { "optional": true } @@ -2528,9 +2536,9 @@ } }, "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "optional": true }, @@ -3046,9 +3054,9 @@ } }, "rollup": { - "version": "2.78.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz", - "integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==", + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -3067,9 +3075,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "shebang-command": { @@ -3330,16 +3338,16 @@ } }, "vite": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.1.8.tgz", - "integrity": "sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz", + "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==", "dev": true, "requires": { "esbuild": "^0.15.9", "fsevents": "~2.3.2", - "postcss": "^8.4.16", + "postcss": "^8.4.18", "resolve": "^1.22.1", - "rollup": "~2.78.0" + "rollup": "^2.79.1" } }, "vitest": { diff --git a/v1/vendor/github.com/wailsapp/wails/v2/internal/goversion/min.go b/v1/vendor/github.com/wailsapp/wails/v2/internal/goversion/min.go index 17edc0c..8c057b3 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/internal/goversion/min.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/internal/goversion/min.go @@ -1,3 +1,3 @@ package goversion -const MinRequirement string = "1.18" +const MinRequirement string = "1.20" diff --git a/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assethandler.go b/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assethandler.go index 76d4114..b8e2df0 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assethandler.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assethandler.go @@ -110,7 +110,7 @@ func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } -// serveFile will try to load the file from the fs.FS and write it to the response +// serveFSFile will try to load the file from the fs.FS and write it to the response func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error { if d.fs == nil { return os.ErrNotExist diff --git a/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assetserver.go b/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assetserver.go index 54fe5d0..59665c0 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assetserver.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assetserver.go @@ -8,6 +8,7 @@ import ( "strings" "golang.org/x/net/html" + "html/template" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" @@ -67,9 +68,11 @@ func NewAssetServer(bindingsJSON string, options assetserver.Options, servingFro } func NewAssetServerWithHandler(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + var buffer bytes.Buffer if bindingsJSON != "" { - buffer.WriteString(`window.wailsbindings='` + bindingsJSON + `';` + "\n") + escapedBindingsJSON := template.JSEscapeString(bindingsJSON) + buffer.WriteString(`window.wailsbindings='` + escapedBindingsJSON + `';` + "\n") } buffer.Write(runtime.RuntimeDesktopJS()) diff --git a/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assetserver_webview.go b/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assetserver_webview.go index dfab1f2..d59e90b 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assetserver_webview.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/pkg/assetserver/assetserver_webview.go @@ -60,7 +60,7 @@ func (d *AssetServer) processWebViewRequest(r webview.Request) { } } -// processHTTPRequest processes the HTTP Request by faking a golang HTTP Server. +// processWebViewRequestInternal processes the HTTP Request by faking a golang HTTP Server. // The request will be finished with a StatusNotImplemented code if no handler has written to the response. func (d *AssetServer) processWebViewRequestInternal(r webview.Request) { uri := "unknown" diff --git a/v1/vendor/github.com/wailsapp/wails/v2/pkg/options/mac/mac.go b/v1/vendor/github.com/wailsapp/wails/v2/pkg/options/mac/mac.go index 9d2ccc9..85e5275 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/pkg/options/mac/mac.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/pkg/options/mac/mac.go @@ -21,6 +21,7 @@ type Options struct { WebviewIsTransparent bool WindowIsTranslucent bool Preferences *Preferences + DisableZoom bool // ActivationPolicy ActivationPolicy About *AboutInfo OnFileOpen func(filePath string) `json:"-"` diff --git a/v1/vendor/github.com/wailsapp/wails/v2/pkg/runtime/screen.go b/v1/vendor/github.com/wailsapp/wails/v2/pkg/runtime/screen.go index af8fb62..c4d5266 100644 --- a/v1/vendor/github.com/wailsapp/wails/v2/pkg/runtime/screen.go +++ b/v1/vendor/github.com/wailsapp/wails/v2/pkg/runtime/screen.go @@ -8,7 +8,7 @@ import ( type Screen = frontend.Screen -// ScreenGetAllScreens returns all screens +// ScreenGetAll returns all screens func ScreenGetAll(ctx context.Context) ([]Screen, error) { appFrontend := getFrontend(ctx) return appFrontend.ScreenGetAll() diff --git a/v1/vendor/modules.txt b/v1/vendor/modules.txt index 20bac96..f4f7fed 100644 --- a/v1/vendor/modules.txt +++ b/v1/vendor/modules.txt @@ -77,7 +77,7 @@ github.com/tkrajina/go-reflector/reflector # github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 ## explicit; go 1.16 github.com/tylertravisty/go-utils/random -# github.com/tylertravisty/rumble-livestream-lib-go v0.5.1 +# github.com/tylertravisty/rumble-livestream-lib-go v0.7.2 ## explicit; go 1.19 github.com/tylertravisty/rumble-livestream-lib-go # github.com/valyala/bytebufferpool v1.0.0 @@ -98,7 +98,7 @@ github.com/wailsapp/mimetype github.com/wailsapp/mimetype/internal/charset github.com/wailsapp/mimetype/internal/json github.com/wailsapp/mimetype/internal/magic -# github.com/wailsapp/wails/v2 v2.8.0 +# github.com/wailsapp/wails/v2 v2.8.1 ## explicit; go 1.20 github.com/wailsapp/wails/v2 github.com/wailsapp/wails/v2/internal/app