diff --git a/go.mod b/go.mod index fec5b81..2fc7fe4 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ require ( github.com/mattn/go-sqlite3 v1.14.52 github.com/rs/zerolog v1.35.1 github.com/u2takey/ffmpeg-go v0.5.0 - go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 - golang.org/x/crypto v0.57.0 + go.mau.fi/util v0.11.0 + golang.org/x/crypto v0.58.0 golang.org/x/image v0.23.0 - golang.org/x/net v0.59.0 - maunium.net/go/mautrix v0.31.1-0.20261012132636-34107524d2ab + golang.org/x/net v0.61.0 + maunium.net/go/mautrix v0.32.0 ) require ( @@ -27,20 +27,20 @@ require ( github.com/lib/pq v1.12.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect + github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b // indirect github.com/rs/xid v1.6.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect - github.com/tidwall/gjson v1.19.0 // indirect + github.com/tidwall/gjson v1.20.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/u2takey/go-utils v0.3.1 // indirect github.com/yuin/goldmark v1.8.6 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect - golang.org/x/sync v0.23.0 // indirect - golang.org/x/sys v0.48.0 // indirect - golang.org/x/text v0.42.0 // indirect + golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.43.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect maunium.net/go/mauflag v1.0.0 // indirect diff --git a/go.sum b/go.sum index 1f1b1da..15928a1 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= -github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E= -github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E= +github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -63,8 +63,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= -github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= +github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -78,38 +78,38 @@ github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4y github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs= github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcUkqKshE+n239UtbbJOn4= -go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= +go.mau.fi/util v0.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY= +go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= -golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= +golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A= +golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= -golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.3.0/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.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -126,6 +126,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.31.1-0.20261012132636-34107524d2ab h1:rCv0s8QwcLeb9Vl68UGIr28jkJkmXE6XLp1Mje0iOYU= -maunium.net/go/mautrix v0.31.1-0.20261012132636-34107524d2ab/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= +maunium.net/go/mautrix v0.32.0 h1:xuAZzzyWce0pFkBhV/MCLFiJdVs2rFkY3Eep7hNng4I= +maunium.net/go/mautrix v0.32.0/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index cdee4e9..0de4c35 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -3,15 +3,18 @@ package connector import ( "bytes" "context" + "errors" "fmt" "io" "net/http" + "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/mediaproxy" "github.com/beeper/line/pkg/connector/handlers" + "github.com/beeper/line/pkg/line" ) const ( @@ -91,7 +94,7 @@ func (lc *LineConnector) Download(ctx context.Context, mediaID networkid.MediaID } data, err := client.newMessageHandler().DownloadDirectMedia(ctx, string(parsed.MessageID), metadata.DirectMedia) if err != nil { - return nil, err + return nil, wrapDirectMediaError(err) } if len(data) > handlers.BeeperMaxFileSize { return nil, fmt.Errorf("%w: LINE %s is %.2f MiB", bridgev2.ErrMediaTooLarge, metadata.DirectMedia.Kind, float64(len(data))/(1024*1024)) @@ -104,3 +107,24 @@ func (lc *LineConnector) Download(ctx context.Context, mediaID networkid.MediaID Reader: io.NopCloser(bytes.NewReader(data)), ContentType: contentType, ContentLength: int64(len(data)), }, nil } + +// wrapDirectMediaError gives the media proxy a status for a failed LINE media +// download, keeping the original error in the chain. +func wrapDirectMediaError(err error) error { + var mapped mautrix.RespError + switch statusCode := line.HTTPStatusCode(err); { + case errors.Is(err, line.ErrOBSObjectNotFound), statusCode == http.StatusNotFound, statusCode == http.StatusGone: + mapped = mautrix.MNotFound.WithMessage("This media is no longer available on LINE") + case errors.Is(err, line.ErrOBSEncodingIncomplete): + mapped = mautrix.MUnknown.WithMessage("LINE is still processing this media").WithStatus(http.StatusGatewayTimeout).WithCanRetry(true) + case statusCode == http.StatusUnauthorized, statusCode == http.StatusForbidden: + mapped = mautrix.MForbidden.WithMessage("LINE refused access to this media") + case statusCode == http.StatusTooManyRequests: + mapped = mautrix.MLimitExceeded.WithMessage("LINE is rate limiting media downloads").WithCanRetry(true) + case statusCode != 0, errors.Is(err, line.ErrOBSObjectValidation): + mapped = mautrix.MUnknown.WithMessage("LINE couldn't return this media").WithStatus(http.StatusBadGateway).WithCanRetry(true) + default: + return err + } + return fmt.Errorf("%w: %w", mapped, err) +} diff --git a/pkg/connector/senderrors.go b/pkg/connector/senderrors.go index 11b099e..8bd3fd8 100644 --- a/pkg/connector/senderrors.go +++ b/pkg/connector/senderrors.go @@ -51,13 +51,13 @@ func wrapSendError(err error) error { WithIsCertain(false). WithSendNotice(true). WithMessage("timed out waiting for LINE") - case statusCode == http.StatusTooManyRequests: + case statusCode == http.StatusTooManyRequests, line.IsTalkExceptionRateLimited(err): return bridgev2.WrapErrorInStatus(err). WithIsCertain(true). WithSendNotice(true). WithErrorReason(event.MessageStatusNetworkError). WithMessage("LINE is rate limiting requests, try again later") - case statusCode >= 500: + case statusCode >= 500, line.IsTalkExceptionServerError(err): return bridgev2.WrapErrorInStatus(err). WithIsCertain(false). WithSendNotice(true). @@ -68,6 +68,7 @@ func wrapSendError(err error) error { WithIsCertain(true). WithSendNotice(true). WithErrorReason(event.MessageStatusNetworkError). + WithStatus(event.MessageStatusFail). WithMessage("LINE rejected the request") default: return bridgev2.WrapErrorInStatus(err). diff --git a/pkg/line/errors.go b/pkg/line/errors.go index 6a80d97..fcc46d4 100644 --- a/pkg/line/errors.go +++ b/pkg/line/errors.go @@ -292,6 +292,14 @@ type talkExceptionData struct { Reason string `json:"reason"` } +const ( + talkExceptionDBFailed = 2 + talkExceptionExcessiveAccess = 4 + talkExceptionSystemError = 15 + talkExceptionInternalError = 20 + talkExceptionMaintenanceError = 33 +) + func loginPollingFailure(httpStatus int, body []byte) error { var response struct { Code *int `json:"code"` @@ -352,25 +360,51 @@ func safeLoginPollingReason(reason string) string { // IsE2EEGroupMemberMismatch identifies a rejected registration that needs a // fresh server member/key snapshot, not a plaintext fallback. func IsE2EEGroupMemberMismatch(err error) bool { - if err == nil { + talk, ok := parseTalkException(err) + return ok && talk.Code == 99 && strings.EqualFold(strings.TrimSpace(talk.Reason), "member count mismatch") +} + +// IsTalkExceptionRateLimited returns true when LINE rejected a Talk API call +// for excessive access. +func IsTalkExceptionRateLimited(err error) bool { + talk, ok := parseTalkException(err) + return ok && talk.Code == talkExceptionExcessiveAccess +} + +// IsTalkExceptionServerError returns true when a TalkException reports that +// LINE failed to handle a Talk API call, rather than rejecting it. +func IsTalkExceptionServerError(err error) bool { + talk, ok := parseTalkException(err) + if !ok { return false } + switch talk.Code { + case talkExceptionDBFailed, talkExceptionSystemError, talkExceptionInternalError, talkExceptionMaintenanceError: + return true + } + return false +} + +func parseTalkException(err error) (talkExceptionData, bool) { + if err == nil { + return talkExceptionData{}, false + } msg := err.Error() start := strings.IndexByte(msg, '{') if start < 0 { - return false + return talkExceptionData{}, false } var response struct { Code int `json:"code"` Message string `json:"message"` Data talkExceptionData `json:"data"` } - if json.NewDecoder(strings.NewReader(msg[start:])).Decode(&response) != nil { - return false + if json.NewDecoder(strings.NewReader(msg[start:])).Decode(&response) != nil || + response.Code != 10051 || !strings.EqualFold(response.Message, "RESPONSE_ERROR") || + !strings.EqualFold(response.Data.Name, "TalkException") { + return talkExceptionData{}, false } - return response.Code == 10051 && strings.EqualFold(response.Message, "RESPONSE_ERROR") && - strings.EqualFold(response.Data.Name, "TalkException") && response.Data.Code == 99 && - strings.EqualFold(strings.TrimSpace(response.Data.Reason), "member count mismatch") + return response.Data, true } // IsGroupKeyNotRegisteredError returns true when SendMessage returns code 99 diff --git a/pkg/line/methods.go b/pkg/line/methods.go index 08a0ca0..17643a3 100644 --- a/pkg/line/methods.go +++ b/pkg/line/methods.go @@ -427,7 +427,7 @@ func (c *Client) UpdateSettingsAttributes2Context(ctx context.Context, reqSeq in if wrapper.Code != 0 { // Preserve the response data because TalkException details contain the // error code used by connector-level auth recovery. - return fmt.Errorf("updateSettingsAttributes2 failed: code %d message %s data %s", wrapper.Code, wrapper.Message, string(wrapper.Data)) + return fmt.Errorf("updateSettingsAttributes2 failed: %s", resp) } return nil } @@ -451,7 +451,7 @@ func (c *Client) React(reqSeq int64, messageID string, reactionType ReactionType return err } if wrapper.Code != 0 { - return fmt.Errorf("react failed: code %d message %s data %s", wrapper.Code, wrapper.Message, string(wrapper.Data)) + return fmt.Errorf("react failed: %s", resp) } return nil } @@ -474,7 +474,7 @@ func (c *Client) CancelReaction(reqSeq int64, messageID string) error { return err } if wrapper.Code != 0 { - return fmt.Errorf("cancelReaction failed: code %d message %s data %s", wrapper.Code, wrapper.Message, string(wrapper.Data)) + return fmt.Errorf("cancelReaction failed: %s", resp) } return nil }