diff --git a/bridge/config/config.go b/bridge/config/config.go index 994b83488..f73fc00d1 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -30,24 +30,27 @@ const ( EventUserTyping = "user_typing" EventGetChannelMembers = "get_channel_members" EventNoticeIRC = "notice_irc" + EventReaction = "reaction" ) const ParentIDNotFound = "msg-parent-not-found" type Message struct { - Text string `json:"text"` - Channel string `json:"channel"` - Username string `json:"username"` - UserID string `json:"userid"` // userid on the bridge - Avatar string `json:"avatar"` - Account string `json:"account"` - Event string `json:"event"` - Protocol string `json:"protocol"` - Gateway string `json:"gateway"` - ParentID string `json:"parent_id"` - Timestamp time.Time `json:"timestamp"` - ID string `json:"id"` - Extra map[string][]interface{} + Text string `json:"text"` + Channel string `json:"channel"` + Username string `json:"username"` + UserID string `json:"userid"` // userid on the bridge + Avatar string `json:"avatar"` + Account string `json:"account"` + Event string `json:"event"` + Protocol string `json:"protocol"` + Gateway string `json:"gateway"` + ParentID string `json:"parent_id"` + ParentText string `json:"parent_text"` + Reactions []string `json:"reactions"` + Timestamp time.Time `json:"timestamp"` + ID string `json:"id"` + Extra map[string][]interface{} } func (m Message) ParentNotFound() bool { diff --git a/bridge/discord/discord.go b/bridge/discord/discord.go index 5b6b7e413..94c26ff78 100644 --- a/bridge/discord/discord.go +++ b/bridge/discord/discord.go @@ -226,6 +226,7 @@ func (b *Bdiscord) Connect() error { b.c.AddHandler(b.messageCreate) b.c.AddHandler(b.messageTyping) b.c.AddHandler(b.messageUpdate) + b.c.AddHandler(b.messageReaction) b.c.AddHandler(b.messageDelete) b.c.AddHandler(b.messageDeleteBulk) b.c.AddHandler(b.memberAdd) @@ -278,7 +279,7 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { // Use webhook to send the message useWebhooks := b.shouldMessageUseWebhooks(&msg) - if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" { + if useWebhooks && msg.Event != config.EventReaction && msg.Event != config.EventMsgDelete && msg.ParentID == "" { return b.handleEventWebhook(&msg, channelID) } @@ -289,6 +290,22 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) { b.Log.Debugf("Broadcasting using token (API)") + if msg.Event == config.EventReaction { + // this is a reaction, not a message + b.Log.Debugf("Sending reactions: %#v", msg.Reactions) + for _, reaction := range msg.Reactions { + // TODO: should we verify that reaction is, in fact, a single emoji? + // Also: Discord has server-private emojis named like "hello:1234567654321"; can these be bridged in some generic way? + b.Log.Debugf("MessageReactionAdd(%#v, %#v, %#v)", channelID, msg.ParentID, reaction) + err := b.c.MessageReactionAdd(channelID, msg.ParentID, reaction) + if err != nil { + return "", err + } + } + // reactions don't get an ID string + return "", nil + } + // Delete message if msg.Event == config.EventMsgDelete { if msg.ID == "" { diff --git a/bridge/discord/handlers.go b/bridge/discord/handlers.go index 16bb9977f..9f77255b6 100644 --- a/bridge/discord/handlers.go +++ b/bridge/discord/handlers.go @@ -8,6 +8,34 @@ import ( "github.com/matterbridge-org/matterbridge/bridge/config" ) +func (b *Bdiscord) messageReaction(s *discordgo.Session, m *discordgo.MessageReactionAdd) { //nolint:unparam + b.Log.Debugf("Got a Discord Reaction:i %#v", m) + b.Log.Debugf("emoji = %#v", m.Emoji) + + if m.GuildID != b.guildID { + b.Log.Debugf("Ignoring messageDelete because it originates from a different guild") + return + } + + var reaction string + if m.Emoji.ID != "" { + // Discord has server-private emojis named with an ID + // Bridged these with a generic, neutral emoji. + // XXX chosing the neutralest of emojis might be .. hard. + reaction = "⬛" + } else { + reaction = m.Emoji.Name + } + + // TODO: we need to bridge m.Emoji.User.Username or m.UserID + rmsg := config.Message{Account: b.Account, Event: config.EventReaction, ParentID: m.MessageID, Reactions: []string{reaction}} + rmsg.Channel = b.getChannelName(m.ChannelID) + + b.Log.Debugf("<= Sending reaction from %s to gateway", b.Account) + b.Log.Debugf("<= Message is %#v", rmsg) + b.Remote <- rmsg +} + func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam if m.GuildID != b.guildID { b.Log.Debugf("Ignoring messageDelete because it originates from a different guild") diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 6a291c5ad..14da37547 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -8,6 +8,7 @@ import ( "sync" "time" + lru "github.com/hashicorp/golang-lru/v2" "github.com/jpillora/backoff" "github.com/matterbridge-org/matterbridge/bridge" "github.com/matterbridge-org/matterbridge/bridge/config" @@ -29,10 +30,12 @@ type UploadBufferEntry struct { type Bxmpp struct { *bridge.Config - startTime time.Time - xc *xmpp.Client - xmppMap map[string]string - connected bool + startTime time.Time + xc *xmpp.Client + xmppMap map[string]string + connected bool + stanzaIDs *lru.Cache[string, string] // stanzaID -> ID + replyHeaders *lru.Cache[string, xmpp.Reply] // ID -> Reply{stanzaID, to} sync.RWMutex avatarAvailability map[string]bool @@ -58,12 +61,23 @@ type Bxmpp struct { } func New(cfg *bridge.Config) bridge.Bridger { + stanzaIDs, err := lru.New[string, string](5000) + if err != nil { + cfg.Log.Fatalf("Could not create LRU cache: %v", err) + } + replyHeaders, err := lru.New[string, xmpp.Reply](5000) + if err != nil { + cfg.Log.Fatalf("Could not create LRU cache: %v", err) + } + return &Bxmpp{ Config: cfg, xmppMap: make(map[string]string), avatarAvailability: make(map[string]bool), avatarMap: make(map[string]string), httpUploadBuffer: make(map[string]*UploadBufferEntry), + stanzaIDs: stanzaIDs, + replyHeaders: replyHeaders, } } @@ -142,19 +156,46 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { } } + // XEP-0461: populate reply fields if this message is a reply. + var reply *xmpp.Reply + var reactions *xmpp.Reactions + if msg.ParentValid() { + // either a Reaction or a Reply + // + if msg.Event == config.EventReaction { + b.Log.Debugf("relaying a Reaction; the reactions are %#v", msg.Reactions) + if _reply, ok := b.replyHeaders.Get(msg.ParentID); ok { + reactions = &xmpp.Reactions{ + ID: _reply.ID, + Reactions: msg.Reactions, + } + } + + // XXX TODO: XEP-0444 says an update requires a *full* update for all reactions from a given user. + // since the bridge is the source of ALL reactions for all users on the other side, + // it needs to track what has been sent and *resend all of them* + // also it's going to lose past reactions messages... + } else { + if _reply, ok := b.replyHeaders.Get(msg.ParentID); ok { + reply = &_reply + } + } + } + // Post normal message. b.Log.Debugf("=> Sending message %#v", msg) + // Generate a dummy ID because to avoid collision with other internal messages + msgID := xid.New().String() if _, err := b.xc.Send(xmpp.Chat{ - Type: "groupchat", - Remote: msg.Channel + "@" + b.GetString("Muc"), - Text: msg.Username + msg.Text, + Type: "groupchat", + Remote: msg.Channel + "@" + b.GetString("Muc"), + Text: msg.Username + msg.Text, + OriginID: msgID, + Reply: reply, + Reactions: reactions, }); err != nil { return "", err } - - // Generate a dummy ID because to avoid collision with other internal messages - // However this does not provide proper Edits/Replies integration on XMPP side. - msgID := xid.New().String() return msgID, nil } @@ -300,6 +341,13 @@ func (b *Bxmpp) handleXMPP() error { if v.Type == "groupchat" { b.Log.Debugf("== Receiving %#v", v) + if v.StanzaID.ID != "" { + // Here the stanza-id has been set by the server and can be used to provide replies + // as explained in XEP-0461 https://xmpp.org/extensions/xep-0461.html#business-id + b.stanzaIDs.Add(v.StanzaID.ID, v.ID) + b.replyHeaders.Add(v.ID, xmpp.Reply{ID: v.StanzaID.ID, To: v.Remote}) + } + // Skip invalid messages. if b.skipMessage(v) { continue @@ -320,18 +368,42 @@ func (b *Bxmpp) handleXMPP() error { avatar = getAvatar(b.avatarMap, v.Remote, b.General) } + // If there was a , map the StanzaID to the local matterbridge message ID + // so we can inform the other bridges of this message has a parent + var parentID string + var parentText string + if v.Reply != nil { + if _parentID, ok := b.stanzaIDs.Get(v.Reply.ID); ok { + parentID = _parentID + } + + // TODO: ignore XMPP quote and rely on a cross-bridge message cache (see https://github.com/matterbridge-org/matterbridge/issues/142) + parentText = v.Reply.Quote + } + + // If there was a // .... + var reactions []string + if v.Reactions != nil { + if _parentID, ok := b.stanzaIDs.Get(v.Reactions.ID); ok { + parentID = _parentID + b.Log.Debugf("Got reactions: %#v", v.Reactions) + reactions = append(reactions, v.Reactions.Reactions...) // XXX is the append() necessary? + event = config.EventReaction + } + } + rmsg := config.Message{ - Username: b.parseNick(v.Remote), - Text: v.Text, - Channel: b.parseChannel(v.Remote), - Account: b.Account, - Avatar: avatar, - UserID: v.Remote, - // Here the stanza-id has been set by the server and can be used to provide replies - // as explained in XEP-0461 https://xmpp.org/extensions/xep-0461.html#business-id - ID: v.StanzaID.ID, - Event: event, - Extra: make(map[string][]any), + Username: b.parseNick(v.Remote), + Text: v.Text, + Channel: b.parseChannel(v.Remote), + Account: b.Account, + Avatar: avatar, + UserID: v.Remote, + ID: v.ID, + Event: event, + ParentID: parentID, + ParentText: parentText, + Reactions: reactions, } // Check if we have an action event. @@ -458,7 +530,7 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool { } // skip empty messages - if message.Text == "" { + if message.Text == "" && message.Reactions == nil { return true } diff --git a/gateway/gateway.go b/gateway/gateway.go index 7ca84e4f4..4384a90f0 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -291,6 +291,9 @@ func (gw *Gateway) ignoreTextEmpty(msg *config.Message) bool { if msg.Text != "" { return false } + if msg.Event == config.EventReaction { + return false + } if msg.Event == config.EventUserTyping { return false } diff --git a/go.mod b/go.mod index 9d0fd08eb..757ce0fd0 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/google/gops v0.3.27 github.com/gorilla/schema v1.4.1 github.com/hashicorp/golang-lru v1.0.2 + github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/jpillora/backoff v1.0.0 github.com/kyokomi/emoji/v2 v2.2.13 github.com/labstack/echo/v4 v4.12.0 @@ -44,7 +45,7 @@ require ( go.mau.fi/whatsmeow v0.0.0-20260123132415-83db04703aee golang.org/x/image v0.19.0 golang.org/x/oauth2 v0.22.0 - golang.org/x/text v0.33.0 + golang.org/x/text v0.34.0 gomod.garykim.dev/nc-talk v0.3.0 google.golang.org/protobuf v1.36.11 layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14 @@ -55,6 +56,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989 // indirect + github.com/Figure1/go-intervals v0.0.0-20180124190743-0109751545d5 // indirect github.com/Jeffail/gabs v1.4.0 // indirect github.com/apex/log v1.9.0 // indirect github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf // indirect @@ -76,7 +78,6 @@ require ( github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.1 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect @@ -133,11 +134,11 @@ require ( go.mau.fi/libsignal v0.2.1 // indirect go.mau.fi/util v0.9.5 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.47.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect google.golang.org/grpc v1.65.0 // indirect @@ -156,4 +157,6 @@ require ( //replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419 -go 1.24.0 +go 1.25.0 + +replace github.com/xmppo/go-xmpp => github.com/sh4sh/go-xmpp v0.0.0-20260308194659-485ad27aa55a diff --git a/go.sum b/go.sum index ce4611f49..85089d57f 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f/go.mod h1:AQiQK github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/Figure1/go-intervals v0.0.0-20180124190743-0109751545d5 h1:XeHDLbi4ku2IyYoiDlE2h627nnUsaJXGydDCR33o7Ek= +github.com/Figure1/go-intervals v0.0.0-20180124190743-0109751545d5/go.mod h1:3yFSM9nNXS5nSaIwjpF+cPfSK3sqXn3HW+KyB40nLQM= github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= github.com/SevereCloud/vksdk/v2 v2.17.0 h1:Wll63JSuBTdE0L7+V/PMn9PyhLrWSWIjX76XpWbXTFw= @@ -311,6 +313,8 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sh4sh/go-xmpp v0.0.0-20260308194659-485ad27aa55a h1:/Id+09ZFYZGdBqpVqMUTrRznLUpD66dOunIWeRiqMTQ= +github.com/sh4sh/go-xmpp v0.0.0-20260308194659-485ad27aa55a/go.mod h1:U1T7Fv9GY+8pJpOwEpNII37JLngt31TiRHzHV3m66tc= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 h1:zwQ1HBo5FYwn1ksMd19qBCKO8JAWE9wmHivEpkw/DvE= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI= github.com/shazow/ssh-chat v1.10.1 h1:ePS+ngEYqm+yUuXegDPutysqLV2WoI22XDOeRgI6CE0= @@ -419,8 +423,6 @@ github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6Fk github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= -github.com/xmppo/go-xmpp v0.3.2 h1:RAQf7yD2XNH1UAy4OPlofnI2qLVx1iQzAR7ghptvaA8= -github.com/xmppo/go-xmpp v0.3.2/go.mod h1:EBLbzPt4Y9OJBEF58Lhc4IrTnO226aIkbfosQo6KWeA= github.com/yaegashi/msgraph.go v0.1.4 h1:leDXSczAbwBpYFSmmZrdByTiPoUw8dbTfNMetAjJvbw= github.com/yaegashi/msgraph.go v0.1.4/go.mod h1:vgeYhHa5skJt/3lTyjGXThTZhwbhRnGo6uUxzoJIGME= github.com/yaegashi/wtz.go v0.0.2/go.mod h1:nOLA5QXsmdkRxBkP5tljhua13ADHCKirLBrzPf4PEJc= @@ -447,8 +449,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= @@ -476,8 +478,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -517,20 +519,20 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=