diff --git a/bot/command.go b/bot/command.go index c100415..c9379fe 100644 --- a/bot/command.go +++ b/bot/command.go @@ -608,8 +608,8 @@ func (b *Bot) runSendCommand(ctx context.Context, cfg config.Room, tos []string, } } - b.mu.Lock(evt.RoomID.String()) - defer b.mu.Unlock(evt.RoomID.String()) + b.lock(evt.RoomID, evt.ID) + defer b.unlock(evt.RoomID, evt.ID) domain := utils.SanitizeDomain(cfg.Domain()) from := cfg.Mailbox() + "@" + domain diff --git a/bot/email.go b/bot/email.go index 80066be..e95440d 100644 --- a/bot/email.go +++ b/bot/email.go @@ -56,15 +56,19 @@ func (b *Bot) shouldQueue(msg string) bool { // Sendmail tries to send email immediately, but if it gets 4xx error (greylisting), // the email will be added to the queue and retried several times after that func (b *Bot) Sendmail(eventID id.EventID, from, to, data string) (bool, error) { + log := b.log.With().Str("from", from).Str("to", to).Str("eventID", eventID.String()).Logger() + log.Info().Msg("attempting to deliver email") err := b.sendmail(from, to, data) if err != nil { if b.shouldQueue(err.Error()) { - b.log.Info().Err(err).Str("id", eventID.String()).Str("from", from).Str("to", to).Msg("email has been added to the queue") + log.Info().Err(err).Msg("email has been added to the queue") return true, b.q.Add(eventID.String(), from, to, data) } + log.Warn().Err(err).Msg("email delivery failed") return false, err } + log.Warn().Err(err).Msg("email delivery succeeded") return false, nil } @@ -286,8 +290,8 @@ func (b *Bot) SendEmailReply(ctx context.Context) { return } - b.mu.Lock(evt.RoomID.String()) - defer b.mu.Unlock(evt.RoomID.String()) + b.lock(evt.RoomID, evt.ID) + defer b.unlock(evt.RoomID, evt.ID) meta := b.getParentEmail(evt, mailbox) diff --git a/bot/mutex.go b/bot/mutex.go new file mode 100644 index 0000000..c2daa3e --- /dev/null +++ b/bot/mutex.go @@ -0,0 +1,29 @@ +package bot + +import ( + "maunium.net/go/mautrix/id" +) + +func (b *Bot) lock(roomID id.RoomID, optionalEventID ...id.EventID) { + b.mu.Lock(roomID.String()) + + if len(optionalEventID) == 0 { + return + } + evtID := optionalEventID[0] + if _, err := b.lp.GetClient().SendReaction(roomID, evtID, "📨"); err != nil { + b.log.Error().Err(err).Str("roomID", roomID.String()).Str("eventID", evtID.String()).Msg("cannot send reaction on lock") + } +} + +func (b *Bot) unlock(roomID id.RoomID, optionalEventID ...id.EventID) { + b.mu.Unlock(roomID.String()) + + if len(optionalEventID) == 0 { + return + } + evtID := optionalEventID[0] + if _, err := b.lp.GetClient().SendReaction(roomID, evtID, "✅"); err != nil { + b.log.Error().Err(err).Str("roomID", roomID.String()).Str("eventID", evtID.String()).Msg("cannot send reaction on unlock") + } +} diff --git a/go.mod b/go.mod index 677fa12..71bb267 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( gitlab.com/etke.cc/go/secgen v1.1.1 gitlab.com/etke.cc/go/trysmtp v1.1.3 gitlab.com/etke.cc/go/validator v1.0.6 - gitlab.com/etke.cc/linkpearl v0.0.0-20230929133006-84554ee97edb + gitlab.com/etke.cc/linkpearl v0.0.0-20231007103859-01907e2b75f2 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 maunium.net/go/mautrix v0.16.1 ) diff --git a/go.sum b/go.sum index 8acb538..c5ac9b6 100644 --- a/go.sum +++ b/go.sum @@ -111,8 +111,8 @@ gitlab.com/etke.cc/go/trysmtp v1.1.3 h1:e2EHond77onMaecqCg6mWumffTSEf+ycgj88nbee gitlab.com/etke.cc/go/trysmtp v1.1.3/go.mod h1:lOO7tTdAE0a3ETV3wN3GJ7I1Tqewu7YTpPWaOmTteV0= gitlab.com/etke.cc/go/validator v1.0.6 h1:w0Muxf9Pqw7xvF7NaaswE6d7r9U3nB2t2l5PnFMrecQ= gitlab.com/etke.cc/go/validator v1.0.6/go.mod h1:Id0SxRj0J3IPhiKlj0w1plxVLZfHlkwipn7HfRZsDts= -gitlab.com/etke.cc/linkpearl v0.0.0-20230929133006-84554ee97edb h1:QoabJtxrQg7P2kuxwOp3iU6uKoep+5QQAtMomajXcpA= -gitlab.com/etke.cc/linkpearl v0.0.0-20230929133006-84554ee97edb/go.mod h1:IZ0TE+ZnIdJLb538owDMxhtpWH7blfW+oR7e5XRXxNY= +gitlab.com/etke.cc/linkpearl v0.0.0-20231007103859-01907e2b75f2 h1:QPbGnfbrGdDT8NtxZuwzRQou5I62b/mvesmf5iXClPc= +gitlab.com/etke.cc/linkpearl v0.0.0-20231007103859-01907e2b75f2/go.mod h1:IZ0TE+ZnIdJLb538owDMxhtpWH7blfW+oR7e5XRXxNY= go.mau.fi/util v0.1.0 h1:BwIFWIOEeO7lsiI2eWKFkWTfc5yQmoe+0FYyOFVyaoE= go.mau.fi/util v0.1.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84= golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/smtp/client.go b/smtp/client.go index 514d365..1128c00 100644 --- a/smtp/client.go +++ b/smtp/client.go @@ -29,40 +29,43 @@ func newClient(cfg *RelayConfig, log *zerolog.Logger) *Client { // Send email func (c Client) Send(from, to, data string) error { - c.log.Debug().Str("from", from).Str("to", to).Msg("sending email") + log := c.log.With().Str("from", from).Str("to", to).Logger() + log.Debug().Msg("sending email") var conn *smtp.Client var err error if c.config.Host != "" { + log.Debug().Msg("creating relay client...") conn, err = c.createDirectClient(from, to) } else { + log.Debug().Msg("trying direct SMTP connection...") conn, err = trysmtp.Connect(from, to) } if conn == nil { - c.log.Error().Err(err).Str("server_of", to).Msg("cannot connect to SMTP server") + log.Error().Err(err).Str("server_of", to).Msg("cannot connect to SMTP server") return err } if err != nil { - c.log.Warn().Err(err).Str("server_of", to).Msg("connection to the SMTP server returned non-fatal error(-s)") + log.Warn().Err(err).Str("server_of", to).Msg("connection to the SMTP server returned non-fatal error(-s)") } defer conn.Close() var w io.WriteCloser w, err = conn.Data() if err != nil { - c.log.Error().Err(err).Msg("cannot send DATA command") + log.Error().Err(err).Msg("cannot send DATA command") return err } defer w.Close() c.log.Debug().Str("DATA", data).Msg("sending command") _, err = strings.NewReader(data).WriteTo(w) if err != nil { - c.log.Error().Err(err).Msg("cannot write DATA") + log.Error().Err(err).Msg("cannot write DATA") return err } - c.log.Debug().Msg("email has been sent") + log.Debug().Msg("email has been sent") return nil } diff --git a/vendor/gitlab.com/etke.cc/linkpearl/config.go b/vendor/gitlab.com/etke.cc/linkpearl/config.go index 0fe5d17..f132c84 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/config.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/config.go @@ -39,6 +39,9 @@ type Config struct { // MaxRetries for operations like auto join MaxRetries int + // EventsLimit for methods like lp.Threads() or lp.FindEventBy() + EventsLimit int + // Logger Logger zerolog.Logger diff --git a/vendor/gitlab.com/etke.cc/linkpearl/events.go b/vendor/gitlab.com/etke.cc/linkpearl/events.go new file mode 100644 index 0000000..6ce407d --- /dev/null +++ b/vendor/gitlab.com/etke.cc/linkpearl/events.go @@ -0,0 +1,73 @@ +package linkpearl + +import ( + "strconv" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +// RespThreads is response of https://spec.matrix.org/v1.8/client-server-api/#get_matrixclientv1roomsroomidthreads +type RespThreads struct { + Chunk []*event.Event `json:"chunk"` + NextBatch string `json:"next_batch"` +} + +// Threads endpoint, ref: https://spec.matrix.org/v1.8/client-server-api/#get_matrixclientv1roomsroomidthreads +func (l *Linkpearl) Threads(roomID id.RoomID, fromToken ...string) (*RespThreads, error) { + var from string + if len(fromToken) > 0 { + from = fromToken[0] + } + + query := map[string]string{ + "from": from, + "limit": strconv.Itoa(l.eventsLimit), + } + + var resp *RespThreads + urlPath := l.GetClient().BuildURLWithQuery(mautrix.ClientURLPath{"v1", "rooms", roomID, "threads"}, query) + _, err := l.GetClient().MakeRequest("GET", urlPath, nil, &resp) + return resp, UnwrapError(err) +} + +// FindEventBy tries to find event by field and value +func (l *Linkpearl) FindEventBy(roomID id.RoomID, field, value string, fromToken ...string) *event.Event { + var from string + if len(fromToken) > 0 { + from = fromToken[0] + } + + resp, err := l.GetClient().Messages(roomID, from, "", mautrix.DirectionBackward, nil, l.eventsLimit) + err = UnwrapError(err) + if err != nil { + l.log.Warn().Err(err).Str("roomID", roomID.String()).Msg("cannot get room events") + return nil + } + + for _, msg := range resp.Chunk { + evt, contains := l.eventContains(msg, field, value) + if contains { + return evt + } + } + + if resp.End == "" { // nothing more + return nil + } + + return l.FindEventBy(roomID, field, value, resp.End) +} + +func (l *Linkpearl) eventContains(evt *event.Event, field, value string) (*event.Event, bool) { + if evt.Type == event.EventEncrypted { + ParseContent(evt, &l.log) + decrypted, err := l.GetClient().Crypto.Decrypt(evt) + if err == nil { + evt = decrypted + } + } + + return evt, EventContains(evt, field, value) +} diff --git a/vendor/gitlab.com/etke.cc/linkpearl/linkpearl.go b/vendor/gitlab.com/etke.cc/linkpearl/linkpearl.go index fff4be2..2830a6f 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/linkpearl.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/linkpearl.go @@ -18,6 +18,8 @@ const ( DefaultMaxRetries = 10 // DefaultAccountDataCache size DefaultAccountDataCache = 1000 + // DefaultEventsLimit for methods like lp.Threads() and lp.FindEventBy() + DefaultEventsLimit = 1000 ) // Linkpearl object @@ -29,9 +31,10 @@ type Linkpearl struct { log zerolog.Logger api *mautrix.Client - joinPermit func(*event.Event) bool - autoleave bool - maxretries int + joinPermit func(*event.Event) bool + autoleave bool + maxretries int + eventsLimit int } type ReqPresence struct { @@ -46,6 +49,9 @@ func setDefaults(cfg *Config) { if cfg.AccountDataCache == 0 { cfg.AccountDataCache = DefaultAccountDataCache } + if cfg.EventsLimit == 0 { + cfg.EventsLimit = DefaultEventsLimit + } if cfg.JoinPermit == nil { // By default, we approve all join requests cfg.JoinPermit = func(*event.Event) bool { return true } @@ -76,14 +82,15 @@ func New(cfg *Config) (*Linkpearl, error) { } lp := &Linkpearl{ - db: cfg.DB, - acc: acc, - acr: acr, - api: api, - log: cfg.Logger, - joinPermit: cfg.JoinPermit, - autoleave: cfg.AutoLeave, - maxretries: cfg.MaxRetries, + db: cfg.DB, + acc: acc, + acr: acr, + api: api, + log: cfg.Logger, + joinPermit: cfg.JoinPermit, + autoleave: cfg.AutoLeave, + maxretries: cfg.MaxRetries, + eventsLimit: cfg.EventsLimit, } db, err := dbutil.NewWithDB(cfg.DB, cfg.Dialect) diff --git a/vendor/gitlab.com/etke.cc/linkpearl/send.go b/vendor/gitlab.com/etke.cc/linkpearl/send.go index 68713de..dbb04d7 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/send.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/send.go @@ -43,24 +43,37 @@ func (l *Linkpearl) SendNotice(roomID id.RoomID, message string, relates ...*eve } // SendFile to a matrix room -func (l *Linkpearl) SendFile(roomID id.RoomID, req *mautrix.ReqUploadMedia, msgtype event.MessageType, relation *event.RelatesTo) error { +func (l *Linkpearl) SendFile(roomID id.RoomID, req *mautrix.ReqUploadMedia, msgtype event.MessageType, relates ...*event.RelatesTo) error { + var relation *event.RelatesTo + if len(relates) > 0 { + relation = relates[0] + } + resp, err := l.GetClient().UploadMedia(*req) if err != nil { err = UnwrapError(err) l.log.Error().Err(err).Str("file", req.FileName).Msg("cannot upload file") return err } - _, err = l.Send(roomID, &event.Content{ - Parsed: &event.MessageEventContent{ - MsgType: msgtype, - Body: req.FileName, - URL: resp.ContentURI.CUString(), - RelatesTo: relation, - }, - }) + content := &event.MessageEventContent{ + MsgType: msgtype, + Body: req.FileName, + URL: resp.ContentURI.CUString(), + RelatesTo: relation, + } + + _, err = l.Send(roomID, content) err = UnwrapError(err) if err != nil { - l.log.Error().Err(err).Str("file", req.FileName).Msg("cannot send uploaded file") + l.log.Error().Err(err).Str("roomID", roomID.String()).Str("retries", "1/2").Msg("cannot send file into the room") + if relation != nil { + content.RelatesTo = nil + _, err = l.Send(roomID, &content) + err = UnwrapError(err) + if err != nil { + l.log.Error().Err(UnwrapError(err)).Str("roomID", roomID.String()).Str("retries", "2/2").Msg("cannot send file into the room even without relations") + } + } } return err diff --git a/vendor/gitlab.com/etke.cc/linkpearl/sync.go b/vendor/gitlab.com/etke.cc/linkpearl/sync.go index da43491..3f9fd58 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/sync.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/sync.go @@ -79,7 +79,7 @@ func (l *Linkpearl) tryJoin(roomID id.RoomID, retry int) { err = UnwrapError(err) if err != nil { l.log.Error().Err(err).Str("roomID", roomID.String()).Msg("cannot join room") - if strings.HasPrefix(err.Error(), "403") { // no permission to join, no need to retry + if strings.HasPrefix(err.Error(), "403") || strings.HasPrefix(err.Error(), "M_FORBIDDEN") { // no permission to join, no need to retry return } time.Sleep(5 * time.Second) diff --git a/vendor/gitlab.com/etke.cc/linkpearl/utils.go b/vendor/gitlab.com/etke.cc/linkpearl/utils.go index ba28ce7..4387035 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/utils.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/utils.go @@ -84,11 +84,7 @@ func EventContains[T comparable](evt *event.Event, field string, value T) bool { if evt.Content.Raw == nil { return false } - if EventField[T](&evt.Content, field) != value { - return false - } - - return true + return EventField[T](&evt.Content, field) == value } // EventField returns field value from raw event content diff --git a/vendor/modules.txt b/vendor/modules.txt index ec286fa..e58a3c4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -144,7 +144,7 @@ gitlab.com/etke.cc/go/trysmtp # gitlab.com/etke.cc/go/validator v1.0.6 ## explicit; go 1.18 gitlab.com/etke.cc/go/validator -# gitlab.com/etke.cc/linkpearl v0.0.0-20230929133006-84554ee97edb +# gitlab.com/etke.cc/linkpearl v0.0.0-20231007103859-01907e2b75f2 ## explicit; go 1.18 gitlab.com/etke.cc/linkpearl # go.mau.fi/util v0.1.0