mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-11-09 23:59:17 -05:00
156 lines
6.1 KiB
Go
156 lines
6.1 KiB
Go
// 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)
|
|
}
|