Feature parity with v0

This commit is contained in:
tyler 2024-05-22 12:51:46 -04:00
parent 2906a372d5
commit 14ef632e6c
45 changed files with 2264 additions and 273 deletions

363
v1/app.go
View file

@ -7,9 +7,11 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "time"
"github.com/tylertravisty/rum-goggles/v1/internal/chatbot"
"github.com/tylertravisty/rum-goggles/v1/internal/config" "github.com/tylertravisty/rum-goggles/v1/internal/config"
"github.com/tylertravisty/rum-goggles/v1/internal/events" "github.com/tylertravisty/rum-goggles/v1/internal/events"
"github.com/tylertravisty/rum-goggles/v1/internal/models" "github.com/tylertravisty/rum-goggles/v1/internal/models"
@ -47,6 +49,7 @@ func (p *Page) staticLiveStreamUrl() string {
// App struct // App struct
type App struct { type App struct {
cancelProc context.CancelFunc cancelProc context.CancelFunc
chatbot *chatbot.Chatbot
clients map[string]*rumblelivestreamlib.Client clients map[string]*rumblelivestreamlib.Client
clientsMu sync.Mutex clientsMu sync.Mutex
displaying string displaying string
@ -101,24 +104,34 @@ func (a *App) process(ctx context.Context) {
for { for {
select { select {
case apiE := <-a.producers.ApiP.Ch: case apiE := <-a.producers.ApiP.Ch:
err := a.processApi(apiE) a.processApi(apiE)
if err != nil {
a.logError.Println("error handling API event:", err)
}
case chatE := <-a.producers.ChatP.Ch: case chatE := <-a.producers.ChatP.Ch:
err := a.processChat(chatE) a.processChat(chatE)
if err != nil {
a.logError.Println("error handling chat event:", err)
}
case <-ctx.Done(): case <-ctx.Done():
return 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 == "" { if event.Name == "" {
return fmt.Errorf("event name is empty") a.logError.Println("page cannot process API: event name is empty")
} }
a.pagesMu.Lock() a.pagesMu.Lock()
@ -138,7 +151,7 @@ func (a *App) processApi(event events.Api) error {
if event.Stop { if event.Stop {
runtime.EventsEmit(a.wails, "ApiActive-"+page.name, false) runtime.EventsEmit(a.wails, "ApiActive-"+page.name, false)
return nil return
} }
runtime.EventsEmit(a.wails, "ApiActive-"+page.name, true) runtime.EventsEmit(a.wails, "ApiActive-"+page.name, true)
@ -153,12 +166,39 @@ func (a *App) processApi(event events.Api) error {
page.apiSt.respMu.Unlock() page.apiSt.respMu.Unlock()
a.updatePage(page) a.updatePage(page)
return nil
} }
func (a *App) processChat(event events.Chat) error { type chatProcessor func(event events.Chat)
return nil
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) { 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 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 // TODO: check for update - if available, pop up window
// runtime.EventsEmit(a.ctx, "StartupMessage", "Checking for updates...") // runtime.EventsEmit(a.ctx, "StartupMessage", "Checking for updates...")
// update, err = a.checkForUpdate() // update, err = a.checkForUpdate()
@ -229,6 +277,13 @@ func (a *App) Start() (bool, error) {
return signin, nil 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 { func (a *App) initProducers() error {
producers, err := events.NewProducers( producers, err := events.NewProducers(
events.WithLoggers(a.logError, a.logInfo), events.WithLoggers(a.logError, a.logInfo),
@ -261,6 +316,7 @@ func (a *App) initServices() error {
models.WithChannelService(), models.WithChannelService(),
models.WithAccountChannelService(), models.WithAccountChannelService(),
models.WithChatbotService(), models.WithChatbotService(),
models.WithChatbotRuleService(),
) )
if err != nil { if err != nil {
return fmt.Errorf("error initializing services: %v", err) 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.") 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) cb, err := a.services.ChatbotS.ByID(*chatbot.ID)
if err != nil { if err != nil {
a.logError.Println("error getting chatbot by ID:", err) 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.") 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) err = a.services.ChatbotS.Delete(chatbot)
if err != nil { if err != nil {
a.logError.Println("error deleting chatbot:", err) a.logError.Println("error deleting chatbot:", err)
@ -1189,60 +1265,241 @@ func (a *App) chatbotList() ([]models.Chatbot, error) {
return list, err return list, err
} }
type ChatbotRule struct { func (a *App) ChatbotRules(chatbot *models.Chatbot) ([]chatbot.Rule, error) {
Message *ChatbotRuleMessage `json:"message"` if chatbot == nil || chatbot.ID == nil {
SendAs *ChatbotRuleSender `json:"send_as"` return nil, fmt.Errorf("Invalid chatbot. Try again.")
Trigger *ChatbotRuleTrigger `json:"trigger"` }
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 { func (a *App) chatbotRules(chatbotID int64) ([]chatbot.Rule, error) {
FromFile *ChatbotRuleMessageFile `json:"from_file"` modelsRules, err := a.services.ChatbotRuleS.ByChatbotID(chatbotID)
FromText string `json:"from_text"` 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), &params)
if err != nil {
return nil, fmt.Errorf("error un-marshaling chatbot rule parameters from json: %v", err)
}
rule.Parameters = &params
}
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 { func (a *App) DeleteChatbotRule(rule *chatbot.Rule) error {
Filepath string `json:"filepath"` if rule == nil || rule.ID == nil || rule.ChatbotID == nil {
RandomRead bool `json:"random_read"` 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 { func (a *App) NewChatbotRule(rule *chatbot.Rule) error {
Username string `json:"username"` if rule == nil || rule.ChatbotID == nil || rule.Parameters == nil {
ChannelID *int `json:"channel_id"` 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 { func (a *App) RunChatbotRule(rule *chatbot.Rule) error {
OnCommand *ChatbotRuleTriggerCommand `json:"on_command"` if rule == nil || rule.ChatbotID == nil {
OnEvent *ChatbotRuleTriggerEvent `json:"on_event"` return fmt.Errorf("Invalid chatbot rule. Try again.")
OnTimer *time.Duration `json:"on_timer"` }
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 { func (a *App) StopChatbotRule(rule *chatbot.Rule) error {
Command string `json:"command"` err := a.chatbot.Stop(rule)
Restrict *ChatbotRuleTriggerCommandRestriction `json:"restrict"` if err != nil {
Timeout time.Duration `json:"timeout"` a.logError.Println("error stopping chat bot rule:", err)
return fmt.Errorf("Error stopping chatbot rule. Try again.")
}
return nil
} }
type ChatbotRuleTriggerCommandRestriction struct { func (a *App) UpdateChatbotRule(rule *chatbot.Rule) error {
Bypass *ChatbotRuleTriggerCommandRestrictionBypass `json:"bypass"` if rule == nil || rule.ID == nil || rule.ChatbotID == nil {
ToAdmin bool `json:"to_admin"` return fmt.Errorf("Invalid chatbot rule. Try again.")
ToFollower bool `json:"to_follower"` }
ToMod bool `json:"to_mod"`
ToStreamer bool `json:"to_streamer"` mRule, err := rule.ToModelsChatbotRule()
ToSubscriber bool `json:"to_subscriber"` if err != nil {
ToRant int `json:"to_rant"` 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 { func (a *App) RunChatbotRules(chatbotID *int64) error {
IfAdmin bool `json:"if_admin"` if chatbotID == nil {
IfMod bool `json:"if_mod"` return fmt.Errorf("Invalid chatbot. Try again.")
IfStreamer bool `json:"if_streamer"` }
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 { func (a *App) StopChatbotRules(chatbotID *int64) error {
OnFollow bool `json:"on_follow"` if chatbotID == nil {
OnSubscribe bool `json:"on_subscribe"` return fmt.Errorf("Invalid chatbot. Try again.")
OnRaid bool `json:"on_raid"` }
OnRant int `json:"on_rant"`
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) { func (a *App) OpenFileDialog() (string, error) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -7,12 +7,17 @@ import eye from './icons/twbs/eye.png';
import eye_red from './icons/twbs/eye-red.png'; import eye_red from './icons/twbs/eye-red.png';
import eye_slash from './icons/twbs/eye-slash.png'; import eye_slash from './icons/twbs/eye-slash.png';
import gear_fill from './icons/twbs/gear-fill.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 heart from './icons/twbs/heart-fill.png';
import pause from './icons/twbs/pause-circle-green.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 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 plus_circle from './icons/twbs/plus-circle-fill.png';
import robot from './icons/Font-Awesome/robot.png'; import robot from './icons/Font-Awesome/robot.png';
import star from './icons/twbs/star-fill.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_down from './icons/twbs/hand-thumbs-down-fill.png';
import thumbs_up from './icons/twbs/hand-thumbs-up-fill.png'; import thumbs_up from './icons/twbs/hand-thumbs-up-fill.png';
import x_lg from './icons/twbs/x-lg.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 EyeRed = eye_red;
export const EyeSlash = eye_slash; export const EyeSlash = eye_slash;
export const Gear = gear_fill; export const Gear = gear_fill;
export const GearWhite = gear_fill_white;
export const Heart = heart; export const Heart = heart;
export const Logo = logo; export const Logo = logo;
export const Pause = pause; export const Pause = pause;
export const PauseBig = pause_big;
export const Play = play; export const Play = play;
export const PlayBig = play_big;
export const PlayBigGreen = play_big_green;
export const PlusCircle = plus_circle; export const PlusCircle = plus_circle;
export const Robot = robot; export const Robot = robot;
export const Star = star; export const Star = star;
export const StopBigRed = stop_big_red;
export const ThumbsDown = thumbs_down; export const ThumbsDown = thumbs_down;
export const ThumbsUp = thumbs_up; export const ThumbsUp = thumbs_up;
export const XLg = x_lg; export const XLg = x_lg;

View file

@ -84,10 +84,10 @@
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
padding: 0px 10px; padding: 0px;
} }
.chatbot-list-button { .chatbot-list-item-button {
align-items: center; align-items: center;
background-color: #344453; background-color: #344453;
border: none; border: none;
@ -98,7 +98,7 @@
width: 100%; width: 100%;
} }
.chatbot-list-button:hover { .chatbot-list-item-button:hover {
background-color: #415568; background-color: #415568;
cursor: pointer; cursor: pointer;
} }
@ -114,6 +114,7 @@
font-weight: bold; font-weight: bold;
max-width: 300px; max-width: 300px;
overflow: hidden; overflow: hidden;
padding: 0px 10px;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
/* width: 100%; */ /* width: 100%; */
@ -221,6 +222,16 @@
cursor: pointer; 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 { .chatbot-modal-setting {
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
@ -286,6 +297,88 @@
transition: .4s; 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 { .choose-file {
align-items: center; align-items: center;
display: flex; display: flex;
@ -338,12 +431,42 @@ input:checked + .chatbot-modal-toggle-slider:before {
border-radius: 50%; 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 { .timer-input {
border: none; border: none;
border-radius: 34px; border-radius: 34px;
box-sizing: border-box; box-sizing: border-box;
font-family: monospace; font-family: monospace;
font-size: 24px; font-size: 16px;
outline: none; outline: none;
padding: 5px 10px 5px 10px; padding: 5px 10px 5px 10px;
text-align: right; text-align: right;

File diff suppressed because it is too large Load diff

View file

@ -200,6 +200,7 @@
.small-modal-message { .small-modal-message {
font-family: sans-serif; font-family: sans-serif;
font-size: 18px; font-size: 18px;
overflow-x: scroll;
} }
.small-modal-title { .small-modal-title {

View file

@ -6,8 +6,8 @@ toolchain go1.22.0
require ( require (
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
github.com/tylertravisty/rumble-livestream-lib-go v0.5.1 github.com/tylertravisty/rumble-livestream-lib-go v0.7.2
github.com/wailsapp/wails/v2 v2.8.0 github.com/wailsapp/wails/v2 v2.8.1
) )
require ( require (

View file

@ -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/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 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g=
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk= 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.7.2 h1:TRGTKhxB+uK0gnIC+rXbRxfFjMJxPHhjZzbsjDSpK+o=
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/go.mod h1:Odkqvsn+2eoWV3ePcj257Ga0bdOqV4JBTfOJcQ+Sqf8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 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/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 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= 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.1 h1:KAudNjlFaiXnDfFEfSNoLoibJ1ovoutSrJ8poerTPW0=
github.com/wailsapp/wails/v2 v2.8.0/go.mod h1:EFUGWkUX3KofO4fmKR/GmsLy3HhPH7NbyOEaMt8lBF0= 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.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 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=

View file

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

View file

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

180
v1/internal/chatbot/rule.go Normal file
View file

@ -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 = &paramsS
}
return modelsRule, nil
}

View file

@ -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()
}

View file

@ -11,16 +11,18 @@ import (
) )
type Chat struct { type Chat struct {
Message rumblelivestreamlib.ChatView Livestream string
Stop bool Message rumblelivestreamlib.ChatView
Url string Stop bool
Url string
} }
type chatProducer struct { type chatProducer struct {
cancel context.CancelFunc cancel context.CancelFunc
cancelMu sync.Mutex cancelMu sync.Mutex
client *rumblelivestreamlib.Client client *rumblelivestreamlib.Client
url string livestream string
url string
} }
type chatProducerValFunc func(*chatProducer) error type chatProducerValFunc func(*chatProducer) error
@ -82,6 +84,12 @@ func (cp *ChatProducer) Start(liveStreamUrl string) (string, error) {
return "", pkgErr("", fmt.Errorf("url is empty")) 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}) client, err := rumblelivestreamlib.NewClient(rumblelivestreamlib.NewClientOptions{LiveStreamUrl: liveStreamUrl})
if err != nil { if err != nil {
return "", pkgErr("error creating new rumble client", err) return "", pkgErr("error creating new rumble client", err)
@ -93,19 +101,21 @@ func (cp *ChatProducer) Start(liveStreamUrl string) (string, error) {
} }
chatStreamUrl := chatInfo.StreamUrl() chatStreamUrl := chatInfo.StreamUrl()
cp.producersMu.Lock() // cp.producersMu.Lock()
defer cp.producersMu.Unlock() // defer cp.producersMu.Unlock()
if _, active := cp.producers[chatStreamUrl]; active { // if _, active := cp.producers[chatStreamUrl]; active {
return chatStreamUrl, nil // return chatStreamUrl, nil
} // }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
producer := &chatProducer{ producer := &chatProducer{
cancel: cancel, cancel: cancel,
client: client, client: client,
url: chatStreamUrl, livestream: liveStreamUrl,
url: chatStreamUrl,
} }
cp.producers[chatStreamUrl] = producer // cp.producers[chatStreamUrl] = producer
cp.producers[liveStreamUrl] = producer
go cp.run(ctx, producer) go cp.run(ctx, producer)
return chatStreamUrl, nil return chatStreamUrl, nil
@ -138,6 +148,8 @@ func (cp *ChatProducer) run(ctx context.Context, producer *chatProducer) {
return return
} }
// TODO: handle the case when restarting stream with possibly missing messages
// Start new stream, make sure it's running, close old stream
for { for {
err = producer.client.StartChatStream(cp.handleChat(producer), cp.handleError(producer)) err = producer.client.StartChatStream(cp.handleChat(producer), cp.handleError(producer))
if err != nil { if err != nil {
@ -154,6 +166,7 @@ func (cp *ChatProducer) run(ctx context.Context, producer *chatProducer) {
cp.stop(producer) cp.stop(producer)
return return
case <-timer.C: case <-timer.C:
producer.client.StopChatStream()
} }
} }
} }
@ -164,7 +177,7 @@ func (cp *ChatProducer) handleChat(p *chatProducer) func(cv rumblelivestreamlib.
return 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 return
} }
cp.Ch <- Chat{Stop: true, Url: p.url} cp.Ch <- Chat{Livestream: p.livestream, Stop: true, Url: p.url}
cp.producersMu.Lock() cp.producersMu.Lock()
delete(cp.producers, p.url) delete(cp.producers, p.url)

View file

@ -6,18 +6,18 @@ import (
) )
const ( const (
chatbotRuleColumns = "id, chatbot_id, name, rule" chatbotRuleColumns = "id, chatbot_id, parameters"
chatbotRuleTable = "chatbot_rule" chatbotRuleTable = "chatbot_rule"
) )
type ChatbotRule struct { type ChatbotRule struct {
ID *int64 `json:"id"` ID *int64 `json:"id"`
ChatbotID *int64 `json:"chatbot_id"` ChatbotID *int64 `json:"chatbot_id"`
Rule *string `json:"rule"` Parameters *string `json:"parameters"`
} }
func (c *ChatbotRule) values() []any { 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 { func (c *ChatbotRule) valuesNoID() []any {
@ -30,26 +30,27 @@ func (c *ChatbotRule) valuesEndID() []any {
} }
type sqlChatbotRule struct { type sqlChatbotRule struct {
id sql.NullInt64 id sql.NullInt64
chatbotID sql.NullInt64 chatbotID sql.NullInt64
rule sql.NullString parameters sql.NullString
} }
func (sc *sqlChatbotRule) scan(r Row) error { 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 { func (sc sqlChatbotRule) toChatbotRule() *ChatbotRule {
var c ChatbotRule var c ChatbotRule
c.ID = toInt64(sc.id) c.ID = toInt64(sc.id)
c.ChatbotID = toInt64(sc.chatbotID) c.ChatbotID = toInt64(sc.chatbotID)
c.Rule = toString(sc.rule) c.Parameters = toString(sc.parameters)
return &c return &c
} }
type ChatbotRuleService interface { type ChatbotRuleService interface {
AutoMigrate() error AutoMigrate() error
ByChatbotID(cid int64) ([]ChatbotRule, error)
Create(c *ChatbotRule) (int64, error) Create(c *ChatbotRule) (int64, error)
Delete(c *ChatbotRule) error Delete(c *ChatbotRule) error
DestructiveReset() error DestructiveReset() error
@ -82,7 +83,7 @@ func (cs *chatbotRuleService) createChatbotRuleTable() error {
CREATE TABLE IF NOT EXISTS "%s" ( CREATE TABLE IF NOT EXISTS "%s" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
chatbot_id INTEGER NOT NULL, chatbot_id INTEGER NOT NULL,
rule TEXT NOT NULL parameters TEXT NOT NULL,
FOREIGN KEY (chatbot_id) REFERENCES "%s" (id) FOREIGN KEY (chatbot_id) REFERENCES "%s" (id)
) )
`, chatbotRuleTable, chatbotTable) `, chatbotRuleTable, chatbotTable)
@ -95,10 +96,42 @@ func (cs *chatbotRuleService) createChatbotRuleTable() error {
return nil 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) { func (cs *chatbotRuleService) Create(c *ChatbotRule) (int64, error) {
err := runChatbotRuleValFuncs( err := runChatbotRuleValFuncs(
c, c,
chatbotRuleRequireRule, chatbotRuleRequireParameters,
) )
if err != nil { if err != nil {
return -1, pkgErr("invalid chat rule", err) return -1, pkgErr("invalid chat rule", err)
@ -169,7 +202,7 @@ func (cs *chatbotRuleService) Update(c *ChatbotRule) error {
err := runChatbotRuleValFuncs( err := runChatbotRuleValFuncs(
c, c,
chatbotRuleRequireID, chatbotRuleRequireID,
chatbotRuleRequireRule, chatbotRuleRequireParameters,
) )
if err != nil { if err != nil {
return pkgErr("invalid chat rule", err) return pkgErr("invalid chat rule", err)
@ -215,9 +248,9 @@ func chatbotRuleRequireID(c *ChatbotRule) error {
return nil return nil
} }
func chatbotRuleRequireRule(c *ChatbotRule) error { func chatbotRuleRequireParameters(c *ChatbotRule) error {
if c.Rule == nil || *c.Rule == "" { if c.Parameters == nil || *c.Parameters == "" {
return ErrChatbotRuleInvalidRule return ErrChatbotRuleInvalidParameters
} }
return nil return nil

View file

@ -17,8 +17,8 @@ const (
ErrChatbotInvalidID ValidatorError = "invalid chatbot id" ErrChatbotInvalidID ValidatorError = "invalid chatbot id"
ErrChatbotInvalidName ValidatorError = "invalid chatbot name" ErrChatbotInvalidName ValidatorError = "invalid chatbot name"
ErrChatbotRuleInvalidID ValidatorError = "invalid chatbot rule id" ErrChatbotRuleInvalidID ValidatorError = "invalid chatbot rule id"
ErrChatbotRuleInvalidRule ValidatorError = "invalid chatbot rule rule" ErrChatbotRuleInvalidParameters ValidatorError = "invalid chatbot rule parameters"
) )
func pkgErr(prefix string, err error) error { func pkgErr(prefix string, err error) error {

View file

@ -18,6 +18,7 @@ type Services struct {
AccountChannelS AccountChannelService AccountChannelS AccountChannelService
ChannelS ChannelService ChannelS ChannelService
ChatbotS ChatbotService ChatbotS ChatbotService
ChatbotRuleS ChatbotRuleService
Database *sql.DB Database *sql.DB
tables []table tables []table
} }
@ -116,3 +117,12 @@ func WithChatbotService() ServicesInit {
return nil 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
}
}

View file

@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"encoding/csv"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -19,6 +18,20 @@ import (
"gopkg.in/cenkalti/backoff.v1" "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 { type ChatInfo struct {
ChannelID int ChannelID int
ChatID string ChatID string
@ -74,21 +87,12 @@ func (c *Client) getChatInfo() (*ChatInfo, error) {
if end == -1 { if end == -1 {
return nil, fmt.Errorf("error finding end of chat function in webpage") return nil, fmt.Errorf("error finding end of chat function in webpage")
} }
argsS := strings.ReplaceAll(lineS[start:start+end], ", ", ",") args := parseRumbleChatArgs(lineS[start : start+end])
argsS = strings.Replace(argsS, "[", "\"[", 1) channelID, err := strconv.Atoi(args[5])
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])
if err != nil { if err != nil {
return nil, fmt.Errorf("error converting channel ID argument string to int: %v", err) 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") { } else if strings.Contains(lineS, "media-by--a") && strings.Contains(lineS, "author") {
r := strings.NewReader(lineS) r := strings.NewReader(lineS)
node, err := html.Parse(r) node, err := html.Parse(r)
@ -117,6 +121,37 @@ func (c *Client) getChatInfo() (*ChatInfo, error) {
return chatInfo, nil 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 { type ChatMessage struct {
Text string `json:"text"` Text string `json:"text"`
} }
@ -359,6 +394,7 @@ type ChatView struct {
IsFollower bool IsFollower bool
Rant int Rant int
Text string Text string
Time time.Time
Type string Type string
Username string Username string
} }
@ -424,6 +460,11 @@ func parseMessages(eventType string, messages []ChatEventMessage, users map[stri
view.Rant = message.Rant.PriceCents view.Rant = message.Rant.PriceCents
} }
view.Text = message.Text 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.Type = eventType
view.Username = user.Username view.Username = user.Username

View file

@ -19,7 +19,7 @@ func isFunction(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Func 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 { func isStruct(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Struct return reflect.ValueOf(value).Kind() == reflect.Struct
} }

View file

@ -17,7 +17,7 @@
#define WindowStartsMinimised 2 #define WindowStartsMinimised 2
#define WindowStartsFullscreen 3 #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 Run(void*, const char* url);
void SetTitle(void* ctx, const char *title); void SetTitle(void* ctx, const char *title);

View file

@ -14,7 +14,7 @@
#import "WailsMenu.h" #import "WailsMenu.h"
#import "WailsMenuItem.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]; [NSApplication sharedApplication];
@ -27,7 +27,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
fullscreen = 1; 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 SetTitle:safeInit(title)];
[result Center]; [result Center];

View file

@ -64,7 +64,7 @@ struct Preferences {
bool *fullscreenEnabled; 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) SetSize:(int)width :(int)height;
- (void) SetPosition:(int)x :(int) y; - (void) SetPosition:(int)x :(int) y;
- (void) SetMinSize:(int)minWidth :(int)minHeight; - (void) SetMinSize:(int)minWidth :(int)minHeight;

View file

@ -136,7 +136,7 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
return NO; 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; NSWindowStyleMask styleMask = 0;
if( !frameless ) { if( !frameless ) {
@ -158,7 +158,6 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
self.mainWindow = [[WailsWindow alloc] initWithContentRect:NSMakeRect(0, 0, width, height) self.mainWindow = [[WailsWindow alloc] initWithContentRect:NSMakeRect(0, 0, width, height)
styleMask:styleMask backing:NSBackingStoreBuffered defer:NO]; styleMask:styleMask backing:NSBackingStoreBuffered defer:NO];
if (!frameless && useToolbar) { if (!frameless && useToolbar) {
id toolbar = [[NSToolbar alloc] initWithIdentifier:@"wails.toolbar"]; id toolbar = [[NSToolbar alloc] initWithIdentifier:@"wails.toolbar"];
[toolbar autorelease]; [toolbar autorelease];
@ -188,6 +187,10 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
[self.mainWindow setAppearance:nsAppearance]; [self.mainWindow setAppearance:nsAppearance];
} }
if (!zoomable && resizable) {
NSButton *button = [self.mainWindow standardWindowButton:NSWindowZoomButton];
[button setEnabled: NO];
}
NSSize minSize = { minWidth, minHeight }; NSSize minSize = { minWidth, minHeight };
NSSize maxSize = { maxWidth, maxHeight }; NSSize maxSize = { maxWidth, maxHeight };

View file

@ -10,5 +10,7 @@ import (
// BrowserOpenURL Use the default browser to open the url // BrowserOpenURL Use the default browser to open the url
func (f *Frontend) BrowserOpenURL(url string) { func (f *Frontend) BrowserOpenURL(url string) {
// Specific method implementation // Specific method implementation
_ = browser.OpenURL(url) if err := browser.OpenURL(url); err != nil {
f.logger.Error("Unable to open default system browser")
}
} }

View file

@ -203,6 +203,7 @@ int main(int argc, const char * argv[]) {
// insert code here... // insert code here...
int frameless = 0; int frameless = 0;
int resizable = 1; int resizable = 1;
int zoomable = 0;
int fullscreen = 1; int fullscreen = 1;
int fullSizeContent = 1; int fullSizeContent = 1;
int hideTitleBar = 0; int hideTitleBar = 0;
@ -219,7 +220,7 @@ int main(int argc, const char * argv[]) {
int defaultContextMenuEnabled = 1; int defaultContextMenuEnabled = 1;
int windowStartState = 0; int windowStartState = 0;
int startsHidden = 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); startsHidden, 400, 400, 600, 600, false);
SetBackgroundColour(result, 255, 0, 0, 255); SetBackgroundColour(result, 255, 0, 0, 255);
void *m = NewMenu(""); void *m = NewMenu("");

View file

@ -60,7 +60,7 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window
defaultContextMenuEnabled := bool2Cint(debug || frontendOptions.EnableDefaultContextMenu) defaultContextMenuEnabled := bool2Cint(debug || frontendOptions.EnableDefaultContextMenu)
singleInstanceEnabled := bool2Cint(frontendOptions.SingleInstanceLock != nil) 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 titlebarAppearsTransparent, hideToolbarSeparator, windowIsTranslucent C.int
var appearance, title *C.char var appearance, title *C.char
var preferences C.struct_Preferences 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) windowIsTranslucent = bool2Cint(mac.WindowIsTranslucent)
webviewIsTransparent = bool2Cint(mac.WebviewIsTransparent) webviewIsTransparent = bool2Cint(mac.WebviewIsTransparent)
appearance = c.String(string(mac.Appearance)) 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, hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent,
alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled,
windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings, windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings,

View file

@ -8,5 +8,7 @@ import "github.com/pkg/browser"
// BrowserOpenURL Use the default browser to open the url // BrowserOpenURL Use the default browser to open the url
func (f *Frontend) BrowserOpenURL(url string) { func (f *Frontend) BrowserOpenURL(url string) {
// Specific method implementation // Specific method implementation
_ = browser.OpenURL(url) if err := browser.OpenURL(url); err != nil {
f.logger.Error("Unable to open default system browser")
}
} }

View file

@ -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); 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) static gboolean disableContextMenu(GtkWidget *widget, WebKitContextMenu *context_menu, GdkEvent *event, WebKitHitTestResult *hit_test_result, gpointer data)
{ {
// return true to disable the context menu // return true to disable the context menu
@ -254,7 +254,7 @@ static gboolean disableContextMenu(GtkWidget *widget, WebKitContextMenu *context
void DisableContextMenu(void *webview) 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); g_signal_connect(WEBKIT_WEB_VIEW(webview), "context-menu", G_CALLBACK(disableContextMenu), NULL);
} }

View file

@ -5,10 +5,31 @@ package windows
import ( import (
"github.com/pkg/browser" "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 // BrowserOpenURL Use the default browser to open the url
func (f *Frontend) BrowserOpenURL(url string) { func (f *Frontend) BrowserOpenURL(url string) {
// Specific method implementation // 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)
} }

View file

@ -37,7 +37,7 @@ func init() {
w32.InitCommonControlsEx(&initCtrls) 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) { func SetAppIcon(appIconID int) {
AppIconID = appIconID AppIconID = appIconID
} }

View file

@ -54,7 +54,7 @@ func (cb *ComboBox) OnSelectedChange() *EventManager {
return &cb.onSelectedChange return &cb.onSelectedChange
} }
// Message processer // Message processor
func (cb *ComboBox) WndProc(msg uint32, wparam, lparam uintptr) uintptr { func (cb *ComboBox) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg { switch msg {
case w32.WM_COMMAND: case w32.WM_COMMAND:

View file

@ -438,7 +438,7 @@ func (lv *ListView) OnEndScroll() *EventManager {
return &lv.onEndScroll return &lv.onEndScroll
} }
// Message processer // Message processor
func (lv *ListView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { func (lv *ListView) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg { switch msg {
/*case w32.WM_ERASEBKGND: /*case w32.WM_ERASEBKGND:

View file

@ -248,7 +248,7 @@ func (tv *TreeView) OnViewChange() *EventManager {
return &tv.onViewChange return &tv.onViewChange
} }
// Message processer // Message processor
func (tv *TreeView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { func (tv *TreeView) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg { switch msg {
case w32.WM_NOTIFY: case w32.WM_NOTIFY:

File diff suppressed because one or more lines are too long

View file

@ -784,9 +784,9 @@
} }
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.2", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
@ -1514,9 +1514,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "2.78.1", "version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==", "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"dev": true, "dev": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
@ -1541,9 +1541,9 @@
"dev": true "dev": true
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "5.7.1", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true, "dev": true,
"bin": { "bin": {
"semver": "bin/semver" "semver": "bin/semver"
@ -1865,15 +1865,15 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "3.1.8", "version": "3.2.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.1.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz",
"integrity": "sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==", "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.15.9", "esbuild": "^0.15.9",
"postcss": "^8.4.16", "postcss": "^8.4.18",
"resolve": "^1.22.1", "resolve": "^1.22.1",
"rollup": "~2.78.0" "rollup": "^2.79.1"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@ -1885,12 +1885,17 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": ">= 14",
"less": "*", "less": "*",
"sass": "*", "sass": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*",
"terser": "^5.4.0" "terser": "^5.4.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": { "less": {
"optional": true "optional": true
}, },
@ -1900,6 +1905,9 @@
"stylus": { "stylus": {
"optional": true "optional": true
}, },
"sugarss": {
"optional": true
},
"terser": { "terser": {
"optional": true "optional": true
} }
@ -2528,9 +2536,9 @@
} }
}, },
"fsevents": { "fsevents": {
"version": "2.3.2", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
@ -3046,9 +3054,9 @@
} }
}, },
"rollup": { "rollup": {
"version": "2.78.1", "version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==", "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"dev": true, "dev": true,
"requires": { "requires": {
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
@ -3067,9 +3075,9 @@
"dev": true "dev": true
}, },
"semver": { "semver": {
"version": "5.7.1", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true "dev": true
}, },
"shebang-command": { "shebang-command": {
@ -3330,16 +3338,16 @@
} }
}, },
"vite": { "vite": {
"version": "3.1.8", "version": "3.2.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.1.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz",
"integrity": "sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==", "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==",
"dev": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.15.9", "esbuild": "^0.15.9",
"fsevents": "~2.3.2", "fsevents": "~2.3.2",
"postcss": "^8.4.16", "postcss": "^8.4.18",
"resolve": "^1.22.1", "resolve": "^1.22.1",
"rollup": "~2.78.0" "rollup": "^2.79.1"
} }
}, },
"vitest": { "vitest": {

View file

@ -1,3 +1,3 @@
package goversion package goversion
const MinRequirement string = "1.18" const MinRequirement string = "1.20"

View file

@ -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 { func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error {
if d.fs == nil { if d.fs == nil {
return os.ErrNotExist return os.ErrNotExist

View file

@ -8,6 +8,7 @@ import (
"strings" "strings"
"golang.org/x/net/html" "golang.org/x/net/html"
"html/template"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver" "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) { func NewAssetServerWithHandler(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
var buffer bytes.Buffer var buffer bytes.Buffer
if bindingsJSON != "" { if bindingsJSON != "" {
buffer.WriteString(`window.wailsbindings='` + bindingsJSON + `';` + "\n") escapedBindingsJSON := template.JSEscapeString(bindingsJSON)
buffer.WriteString(`window.wailsbindings='` + escapedBindingsJSON + `';` + "\n")
} }
buffer.Write(runtime.RuntimeDesktopJS()) buffer.Write(runtime.RuntimeDesktopJS())

View file

@ -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. // The request will be finished with a StatusNotImplemented code if no handler has written to the response.
func (d *AssetServer) processWebViewRequestInternal(r webview.Request) { func (d *AssetServer) processWebViewRequestInternal(r webview.Request) {
uri := "unknown" uri := "unknown"

View file

@ -21,6 +21,7 @@ type Options struct {
WebviewIsTransparent bool WebviewIsTransparent bool
WindowIsTranslucent bool WindowIsTranslucent bool
Preferences *Preferences Preferences *Preferences
DisableZoom bool
// ActivationPolicy ActivationPolicy // ActivationPolicy ActivationPolicy
About *AboutInfo About *AboutInfo
OnFileOpen func(filePath string) `json:"-"` OnFileOpen func(filePath string) `json:"-"`

View file

@ -8,7 +8,7 @@ import (
type Screen = frontend.Screen type Screen = frontend.Screen
// ScreenGetAllScreens returns all screens // ScreenGetAll returns all screens
func ScreenGetAll(ctx context.Context) ([]Screen, error) { func ScreenGetAll(ctx context.Context) ([]Screen, error) {
appFrontend := getFrontend(ctx) appFrontend := getFrontend(ctx)
return appFrontend.ScreenGetAll() return appFrontend.ScreenGetAll()

View file

@ -77,7 +77,7 @@ github.com/tkrajina/go-reflector/reflector
# github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 # github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909
## explicit; go 1.16 ## explicit; go 1.16
github.com/tylertravisty/go-utils/random 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 ## explicit; go 1.19
github.com/tylertravisty/rumble-livestream-lib-go github.com/tylertravisty/rumble-livestream-lib-go
# github.com/valyala/bytebufferpool v1.0.0 # 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/charset
github.com/wailsapp/mimetype/internal/json github.com/wailsapp/mimetype/internal/json
github.com/wailsapp/mimetype/internal/magic 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 ## explicit; go 1.20
github.com/wailsapp/wails/v2 github.com/wailsapp/wails/v2
github.com/wailsapp/wails/v2/internal/app github.com/wailsapp/wails/v2/internal/app