Watch
1
0
Fork
You've already forked mautrix-signal
0
mirror of https://github.com/mautrix/signal.git synced 2026-11-11 00:29:16 -05:00

Compare commits

...
Author SHA1 Message Date
Gerardo Rodriguez
a96f214609
groupinfo,signalmeow: make ended groups read-only (#725) 2026-11-11 02:36:47 +00:00
Gerardo Rodriguez
561064e8f5
handlematrix: send admin deletes for other members' messages in groups (#722) 2026-11-11 02:36:43 +00:00
Gerardo Rodriguez
03c8d29ae7
signalmeow/receiving: fetch PNI identity from the primary device when verifying PNI signatures (#720) 2026-11-11 02:36:40 +00:00
Gerardo Rodriguez
59a4ff2afc
msgconv/from-signal: send notice when a message is pinned/unpinned (#724) 2026-11-10 23:18:34 +00:00
Gerardo Rodriguez
27e3fc2582
signalmeow/storageservice: fix unnecessarily marking recipients as changed (#712) 2026-11-10 23:18:23 +00:00
Gerardo Rodriguez
15674cc755
signalmeow/groupcache: keep send endorsements for group changes without a response (#714) 2026-11-10 23:18:13 +00:00
Gerardo Rodriguez
fa622888a9
client: don't connect logins that are already logged out (#713) 2026-11-10 23:18:04 +00:00
Gerardo Rodriguez
d59791e7d3
signalmeow: send typing messages as online-only (#716) 2026-11-10 23:18:01 +00:00
Gerardo Rodriguez
cd8bc557d6
signalmeow/storageservice: ignore record types the bridge doesn't use (#717) 2026-11-10 23:17:56 +00:00
Gerardo Rodriguez
5c740406db
signalmeow: handle server delivery receipts as delivery receipts (#718) 2026-11-10 23:17:50 +00:00
Gerardo Rodriguez
c6abc27db6
signalmeow/storageservice: only fetch new records after the first sync (#719) 2026-11-10 23:17:39 +00:00
Gerardo Rodriguez
0a4f1d81c8
signalmeow/web: don't return an error when closing an already closed websocket (#721) 2026-11-10 23:17:31 +00:00
Gerardo Rodriguez
74b4c70ee4
handlesignal: send a notice in DMs when a safety number changes (#723) 2026-11-10 23:16:58 +00:00
Gerardo Rodriguez
8cb696dcfc
signalmeow/groups: decode announcements-only flag in full group state (#727) 2026-11-10 23:04:39 +00:00
indent[bot]
d451962ba2
msgconv: download stickers from their pack when the attachment is gone (#708)
Co-authored-by: Indent <noreply@indent.com>
2026-11-09 17:42:33 +00:00
indent[bot]
c827c97adb
chatinfo: fall back to profile avatar when contact avatar isn't loaded (#710)
Co-authored-by: Indent <noreply@indent.com>
2026-11-09 17:41:17 +00:00
indent[bot]
c820321548
signalmeow/websocket: log alerts sent by the server when connecting (#709)
Co-authored-by: Indent <noreply@indent.com>
2026-11-09 17:34:19 +00:00
indent[bot]
34b4187e07
chatinfo: stop listing contacts after request cancellation (#705)
Co-authored-by: Indent <noreply@indent.com>
2026-11-05 15:33:19 +00:00
Nick Mills-Barrett
a630835d70
pre-commit: install bridgelint with the CI Go toolchain (#706) 2026-11-05 13:09:30 +00:00
19 changed files with 292 additions and 33 deletions

View file

@ -30,6 +30,7 @@ jobs:
run: | run: |
go install golang.org/x/tools/cmd/goimports@latest go install golang.org/x/tools/cmd/goimports@latest
go install honnef.co/go/tools/cmd/staticcheck@latest go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/beeper/pre-commit-go/bridgelint@latest
export PATH="$HOME/go/bin:$PATH" export PATH="$HOME/go/bin:$PATH"
- name: Run pre-commit - name: Run pre-commit

View file

@ -27,4 +27,5 @@ repos:
- id: zerolog-ban-msgf - id: zerolog-ban-msgf
- id: zerolog-use-stringer - id: zerolog-use-stringer
- id: bridgelint - id: bridgelint
language: system
verbose: true verbose: true

View file

@ -125,9 +125,10 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec
} }
name := s.Main.Config.FormatDisplayname(contact, username) name := s.Main.Config.FormatDisplayname(contact, username)
ui.Name = &name ui.Name = &name
if s.Main.Config.UseContactAvatars && contact.ContactAvatar.Hash != "" { contactAvatarID := networkid.AvatarID("hash:" + contact.ContactAvatar.Hash)
if s.Main.Config.UseContactAvatars && contact.ContactAvatar.Hash != "" && s.canUseContactAvatar(ctx, contact, contactAvatarID) {
ui.Avatar = &bridgev2.Avatar{ ui.Avatar = &bridgev2.Avatar{
ID: networkid.AvatarID("hash:" + contact.ContactAvatar.Hash), ID: contactAvatarID,
Get: func(ctx context.Context) ([]byte, error) { Get: func(ctx context.Context) ([]byte, error) {
if contact.ContactAvatar.Image == nil { if contact.ContactAvatar.Image == nil {
return nil, fmt.Errorf("contact avatar not available") return nil, fmt.Errorf("contact avatar not available")
@ -172,6 +173,20 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec
return ui, nil return ui, nil
} }
func (s *SignalClient) canUseContactAvatar(ctx context.Context, contact *types.Recipient, avatarID networkid.AvatarID) bool {
if contact.ContactAvatar.Image != nil {
return true
} else if contact.ACI == uuid.Nil {
return false
}
ghost, err := s.Main.Bridge.GetExistingGhostByID(ctx, signalid.MakeUserID(contact.ACI))
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get ghost to check contact avatar")
return true
}
return ghost != nil && ghost.AvatarID == avatarID && ghost.AvatarMXC != "" && ghost.AvatarSet
}
func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool { func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool {
_, err := signalid.ParseUserIDAsServiceID(id) _, err := signalid.ParseUserIDAsServiceID(id)
return err == nil return err == nil
@ -445,11 +460,15 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI
} }
resp := make([]*bridgev2.ResolveIdentifierResponse, len(recipients)) resp := make([]*bridgev2.ResolveIdentifierResponse, len(recipients))
for i, recipient := range recipients { for i, recipient := range recipients {
recipientResp := &bridgev2.ResolveIdentifierResponse{ if err = ctx.Err(); err != nil {
Chat: s.makeCreateDMResponse(ctx, recipient, nil), return nil, err
} }
recipientResp := &bridgev2.ResolveIdentifierResponse{}
var username string var username string
if recipient.ACI != uuid.Nil { if recipient.ACI != uuid.Nil {
recipientResp.Chat = &bridgev2.CreateChatResponse{
PortalKey: s.makeDMPortalKey(libsignalgo.NewACIServiceID(recipient.ACI)),
}
recipientResp.UserID = signalid.MakeUserID(recipient.ACI) recipientResp.UserID = signalid.MakeUserID(recipient.ACI)
ghost, err := s.Main.Bridge.GetGhostByID(ctx, recipientResp.UserID) ghost, err := s.Main.Bridge.GetGhostByID(ctx, recipientResp.UserID)
if err != nil { if err != nil {
@ -458,7 +477,11 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI
recipientResp.Ghost = ghost recipientResp.Ghost = ghost
username = ghost.Metadata.(*signalid.GhostMetadata).Username username = ghost.Metadata.(*signalid.GhostMetadata).Username
} else { } else {
recipientResp.UserID = signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(recipient.PNI)) pniServiceID := libsignalgo.NewPNIServiceID(recipient.PNI)
recipientResp.Chat = &bridgev2.CreateChatResponse{
PortalKey: s.makeDMPortalKey(pniServiceID),
}
recipientResp.UserID = signalid.MakeUserIDFromServiceID(pniServiceID)
} }
recipientResp.UserInfo, err = s.contactToUserInfo(ctx, recipient, username, false) recipientResp.UserInfo, err = s.contactToUserInfo(ctx, recipient, username, false)
if err != nil { if err != nil {

View file

@ -221,12 +221,18 @@ func (s *SignalClient) Connect(ctx context.Context) {
if s.Client == nil { if s.Client == nil {
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Error: "signal-not-logged-in", UserAction: status.UserActionRelogin}) s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Error: "signal-not-logged-in", UserAction: status.UserActionRelogin})
return return
} else if !s.Client.IsLoggedIn() {
s.sendLoggedOutState(nil)
return
} }
s.updateRemoteProfile(ctx, false) s.updateRemoteProfile(ctx, false)
s.tryConnect(ctx, 0, true) s.tryConnect(ctx, 0, true)
} }
func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.ConnectBackgroundParams) error { func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.ConnectBackgroundParams) error {
if !s.IsLoggedIn() {
return bridgev2.ErrNotLoggedIn
}
s.queueEmptyWaiter.Clear() s.queueEmptyWaiter.Clear()
ch, unauthCh, err := s.Client.StartWebsockets(ctx) ch, unauthCh, err := s.Client.StartWebsockets(ctx)
if err != nil { if err != nil {

View file

@ -42,6 +42,7 @@ type SignalConfig struct {
NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"` NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
LocationFormat string `yaml:"location_format"` LocationFormat string `yaml:"location_format"`
ExtEvPolls bool `yaml:"extev_polls"` ExtEvPolls bool `yaml:"extev_polls"`
IdentityChangeNotices bool `yaml:"identity_change_notices"`
displaynameTemplate *template.Template `yaml:"-"` displaynameTemplate *template.Template `yaml:"-"`
} }
@ -106,6 +107,7 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Str, "note_to_self_avatar") helper.Copy(up.Str, "note_to_self_avatar")
helper.Copy(up.Str, "location_format") helper.Copy(up.Str, "location_format")
helper.Copy(up.Bool, "extev_polls") helper.Copy(up.Bool, "extev_polls")
helper.Copy(up.Bool, "identity_change_notices")
} }
func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) { func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) {

View file

@ -25,3 +25,5 @@ note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL
location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s' location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s'
# Should polls be sent using unstable MSC3381 event types? # Should polls be sent using unstable MSC3381 event types?
extev_polls: false extev_polls: false
# Should another user's safety number changing send a message to Matrix?
identity_change_notices: false

View file

@ -39,6 +39,7 @@ import (
var defaultPL = 0 var defaultPL = 0
var moderatorPL = 50 var moderatorPL = 50
var terminatedPL = 100
func roleToPL(role signalmeow.GroupMemberRole) *int { func roleToPL(role signalmeow.GroupMemberRole) *int {
switch role { switch role {
@ -70,6 +71,21 @@ func applyAttributesAccess(plc *bridgev2.PowerLevelOverrides, attributeAccess si
plc.Events[event.StateBeeperDisappearingTimer] = attributePL plc.Events[event.StateBeeperDisappearingTimer] = attributePL
} }
func applyTerminated(plc *bridgev2.PowerLevelOverrides) {
plc.EventsDefault = &terminatedPL
plc.StateDefault = &terminatedPL
plc.Invite = &terminatedPL
plc.Kick = &terminatedPL
plc.Ban = &terminatedPL
plc.Redact = &terminatedPL
for _, evtType := range []event.Type{
event.EventReaction, event.EventRedaction, event.StatePowerLevels, event.StateRoomName,
event.StateRoomAvatar, event.StateTopic, event.StateBeeperDisappearingTimer,
} {
plc.Events[evtType] = terminatedPL
}
}
func applyMembersAccess(plc *bridgev2.PowerLevelOverrides, memberAccess signalmeow.AccessControl) { func applyMembersAccess(plc *bridgev2.PowerLevelOverrides, memberAccess signalmeow.AccessControl) {
if memberAccess == signalmeow.AccessControl_ADMINISTRATOR { if memberAccess == signalmeow.AccessControl_ADMINISTRATOR {
plc.Invite = &moderatorPL plc.Invite = &moderatorPL
@ -125,6 +141,9 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members) applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members)
joinRule = inviteLinkToJoinRule(groupInfo.AccessControl.AddFromInviteLink) joinRule = inviteLinkToJoinRule(groupInfo.AccessControl.AddFromInviteLink)
} }
if groupInfo.Terminated {
applyTerminated(members.PowerLevels)
}
for _, member := range groupInfo.RequestingMembers { for _, member := range groupInfo.RequestingMembers {
members.MemberMap.Set(bridgev2.ChatMember{ members.MemberMap.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI), EventSender: s.makeEventSender(member.ACI),
@ -265,7 +284,8 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID
var pls *bridgev2.PowerLevelOverrides var pls *bridgev2.PowerLevelOverrides
if groupChange.ModifyAnnouncementsOnly != nil || if groupChange.ModifyAnnouncementsOnly != nil ||
groupChange.ModifyAttributesAccess != nil || groupChange.ModifyAttributesAccess != nil ||
groupChange.ModifyMemberAccess != nil { groupChange.ModifyMemberAccess != nil ||
groupChange.TerminateGroup {
pls = &bridgev2.PowerLevelOverrides{Events: make(map[event.Type]int)} pls = &bridgev2.PowerLevelOverrides{Events: make(map[event.Type]int)}
if groupChange.ModifyAnnouncementsOnly != nil { if groupChange.ModifyAnnouncementsOnly != nil {
applyAnnouncementsOnly(pls, *groupChange.ModifyAnnouncementsOnly) applyAnnouncementsOnly(pls, *groupChange.ModifyAnnouncementsOnly)
@ -276,6 +296,9 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID
if groupChange.ModifyMemberAccess != nil { if groupChange.ModifyMemberAccess != nil {
applyMembersAccess(pls, *groupChange.ModifyMemberAccess) applyMembersAccess(pls, *groupChange.ModifyMemberAccess)
} }
if groupChange.TerminateGroup {
applyTerminated(pls)
}
} }
if groupChange.ModifyAddFromInviteLinkAccess != nil { if groupChange.ModifyAddFromInviteLinkAccess != nil {
ic.ChatInfo.JoinRule = &event.JoinRulesEventContent{ ic.ChatInfo.JoinRule = &event.JoinRulesEventContent{

View file

@ -302,20 +302,48 @@ func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *brid
} }
func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error { func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
_, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID) targetAuthorACI, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err) return fmt.Errorf("failed to parse target message ID: %w", err)
} else if msg.TargetMessage.SenderID != signalid.MakeUserID(s.Client.Store.ACI) {
return errDeleteOthersMessage
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) dm := &signalpb.DataMessage{
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ Timestamp: proto.Uint64(getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)),
Timestamp: proto.Uint64(ts), }
Delete: &signalpb.DataMessage_Delete{ if msg.TargetMessage.SenderID == signalid.MakeUserID(s.Client.Store.ACI) {
dm.Delete = &signalpb.DataMessage_Delete{
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
}, }
})) } else if isAdmin, err := s.isGroupAdmin(ctx, msg.Portal); err != nil {
return wrapSendError(err) return wrapSendError(err)
} else if !isAdmin {
return errDeleteOthersMessage
} else {
dm.AdminDelete = &signalpb.DataMessage_AdminDelete{
TargetAuthorAciBinary: targetAuthorACI[:],
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
}
}
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(dm))
return wrapSendError(err)
}
func (s *SignalClient) isGroupAdmin(ctx context.Context, portal *bridgev2.Portal) (bool, error) {
_, groupID, err := signalid.ParsePortalID(portal.ID)
if err != nil {
return false, err
} else if groupID == "" {
return false, nil
}
group, _, err := s.Client.RetrieveGroupByID(ctx, groupID, portal.Metadata.(*signalid.PortalMetadata).Revision)
if err != nil {
return false, fmt.Errorf("failed to get group info: %w", err)
}
for _, member := range group.Members {
if member.ACI == s.Client.Store.ACI {
return member.Role == signalmeow.GroupMember_ADMINISTRATOR, nil
}
}
return false, nil
} }
func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error { func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error {

View file

@ -75,7 +75,7 @@ func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool {
case *events.LoggedOut: case *events.LoggedOut:
s.sendLoggedOutState(evt.Error) s.sendLoggedOutState(evt.Error)
case *events.IdentityChange: case *events.IdentityChange:
// TODO send notice s.handleSignalIdentityChange(evt)
default: default:
s.UserLogin.Log.Warn().Type("event_type", evt).Msg("Unrecognized signalmeow event type") s.UserLogin.Log.Warn().Type("event_type", evt).Msg("Unrecognized signalmeow event type")
} }
@ -161,6 +161,44 @@ func convertDecryptionError(_ context.Context, _ *bridgev2.Portal, _ bridgev2.Ma
}, nil }, nil
} }
func (s *SignalClient) handleSignalIdentityChange(evt *events.IdentityChange) {
if !s.Main.Config.IdentityChangeNotices || evt.ServiceID.Type != libsignalgo.ServiceIDTypeACI || evt.ServiceID.UUID == s.Client.Store.ACI {
return
}
s.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
LogContext: func(c zerolog.Context) zerolog.Context {
return c.Stringer("sender_id", evt.ServiceID)
},
PortalKey: s.makeDMPortalKey(evt.ServiceID),
Sender: s.makeEventSender(evt.ServiceID.UUID),
},
Data: evt,
ID: "idchange|" + signalid.MakeMessageID(evt.ServiceID.UUID, uint64(time.Now().UnixMilli())),
ConvertMessageFunc: convertIdentityChange,
})
}
func convertIdentityChange(ctx context.Context, portal *bridgev2.Portal, _ bridgev2.MatrixAPI, evt *events.IdentityChange) (*bridgev2.ConvertedMessage, error) {
name := evt.ServiceID.UUID.String()
ghost, err := portal.Bridge.GetGhostByID(ctx, signalid.MakeUserID(evt.ServiceID.UUID))
if err != nil {
return nil, err
} else if ghost.Name != "" {
name = ghost.Name
}
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: fmt.Sprintf("Your safety number with %s has changed.", name),
},
}},
}, nil
}
type Bv2ChatEvent struct { type Bv2ChatEvent struct {
*events.ChatEvent *events.ChatEvent
s *SignalClient s *SignalClient
@ -185,6 +223,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
switch { switch {
case innerEvt.GetIsViewOnce(), innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil, case innerEvt.GetIsViewOnce(), innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil,
innerEvt.Payment != nil, innerEvt.GiftBadge != nil, innerEvt.PollCreate != nil, innerEvt.PollVote != nil, innerEvt.Payment != nil, innerEvt.GiftBadge != nil, innerEvt.PollCreate != nil, innerEvt.PollVote != nil,
innerEvt.PinMessage != nil, innerEvt.UnpinMessage != nil,
innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT), innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT),
innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0: innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0:
return bridgev2.RemoteEventMessage return bridgev2.RemoteEventMessage

View file

@ -153,6 +153,12 @@ func (mc *MessageConverter) ToMatrix(
cm.Parts = append(cm.Parts, mc.convertPollTerminateToMatrix(ctx, sender, dm.PollTerminate)) cm.Parts = append(cm.Parts, mc.convertPollTerminateToMatrix(ctx, sender, dm.PollTerminate))
return cm return cm
} }
if dm.PinMessage != nil || dm.UnpinMessage != nil {
var part *bridgev2.ConvertedMessagePart
part, cm.ReplyTo = mc.convertPinToMatrix(dm)
cm.Parts = append(cm.Parts, part)
return cm
}
if dm.GetIsViewOnce() { if dm.GetIsViewOnce() {
if !mc.Bridge.Config.ViewLimitedMedia && (cm.Disappear.Timer == 0 || cm.Disappear.Timer > 5*time.Minute) { if !mc.Bridge.Config.ViewLimitedMedia && (cm.Disappear.Timer == 0 || cm.Disappear.Timer > 5*time.Minute) {
cm.Disappear = database.DisappearingSetting{ cm.Disappear = database.DisappearingSetting{
@ -502,6 +508,11 @@ func (mc *MessageConverter) convertAttachmentToMatrix(ctx context.Context, index
func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker *signalpb.DataMessage_Sticker, attMap AttachmentMap) *bridgev2.ConvertedMessagePart { func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker *signalpb.DataMessage_Sticker, attMap AttachmentMap) *bridgev2.ConvertedMessagePart {
converted, err := mc.reuploadAttachment(ctx, sticker.GetData(), attMap) converted, err := mc.reuploadAttachment(ctx, sticker.GetData(), attMap)
hasPack := len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID)
if err != nil && hasPack && (errors.Is(err, signalmeow.ErrAttachmentNotFound) || errors.Is(err, ErrAttachmentNotInBackup) || errors.Is(err, ErrBackupNotSupported)) {
zerolog.Ctx(ctx).Debug().Err(err).Msg("Sticker attachment is unavailable, downloading it from the sticker pack")
converted, err = mc.reuploadStickerFromPack(ctx, sticker)
}
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to handle sticker") zerolog.Ctx(ctx).Err(err).Msg("Failed to handle sticker")
return &bridgev2.ConvertedMessagePart{ return &bridgev2.ConvertedMessagePart{
@ -519,7 +530,7 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker
converted.Content.Info.Height = 200 converted.Content.Info.Height = 200
} }
converted.Content.Body = sticker.GetEmoji() converted.Content.Body = sticker.GetEmoji()
if len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID) { if hasPack {
converted.Content.Info.BridgedSticker = &event.BridgedSticker{ converted.Content.Info.BridgedSticker = &event.BridgedSticker{
Network: StickerSourceID, Network: StickerSourceID,
ID: strconv.FormatUint(uint64(sticker.GetStickerId()), 10), ID: strconv.FormatUint(uint64(sticker.GetStickerId()), 10),
@ -532,6 +543,49 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker
return converted return converted
} }
func (mc *MessageConverter) reuploadStickerFromPack(ctx context.Context, sticker *signalpb.DataMessage_Sticker) (*bridgev2.ConvertedMessagePart, error) {
att := sticker.GetData()
content := &event.MessageEventContent{
Body: att.GetFileName(),
Info: &event.FileInfo{
MimeType: att.GetContentType(),
Width: int(att.GetWidth()),
Height: int(att.GetHeight()),
},
}
if mc.DirectMedia {
mediaID, err := signalid.DirectMediaSticker{
PackID: sticker.GetPackId(),
PackKey: sticker.GetPackKey(),
StickerID: sticker.GetStickerId(),
}.AsMediaID()
if err != nil {
return nil, err
}
content.URL, content.File, err = mc.Bridge.GenerateContentURI(ctx, getPortal(ctx).MXID, mediaID)
if err != nil {
return nil, err
}
} else {
data, err := signalmeow.DownloadStickerPackItem(ctx, sticker.GetPackId(), sticker.GetPackKey(), sticker.GetStickerId())
if err != nil {
return nil, fmt.Errorf("failed to download sticker from pack: %w", err)
}
if content.Info.MimeType == "" {
content.Info.MimeType = http.DetectContentType(data)
}
content.Info.Size = len(data)
content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, content.Body, content.Info.MimeType)
if err != nil {
return nil, fmt.Errorf("failed to upload sticker from pack: %w", err)
}
}
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: content,
}, nil
}
func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*string, error) { func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*string, error) {
data, err := mc.downloadAttachment(ctx, att, attMap, nil) data, err := mc.downloadAttachment(ctx, att, attMap, nil)
if err != nil { if err != nil {
@ -793,6 +847,28 @@ func (mc *MessageConverter) convertPollTerminateToMatrix(ctx context.Context, se
} }
} }
func (mc *MessageConverter) convertPinToMatrix(dm *signalpb.DataMessage) (*bridgev2.ConvertedMessagePart, *networkid.MessageOptionalPartID) {
body := "Pinned a message"
targetAuthor, targetTS := dm.GetPinMessage().GetTargetAuthorAciBinary(), dm.GetPinMessage().GetTargetSentTimestamp()
if dm.PinMessage == nil {
body = "Unpinned a message"
targetAuthor, targetTS = dm.GetUnpinMessage().GetTargetAuthorAciBinary(), dm.GetUnpinMessage().GetTargetSentTimestamp()
}
var replyTo *networkid.MessageOptionalPartID
if len(targetAuthor) == 16 {
replyTo = &networkid.MessageOptionalPartID{
MessageID: signalid.MakeMessageID(uuid.UUID(targetAuthor), targetTS),
}
}
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: body,
},
}, replyTo
}
var invalidPollVote = &bridgev2.ConvertedMessagePart{ var invalidPollVote = &bridgev2.ConvertedMessagePart{
Type: event.EventUnstablePollResponse, Type: event.EventUnstablePollResponse,
Content: &event.MessageEventContent{}, Content: &event.MessageEventContent{},

View file

@ -72,6 +72,8 @@ type Client struct {
storageSyncLock sync.Mutex storageSyncLock sync.Mutex
storageSyncQueued atomic.Bool storageSyncQueued atomic.Bool
storageVersion uint64
storageRecords []string
writeCallbackCounter chan time.Time writeCallbackCounter chan time.Time
} }

View file

@ -203,6 +203,7 @@ func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsi
delete(gc.data, id) delete(gc.data, id)
return nil return nil
} }
prevMembers := cached.getMemberServiceIDs()
// Pending member adds, promotes and removes // Pending member adds, promotes and removes
cached.PendingMembers = append(cached.PendingMembers, change.AddPendingMembers...) cached.PendingMembers = append(cached.PendingMembers, change.AddPendingMembers...)
@ -304,6 +305,9 @@ func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsi
if change.ModifyAnnouncementsOnly != nil { if change.ModifyAnnouncementsOnly != nil {
cached.AnnouncementsOnly = *change.ModifyAnnouncementsOnly cached.AnnouncementsOnly = *change.ModifyAnnouncementsOnly
} }
if change.TerminateGroup {
cached.Terminated = true
}
if change.ModifyDisappearingMessagesDuration != nil { if change.ModifyDisappearingMessagesDuration != nil {
cached.DisappearingMessagesDuration = *change.ModifyDisappearingMessagesDuration cached.DisappearingMessagesDuration = *change.ModifyDisappearingMessagesDuration
} }
@ -319,6 +323,12 @@ func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsi
cached.UpdatedAt = time.Now() cached.UpdatedAt = time.Now()
cached.Revision = change.Revision cached.Revision = change.Revision
if len(endorsementResponse) == 0 {
if !slices.Equal(prevMembers, cached.getMemberServiceIDs()) {
delete(gc.data, id)
}
return nil
}
endorsement, memberEndorsements, err := endorsementResponse.ReceiveWithServiceIDs( endorsement, memberEndorsements, err := endorsementResponse.ReceiveWithServiceIDs(
cached.getMemberServiceIDs(), cached.getMemberServiceIDs(),
gc.serviceID, gc.serviceID,

View file

@ -81,6 +81,7 @@ type Group struct {
Members []*GroupMember Members []*GroupMember
Description string Description string
AnnouncementsOnly bool AnnouncementsOnly bool
Terminated bool
Revision uint32 Revision uint32
DisappearingMessagesDuration uint32 DisappearingMessagesDuration uint32
AccessControl *GroupAccessControl AccessControl *GroupAccessControl
@ -206,6 +207,7 @@ type GroupChange struct {
DeleteBannedMembers []*libsignalgo.ServiceID DeleteBannedMembers []*libsignalgo.ServiceID
PromotePendingPniAciMembers []*PromotePendingPniAciMember PromotePendingPniAciMembers []*PromotePendingPniAciMember
ModifyInviteLinkPassword *types.SerializedInviteLinkPassword ModifyInviteLinkPassword *types.SerializedInviteLinkPassword
TerminateGroup bool
} }
func (groupChange *GroupChange) isEmpty() bool { func (groupChange *GroupChange) isEmpty() bool {
@ -226,7 +228,8 @@ func (groupChange *GroupChange) isEmpty() bool {
len(groupChange.PromoteRequestingMembers) == 0 && len(groupChange.PromoteRequestingMembers) == 0 &&
groupChange.ModifyDescription == nil && groupChange.ModifyDescription == nil &&
groupChange.ModifyAnnouncementsOnly == nil && groupChange.ModifyAnnouncementsOnly == nil &&
len(groupChange.AddBannedMembers) == 0 len(groupChange.AddBannedMembers) == 0 &&
!groupChange.TerminateGroup
} }
func (groupChange *GroupChange) resolveConflict(group *Group) { func (groupChange *GroupChange) resolveConflict(group *Group) {
@ -497,6 +500,8 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
// These aren't encrypted // These aren't encrypted
decryptedGroup.AvatarPath = encryptedGroup.AvatarUrl decryptedGroup.AvatarPath = encryptedGroup.AvatarUrl
decryptedGroup.Revision = encryptedGroup.Version decryptedGroup.Revision = encryptedGroup.Version
decryptedGroup.Terminated = encryptedGroup.Terminated
decryptedGroup.AnnouncementsOnly = encryptedGroup.AnnouncementsOnly
// Decrypt members // Decrypt members
for _, member := range encryptedGroup.Members { for _, member := range encryptedGroup.Members {
@ -1076,6 +1081,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
if encryptedActions.ModifyAnnouncementsOnly != nil { if encryptedActions.ModifyAnnouncementsOnly != nil {
decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly
} }
decryptedGroupChange.TerminateGroup = encryptedActions.TerminateGroup != nil
if encryptedActions.ModifyDisappearingMessageTimer != nil && len(encryptedActions.ModifyDisappearingMessageTimer.Timer) > 0 { if encryptedActions.ModifyDisappearingMessageTimer != nil && len(encryptedActions.ModifyDisappearingMessageTimer.Timer) > 0 {
timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessageTimer.Timer) timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessageTimer.Timer)
if err != nil { if err != nil {
@ -1801,8 +1807,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
"Cached-Send-Endorsements": "0", "Cached-Send-Endorsements": "0",
}, },
} }
// highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false path := fmt.Sprintf("/v2/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 7, includeFirstState)
path := fmt.Sprintf("/v2/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 5, includeFirstState)
response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, path, opts) response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, path, opts)
defer web.CloseBody(response) defer web.CloseBody(response)
if err != nil { if err != nil {

View file

@ -378,6 +378,19 @@ func (cli *Client) incomingAPIMessageHandler(ctx context.Context, req *signalpb.
Str("envelope_type", signalpb.Envelope_Type_name[int32(envelope.GetType())]). Str("envelope_type", signalpb.Envelope_Type_name[int32(envelope.GetType())]).
Msg("Received envelope") Msg("Received envelope")
if envelope.GetType() == signalpb.Envelope_SERVER_DELIVERY_RECEIPT {
if sourceServiceID.Type == libsignalgo.ServiceIDTypeACI && sourceServiceID.UUID != uuid.Nil && !cli.handleEvent(&events.Receipt{
Sender: sourceServiceID.UUID,
Content: &signalpb.ReceiptMessage{
Type: signalpb.ReceiptMessage_DELIVERY.Enum(),
Timestamp: []uint64{envelope.GetClientTimestamp()},
},
}) {
return nil, ErrHandlerFailed
}
return &web.SimpleResponse{Status: 200}, nil
}
result := cli.decryptEnvelope(ctx, envelope, sourceServiceID, destinationServiceID) result := cli.decryptEnvelope(ctx, envelope, sourceServiceID, destinationServiceID)
err = cli.handleDecryptedResult(ctx, result, envelope, destinationServiceID) err = cli.handleDecryptedResult(ctx, result, envelope, destinationServiceID)
@ -909,7 +922,7 @@ func (cli *Client) handlePNISignatureMessage(ctx context.Context, sender libsign
Stringer("aci", sender.UUID). Stringer("aci", sender.UUID).
Stringer("pni", pni). Stringer("pni", pni).
Msg("Fetching PNI identity for signature verification as it wasn't found in store") Msg("Fetching PNI identity for signature verification as it wasn't found in store")
err = cli.FetchAndProcessPreKey(ctx, pniServiceID, 0) err = cli.FetchAndProcessPreKey(ctx, pniServiceID, 1)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch prekey for PNI %s after identity wasn't found in store: %w", pni, err) return fmt.Errorf("failed to fetch prekey for PNI %s after identity wasn't found in store: %w", pni, err)
} else if pniIdentity, err = cli.Store.IdentityKeyStore.GetIdentityKey(ctx, pniServiceID); err != nil { } else if pniIdentity, err = cli.Store.IdentityKeyStore.GetIdentityKey(ctx, pniServiceID); err != nil {

View file

@ -107,9 +107,6 @@ func (cli *Client) decryptEnvelope(
Unencrypted: true, Unencrypted: true,
} }
case signalpb.Envelope_SERVER_DELIVERY_RECEIPT:
return DecryptionResult{Err: fmt.Errorf("server delivery receipt envelopes are not yet supported")}
case signalpb.Envelope_UNKNOWN: case signalpb.Envelope_UNKNOWN:
return DecryptionResult{Err: fmt.Errorf("unknown envelope type")} return DecryptionResult{Err: fmt.Errorf("unknown envelope type")}

View file

@ -221,8 +221,8 @@ func (cli *Client) sendToGroupWithSenderKey(
header.Set("Unidentified-Access-Key", xak.String()) header.Set("Unidentified-Access-Key", xak.String())
} }
path := fmt.Sprintf( path := fmt.Sprintf(
"/v1/messages/multi_recipient?ts=%d&urgent=%t&online=false", "/v1/messages/multi_recipient?ts=%d&urgent=%t&online=%t",
messageTimestamp, isUrgent(content), messageTimestamp, isUrgent(content), content.GetTypingMessage() != nil,
) )
log.Debug(). log.Debug().
Any("recipients", ski.SharedWith). Any("recipients", ski.SharedWith).

View file

@ -964,7 +964,7 @@ func (cli *Client) sendContent(
outgoingMessages := MyMessages{ outgoingMessages := MyMessages{
Timestamp: messageTimestamp, Timestamp: messageTimestamp,
Online: false, Online: content.GetTypingMessage() != nil,
Urgent: isUrgent(content), Urgent: isUrgent(content),
Messages: messages, Messages: messages,
} }

View file

@ -55,18 +55,30 @@ func (cli *Client) StorageSync(ctx context.Context) {
cli.storageSyncQueued.Store(false) cli.storageSyncQueued.Store(false)
log := cli.Log.With().Str("action", "sync storage").Logger() log := cli.Log.With().Str("action", "sync storage").Logger()
ctx = log.WithContext(ctx) ctx = log.WithContext(ctx)
// TODO only fetch changed entries update, err := cli.FetchStorage(ctx, cli.Store.MasterKey, cli.storageVersion, cli.storageRecords)
update, err := cli.FetchStorage(ctx, cli.Store.MasterKey, 0, nil)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to fetch storage") log.Err(err).Msg("Failed to fetch storage")
return return
} else if update == nil {
log.Debug().Uint64("version", cli.storageVersion).Msg("Storage manifest is already up to date")
return
} }
err = cli.Store.DoContactTxn(ctx, func(ctx context.Context) error { err = cli.Store.DoContactTxn(ctx, func(ctx context.Context) error {
return cli.processStorageInTxn(ctx, update) return cli.processStorageInTxn(ctx, update)
}) })
if err != nil { if err != nil {
log.Err(err).Msg("Failed to process storage update") log.Err(err).Msg("Failed to process storage update")
return
} }
log.Debug().
Uint64("previous_version", cli.storageVersion).
Uint64("version", update.Version).
Int("new_records", len(update.NewRecords)).
Int("removed_records", len(update.RemovedRecords)).
Int("missing_records", len(update.MissingRecords)).
Msg("Processed storage update")
cli.storageVersion = update.Version
cli.storageRecords = update.KnownRecords
} }
func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdate) error { func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdate) error {
@ -101,8 +113,9 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat
recipient.Profile.Name = strings.TrimSpace(fmt.Sprintf("%s %s", contact.GivenName, contact.FamilyName)) recipient.Profile.Name = strings.TrimSpace(fmt.Sprintf("%s %s", contact.GivenName, contact.FamilyName))
} }
if contact.SystemGivenName != "" || contact.SystemFamilyName != "" { if contact.SystemGivenName != "" || contact.SystemFamilyName != "" {
changed = true newContactName := strings.TrimSpace(fmt.Sprintf("%s %s", contact.SystemGivenName, contact.SystemFamilyName))
recipient.ContactName = strings.TrimSpace(fmt.Sprintf("%s %s", contact.SystemGivenName, contact.SystemFamilyName)) changed = changed || recipient.ContactName != newContactName
recipient.ContactName = newContactName
} }
newNickname := "" newNickname := ""
if contact.Nickname != nil { if contact.Nickname != nil {
@ -120,7 +133,7 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat
changed = true changed = true
recipient.Blocked = contact.Blocked recipient.Blocked = contact.Blocked
} }
if !ptr.Val(recipient.Whitelisted) { if !ptr.Val(recipient.Whitelisted) && (recipient.Whitelisted == nil || contact.Whitelisted) {
changed = true changed = true
recipient.Whitelisted = &contact.Whitelisted recipient.Whitelisted = &contact.Whitelisted
} }
@ -158,7 +171,9 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat
return fmt.Errorf("failed to save device after receiving account record: %w", err) return fmt.Errorf("failed to save device after receiving account record: %w", err)
} }
log.Debug().Msg("Saved device after receiving account record") log.Debug().Msg("Saved device after receiving account record")
case *signalpb.StorageRecord_GroupV1, *signalpb.StorageRecord_StoryDistributionList: case *signalpb.StorageRecord_GroupV1, *signalpb.StorageRecord_StoryDistributionList, *signalpb.StorageRecord_StickerPack,
*signalpb.StorageRecord_CallLink, *signalpb.StorageRecord_ChatFolder, *signalpb.StorageRecord_NotificationProfile,
*signalpb.StorageRecord_FavoriteSticker:
// irrelevant data // irrelevant data
default: default:
log.Warn().Type("type", data).Str("item_id", record.StorageID).Msg("Unknown storage record type") log.Warn().Type("type", data).Str("item_id", record.StorageID).Msg("Unknown storage record type")
@ -178,6 +193,7 @@ type StorageUpdate struct {
NewRecords []*DecryptedStorageRecord NewRecords []*DecryptedStorageRecord
RemovedRecords []string RemovedRecords []string
MissingRecords []string MissingRecords []string
KnownRecords []string
} }
func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVersion uint64, existingKeys []string) (*StorageUpdate, error) { func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVersion uint64, existingKeys []string) (*StorageUpdate, error) {
@ -189,6 +205,7 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe
return nil, nil return nil, nil
} }
removedKeys := make([]string, 0) removedKeys := make([]string, 0)
knownKeys := make([]string, 0, len(manifest.GetIdentifiers()))
newKeys := manifestRecordToMap(manifest.GetIdentifiers()) newKeys := manifestRecordToMap(manifest.GetIdentifiers())
slices.Sort(existingKeys) slices.Sort(existingKeys)
existingKeys = slices.Compact(existingKeys) existingKeys = slices.Compact(existingKeys)
@ -196,6 +213,7 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe
_, isStillThere := newKeys[key] _, isStillThere := newKeys[key]
if isStillThere { if isStillThere {
delete(newKeys, key) delete(newKeys, key)
knownKeys = append(knownKeys, key)
} else { } else {
removedKeys = append(removedKeys, key) removedKeys = append(removedKeys, key)
} }
@ -205,11 +223,15 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, record := range newRecords {
knownKeys = append(knownKeys, record.StorageID)
}
return &StorageUpdate{ return &StorageUpdate{
Version: manifest.GetVersion(), Version: manifest.GetVersion(),
NewRecords: newRecords, NewRecords: newRecords,
RemovedRecords: removedKeys, RemovedRecords: removedKeys,
MissingRecords: missingKeys, MissingRecords: missingKeys,
KnownRecords: knownKeys,
}, nil }, nil
} }

View file

@ -22,6 +22,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -116,6 +117,9 @@ func (s *SignalWebsocket) Close() (err error) {
s.closeCalled.Store(true) s.closeCalled.Store(true)
if ws := s.ws.Swap(nil); ws != nil { if ws := s.ws.Swap(nil); ws != nil {
err = ws.Close(websocket.StatusNormalClosure, "") err = ws.Close(websocket.StatusNormalClosure, "")
if errors.Is(err, net.ErrClosed) {
err = nil
}
} }
if cancelLoop := s.cancel.Swap(nil); cancelLoop != nil { if cancelLoop := s.cancel.Swap(nil); cancelLoop != nil {
(*cancelLoop)() (*cancelLoop)()
@ -319,6 +323,11 @@ func (s *SignalWebsocket) connectLoop(
} }
// Succssfully connected // Succssfully connected
if resp != nil {
if alerts := resp.Header.Values("X-Signal-Alert"); len(alerts) > 0 {
log.Warn().Strs("alerts", alerts).Msg("Signal server sent alerts when opening websocket")
}
}
s.pushStatus(ctx, SignalWebsocketConnectionEventConnected, nil) s.pushStatus(ctx, SignalWebsocketConnectionEventConnected, nil)
s.ws.Store(ws) s.ws.Store(ws)
retrying = false retrying = false