mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
commands: add reaction button system
Some checks failed
Some checks failed
This commit is contained in:
parent
da9e72e616
commit
89fad2f462
3 changed files with 156 additions and 2 deletions
|
|
@ -40,6 +40,8 @@ type Event[MetaType any] struct {
|
|||
Proc *Processor[MetaType]
|
||||
Handler *Handler[MetaType]
|
||||
Meta MetaType
|
||||
|
||||
redactedBy id.EventID
|
||||
}
|
||||
|
||||
var IDHTMLParser = &format.HTMLParser{
|
||||
|
|
@ -71,7 +73,14 @@ func ParseEvent[MetaType any](ctx context.Context, evt *event.Event) *Event[Meta
|
|||
if len(text) == 0 {
|
||||
return nil
|
||||
}
|
||||
return RawTextToEvent[MetaType](ctx, evt, text)
|
||||
}
|
||||
|
||||
func RawTextToEvent[MetaType any](ctx context.Context, evt *event.Event, text string) *Event[MetaType] {
|
||||
parts := strings.Fields(text)
|
||||
if len(parts) == 0 {
|
||||
parts = []string{""}
|
||||
}
|
||||
return &Event[MetaType]{
|
||||
Event: evt,
|
||||
RawInput: text,
|
||||
|
|
@ -91,6 +100,7 @@ type ReplyOpts struct {
|
|||
SendAsText bool
|
||||
Edit id.EventID
|
||||
OverrideMentions *event.Mentions
|
||||
Extra map[string]any
|
||||
}
|
||||
|
||||
func (evt *Event[MetaType]) Reply(msg string, args ...any) id.EventID {
|
||||
|
|
@ -117,7 +127,14 @@ func (evt *Event[MetaType]) Respond(msg string, opts ReplyOpts) id.EventID {
|
|||
if opts.OverrideMentions != nil {
|
||||
content.Mentions = opts.OverrideMentions
|
||||
}
|
||||
resp, err := evt.Proc.Client.SendMessageEvent(evt.Ctx, evt.RoomID, event.EventMessage, content)
|
||||
var wrapped any = &content
|
||||
if opts.Extra != nil {
|
||||
wrapped = &event.Content{
|
||||
Parsed: &content,
|
||||
Raw: opts.Extra,
|
||||
}
|
||||
}
|
||||
resp, err := evt.Proc.Client.SendMessageEvent(evt.Ctx, evt.RoomID, event.EventMessage, wrapped)
|
||||
if err != nil {
|
||||
zerolog.Ctx(evt.Ctx).Err(err).Msg("Failed to send reply")
|
||||
return ""
|
||||
|
|
@ -135,11 +152,15 @@ func (evt *Event[MetaType]) React(emoji string) id.EventID {
|
|||
}
|
||||
|
||||
func (evt *Event[MetaType]) Redact() id.EventID {
|
||||
if evt.redactedBy != "" {
|
||||
return evt.redactedBy
|
||||
}
|
||||
resp, err := evt.Proc.Client.RedactEvent(evt.Ctx, evt.RoomID, evt.ID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(evt.Ctx).Err(err).Msg("Failed to redact command")
|
||||
return ""
|
||||
}
|
||||
evt.redactedBy = resp.EventID
|
||||
return resp.EventID
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ type Processor[MetaType any] struct {
|
|||
LogArgs bool
|
||||
PreValidator PreValidator[MetaType]
|
||||
Meta MetaType
|
||||
|
||||
ReactionCommandPrefix string
|
||||
}
|
||||
|
||||
// UnknownCommandName is the name of the fallback handler which is used if no other handler is found.
|
||||
|
|
@ -65,7 +67,13 @@ func (proc *Processor[MetaType]) Process(ctx context.Context, evt *event.Event)
|
|||
}
|
||||
}
|
||||
}()
|
||||
parsed := ParseEvent[MetaType](ctx, evt)
|
||||
var parsed *Event[MetaType]
|
||||
switch evt.Type {
|
||||
case event.EventReaction:
|
||||
parsed = proc.ParseReaction(ctx, evt)
|
||||
case event.EventMessage:
|
||||
parsed = ParseEvent[MetaType](ctx, evt)
|
||||
}
|
||||
if parsed == nil || !proc.PreValidator.Validate(parsed) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
125
commands/reactions.go
Normal file
125
commands/reactions.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright (c) 2025 Tulir Asokan
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
const ReactionCommandsKey = "fi.mau.reaction_commands"
|
||||
const ReactionMultiUseKey = "fi.mau.reaction_multi_use"
|
||||
|
||||
func (proc *Processor[MetaType]) ParseReaction(ctx context.Context, evt *event.Event) *Event[MetaType] {
|
||||
content, ok := evt.Content.Parsed.(*event.ReactionEventContent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
evtID := content.RelatesTo.EventID
|
||||
if evtID == "" || !strings.HasPrefix(content.RelatesTo.Key, proc.ReactionCommandPrefix) {
|
||||
return nil
|
||||
}
|
||||
targetEvt, err := proc.Client.GetEvent(ctx, evt.RoomID, evtID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("target_event_id", evtID).Msg("Failed to get target event for reaction")
|
||||
return nil
|
||||
} else if targetEvt.Sender != proc.Client.UserID || targetEvt.Unsigned.RedactedBecause != nil {
|
||||
return nil
|
||||
}
|
||||
if targetEvt.Type == event.EventEncrypted {
|
||||
if proc.Client.Crypto == nil {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Stringer("target_event_id", evtID).
|
||||
Msg("Received reaction to encrypted event, but don't have crypto helper in client")
|
||||
return nil
|
||||
}
|
||||
_ = targetEvt.Content.ParseRaw(targetEvt.Type)
|
||||
targetEvt, err = proc.Client.Crypto.Decrypt(ctx, targetEvt)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Stringer("target_event_id", evtID).
|
||||
Msg("Failed to decrypt target event for reaction")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
reactionCommands, ok := targetEvt.Content.Raw[ReactionCommandsKey].(map[string]any)
|
||||
if !ok {
|
||||
zerolog.Ctx(ctx).Trace().
|
||||
Stringer("target_event_id", evtID).
|
||||
Msg("Reaction target event doesn't have commands key")
|
||||
return nil
|
||||
}
|
||||
isMultiUse, _ := targetEvt.Content.Raw[ReactionMultiUseKey].(bool)
|
||||
rawCmd, ok := reactionCommands[content.RelatesTo.Key]
|
||||
if !ok {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("target_event_id", evtID).
|
||||
Str("reaction_key", content.RelatesTo.Key).
|
||||
Msg("Reaction command not found in target event")
|
||||
return nil
|
||||
}
|
||||
cmdString, ok := rawCmd.(string)
|
||||
if !ok {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("target_event_id", evtID).
|
||||
Str("reaction_key", content.RelatesTo.Key).
|
||||
Msg("Reaction command data is invalid")
|
||||
return nil
|
||||
}
|
||||
wrappedEvt := RawTextToEvent[MetaType](ctx, evt, cmdString)
|
||||
wrappedEvt.Proc = proc
|
||||
wrappedEvt.Redact()
|
||||
if !isMultiUse {
|
||||
DeleteAllReactions(ctx, proc.Client, evt)
|
||||
}
|
||||
if cmdString == "" {
|
||||
return nil
|
||||
}
|
||||
return wrappedEvt
|
||||
}
|
||||
|
||||
func DeleteAllReactionsCommandFunc[MetaType any](ce *Event[MetaType]) {
|
||||
DeleteAllReactions(ce.Ctx, ce.Proc.Client, ce.Event)
|
||||
}
|
||||
|
||||
func DeleteAllReactions(ctx context.Context, client *mautrix.Client, evt *event.Event) {
|
||||
rel, ok := evt.Content.Parsed.(event.Relatable)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
relation := rel.OptionalGetRelatesTo()
|
||||
if relation == nil {
|
||||
return
|
||||
}
|
||||
targetEvt := relation.GetReplyTo()
|
||||
if targetEvt == "" {
|
||||
targetEvt = relation.GetAnnotationID()
|
||||
}
|
||||
if targetEvt == "" {
|
||||
return
|
||||
}
|
||||
relations, err := client.GetRelations(ctx, evt.RoomID, targetEvt, &mautrix.ReqGetRelations{
|
||||
RelationType: event.RelAnnotation,
|
||||
EventType: event.EventReaction,
|
||||
Limit: 20,
|
||||
})
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get reactions to delete")
|
||||
return
|
||||
}
|
||||
for _, relEvt := range relations.Chunk {
|
||||
_, err = client.RedactEvent(ctx, relEvt.RoomID, relEvt.ID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("event_id", relEvt.ID).Msg("Failed to redact reaction event")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue