mirror of
https://github.com/mautrix/signal.git
synced 2026-11-09 07:39:20 -05:00
183 lines
6.8 KiB
Go
183 lines
6.8 KiB
Go
// mautrix-signal - A Matrix-Signal 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 (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
grpcstatus "google.golang.org/grpc/status"
|
|
"maunium.net/go/mautrix"
|
|
"maunium.net/go/mautrix/bridgev2"
|
|
"maunium.net/go/mautrix/bridgev2/status"
|
|
"maunium.net/go/mautrix/event"
|
|
|
|
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
|
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
|
|
)
|
|
|
|
func init() {
|
|
status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{
|
|
"signal-logged-out": "You have been logged out of Signal, please reconnect",
|
|
"signal-not-logged-in": "You're not logged into Signal",
|
|
"signal-disconnected": "Disconnected from Signal, reconnecting",
|
|
"signal-connection-error": "Connection to Signal failed, reconnecting",
|
|
"signal-server-error": "Signal server error, reconnecting",
|
|
"signal-rate-limited": "Rate limited by Signal, reconnecting",
|
|
"signal-connect-failed": "Failed to connect to Signal, retrying",
|
|
"signal-connection-failed": "Connection to Signal failed",
|
|
})
|
|
}
|
|
|
|
var (
|
|
errEditOthersMessage = bridgev2.WrapErrorInStatus(errors.New("you can only edit your own messages")).
|
|
WithErrorAsMessage().
|
|
WithErrorReason(event.MessageStatusNoPermission).
|
|
WithStatus(event.MessageStatusFail).
|
|
WithIsCertain(true).
|
|
WithSendNotice(true)
|
|
errDeleteOthersMessage = bridgev2.WrapErrorInStatus(errors.New("you can only delete your own messages")).
|
|
WithErrorAsMessage().
|
|
WithErrorReason(event.MessageStatusNoPermission).
|
|
WithStatus(event.MessageStatusFail).
|
|
WithIsCertain(true).
|
|
WithSendNotice(true)
|
|
errInviteToDM = bridgev2.WrapErrorInStatus(errors.New("can't invite more users to Signal DMs")).
|
|
WithErrorAsMessage().
|
|
WithErrorReason(event.MessageStatusUnsupported).
|
|
WithStatus(event.MessageStatusFail).
|
|
WithIsCertain(true).
|
|
WithSendNotice(true)
|
|
errUnsupportedDisappearingType = bridgev2.WrapErrorInStatus(errors.New("only disappearing timers that start after reading are supported on Signal")).
|
|
WithErrorAsMessage().
|
|
WithErrorReason(event.MessageStatusUnsupported).
|
|
WithStatus(event.MessageStatusFail).
|
|
WithIsCertain(true).
|
|
WithSendNotice(true)
|
|
)
|
|
|
|
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 Signal").
|
|
WithIsCertain(true).
|
|
WithSendNotice(true)
|
|
}
|
|
|
|
func wrapSendError(err error) error {
|
|
if err == nil || errors.As(err, &bridgev2.MessageStatus{}) {
|
|
return err
|
|
}
|
|
switch {
|
|
case errors.Is(err, signalmeow.ErrUnregisteredUser):
|
|
return bridgev2.WrapErrorInStatus(err).
|
|
WithMessage("recipient isn't registered on Signal").
|
|
WithErrorReason(event.MessageStatusNetworkError).
|
|
WithStatus(event.MessageStatusFail).
|
|
WithIsCertain(true).
|
|
WithSendNotice(true)
|
|
case errors.Is(err, signalmeow.ErrRateLimited), errors.Is(err, signalmeow.RateLimitError):
|
|
return bridgev2.WrapErrorInStatus(err).
|
|
WithMessage("rate limited by Signal, try again later").
|
|
WithErrorReason(event.MessageStatusNetworkError).
|
|
WithIsCertain(true).
|
|
WithSendNotice(true)
|
|
case errors.Is(err, signalmeow.AuthorizationFailedError):
|
|
return bridgev2.WrapErrorInStatus(err).
|
|
WithMessage("you don't have permission to do that in this Signal group").
|
|
WithErrorReason(event.MessageStatusNoPermission).
|
|
WithStatus(event.MessageStatusFail).
|
|
WithIsCertain(true).
|
|
WithSendNotice(true)
|
|
case errors.Is(err, signalmeow.GroupPatchNotAcceptedError):
|
|
return bridgev2.WrapErrorInStatus(err).
|
|
WithMessage("Signal rejected the group change").
|
|
WithErrorReason(event.MessageStatusNetworkError).
|
|
WithStatus(event.MessageStatusFail).
|
|
WithIsCertain(true).
|
|
WithSendNotice(true)
|
|
default:
|
|
return bridgev2.WrapErrorInStatus(err).
|
|
WithMessage("failed to send to Signal").
|
|
WithSendNotice(true)
|
|
}
|
|
}
|
|
|
|
func wrapRespError(err error) error {
|
|
var mapped bridgev2.RespError
|
|
var cdsRateLimit signalmeow.ContactDiscoveryRateLimitError
|
|
switch {
|
|
case errors.As(err, &cdsRateLimit), errors.Is(err, signalmeow.RateLimitError), errors.Is(err, signalmeow.ErrRateLimited),
|
|
grpcstatus.Code(err) == codes.ResourceExhausted:
|
|
mapped = bridgev2.RespError(mautrix.MLimitExceeded.WithMessage("Rate limited by Signal, please try again later").WithStatus(http.StatusBadRequest))
|
|
case errors.Is(err, signalmeow.AuthorizationFailedError):
|
|
mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("Signal denied the request"))
|
|
default:
|
|
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Request to Signal failed"))
|
|
}
|
|
return fmt.Errorf("%w: %w", mapped, err)
|
|
}
|
|
|
|
func wrapDirectMediaError(err error) error {
|
|
var statusErr web.HTTPStatusError
|
|
if !errors.As(err, &statusErr) {
|
|
return err
|
|
}
|
|
switch statusErr.StatusCode {
|
|
case http.StatusNotFound, http.StatusGone:
|
|
return mautrix.MNotFound.WithMessage("Media not found on Signal")
|
|
case http.StatusUnauthorized, http.StatusForbidden:
|
|
return mautrix.MForbidden.WithMessage("Signal denied access to the media")
|
|
case http.StatusTooManyRequests:
|
|
return mautrix.MLimitExceeded.WithMessage("Rate limited by Signal, please try again later").WithCanRetry(true)
|
|
default:
|
|
return mautrix.MUnknown.WithMessage("Unexpected response from Signal").WithStatus(http.StatusBadGateway).WithCanRetry(true)
|
|
}
|
|
}
|
|
|
|
func connectionErrorState(err error) status.BridgeState {
|
|
state := status.BridgeState{
|
|
StateEvent: status.StateTransientDisconnect,
|
|
Error: "signal-connection-error",
|
|
Info: map[string]any{"go_error": err.Error()},
|
|
}
|
|
var statusErr web.HTTPStatusError
|
|
if errors.As(err, &statusErr) {
|
|
if statusErr.StatusCode == http.StatusTooManyRequests {
|
|
state.Error = "signal-rate-limited"
|
|
} else if statusErr.StatusCode >= 500 {
|
|
state.Error = "signal-server-error"
|
|
}
|
|
}
|
|
return state
|
|
}
|
|
|
|
func (s *SignalClient) sendLoggedOutState(err error) {
|
|
state := status.BridgeState{
|
|
StateEvent: status.StateBadCredentials,
|
|
Error: "signal-logged-out",
|
|
UserAction: status.UserActionRelogin,
|
|
}
|
|
if err != nil {
|
|
state.Info = map[string]any{"go_error": err.Error()}
|
|
}
|
|
s.UserLogin.BridgeState.Send(state)
|
|
}
|