commands: add reaction button system
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled

This commit is contained in:
Tulir Asokan 2025-05-24 16:29:40 +03:00
commit 89fad2f462
3 changed files with 156 additions and 2 deletions

View file

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

View file

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