Watch
1
0
Fork
You've already forked mautrix-whatsapp
0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-11-09 23:59:17 -05:00
mautrix-whatsapp/pkg/connector/errors.go

156 lines
6.1 KiB
Go
Raw Permalink Normal View History

// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2026 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package connector
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"go.mau.fi/whatsmeow"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
// wrapConversionError turns a failure to convert a Matrix event into a message status. Nothing has been
// sent to WhatsApp at that point, so the failure is certain.
func wrapConversionError(err error) error {
if err == nil || errors.As(err, &bridgev2.MessageStatus{}) {
return err
}
return bridgev2.WrapErrorInStatus(err).
WithMessage("failed to convert message for WhatsApp").
WithIsCertain(true).
WithSendNotice(true)
}
// wrapWhatsAppError turns an error from a whatsmeow request made while handling a Matrix event into a
// message status.
func wrapWhatsAppError(err error) error {
if err == nil || errors.As(err, &bridgev2.MessageStatus{}) {
return err
}
var iqErr *whatsmeow.IQError
var disconnectErr *whatsmeow.DisconnectedError
switch {
case errors.Is(err, whatsmeow.ErrNotConnected), errors.Is(err, whatsmeow.ErrClientIsNil):
return bridgev2.WrapErrorInStatus(err).
WithMessage("not connected to WhatsApp").
WithIsCertain(true).
WithSendNotice(true)
case errors.Is(err, whatsmeow.ErrMessageTimedOut), errors.Is(err, whatsmeow.ErrIQTimedOut),
errors.Is(err, context.DeadlineExceeded), errors.As(err, &disconnectErr):
return bridgev2.WrapErrorInStatus(err).
WithMessage("no response from WhatsApp").
WithIsCertain(false).
WithSendNotice(true)
case errors.Is(err, whatsmeow.ErrIQRateOverLimit):
return bridgev2.WrapErrorInStatus(err).
WithMessage("rate limited by WhatsApp").
WithErrorReason(event.MessageStatusNetworkError).
WithIsCertain(true).
WithSendNotice(true)
case errors.Is(err, whatsmeow.ErrIQForbidden), errors.Is(err, whatsmeow.ErrIQNotAuthorized):
return bridgev2.WrapErrorInStatus(err).
WithMessage("you don't have permission to do that on WhatsApp").
WithErrorReason(event.MessageStatusNoPermission).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
case errors.Is(err, whatsmeow.ErrServerReturnedError), errors.As(err, &iqErr):
return bridgev2.WrapErrorInStatus(err).
WithMessage("WhatsApp returned an error").
WithIsCertain(true).
WithSendNotice(true)
default:
return bridgev2.WrapErrorInStatus(err).
WithMessage("unexpected error from WhatsApp").
WithSendNotice(true)
}
}
// wrapSendError turns an error from sending a message to WhatsApp into a message status.
func (wa *WhatsAppClient) wrapSendError(err error) error {
switch sendErrorCode(err) {
case 403:
return bridgev2.WrapErrorInStatus(err).
WithMessage("you don't have permission to do that on WhatsApp").
WithErrorReason(event.MessageStatusNoPermission).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
case 463:
msg := "WhatsApp has temporarily restricted your account from messaging new contacts"
if until := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil; until.After(time.Now()) {
msg = fmt.Sprintf("WhatsApp has restricted your account from messaging new contacts until %s", until.UTC().Format("Jan 2, 2006 at 3:04 PM MST"))
}
return bridgev2.WrapErrorInStatus(err).
WithMessage(msg).
WithErrorReason(event.MessageStatusNoPermission).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
case 421, 479:
return bridgev2.WrapErrorInStatus(err).
WithMessage("WhatsApp rejected the message").
WithErrorReason(event.MessageStatusNetworkError).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
default:
return wrapWhatsAppError(err)
}
}
// sendErrorCode returns the code WhatsApp rejected a message with, which whatsmeow only includes in the
// error text.
func sendErrorCode(err error) int {
if !errors.Is(err, whatsmeow.ErrServerReturnedError) {
return 0
}
_, codeStr, _ := strings.Cut(err.Error(), whatsmeow.ErrServerReturnedError.Error()+" ")
code, _ := strconv.Atoi(codeStr)
return code
}
// wrapWhatsAppRespError turns an error from a whatsmeow request made during login or provisioning into
// an HTTP error, keeping the original error in the chain. Clients retry 429 and 502-504 automatically,
// which isn't wanted for provisioning requests, so those statuses aren't used.
func wrapWhatsAppRespError(err error) error {
var mapped bridgev2.RespError
var disconnectErr *whatsmeow.DisconnectedError
switch {
case errors.Is(err, whatsmeow.ErrIQRateOverLimit):
mapped = bridgev2.RespError(mautrix.MLimitExceeded.WithMessage("Rate limited by WhatsApp. Try again later.").WithStatus(http.StatusBadRequest))
case errors.Is(err, whatsmeow.ErrNotConnected), errors.Is(err, whatsmeow.ErrClientIsNil), errors.As(err, &disconnectErr):
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Not connected to WhatsApp"))
case errors.Is(err, whatsmeow.ErrIQTimedOut), errors.Is(err, context.DeadlineExceeded):
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("WhatsApp didn't respond in time"))
case errors.Is(err, whatsmeow.ErrIQForbidden), errors.Is(err, whatsmeow.ErrIQNotAuthorized):
mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("WhatsApp denied the request"))
default:
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Request to WhatsApp failed"))
}
return fmt.Errorf("%w: %w", mapped.WithInternalError(err), err)
}