diff --git a/configEditor.go b/configEditor.go index 704e7e5..ace5932 100644 --- a/configEditor.go +++ b/configEditor.go @@ -5,18 +5,14 @@ import ( "encoding/json" "io" "net/http" - "regexp" "sort" "sync" "time" "github.com/Luzifer/twitch-bot/plugins" "github.com/Luzifer/twitch-bot/twitch" - "github.com/gofrs/uuid/v3" - "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" ) const websocketPingInterval = 30 * time.Second @@ -41,13 +37,6 @@ func registerActorDocumentation(doc plugins.ActionDocumentation) { }) } -type ( - configEditorGeneralConfig struct { - BotEditors []string `json:"bot_editors"` - Channels []string `json:"channels"` - } -) - func init() { registerEditorAutoMessageRoutes() registerEditorFrontend() @@ -56,155 +45,6 @@ func init() { registerEditorRulesRoutes() } -//nolint:funlen // This is a logic unit and shall not be split up -func registerEditorAutoMessageRoutes() { - for _, rd := range []plugins.HTTPRouteRegistrationArgs{ - { - Description: "Returns the current set of configured auto-messages in JSON format", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - if err := json.NewEncoder(w).Encode(config.AutoMessages); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - }, - Method: http.MethodGet, - Module: "config-editor", - Name: "Get current auto-messages", - Path: "/auto-messages", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeJSON, - }, - { - Description: "Adds a new Auto-Message", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - user, _, err := getAuthorizationFromRequest(r) - if err != nil { - http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) - } - - msg := &autoMessage{} - if err := json.NewDecoder(r.Body).Decode(msg); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - msg.UUID = uuid.Must(uuid.NewV4()).String() - - if err := patchConfig(cfg.Config, user, "", "Add auto-message", func(c *configFile) error { - c.AutoMessages = append(c.AutoMessages, msg) - return nil - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) - }, - Method: http.MethodPost, - Module: "config-editor", - Name: "Add Auto-Message", - Path: "/auto-messages", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeTextPlain, - }, - { - Description: "Deletes the given Auto-Message", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - user, _, err := getAuthorizationFromRequest(r) - if err != nil { - http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) - } - - if err := patchConfig(cfg.Config, user, "", "Delete auto-message", func(c *configFile) error { - var ( - id = mux.Vars(r)["uuid"] - tmp []*autoMessage - ) - - for i := range c.AutoMessages { - if c.AutoMessages[i].ID() == id { - continue - } - tmp = append(tmp, c.AutoMessages[i]) - } - - c.AutoMessages = tmp - - return nil - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) - }, - Method: http.MethodDelete, - Module: "config-editor", - Name: "Delete Auto-Message", - Path: "/auto-messages/{uuid}", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeTextPlain, - RouteParams: []plugins.HTTPRouteParamDocumentation{ - { - Description: "UUID of the auto-message to delete", - Name: "uuid", - Required: false, - Type: "string", - }, - }, - }, - { - Description: "Updates the given Auto-Message", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - user, _, err := getAuthorizationFromRequest(r) - if err != nil { - http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) - } - - msg := &autoMessage{} - if err := json.NewDecoder(r.Body).Decode(msg); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if err := patchConfig(cfg.Config, user, "", "Update auto-message", func(c *configFile) error { - id := mux.Vars(r)["uuid"] - - for i := range c.AutoMessages { - if c.AutoMessages[i].ID() == id { - c.AutoMessages[i] = msg - } - } - - return nil - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) - }, - Method: http.MethodPut, - Module: "config-editor", - Name: "Update Auto-Message", - Path: "/auto-messages/{uuid}", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeTextPlain, - RouteParams: []plugins.HTTPRouteParamDocumentation{ - { - Description: "UUID of the auto-message to update", - Name: "uuid", - Required: false, - Type: "string", - }, - }, - }, - } { - if err := registerRoute(rd); err != nil { - log.WithError(err).Fatal("Unable to register config editor route") - } - } -} - func registerEditorFrontend() { router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { f, err := configEditorFrontend.Open("editor/index.html") @@ -232,401 +72,3 @@ func registerEditorFrontend() { router.PathPrefix("/editor").Handler(http.FileServer(http.FS(configEditorFrontend))) } - -func registerEditorGeneralConfigRoutes() { - for _, rd := range []plugins.HTTPRouteRegistrationArgs{ - { - Description: "Returns the current general config", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - if err := json.NewEncoder(w).Encode(configEditorGeneralConfig{ - BotEditors: config.BotEditors, - Channels: config.Channels, - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - }, - Method: http.MethodGet, - Module: "config-editor", - Name: "Get general config", - Path: "/general", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeJSON, - }, - { - Description: "Updates the general config", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - user, _, err := getAuthorizationFromRequest(r) - if err != nil { - http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) - } - - var payload configEditorGeneralConfig - - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - for i := range payload.BotEditors { - usr, err := twitchClient.GetUserInformation(payload.BotEditors[i]) - if err != nil { - http.Error(w, errors.Wrap(err, "getting bot editor profile").Error(), http.StatusInternalServerError) - return - } - - payload.BotEditors[i] = usr.ID - } - - if err := patchConfig(cfg.Config, user, "", "Update general config", func(cfg *configFile) error { - cfg.Channels = payload.Channels - cfg.BotEditors = payload.BotEditors - - return nil - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) - }, - Method: http.MethodPut, - Module: "config-editor", - Name: "Update general config", - Path: "/general", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeTextPlain, - }, - } { - if err := registerRoute(rd); err != nil { - log.WithError(err).Fatal("Unable to register config editor route") - } - } -} - -//nolint:funlen,gocognit,gocyclo // This is a logic unit and shall not be split up -func registerEditorGlobalMethods() { - for _, rd := range []plugins.HTTPRouteRegistrationArgs{ - { - Description: "Returns the documentation for available actions", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - availableActorDocsLock.Lock() - defer availableActorDocsLock.Unlock() - - if err := json.NewEncoder(w).Encode(availableActorDocs); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - }, - Method: http.MethodGet, - Module: "config-editor", - Name: "Get available actions", - Path: "/actions", - ResponseType: plugins.HTTPRouteResponseTypeJSON, - }, - { - Description: "Returns information about a Twitch user to properly display bot editors", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - usr, err := twitchClient.GetUserInformation(r.FormValue("user")) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if err := json.NewEncoder(w).Encode(usr); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - }, - Method: http.MethodGet, - Module: "config-editor", - Name: "Get user information", - Path: "/user", - QueryParams: []plugins.HTTPRouteParamDocumentation{ - { - Description: "The user to query the information for", - Name: "user", - Required: true, - Type: "string", - }, - }, - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeJSON, - }, - { - Description: "Subscribe for configuration changes", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer conn.Close() - - var ( - configReloadNotify = make(chan struct{}, 1) - pingTimer = time.NewTicker(websocketPingInterval) - unsubscribe = registerConfigReloadHook(func() { configReloadNotify <- struct{}{} }) - ) - defer unsubscribe() - - type socketMsg struct { - MsgType string `json:"msg_type"` - } - - for { - select { - case <-configReloadNotify: - if err := conn.WriteJSON(socketMsg{ - MsgType: "configReload", - }); err != nil { - log.WithError(err).Debug("Unable to send websocket notification") - return - } - - case <-pingTimer.C: - if err := conn.WriteJSON(socketMsg{ - MsgType: "ping", - }); err != nil { - log.WithError(err).Debug("Unable to send websocket ping") - return - } - - } - } - }, - Method: http.MethodGet, - Module: "config-editor", - Name: "Websocket: Subscribe config changes", - Path: "/notify-config", - ResponseType: plugins.HTTPRouteResponseTypeTextPlain, - }, - { - Description: "Validate a cron expression and return the next executions", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - sched, err := cronParser.Parse(r.FormValue("cron")) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - var ( - lt = time.Now() - out []time.Time - ) - - if id := r.FormValue("uuid"); id != "" { - for _, a := range config.AutoMessages { - if a.ID() != id { - continue - } - lt = a.lastMessageSent - break - } - } - - for i := 0; i < 3; i++ { - lt = sched.Next(lt) - out = append(out, lt) - } - - if err := json.NewEncoder(w).Encode(out); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - }, - Method: http.MethodPut, - Module: "config-editor", - Name: "Validate cron expression", - Path: "/validate-cron", - QueryParams: []plugins.HTTPRouteParamDocumentation{ - { - Description: "The cron expression to test", - Name: "cron", - Required: true, - Type: "string", - }, - { - Description: "Check cron with last execution of auto-message", - Name: "uuid", - Required: false, - Type: "string", - }, - }, - ResponseType: plugins.HTTPRouteResponseTypeJSON, - }, - { - Description: "Validate a regular expression against the RE2 regex parser", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - if _, err := regexp.Compile(r.FormValue("regexp")); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - w.WriteHeader(http.StatusNoContent) - }, - Method: http.MethodPut, - Module: "config-editor", - Name: "Validate regular expression", - Path: "/validate-regex", - QueryParams: []plugins.HTTPRouteParamDocumentation{ - { - Description: "The regular expression to test", - Name: "regexp", - Required: true, - Type: "string", - }, - }, - ResponseType: plugins.HTTPRouteResponseTypeTextPlain, - }, - } { - if err := registerRoute(rd); err != nil { - log.WithError(err).Fatal("Unable to register config editor route") - } - } -} - -//nolint:funlen // This is a logic unit and shall not be split up -func registerEditorRulesRoutes() { - for _, rd := range []plugins.HTTPRouteRegistrationArgs{ - { - Description: "Returns the current set of configured rules in JSON format", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - if err := json.NewEncoder(w).Encode(config.Rules); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - }, - Method: http.MethodGet, - Module: "config-editor", - Name: "Get current rules", - Path: "/rules", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeJSON, - }, - { - Description: "Adds a new Rule", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - user, _, err := getAuthorizationFromRequest(r) - if err != nil { - http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) - } - - msg := &plugins.Rule{} - if err := json.NewDecoder(r.Body).Decode(msg); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - msg.UUID = uuid.Must(uuid.NewV4()).String() - - if err := patchConfig(cfg.Config, user, "", "Add rule", func(c *configFile) error { - c.Rules = append(c.Rules, msg) - return nil - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) - }, - Method: http.MethodPost, - Module: "config-editor", - Name: "Add Rule", - Path: "/rules", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeTextPlain, - }, - { - Description: "Deletes the given Rule", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - user, _, err := getAuthorizationFromRequest(r) - if err != nil { - http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) - } - - if err := patchConfig(cfg.Config, user, "", "Delete rule", func(c *configFile) error { - var ( - id = mux.Vars(r)["uuid"] - tmp []*plugins.Rule - ) - - for i := range c.Rules { - if c.Rules[i].MatcherID() == id { - continue - } - tmp = append(tmp, c.Rules[i]) - } - - c.Rules = tmp - - return nil - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) - }, - Method: http.MethodDelete, - Module: "config-editor", - Name: "Delete Rule", - Path: "/rules/{uuid}", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeTextPlain, - RouteParams: []plugins.HTTPRouteParamDocumentation{ - { - Description: "UUID of the rule to delete", - Name: "uuid", - Required: false, - Type: "string", - }, - }, - }, - { - Description: "Updates the given Rule", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - user, _, err := getAuthorizationFromRequest(r) - if err != nil { - http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) - } - - msg := &plugins.Rule{} - if err := json.NewDecoder(r.Body).Decode(msg); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if err := patchConfig(cfg.Config, user, "", "Update rule", func(c *configFile) error { - id := mux.Vars(r)["uuid"] - - for i := range c.Rules { - if c.Rules[i].MatcherID() == id { - c.Rules[i] = msg - } - } - - return nil - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) - }, - Method: http.MethodPut, - Module: "config-editor", - Name: "Update Rule", - Path: "/rules/{uuid}", - RequiresEditorsAuth: true, - ResponseType: plugins.HTTPRouteResponseTypeTextPlain, - RouteParams: []plugins.HTTPRouteParamDocumentation{ - { - Description: "UUID of the rule to update", - Name: "uuid", - Required: false, - Type: "string", - }, - }, - }, - } { - if err := registerRoute(rd); err != nil { - log.WithError(err).Fatal("Unable to register config editor route") - } - } -} diff --git a/configEditor_automessage.go b/configEditor_automessage.go new file mode 100644 index 0000000..c7abe82 --- /dev/null +++ b/configEditor_automessage.go @@ -0,0 +1,168 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/Luzifer/twitch-bot/plugins" + "github.com/gofrs/uuid/v3" + "github.com/gorilla/mux" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func registerEditorAutoMessageRoutes() { + for _, rd := range []plugins.HTTPRouteRegistrationArgs{ + { + Description: "Returns the current set of configured auto-messages in JSON format", + HandlerFunc: configEditorHandleAutoMessagesGet, + Method: http.MethodGet, + Module: "config-editor", + Name: "Get current auto-messages", + Path: "/auto-messages", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeJSON, + }, + { + Description: "Adds a new Auto-Message", + HandlerFunc: configEditorHandleAutoMessageAdd, + Method: http.MethodPost, + Module: "config-editor", + Name: "Add Auto-Message", + Path: "/auto-messages", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeTextPlain, + }, + { + Description: "Deletes the given Auto-Message", + HandlerFunc: configEditorHandleAutoMessageDelete, + Method: http.MethodDelete, + Module: "config-editor", + Name: "Delete Auto-Message", + Path: "/auto-messages/{uuid}", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeTextPlain, + RouteParams: []plugins.HTTPRouteParamDocumentation{ + { + Description: "UUID of the auto-message to delete", + Name: "uuid", + Required: false, + Type: "string", + }, + }, + }, + { + Description: "Updates the given Auto-Message", + HandlerFunc: configEditorHandleAutoMessageUpdate, + Method: http.MethodPut, + Module: "config-editor", + Name: "Update Auto-Message", + Path: "/auto-messages/{uuid}", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeTextPlain, + RouteParams: []plugins.HTTPRouteParamDocumentation{ + { + Description: "UUID of the auto-message to update", + Name: "uuid", + Required: false, + Type: "string", + }, + }, + }, + } { + if err := registerRoute(rd); err != nil { + log.WithError(err).Fatal("Unable to register config editor route") + } + } +} + +func configEditorHandleAutoMessageAdd(w http.ResponseWriter, r *http.Request) { + user, _, err := getAuthorizationFromRequest(r) + if err != nil { + http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) + } + + msg := &autoMessage{} + if err := json.NewDecoder(r.Body).Decode(msg); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + msg.UUID = uuid.Must(uuid.NewV4()).String() + + if err := patchConfig(cfg.Config, user, "", "Add auto-message", func(c *configFile) error { + c.AutoMessages = append(c.AutoMessages, msg) + return nil + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} + +func configEditorHandleAutoMessageDelete(w http.ResponseWriter, r *http.Request) { + user, _, err := getAuthorizationFromRequest(r) + if err != nil { + http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) + } + + if err := patchConfig(cfg.Config, user, "", "Delete auto-message", func(c *configFile) error { + var ( + id = mux.Vars(r)["uuid"] + tmp []*autoMessage + ) + + for i := range c.AutoMessages { + if c.AutoMessages[i].ID() == id { + continue + } + tmp = append(tmp, c.AutoMessages[i]) + } + + c.AutoMessages = tmp + + return nil + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func configEditorHandleAutoMessagesGet(w http.ResponseWriter, r *http.Request) { + if err := json.NewEncoder(w).Encode(config.AutoMessages); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func configEditorHandleAutoMessageUpdate(w http.ResponseWriter, r *http.Request) { + user, _, err := getAuthorizationFromRequest(r) + if err != nil { + http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) + } + + msg := &autoMessage{} + if err := json.NewDecoder(r.Body).Decode(msg); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := patchConfig(cfg.Config, user, "", "Update auto-message", func(c *configFile) error { + id := mux.Vars(r)["uuid"] + + for i := range c.AutoMessages { + if c.AutoMessages[i].ID() == id { + c.AutoMessages[i] = msg + } + } + + return nil + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/configEditor_general.go b/configEditor_general.go new file mode 100644 index 0000000..0b2cc07 --- /dev/null +++ b/configEditor_general.go @@ -0,0 +1,91 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/Luzifer/twitch-bot/plugins" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +type ( + configEditorGeneralConfig struct { + BotEditors []string `json:"bot_editors"` + Channels []string `json:"channels"` + } +) + +func registerEditorGeneralConfigRoutes() { + for _, rd := range []plugins.HTTPRouteRegistrationArgs{ + { + Description: "Returns the current general config", + HandlerFunc: configEditorHandleGeneralGet, + Method: http.MethodGet, + Module: "config-editor", + Name: "Get general config", + Path: "/general", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeJSON, + }, + { + Description: "Updates the general config", + HandlerFunc: configEditorHandleGeneralUpdate, + Method: http.MethodPut, + Module: "config-editor", + Name: "Update general config", + Path: "/general", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeTextPlain, + }, + } { + if err := registerRoute(rd); err != nil { + log.WithError(err).Fatal("Unable to register config editor route") + } + } +} + +func configEditorHandleGeneralGet(w http.ResponseWriter, r *http.Request) { + if err := json.NewEncoder(w).Encode(configEditorGeneralConfig{ + BotEditors: config.BotEditors, + Channels: config.Channels, + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func configEditorHandleGeneralUpdate(w http.ResponseWriter, r *http.Request) { + user, _, err := getAuthorizationFromRequest(r) + if err != nil { + http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) + } + + var payload configEditorGeneralConfig + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + for i := range payload.BotEditors { + usr, err := twitchClient.GetUserInformation(payload.BotEditors[i]) + if err != nil { + http.Error(w, errors.Wrap(err, "getting bot editor profile").Error(), http.StatusInternalServerError) + return + } + + payload.BotEditors[i] = usr.ID + } + + if err := patchConfig(cfg.Config, user, "", "Update general config", func(cfg *configFile) error { + cfg.Channels = payload.Channels + cfg.BotEditors = payload.BotEditors + + return nil + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/configEditor_global.go b/configEditor_global.go new file mode 100644 index 0000000..c18686e --- /dev/null +++ b/configEditor_global.go @@ -0,0 +1,199 @@ +package main + +import ( + "encoding/json" + "net/http" + "regexp" + "time" + + "github.com/Luzifer/twitch-bot/plugins" + log "github.com/sirupsen/logrus" +) + +func registerEditorGlobalMethods() { + for _, rd := range []plugins.HTTPRouteRegistrationArgs{ + { + Description: "Returns the documentation for available actions", + HandlerFunc: configEditorGlobalGetActions, + Method: http.MethodGet, + Module: "config-editor", + Name: "Get available actions", + Path: "/actions", + ResponseType: plugins.HTTPRouteResponseTypeJSON, + }, + { + Description: "Returns information about a Twitch user to properly display bot editors", + HandlerFunc: configEditorGlobalGetUser, + Method: http.MethodGet, + Module: "config-editor", + Name: "Get user information", + Path: "/user", + QueryParams: []plugins.HTTPRouteParamDocumentation{ + { + Description: "The user to query the information for", + Name: "user", + Required: true, + Type: "string", + }, + }, + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeJSON, + }, + { + Description: "Subscribe for configuration changes", + HandlerFunc: configEditorGlobalSubscribe, + Method: http.MethodGet, + Module: "config-editor", + Name: "Websocket: Subscribe config changes", + Path: "/notify-config", + ResponseType: plugins.HTTPRouteResponseTypeTextPlain, + }, + { + Description: "Validate a cron expression and return the next executions", + HandlerFunc: configEditorGlobalValidateCron, + Method: http.MethodPut, + Module: "config-editor", + Name: "Validate cron expression", + Path: "/validate-cron", + QueryParams: []plugins.HTTPRouteParamDocumentation{ + { + Description: "The cron expression to test", + Name: "cron", + Required: true, + Type: "string", + }, + { + Description: "Check cron with last execution of auto-message", + Name: "uuid", + Required: false, + Type: "string", + }, + }, + ResponseType: plugins.HTTPRouteResponseTypeJSON, + }, + { + Description: "Validate a regular expression against the RE2 regex parser", + HandlerFunc: configEditorGlobalValidateRegex, + Method: http.MethodPut, + Module: "config-editor", + Name: "Validate regular expression", + Path: "/validate-regex", + QueryParams: []plugins.HTTPRouteParamDocumentation{ + { + Description: "The regular expression to test", + Name: "regexp", + Required: true, + Type: "string", + }, + }, + ResponseType: plugins.HTTPRouteResponseTypeTextPlain, + }, + } { + if err := registerRoute(rd); err != nil { + log.WithError(err).Fatal("Unable to register config editor route") + } + } +} + +func configEditorGlobalGetActions(w http.ResponseWriter, r *http.Request) { + availableActorDocsLock.Lock() + defer availableActorDocsLock.Unlock() + + if err := json.NewEncoder(w).Encode(availableActorDocs); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func configEditorGlobalGetUser(w http.ResponseWriter, r *http.Request) { + usr, err := twitchClient.GetUserInformation(r.FormValue("user")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err := json.NewEncoder(w).Encode(usr); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func configEditorGlobalSubscribe(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + + var ( + configReloadNotify = make(chan struct{}, 1) + pingTimer = time.NewTicker(websocketPingInterval) + unsubscribe = registerConfigReloadHook(func() { configReloadNotify <- struct{}{} }) + ) + defer unsubscribe() + + type socketMsg struct { + MsgType string `json:"msg_type"` + } + + for { + select { + case <-configReloadNotify: + if err := conn.WriteJSON(socketMsg{ + MsgType: "configReload", + }); err != nil { + log.WithError(err).Debug("Unable to send websocket notification") + return + } + + case <-pingTimer.C: + if err := conn.WriteJSON(socketMsg{ + MsgType: "ping", + }); err != nil { + log.WithError(err).Debug("Unable to send websocket ping") + return + } + + } + } +} + +func configEditorGlobalValidateCron(w http.ResponseWriter, r *http.Request) { + sched, err := cronParser.Parse(r.FormValue("cron")) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var ( + lt = time.Now() + out []time.Time + ) + + if id := r.FormValue("uuid"); id != "" { + for _, a := range config.AutoMessages { + if a.ID() != id { + continue + } + lt = a.lastMessageSent + break + } + } + + for i := 0; i < 3; i++ { + lt = sched.Next(lt) + out = append(out, lt) + } + + if err := json.NewEncoder(w).Encode(out); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func configEditorGlobalValidateRegex(w http.ResponseWriter, r *http.Request) { + if _, err := regexp.Compile(r.FormValue("regexp")); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/configEditor_rules.go b/configEditor_rules.go new file mode 100644 index 0000000..aaf28ea --- /dev/null +++ b/configEditor_rules.go @@ -0,0 +1,168 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/Luzifer/twitch-bot/plugins" + "github.com/gofrs/uuid/v3" + "github.com/gorilla/mux" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func registerEditorRulesRoutes() { + for _, rd := range []plugins.HTTPRouteRegistrationArgs{ + { + Description: "Returns the current set of configured rules in JSON format", + HandlerFunc: configEditorRulesGet, + Method: http.MethodGet, + Module: "config-editor", + Name: "Get current rules", + Path: "/rules", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeJSON, + }, + { + Description: "Adds a new Rule", + HandlerFunc: configEditorRulesAdd, + Method: http.MethodPost, + Module: "config-editor", + Name: "Add Rule", + Path: "/rules", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeTextPlain, + }, + { + Description: "Deletes the given Rule", + HandlerFunc: configEditorRulesDelete, + Method: http.MethodDelete, + Module: "config-editor", + Name: "Delete Rule", + Path: "/rules/{uuid}", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeTextPlain, + RouteParams: []plugins.HTTPRouteParamDocumentation{ + { + Description: "UUID of the rule to delete", + Name: "uuid", + Required: false, + Type: "string", + }, + }, + }, + { + Description: "Updates the given Rule", + HandlerFunc: configEditorRulesUpdate, + Method: http.MethodPut, + Module: "config-editor", + Name: "Update Rule", + Path: "/rules/{uuid}", + RequiresEditorsAuth: true, + ResponseType: plugins.HTTPRouteResponseTypeTextPlain, + RouteParams: []plugins.HTTPRouteParamDocumentation{ + { + Description: "UUID of the rule to update", + Name: "uuid", + Required: false, + Type: "string", + }, + }, + }, + } { + if err := registerRoute(rd); err != nil { + log.WithError(err).Fatal("Unable to register config editor route") + } + } +} + +func configEditorRulesAdd(w http.ResponseWriter, r *http.Request) { + user, _, err := getAuthorizationFromRequest(r) + if err != nil { + http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) + } + + msg := &plugins.Rule{} + if err := json.NewDecoder(r.Body).Decode(msg); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + msg.UUID = uuid.Must(uuid.NewV4()).String() + + if err := patchConfig(cfg.Config, user, "", "Add rule", func(c *configFile) error { + c.Rules = append(c.Rules, msg) + return nil + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} + +func configEditorRulesDelete(w http.ResponseWriter, r *http.Request) { + user, _, err := getAuthorizationFromRequest(r) + if err != nil { + http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) + } + + if err := patchConfig(cfg.Config, user, "", "Delete rule", func(c *configFile) error { + var ( + id = mux.Vars(r)["uuid"] + tmp []*plugins.Rule + ) + + for i := range c.Rules { + if c.Rules[i].MatcherID() == id { + continue + } + tmp = append(tmp, c.Rules[i]) + } + + c.Rules = tmp + + return nil + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func configEditorRulesGet(w http.ResponseWriter, r *http.Request) { + if err := json.NewEncoder(w).Encode(config.Rules); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func configEditorRulesUpdate(w http.ResponseWriter, r *http.Request) { + user, _, err := getAuthorizationFromRequest(r) + if err != nil { + http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError) + } + + msg := &plugins.Rule{} + if err := json.NewDecoder(r.Body).Decode(msg); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := patchConfig(cfg.Config, user, "", "Update rule", func(c *configFile) error { + id := mux.Vars(r)["uuid"] + + for i := range c.Rules { + if c.Rules[i].MatcherID() == id { + c.Rules[i] = msg + } + } + + return nil + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +}