// Package respond contains an actor to send a message package respond import ( "encoding/json" "fmt" "net/http" "strings" "github.com/gorilla/mux" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "gopkg.in/irc.v4" "github.com/Luzifer/go_helpers/v2/fieldcollection" "github.com/Luzifer/twitch-bot/v3/internal/helpers" "github.com/Luzifer/twitch-bot/v3/plugins" ) const actorName = "respond" var ( formatMessage plugins.MsgFormatter send plugins.SendMessageFunc ) // Register provides the plugins.RegisterFunc func Register(args plugins.RegistrationArguments) (err error) { formatMessage = args.FormatMessage send = args.SendMessage args.RegisterActor(actorName, func() plugins.Actor { return &actor{} }) args.RegisterActorDocumentation(plugins.ActionDocumentation{ Description: "Respond to message with a new message", Name: "Respond to Message", Type: "respond", Fields: []plugins.ActionDocumentationField{ { Default: "", Description: "Message text to send", Key: "message", Long: true, Name: "Message", Optional: false, SupportTemplate: true, Type: plugins.ActionDocumentationFieldTypeString, }, { Default: "", Description: "Fallback message text to send if message cannot be generated", Key: "fallback", Name: "Fallback", Optional: true, SupportTemplate: true, Type: plugins.ActionDocumentationFieldTypeString, }, { Default: "false", Description: "Send message as a native Twitch-reply to the original message", Key: "as_reply", Name: "As Reply", Optional: true, SupportTemplate: false, Type: plugins.ActionDocumentationFieldTypeBool, }, { Default: "", Description: "Send message to a different channel than the original message", Key: "to_channel", Name: "To Channel", Optional: true, SupportTemplate: false, Type: plugins.ActionDocumentationFieldTypeString, }, }, }) if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{ Description: "Send a message on behalf of the bot (send JSON object with `message` key)", HandlerFunc: handleAPISend, Method: http.MethodPost, Module: actorName, Name: "Send message", Path: "/{channel}", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeTextPlain, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "Channel to send the message to", Name: "channel", }, }, }); err != nil { return fmt.Errorf("registering API route: %w", err) } return nil } type actor struct{} func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) { msg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData) if err != nil { if !attrs.CanString("fallback") || attrs.MustString("fallback", nil) == "" { return false, errors.Wrap(err, "preparing response") } log.WithError(err).Error("Response message processing caused error, trying fallback") if msg, err = formatMessage(attrs.MustString("fallback", nil), m, r, eventData); err != nil { return false, errors.Wrap(err, "preparing response fallback") } } toChannel := plugins.DeriveChannel(m, eventData) if attrs.CanString("to_channel") && attrs.MustString("to_channel", nil) != "" { toChannel = fmt.Sprintf("#%s", strings.TrimLeft(attrs.MustString("to_channel", nil), "#")) } ircMessage := &irc.Message{ Command: "PRIVMSG", Params: []string{ toChannel, msg, }, } if attrs.MustBool("as_reply", helpers.Ptr(false)) { id, ok := m.Tags["id"] if ok { if ircMessage.Tags == nil { ircMessage.Tags = make(irc.Tags) } ircMessage.Tags["reply-parent-msg-id"] = id } } return false, errors.Wrap( send(ircMessage), "sending response", ) } func (actor) IsAsync() bool { return false } func (actor) Name() string { return actorName } func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) { if err = attrs.ValidateSchema( fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "message", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}), fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "fallback", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}), fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "as_reply", Type: fieldcollection.SchemaFieldTypeBool}), fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "to_channel", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}), fieldcollection.MustHaveNoUnknowFields, helpers.SchemaValidateTemplateField(tplValidator, "message", "fallback"), ); err != nil { return fmt.Errorf("validating attributes: %w", err) } return nil } func handleAPISend(w http.ResponseWriter, r *http.Request) { var payload struct { Message string `json:"message"` } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, errors.Wrap(err, "parsing payload").Error(), http.StatusBadRequest) return } if strings.TrimSpace(payload.Message) == "" { http.Error(w, errors.New("no message found").Error(), http.StatusBadRequest) return } if err := send(&irc.Message{ Command: "PRIVMSG", Params: []string{ "#" + strings.TrimLeft(mux.Vars(r)["channel"], "#"), strings.TrimSpace(payload.Message), }, }); err != nil { http.Error(w, errors.Wrap(err, "sending message").Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) }