272 lines
9.8 KiB
Go
272 lines
9.8 KiB
Go
package connector
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
"go.mau.fi/util/ptr"
|
|
"maunium.net/go/mautrix/bridge/status"
|
|
"maunium.net/go/mautrix/bridgev2"
|
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
|
|
|
"go.mau.fi/mautrix-signal/pkg/signalid"
|
|
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
|
)
|
|
|
|
type SignalClient struct {
|
|
Main *SignalConnector
|
|
UserLogin *bridgev2.UserLogin
|
|
Client *signalmeow.Client
|
|
Ghost *bridgev2.Ghost
|
|
}
|
|
|
|
var signalCaps = &bridgev2.NetworkRoomCapabilities{
|
|
FormattedText: true,
|
|
UserMentions: true,
|
|
LocationMessages: true,
|
|
Captions: true,
|
|
Replies: true,
|
|
Edits: true,
|
|
EditMaxCount: 10,
|
|
EditMaxAge: 24 * time.Hour,
|
|
Deletes: true,
|
|
DeleteMaxAge: 24 * time.Hour,
|
|
DefaultFileRestriction: &bridgev2.FileRestriction{
|
|
MaxSize: 100 * 1024 * 1024,
|
|
},
|
|
ReadReceipts: true,
|
|
Reactions: true,
|
|
ReactionCount: 1,
|
|
}
|
|
|
|
var signalCapsNoteToSelf *bridgev2.NetworkRoomCapabilities
|
|
|
|
func init() {
|
|
signalCapsNoteToSelf = ptr.Clone(signalCaps)
|
|
signalCapsNoteToSelf.EditMaxAge = 0
|
|
}
|
|
|
|
func (s *SignalClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *bridgev2.NetworkRoomCapabilities {
|
|
if portal.Receiver == s.UserLogin.ID && portal.ID == networkid.PortalID(s.UserLogin.ID) {
|
|
return signalCapsNoteToSelf
|
|
}
|
|
return signalCaps
|
|
}
|
|
|
|
var (
|
|
_ bridgev2.NetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.EditHandlingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.ReactionHandlingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.RedactionHandlingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.TypingHandlingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.IdentifierResolvingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.GroupCreatingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.ContactListingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.RoomNameHandlingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.RoomAvatarHandlingNetworkAPI = (*SignalClient)(nil)
|
|
_ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil)
|
|
)
|
|
|
|
var pushCfg = &bridgev2.PushConfig{
|
|
FCM: &bridgev2.FCMPushConfig{
|
|
// https://github.com/signalapp/Signal-Android/blob/main/app/src/main/res/values/firebase_messaging.xml#L4
|
|
SenderID: "312334754206",
|
|
},
|
|
APNs: &bridgev2.APNsPushConfig{
|
|
BundleID: "org.whispersystems.signal",
|
|
},
|
|
}
|
|
|
|
func (s *SignalClient) GetPushConfigs() *bridgev2.PushConfig {
|
|
return pushCfg
|
|
}
|
|
|
|
func (s *SignalClient) RegisterPushNotifications(ctx context.Context, pushType bridgev2.PushType, token string) error {
|
|
if s.Client == nil {
|
|
return bridgev2.ErrNotLoggedIn
|
|
}
|
|
if pushType != bridgev2.PushTypeFCM {
|
|
return fmt.Errorf("unsupported push type: %s", pushType)
|
|
}
|
|
return s.Client.RegisterFCM(ctx, token)
|
|
}
|
|
|
|
func (s *SignalClient) LogoutRemote(ctx context.Context) {
|
|
if s.Client == nil {
|
|
return
|
|
}
|
|
err := s.Client.StopReceiveLoops()
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to stop receive loops for logout")
|
|
}
|
|
err = s.Main.Store.DeleteDevice(context.TODO(), &s.Client.Store.DeviceData)
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete device from store")
|
|
}
|
|
}
|
|
|
|
func (s *SignalClient) IsThisUser(_ context.Context, userID networkid.UserID) bool {
|
|
if s.Client == nil {
|
|
return false
|
|
}
|
|
return userID == signalid.MakeUserID(s.Client.Store.ACI)
|
|
}
|
|
|
|
func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnectionStatus) {
|
|
var peekedConnectionStatus signalmeow.SignalConnectionStatus
|
|
for {
|
|
var connectionStatus signalmeow.SignalConnectionStatus
|
|
if peekedConnectionStatus.Event != signalmeow.SignalConnectionEventNone {
|
|
s.UserLogin.Log.Debug().
|
|
Stringer("peeked_connection_status_event", peekedConnectionStatus.Event).
|
|
Msg("Using peeked connectionStatus event")
|
|
connectionStatus = peekedConnectionStatus
|
|
peekedConnectionStatus = signalmeow.SignalConnectionStatus{}
|
|
} else {
|
|
var ok bool
|
|
connectionStatus, ok = <-statusChan
|
|
if !ok {
|
|
s.UserLogin.Log.Debug().Msg("statusChan channel closed")
|
|
return
|
|
}
|
|
}
|
|
|
|
err := connectionStatus.Err
|
|
switch connectionStatus.Event {
|
|
case signalmeow.SignalConnectionEventConnected:
|
|
s.UserLogin.Log.Debug().Msg("Sending Connected BridgeState")
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
|
|
|
case signalmeow.SignalConnectionEventDisconnected:
|
|
s.UserLogin.Log.Debug().Msg("Received SignalConnectionEventDisconnected")
|
|
|
|
// Debounce: wait 7s before sending TransientDisconnect, in case we get a reconnect
|
|
// We should wait until the next message comes in, or 7 seconds has passed.
|
|
// - If a disconnected event comes in, just loop again, unless it's been more than 7 seconds.
|
|
// - If a non-disconnected event comes in, store it in peekedConnectionStatus,
|
|
// break out of this loop and go back to the top of the goroutine to handle it in the switch.
|
|
// - If 7 seconds passes without any non-disconnect messages, send the TransientDisconnect.
|
|
// (Why 7 seconds? It was 5 at first, but websockets min retry is 5 seconds,
|
|
// so it would send TransientDisconnect right before reconnecting. 7 seems to work well.)
|
|
debounceTimer := time.NewTimer(7 * time.Second)
|
|
PeekLoop:
|
|
for {
|
|
var ok bool
|
|
select {
|
|
case peekedConnectionStatus, ok = <-statusChan:
|
|
// Handle channel closing
|
|
if !ok {
|
|
s.UserLogin.Log.Debug().Msg("connectionStatus channel closed")
|
|
return
|
|
}
|
|
// If it's another Disconnected event, just keep looping
|
|
if peekedConnectionStatus.Event == signalmeow.SignalConnectionEventDisconnected {
|
|
peekedConnectionStatus = signalmeow.SignalConnectionStatus{}
|
|
continue
|
|
}
|
|
// If it's a non-disconnect event, break out of the PeekLoop and handle it in the switch
|
|
break PeekLoop
|
|
case <-debounceTimer.C:
|
|
// Time is up, so break out of the loop and send the TransientDisconnect
|
|
break PeekLoop
|
|
}
|
|
}
|
|
// We're out of the PeekLoop, so either we got a non-disconnect event, or it's been 7 seconds (or both).
|
|
// We want to send TransientDisconnect if it's been 7 seconds, but not if the latest event was something
|
|
// other than Disconnected
|
|
if !debounceTimer.Stop() { // If the timer has already expired
|
|
// Send TransientDisconnect only if the latest event is a disconnect or no event
|
|
// (peekedConnectionStatus could be something else if the timer and the event race)
|
|
if peekedConnectionStatus.Event == signalmeow.SignalConnectionEventDisconnected ||
|
|
peekedConnectionStatus.Event == signalmeow.SignalConnectionEventNone {
|
|
s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState")
|
|
if err == nil {
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect})
|
|
} else {
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
|
|
}
|
|
}
|
|
}
|
|
|
|
case signalmeow.SignalConnectionEventLoggedOut:
|
|
s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState")
|
|
if err == nil {
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
|
|
} else {
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: err.Error()})
|
|
}
|
|
err = s.Client.ClearKeysAndDisconnect(context.TODO())
|
|
if err != nil {
|
|
s.UserLogin.Log.Error().Err(err).Msg("Failed to clear keys and disconnect")
|
|
}
|
|
|
|
case signalmeow.SignalConnectionEventError:
|
|
s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState")
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()})
|
|
|
|
case signalmeow.SignalConnectionCleanShutdown:
|
|
if s.Client.IsLoggedIn() {
|
|
s.UserLogin.Log.Debug().Msg("Clean Shutdown - sending no BridgeState")
|
|
} else {
|
|
s.UserLogin.Log.Debug().Msg("Clean Shutdown, but logged out - Sending BadCredentials BridgeState")
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *SignalClient) Connect(ctx context.Context) error {
|
|
if s.Client == nil {
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You're not logged into Signal"})
|
|
return nil
|
|
}
|
|
s.updateRemoteProfile(ctx, false)
|
|
s.tryConnect(ctx, 0)
|
|
return nil
|
|
}
|
|
|
|
func (s *SignalClient) Disconnect() {
|
|
if s.Client == nil {
|
|
return
|
|
}
|
|
err := s.Client.StopReceiveLoops()
|
|
if err != nil {
|
|
s.UserLogin.Log.Err(err).Msg("Failed to stop receive loops")
|
|
}
|
|
}
|
|
|
|
func (s *SignalClient) tryConnect(ctx context.Context, retryCount int) {
|
|
err := s.Client.RegisterCapabilities(ctx)
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to register capabilities")
|
|
} else {
|
|
zerolog.Ctx(ctx).Debug().Msg("Successfully registered capabilities")
|
|
}
|
|
ch, err := s.Client.StartReceiveLoops(ctx)
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to start receive loops")
|
|
if retryCount < 6 {
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
|
|
retryInSeconds := 2 << retryCount
|
|
zerolog.Ctx(ctx).Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection")
|
|
time.Sleep(time.Duration(retryInSeconds) * time.Second)
|
|
s.tryConnect(ctx, retryCount+1)
|
|
} else {
|
|
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()})
|
|
}
|
|
} else {
|
|
go s.bridgeStateLoop(ch)
|
|
}
|
|
}
|
|
|
|
func (s *SignalClient) IsLoggedIn() bool {
|
|
if s.Client == nil {
|
|
return false
|
|
}
|
|
return s.Client.IsLoggedIn()
|
|
}
|