Implement WhatsApp->Matrix group info updates

This commit is contained in:
Tulir Asokan 2021-10-28 12:59:22 +03:00
parent 1ad17048cc
commit 149e9bc8af
6 changed files with 280 additions and 324 deletions

View file

@ -42,15 +42,13 @@ type BridgeConfig struct {
End bool `yaml:"end"`
} `yaml:"call_notices"`
InitialChatSync int `yaml:"initial_chat_sync_count"`
InitialHistoryFill int `yaml:"initial_history_fill_count"`
HistoryDisableNotifs bool `yaml:"initial_history_disable_notifications"`
RecoverChatSync int `yaml:"recovery_chat_sync_count"`
RecoverHistory bool `yaml:"recovery_history_backfill"`
ChatMetaSync bool `yaml:"chat_meta_sync"`
UserAvatarSync bool `yaml:"user_avatar_sync"`
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
SyncChatMaxAge int64 `yaml:"sync_max_chat_age"`
HistorySync struct {
CreatePortals bool `yaml:"create_portals"`
Backfill bool `yaml:"backfill"`
DoublePuppetBackfill bool `yaml:"double_puppet_backfill"`
}
UserAvatarSync bool `yaml:"user_avatar_sync"`
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"`
SyncDirectChatList bool `yaml:"sync_direct_chat_list"`
@ -58,7 +56,6 @@ type BridgeConfig struct {
DefaultBridgePresence bool `yaml:"default_bridge_presence"`
LoginSharedSecret string `yaml:"login_shared_secret"`
DoublePuppetBackfill bool `yaml:"double_puppet_backfill"`
PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
BridgeNotices bool `yaml:"bridge_notices"`
ResendBridgeInfo bool `yaml:"resend_bridge_info"`
@ -95,7 +92,6 @@ type BridgeConfig struct {
}
func (bc *BridgeConfig) setDefaults() {
bc.DeliveryReceipts = false
bc.MaxConnectionAttempts = 3
bc.ConnectionRetryDelay = -1
bc.ReportConnectionRetry = true
@ -104,22 +100,14 @@ func (bc *BridgeConfig) setDefaults() {
bc.CallNotices.Start = true
bc.CallNotices.End = true
bc.InitialChatSync = 10
bc.InitialHistoryFill = 20
bc.RecoverChatSync = -1
bc.RecoverHistory = true
bc.ChatMetaSync = true
bc.HistorySync.CreatePortals = true
bc.UserAvatarSync = true
bc.BridgeMatrixLeave = true
bc.SyncChatMaxAge = 259200
bc.SyncWithCustomPuppets = true
bc.DefaultBridgePresence = true
bc.DefaultBridgeReceipts = true
bc.LoginSharedSecret = ""
bc.DoublePuppetBackfill = false
bc.PrivateChatPortalMeta = false
bc.BridgeNotices = true
bc.EnableStatusBroadcast = true
}

View file

@ -101,32 +101,20 @@ bridge:
start: true
end: true
# Number of chats to sync for new users.
initial_chat_sync_count: 10
# Number of old messages to fill when creating new portal rooms.
initial_history_fill_count: 20
# Whether or not notifications should be turned off while filling initial history.
# Only applicable when using double puppeting.
initial_history_disable_notifications: false
# Maximum number of chats to sync when recovering from downtime.
# Set to -1 to sync all new chats during downtime.
recovery_chat_sync_limit: -1
# Whether or not to sync history when recovering from downtime.
recovery_history_backfill: true
# Whether or not portal info should be fetched from the server when syncing,
# instead of relying on finding any changes in the message history.
# If you get 599 errors often, you should try disabling this.
chat_meta_sync: true
history_sync:
# Whether to create portals from history sync payloads from WhatsApp.
create_portals: true
# Whether to enable backfilling history sync payloads from WhatsApp using batch sending
# This requires a server with MSC2716 support, which is currently an experimental feature in synapse.
# It can be enabled by setting experimental_features -> enable_msc2716 to true in homeserver.yaml.
backfill: false
# Whether to use custom puppet for backfilling.
# In order to use this, the custom puppets must be in the appservice's user ID namespace.
double_puppet_backfill: false
# Whether or not puppet avatars should be fetched from the server even if an avatar is already set.
# If you get 599 errors often, you should try disabling this.
user_avatar_sync: true
# Whether or not Matrix users leaving groups should be bridged to WhatsApp
bridge_matrix_leave: true
# Maximum number of seconds since last message in chat to skip
# syncing the chat in any case. This setting will take priority
# over both recovery_chat_sync_limit and initial_chat_sync_count.
# Default is 3 days = 259200 seconds
sync_max_chat_age: 259200
# Whether or not to sync with custom puppets to receive EDUs that
# are not normally sent to appservices.
@ -147,18 +135,12 @@ bridge:
# manually.
login_shared_secret: null
# Whether to use custom puppet for backfilling.
# In order to use this, the custom puppets must be in the appservice's user ID namespace.
double_puppet_backfill: false
# Whether or not to explicitly set the avatar and room name for private
# chat portal rooms. This can be useful if the previous field works fine,
# but causes room avatar/name bugs.
# Whether to explicitly set the avatar and room name for private chat portal rooms.
private_chat_portal_meta: false
# Whether or not Matrix m.notice-type messages should be bridged.
# Whether Matrix m.notice-type messages should be bridged.
bridge_notices: true
# Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.
# This field will automatically be changed back to false after it,
# except if the config file is not writable.
# This field will automatically be changed back to false after it, except if the config file is not writable.
resend_bridge_info: false
# When using double puppeting, should muted chats be muted in Matrix?
mute_bridging: false

2
go.mod
View file

@ -8,7 +8,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.9
github.com/prometheus/client_golang v1.11.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
go.mau.fi/whatsmeow v0.0.0-20211027183133-07bcb11ceb48
go.mau.fi/whatsmeow v0.0.0-20211028095847-2a72655ef600
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
google.golang.org/protobuf v1.27.1
gopkg.in/yaml.v2 v2.4.0

4
go.sum
View file

@ -139,8 +139,8 @@ github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8=
github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 h1:xpQTMgJGGaF+c8jV/LA/FVXAPJxZbSAGeflOc+Ly6uQ=
go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2/go.mod h1:3XlVlwOfp8f9Wri+C1D4ORqgUsN4ZvunJOoPjQMBhos=
go.mau.fi/whatsmeow v0.0.0-20211027183133-07bcb11ceb48 h1:e4cAP66APziJd8YFAJbYtPtkMJLi4wullnqs87lWZWo=
go.mau.fi/whatsmeow v0.0.0-20211027183133-07bcb11ceb48/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI=
go.mau.fi/whatsmeow v0.0.0-20211028095847-2a72655ef600 h1:3huw0OOUNmU1c9vJHifEdTJJnFn6UchoHFaazdHkd34=
go.mau.fi/whatsmeow v0.0.0-20211028095847-2a72655ef600/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

313
portal.go
View file

@ -193,14 +193,6 @@ type Portal struct {
hasRelaybot *bool
}
func (portal *Portal) syncDoublePuppetDetailsAfterCreate(source *User) {
doublePuppet := portal.bridge.GetPuppetByCustomMXID(source.MXID)
if doublePuppet == nil {
return
}
source.syncChatDoublePuppetDetails(doublePuppet, portal, true)
}
func (portal *Portal) handleMessageLoop() {
for msg := range portal.messages {
if len(portal.MXID) == 0 {
@ -214,7 +206,6 @@ func (portal *Portal) handleMessageLoop() {
portal.log.Errorln("Failed to create portal room:", err)
continue
}
portal.syncDoublePuppetDetailsAfterCreate(msg.source)
}
if msg.evt != nil {
portal.handleMessage(msg.source, msg.evt)
@ -310,6 +301,7 @@ func (portal *Portal) convertMessage(intent *appservice.IntentAPI, source *User,
const UndecryptableMessageNotice = "Decrypting message from WhatsApp failed, waiting for sender to re-send... " +
"([learn more](https://faq.whatsapp.com/general/security-and-privacy/seeing-waiting-for-this-message-this-may-take-a-while))"
var undecryptableMessageContent event.MessageEventContent
func init() {
@ -391,7 +383,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
_, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{
Reason: "The undecryptable message was actually the deletion of another message",
})
existingMsg.UpdateMXID("net.maunium.whatsapp.fake::" + existingMsg.MXID, false)
existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, false)
}
} else {
portal.log.Warnln("Unhandled message:", evt.Info, evt.Message)
@ -399,7 +391,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
_, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{
Reason: "The undecryptable message contained an unsupported message type",
})
existingMsg.UpdateMXID("net.maunium.whatsapp.fake::" + existingMsg.MXID, false)
existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, false)
}
return
}
@ -557,7 +549,7 @@ func (portal *Portal) SyncParticipants(source *User, metadata *types.GroupInfo)
portal.kickExtraUsers(participantMap)
}
func (portal *Portal) UpdateAvatar(user *User, updateInfo bool) bool {
func (portal *Portal) UpdateAvatar(user *User, setBy types.JID, updateInfo bool) bool {
avatar, err := user.Client.GetProfilePictureInfo(portal.Key.JID, false)
if err != nil {
if !errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) {
@ -585,7 +577,14 @@ func (portal *Portal) UpdateAvatar(user *User, updateInfo bool) bool {
}
if len(portal.MXID) > 0 {
_, err := portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL)
intent := portal.MainIntent()
if !setBy.IsEmpty() {
intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
}
_, err = intent.SetRoomAvatar(portal.MXID, portal.AvatarURL)
if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
_, err = portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL)
}
if err != nil {
portal.log.Warnln("Failed to set room topic:", err)
return false
@ -598,20 +597,22 @@ func (portal *Portal) UpdateAvatar(user *User, updateInfo bool) bool {
return true
}
func (portal *Portal) UpdateName(name string, setBy types.JID, intent *appservice.IntentAPI, updateInfo bool) bool {
func (portal *Portal) UpdateName(name string, setBy types.JID, updateInfo bool) bool {
if name == "" && portal.IsBroadcastList() {
name = UnnamedBroadcastName
}
if portal.Name != name {
portal.log.Debugfln("Updating name %s -> %s", portal.Name, name)
portal.Name = name
if intent == nil {
intent = portal.MainIntent()
if !setBy.IsEmpty() {
intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
}
intent := portal.MainIntent()
if !setBy.IsEmpty() {
intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
}
_, err := intent.SetRoomName(portal.MXID, name)
if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
_, err = portal.MainIntent().SetRoomName(portal.MXID, name)
}
if err == nil {
if updateInfo {
portal.UpdateBridgeInfo()
@ -625,17 +626,19 @@ func (portal *Portal) UpdateName(name string, setBy types.JID, intent *appservic
return false
}
func (portal *Portal) UpdateTopic(topic string, setBy types.JID, intent *appservice.IntentAPI, updateInfo bool) bool {
func (portal *Portal) UpdateTopic(topic string, setBy types.JID, updateInfo bool) bool {
if portal.Topic != topic {
portal.log.Debugfln("Updating topic %s -> %s", portal.Topic, topic)
portal.Topic = topic
if intent == nil {
intent = portal.MainIntent()
if !setBy.IsEmpty() {
intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
}
intent := portal.MainIntent()
if !setBy.IsEmpty() {
intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
}
_, err := intent.SetRoomTopic(portal.MXID, topic)
if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
_, err = portal.MainIntent().SetRoomTopic(portal.MXID, topic)
}
if err == nil {
if updateInfo {
portal.UpdateBridgeInfo()
@ -654,8 +657,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
return false
} else if portal.IsStatusBroadcastList() {
update := false
update = portal.UpdateName(StatusBroadcastName, types.EmptyJID, nil, false) || update
update = portal.UpdateTopic(StatusBroadcastTopic, types.EmptyJID, nil, false) || update
update = portal.UpdateName(StatusBroadcastName, types.EmptyJID, false) || update
update = portal.UpdateTopic(StatusBroadcastTopic, types.EmptyJID, false) || update
return update
} else if portal.IsBroadcastList() {
update := false
@ -680,8 +683,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
portal.SyncParticipants(user, metadata)
update := false
update = portal.UpdateName(metadata.Name, metadata.NameSetBy, nil, false) || update
update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, nil, false) || update
update = portal.UpdateName(metadata.Name, metadata.NameSetBy, false) || update
update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, false) || update
portal.RestrictMessageSending(metadata.IsAnnounce)
portal.RestrictMetadataChanges(metadata.IsLocked)
@ -764,7 +767,7 @@ func (portal *Portal) Sync(user *User) bool {
update := false
update = portal.UpdateMetadata(user) || update
if !portal.IsPrivateChat() && !portal.IsBroadcastList() && portal.Avatar == "" {
update = portal.UpdateAvatar(user, false) || update
update = portal.UpdateAvatar(user, types.EmptyJID, false) || update
}
if update {
portal.Update()
@ -798,35 +801,35 @@ func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent {
}
}
//func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) id.EventID {
// levels, err := portal.MainIntent().PowerLevels(portal.MXID)
// if err != nil {
// levels = portal.GetBasePowerLevels()
// }
// newLevel := 0
// if setAdmin {
// newLevel = 50
// }
// changed := false
// for _, jid := range jids {
// puppet := portal.bridge.GetPuppetByJID(jid)
// changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed
//
// user := portal.bridge.GetUserByJID(jid)
// if user != nil {
// changed = levels.EnsureUserLevel(user.MXID, newLevel) || changed
// }
// }
// if changed {
// resp, err := portal.MainIntent().SetPowerLevels(portal.MXID, levels)
// if err != nil {
// portal.log.Errorln("Failed to change power levels:", err)
// } else {
// return resp.EventID
// }
// }
// return ""
//}
func (portal *Portal) ChangeAdminStatus(jids []types.JID, setAdmin bool) id.EventID {
levels, err := portal.MainIntent().PowerLevels(portal.MXID)
if err != nil {
levels = portal.GetBasePowerLevels()
}
newLevel := 0
if setAdmin {
newLevel = 50
}
changed := false
for _, jid := range jids {
puppet := portal.bridge.GetPuppetByJID(jid)
changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed
user := portal.bridge.GetUserByJID(jid)
if user != nil {
changed = levels.EnsureUserLevel(user.MXID, newLevel) || changed
}
}
if changed {
resp, err := portal.MainIntent().SetPowerLevels(portal.MXID, levels)
if err != nil {
portal.log.Errorln("Failed to change power levels:", err)
} else {
return resp.EventID
}
}
return ""
}
func (portal *Portal) RestrictMessageSending(restrict bool) id.EventID {
levels, err := portal.MainIntent().PowerLevels(portal.MXID)
@ -1079,7 +1082,7 @@ func (portal *Portal) backfill(source *User, messages []*waProto.HistorySyncMsg)
intent = puppet.DefaultIntent()
} else {
intent = puppet.IntentFor(portal)
if intent.IsCustomPuppet && !portal.bridge.Config.Bridge.DoublePuppetBackfill {
if intent.IsCustomPuppet && !portal.bridge.Config.Bridge.HistorySync.DoublePuppetBackfill {
intent = puppet.DefaultIntent()
addMember(puppet)
}
@ -1266,7 +1269,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
portal.Name = metadata.Name
portal.Topic = metadata.Topic
}
portal.UpdateAvatar(user, false)
portal.UpdateAvatar(user, types.EmptyJID, false)
}
bridgeInfoStateKey, bridgeInfo := portal.getBridgeInfo()
@ -1337,6 +1340,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
}
portal.ensureUserInvited(user)
user.syncChatDoublePuppetDetails(portal, true)
if metadata != nil {
portal.SyncParticipants(user, metadata)
@ -1678,95 +1682,104 @@ func (portal *Portal) convertContactMessage(intent *appservice.IntentAPI, msg *w
return &ConvertedMessage{Intent: intent, Type: event.EventMessage, Content: content}
}
// FIXME
//func (portal *Portal) tryKickUser(userID id.UserID, intent *appservice.IntentAPI) error {
// _, err := intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID})
// if err != nil {
// httpErr, ok := err.(mautrix.HTTPError)
// if ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_FORBIDDEN" {
// _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID})
// }
// }
// return err
//}
//
//func (portal *Portal) removeUser(isSameUser bool, kicker *appservice.IntentAPI, target id.UserID, targetIntent *appservice.IntentAPI) {
// if !isSameUser || targetIntent == nil {
// err := portal.tryKickUser(target, kicker)
// if err != nil {
// portal.log.Warnfln("Failed to kick %s from %s: %v", target, portal.MXID, err)
// if targetIntent != nil {
// _, _ = targetIntent.LeaveRoom(portal.MXID)
// }
// }
// } else {
// _, err := targetIntent.LeaveRoom(portal.MXID)
// if err != nil {
// portal.log.Warnfln("Failed to leave portal as %s: %v", target, err)
// _, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: target})
// }
// }
//}
//
//func (portal *Portal) HandleWhatsAppKick(source *User, senderJID string, jids []string) {
// sender := portal.bridge.GetPuppetByJID(senderJID)
// senderIntent := sender.IntentFor(portal)
// for _, jid := range jids {
// if source != nil && source.JID == jid {
// portal.log.Debugln("Ignoring self-kick by", source.MXID)
// continue
// }
// puppet := portal.bridge.GetPuppetByJID(jid)
// portal.removeUser(puppet.JID == sender.JID, senderIntent, puppet.MXID, puppet.DefaultIntent())
//
// if !portal.IsBroadcastList() {
// user := portal.bridge.GetUserByJID(jid)
// if user != nil {
// var customIntent *appservice.IntentAPI
// if puppet.CustomMXID == user.MXID {
// customIntent = puppet.CustomIntent()
// }
// portal.removeUser(puppet.JID == sender.JID, senderIntent, user.MXID, customIntent)
// }
// }
// }
//}
//
//func (portal *Portal) HandleWhatsAppInvite(source *User, senderJID string, intent *appservice.IntentAPI, jids []string) (evtID id.EventID) {
// if intent == nil {
// intent = portal.MainIntent()
// if senderJID != "unknown" {
// sender := portal.bridge.GetPuppetByJID(senderJID)
// intent = sender.IntentFor(portal)
// }
// }
// for _, jid := range jids {
// puppet := portal.bridge.GetPuppetByJID(jid)
// puppet.SyncContact(source, true)
// content := event.Content{
// Parsed: event.MemberEventContent{
// Membership: "invite",
// Displayname: puppet.Displayname,
// AvatarURL: puppet.AvatarURL.CUString(),
// },
// Raw: map[string]interface{}{
// "net.maunium.whatsapp.puppet": true,
// },
// }
// resp, err := intent.SendStateEvent(portal.MXID, event.StateMember, puppet.MXID.String(), &content)
// if err != nil {
// portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, intent.UserID, err)
// _ = portal.MainIntent().EnsureInvited(portal.MXID, puppet.MXID)
// } else {
// evtID = resp.EventID
// }
// err = puppet.DefaultIntent().EnsureJoined(portal.MXID)
// if err != nil {
// portal.log.Errorfln("Failed to ensure %s is joined: %v", puppet.MXID, err)
// }
// }
// return
//}
func (portal *Portal) tryKickUser(userID id.UserID, intent *appservice.IntentAPI) error {
_, err := intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID})
if err != nil {
httpErr, ok := err.(mautrix.HTTPError)
if ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_FORBIDDEN" {
_, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID})
}
}
return err
}
func (portal *Portal) removeUser(isSameUser bool, kicker *appservice.IntentAPI, target id.UserID, targetIntent *appservice.IntentAPI) {
if !isSameUser || targetIntent == nil {
err := portal.tryKickUser(target, kicker)
if err != nil {
portal.log.Warnfln("Failed to kick %s from %s: %v", target, portal.MXID, err)
if targetIntent != nil {
_, _ = portal.leaveWithPuppetMeta(targetIntent)
}
}
} else {
_, err := portal.leaveWithPuppetMeta(targetIntent)
if err != nil {
portal.log.Warnfln("Failed to leave portal as %s: %v", target, err)
_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: target})
}
}
}
func (portal *Portal) HandleWhatsAppKick(source *User, senderJID types.JID, jids []types.JID) {
sender := portal.bridge.GetPuppetByJID(senderJID)
senderIntent := sender.IntentFor(portal)
for _, jid := range jids {
if source != nil && source.JID.User == jid.User {
portal.log.Debugln("Ignoring self-kick by", source.MXID)
continue
}
puppet := portal.bridge.GetPuppetByJID(jid)
portal.removeUser(puppet.JID == sender.JID, senderIntent, puppet.MXID, puppet.DefaultIntent())
if !portal.IsBroadcastList() {
user := portal.bridge.GetUserByJID(jid)
if user != nil {
var customIntent *appservice.IntentAPI
if puppet.CustomMXID == user.MXID {
customIntent = puppet.CustomIntent()
}
portal.removeUser(puppet.JID == sender.JID, senderIntent, user.MXID, customIntent)
}
}
}
}
func (portal *Portal) leaveWithPuppetMeta(intent *appservice.IntentAPI) (*mautrix.RespSendEvent, error) {
content := event.Content{
Parsed: event.MemberEventContent{
Membership: event.MembershipLeave,
},
Raw: map[string]interface{}{
"net.maunium.whatsapp.puppet": true,
},
}
return intent.SendStateEvent(portal.MXID, event.StateMember, intent.UserID.String(), &content)
}
func (portal *Portal) HandleWhatsAppInvite(source *User, senderJID *types.JID, jids []types.JID) (evtID id.EventID) {
intent := portal.MainIntent()
if senderJID != nil && !senderJID.IsEmpty() {
sender := portal.bridge.GetPuppetByJID(*senderJID)
intent = sender.IntentFor(portal)
}
for _, jid := range jids {
puppet := portal.bridge.GetPuppetByJID(jid)
puppet.SyncContact(source, true)
content := event.Content{
Parsed: event.MemberEventContent{
Membership: "invite",
Displayname: puppet.Displayname,
AvatarURL: puppet.AvatarURL.CUString(),
},
Raw: map[string]interface{}{
"net.maunium.whatsapp.puppet": true,
},
}
resp, err := intent.SendStateEvent(portal.MXID, event.StateMember, puppet.MXID.String(), &content)
if err != nil {
portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, intent.UserID, err)
_ = portal.MainIntent().EnsureInvited(portal.MXID, puppet.MXID)
} else {
evtID = resp.EventID
}
err = puppet.DefaultIntent().EnsureJoined(portal.MXID)
if err != nil {
portal.log.Errorfln("Failed to ensure %s is joined: %v", puppet.MXID, err)
}
}
return
}
func (portal *Portal) makeMediaBridgeFailureMessage(intent *appservice.IntentAPI, info *types.MessageInfo, bridgeErr error, captionContent *event.MessageEventContent) *ConvertedMessage {
portal.log.Errorfln("Failed to bridge media for %s: %v", info.ID, bridgeErr)

213
user.go
View file

@ -376,13 +376,31 @@ func (user *User) handleHistorySync(evt *waProto.HistorySync) {
continue
}
portal := user.GetPortalByJID(jid)
err = portal.CreateMatrixRoom(user)
if err != nil {
user.log.Warnfln("Failed to create room for %s during backfill: %v", portal.Key.JID, err)
continue
muteEnd := time.Unix(int64(conv.GetMuteEndTime()), 0)
if muteEnd.After(time.Now()) {
_ = user.Client.Store.ChatSettings.PutMutedUntil(jid, muteEnd)
}
if conv.GetArchived() {
_ = user.Client.Store.ChatSettings.PutArchived(jid, true)
}
if conv.GetPinned() > 0 {
_ = user.Client.Store.ChatSettings.PutPinned(jid, true)
}
portal := user.GetPortalByJID(jid)
if user.bridge.Config.Bridge.HistorySync.CreatePortals {
err = portal.CreateMatrixRoom(user)
if err != nil {
user.log.Warnfln("Failed to create room for %s during backfill: %v", portal.Key.JID, err)
continue
}
}
if len(portal.MXID) > 0 && user.bridge.Config.Bridge.HistorySync.Backfill {
portal.backfill(user, conv.GetMessages())
if !conv.GetMarkedAsUnread() && conv.GetUnreadCount() == 0 {
user.markSelfReadFull(portal)
}
}
portal.backfill(user, conv.GetMessages())
}
}
@ -432,6 +450,10 @@ func (user *User) HandleEvent(event interface{}) {
go user.syncPuppet(v.JID)
case *events.PushName:
go user.syncPuppet(v.JID)
case *events.GroupInfo:
go user.handleGroupUpdate(v)
case *events.Picture:
go user.handlePictureUpdate(v)
case *events.Receipt:
go user.handleReceipt(v)
case *events.ChatPresence:
@ -540,29 +562,21 @@ type CustomReadReceipt struct {
DoublePuppet bool `json:"net.maunium.whatsapp.puppet,omitempty"`
}
func (user *User) syncChatDoublePuppetDetails(doublePuppet *Puppet, portal *Portal, justCreated bool) {
func (user *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool) {
doublePuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID)
if doublePuppet == nil {
return
}
if doublePuppet == nil || doublePuppet.CustomIntent() == nil || len(portal.MXID) == 0 {
return
}
intent := doublePuppet.CustomIntent()
// FIXME this might not be possible to do anymore
//if chat.UnreadCount == 0 && (justCreated || !user.bridge.Config.Bridge.MarkReadOnlyOnCreate) {
// lastMessage := user.bridge.DB.Message.GetLastInChatBefore(chat.Portal.Key, chat.ReceivedAt.Unix())
// if lastMessage != nil {
// err := intent.MarkReadWithContent(chat.Portal.MXID, lastMessage.MXID, &CustomReadReceipt{DoublePuppet: true})
// if err != nil {
// user.log.Warnfln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, chat.Portal.MXID, err)
// }
// }
//} else if chat.UnreadCount == -1 {
// user.log.Debugfln("Invalid unread count (missing field?) in chat info %+v", chat.Source)
//}
if justCreated || !user.bridge.Config.Bridge.TagOnlyOnCreate {
chat, err := user.Client.Store.ChatSettings.GetChatSettings(portal.Key.JID)
if err != nil {
user.log.Warnfln("Failed to get settings of %s: %v", portal.Key.JID, err)
return
}
intent := doublePuppet.CustomIntent()
user.updateChatMute(intent, portal, chat.MutedUntil)
user.updateChatTag(intent, portal, user.bridge.Config.Bridge.ArchiveTag, chat.Archived)
user.updateChatTag(intent, portal, user.bridge.Config.Bridge.PinnedTag, chat.Pinned)
@ -709,12 +723,8 @@ func (user *User) markOtherRead(portal *Portal, intent *appservice.IntentAPI, me
}
func (user *User) markSelfRead(portal *Portal, messageID types.MessageID) {
puppet := user.bridge.GetPuppetByJID(user.JID)
if puppet == nil {
return
}
intent := puppet.CustomIntent()
if intent == nil {
puppet := user.bridge.GetPuppetByCustomMXID(user.MXID)
if puppet == nil || puppet.CustomIntent() == nil {
return
}
var message *database.Message
@ -731,106 +741,69 @@ func (user *User) markSelfRead(portal *Portal, messageID types.MessageID) {
}
user.log.Debugfln("User read message %s/%s in %s/%s in WhatsApp mobile", message.JID, message.MXID, portal.Key.JID, portal.MXID)
}
err := intent.MarkReadWithContent(portal.MXID, message.MXID, &CustomReadReceipt{DoublePuppet: true})
err := puppet.CustomIntent().MarkReadWithContent(portal.MXID, message.MXID, &CustomReadReceipt{DoublePuppet: true})
if err != nil {
user.log.Warnfln("Failed to bridge own read receipt in %s: %v", portal.Key.JID, err)
}
}
//func (user *User) HandleCommand(cmd whatsapp.JSONCommand) {
// switch cmd.Type {
// case whatsapp.CommandPicture:
// if strings.HasSuffix(cmd.JID, whatsapp.NewUserSuffix) {
// puppet := user.bridge.GetPuppetByJID(cmd.JID)
// go puppet.UpdateAvatar(user, cmd.ProfilePicInfo)
// } else if user.bridge.Config.Bridge.ChatMetaSync {
// portal := user.GetPortalByJID(cmd.JID)
// go portal.UpdateAvatar(user, cmd.ProfilePicInfo, true)
// }
// case whatsapp.CommandDisconnect:
// if cmd.Kind == "replaced" {
// user.cleanDisconnection = true
// go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server because you opened another WhatsApp Web client.\n\n" +
// "Use the `reconnect` command to disconnect the other client and resume bridging.")
// } else {
// user.log.Warnln("Unknown kind of disconnect:", string(cmd.Raw))
// go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server (reason code: %s).\n\n"+
// "Use the `reconnect` command to reconnect.", cmd.Kind)
// }
// }
//}
func (user *User) markSelfReadFull(portal *Portal) {
puppet := user.bridge.GetPuppetByCustomMXID(user.MXID)
if puppet == nil || puppet.CustomIntent() == nil {
return
}
lastMessage := user.bridge.DB.Message.GetLastInChat(portal.Key)
if lastMessage == nil {
return
}
err := puppet.CustomIntent().MarkReadWithContent(portal.MXID, lastMessage.MXID, &CustomReadReceipt{DoublePuppet: true})
if err != nil {
user.log.Warnfln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, portal.MXID, err)
}
}
//func (user *User) HandleChatUpdate(cmd whatsapp.ChatUpdate) {
// if cmd.Command != whatsapp.ChatUpdateCommandAction {
// return
// }
//
// portal := user.GetPortalByJID(cmd.JID)
// if len(portal.MXID) == 0 {
// if cmd.Data.Action == whatsapp.ChatActionIntroduce || cmd.Data.Action == whatsapp.ChatActionCreate {
// go func() {
// err := portal.CreateMatrixRoom(user)
// if err != nil {
// user.log.Errorln("Failed to create portal room after receiving join event:", err)
// }
// }()
// }
// return
// }
//
// // These don't come down the message history :(
// switch cmd.Data.Action {
// case whatsapp.ChatActionAddTopic:
// go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, nil, true)
// case whatsapp.ChatActionRemoveTopic:
// go portal.UpdateTopic("", cmd.Data.SenderJID, nil, true)
// case whatsapp.ChatActionRemove:
// // We ignore leaving groups in the message history to avoid accidentally leaving rejoined groups,
// // but if we get a real-time command that says we left, it should be safe to bridge it.
// if !user.bridge.Config.Bridge.ChatMetaSync {
// for _, jid := range cmd.Data.UserChange.JIDs {
// if jid == user.JID {
// go portal.HandleWhatsAppKick(nil, cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
// break
// }
// }
// }
// }
//
// if !user.bridge.Config.Bridge.ChatMetaSync {
// // Ignore chat update commands, we're relying on the message history.
// return
// }
//
// switch cmd.Data.Action {
// case whatsapp.ChatActionNameChange:
// go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, nil, true)
// case whatsapp.ChatActionPromote:
// go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, true)
// case whatsapp.ChatActionDemote:
// go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, false)
// case whatsapp.ChatActionAnnounce:
// go portal.RestrictMessageSending(cmd.Data.Announce)
// case whatsapp.ChatActionRestrict:
// go portal.RestrictMetadataChanges(cmd.Data.Restrict)
// case whatsapp.ChatActionRemove:
// go portal.HandleWhatsAppKick(nil, cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
// case whatsapp.ChatActionAdd:
// go portal.HandleWhatsAppInvite(user, cmd.Data.SenderJID, nil, cmd.Data.UserChange.JIDs)
// case whatsapp.ChatActionIntroduce:
// if cmd.Data.SenderJID != "unknown" {
// go portal.Sync(user, whatsapp.Contact{JID: portal.Key.JID})
// }
// }
//}
//
//func (user *User) HandleJSONMessage(evt whatsapp.RawJSONMessage) {
// if !json.Valid(evt.RawMessage) {
// return
// }
// user.log.Debugfln("JSON message with tag %s: %s", evt.Tag, evt.RawMessage)
// user.updateLastConnectionIfNecessary()
//}
func (user *User) handleGroupUpdate(evt *events.GroupInfo) {
portal := user.GetPortalByJID(evt.JID)
if portal == nil || len(portal.MXID) == 0 {
// TODO create portal when added to group
user.log.Debugfln("Ignoring group info update in chat with no portal: %+v", evt)
return
}
switch {
case evt.Announce != nil:
portal.RestrictMessageSending(evt.Announce.IsAnnounce)
case evt.Locked != nil:
portal.RestrictMetadataChanges(evt.Locked.IsLocked)
case evt.Name != nil:
portal.UpdateName(evt.Name.Name, evt.Name.NameSetBy, true)
case evt.Topic != nil:
portal.UpdateTopic(evt.Topic.Topic, evt.Topic.TopicSetBy, true)
case evt.Leave != nil:
if evt.Sender != nil && !evt.Sender.IsEmpty() {
portal.HandleWhatsAppKick(user, *evt.Sender, evt.Leave)
}
case evt.Join != nil:
portal.HandleWhatsAppInvite(user, evt.Sender, evt.Join)
case evt.Promote != nil:
portal.ChangeAdminStatus(evt.Promote, true)
case evt.Demote != nil:
portal.ChangeAdminStatus(evt.Demote, false)
}
}
func (user *User) handlePictureUpdate(evt *events.Picture) {
if evt.JID.Server == types.DefaultUserServer {
puppet := user.bridge.GetPuppetByJID(evt.JID)
if puppet.Avatar != evt.PictureID {
puppet.UpdateAvatar(user)
}
} else {
portal := user.GetPortalByJID(evt.JID)
if portal != nil && portal.Avatar != evt.PictureID {
portal.UpdateAvatar(user, evt.Author, true)
}
}
}
func (user *User) NeedsRelaybot(portal *Portal) bool {
return !user.HasSession() // || !user.IsInPortal(portal.Key)