diff --git a/CHANGELOG.md b/CHANGELOG.md index e26d509..6d893cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Added support for bridging polls from WhatsApp and votes in both directions. * Votes are only bridged if MSC3381 polls are enabled (`extev_polls` in the config). +* Added support for bridging WhatsApp communities as spaces. * Updated backfill logic to mark rooms as read if the only message is a notice about the disappearing message timer. * Switched SQLite config from `sqlite3` to `sqlite3-fk-wal` to enforce foreign diff --git a/ROADMAP.md b/ROADMAP.md index 7206ac4..ac83083 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -36,6 +36,7 @@ * [ ] Chat types * [x] Private chat * [x] Group chat + * [x] Communities * [x] Status broadcast * [ ] Broadcast list (not currently supported on WhatsApp web) * [x] Message deletions diff --git a/commands.go b/commands.go index 4c5c4bc..1a631e8 100644 --- a/commands.go +++ b/commands.go @@ -356,6 +356,13 @@ func fnCreate(ce *WrappedCommandEvent) { return } + var createEvent event.CreateEventContent + err = ce.Bot.StateEvent(ce.RoomID, event.StateCreate, "", &createEvent) + if err != nil && !errors.Is(err, mautrix.MNotFound) { + ce.Reply("Failed to get room create event") + return + } + var participants []types.JID participantDedup := make(map[types.JID]bool) participantDedup[ce.User.JID.ToNonAD()] = true @@ -373,9 +380,16 @@ func fnCreate(ce *WrappedCommandEvent) { participants = append(participants, jid) } } + // TODO check m.space.parent to create rooms directly in communities ce.Log.Infofln("Creating group for %s with name %s and participants %+v", ce.RoomID, roomNameEvent.Name, participants) - resp, err := ce.User.Client.CreateGroup(roomNameEvent.Name, participants, "") + resp, err := ce.User.Client.CreateGroup(whatsmeow.ReqCreateGroup{ + Name: roomNameEvent.Name, + Participants: participants, + GroupParent: types.GroupParent{ + IsParent: createEvent.Type == event.RoomTypeSpace, + }, + }) if err != nil { ce.Reply("Failed to create group: %v", err) return @@ -389,6 +403,7 @@ func fnCreate(ce *WrappedCommandEvent) { } portal.MXID = ce.RoomID portal.Name = roomNameEvent.Name + portal.IsParent = resp.IsParent portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1 if !portal.Encrypted && ce.Bridge.Config.Bridge.Encryption.Default { _, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", portal.GetEncryptionEventContent()) @@ -1127,7 +1142,7 @@ func fnSync(ce *WrappedCommandEvent) { count := 0 for _, key := range keys { portal := ce.Bridge.GetPortalByJID(key) - portal.addToSpace(ce.User) + portal.addToPersonalSpace(ce.User) count++ } plural := "s" diff --git a/database/portal.go b/database/portal.go index 1fbfe77..bd5a729 100644 --- a/database/portal.go +++ b/database/portal.go @@ -65,7 +65,7 @@ func (pq *PortalQuery) New() *Portal { } } -const portalColumns = "jid, receiver, mxid, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, encrypted, last_sync, first_event_id, next_batch_id, relay_user_id, expiration_time" +const portalColumns = "jid, receiver, mxid, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, encrypted, last_sync, is_parent, parent_group, in_space, first_event_id, next_batch_id, relay_user_id, expiration_time" func (pq *PortalQuery) GetAll() []*Portal { return pq.getAll(fmt.Sprintf("SELECT %s FROM portal", portalColumns)) @@ -145,18 +145,20 @@ type Portal struct { Encrypted bool LastSync time.Time - FirstEventID id.EventID - NextBatchID id.BatchID - - RelayUserID id.UserID + IsParent bool + ParentGroup types.JID + InSpace bool + FirstEventID id.EventID + NextBatchID id.BatchID + RelayUserID id.UserID ExpirationTime uint32 } func (portal *Portal) Scan(row dbutil.Scannable) *Portal { - var mxid, avatarURL, firstEventID, nextBatchID, relayUserID sql.NullString + var mxid, avatarURL, firstEventID, nextBatchID, relayUserID, parentGroupJID sql.NullString var lastSyncTs int64 - err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.NameSet, &portal.Topic, &portal.TopicSet, &portal.Avatar, &avatarURL, &portal.AvatarSet, &portal.Encrypted, &lastSyncTs, &firstEventID, &nextBatchID, &relayUserID, &portal.ExpirationTime) + err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.NameSet, &portal.Topic, &portal.TopicSet, &portal.Avatar, &avatarURL, &portal.AvatarSet, &portal.Encrypted, &lastSyncTs, &portal.IsParent, &parentGroupJID, &portal.InSpace, &firstEventID, &nextBatchID, &relayUserID, &portal.ExpirationTime) if err != nil { if err != sql.ErrNoRows { portal.log.Errorln("Database scan failed:", err) @@ -168,6 +170,9 @@ func (portal *Portal) Scan(row dbutil.Scannable) *Portal { } portal.MXID = id.RoomID(mxid.String) portal.AvatarURL, _ = id.ParseContentURI(avatarURL.String) + if parentGroupJID.Valid { + portal.ParentGroup, _ = types.ParseJID(parentGroupJID.String) + } portal.FirstEventID = id.EventID(firstEventID.String) portal.NextBatchID = id.BatchID(nextBatchID.String) portal.RelayUserID = id.UserID(relayUserID.String) @@ -188,6 +193,14 @@ func (portal *Portal) relayUserPtr() *id.UserID { return nil } +func (portal *Portal) parentGroupPtr() *string { + if !portal.ParentGroup.IsEmpty() { + val := portal.ParentGroup.String() + return &val + } + return nil +} + func (portal *Portal) lastSyncTs() int64 { if portal.LastSync.IsZero() { return 0 @@ -198,12 +211,14 @@ func (portal *Portal) lastSyncTs() int64 { func (portal *Portal) Insert() { _, err := portal.db.Exec(` INSERT INTO portal (jid, receiver, mxid, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, - encrypted, last_sync, first_event_id, next_batch_id, relay_user_id, expiration_time) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) + encrypted, last_sync, is_parent, parent_group, in_space, first_event_id, next_batch_id, + relay_user_id, expiration_time) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) `, portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.NameSet, portal.Topic, portal.TopicSet, portal.Avatar, portal.AvatarURL.String(), portal.AvatarSet, portal.Encrypted, portal.lastSyncTs(), - portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr(), portal.ExpirationTime) + portal.IsParent, portal.parentGroupPtr(), portal.InSpace, portal.FirstEventID.String(), portal.NextBatchID.String(), + portal.relayUserPtr(), portal.ExpirationTime) if err != nil { portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err) } @@ -216,11 +231,13 @@ func (portal *Portal) Update(txn dbutil.Execable) { _, err := txn.Exec(` UPDATE portal SET mxid=$1, name=$2, name_set=$3, topic=$4, topic_set=$5, avatar=$6, avatar_url=$7, avatar_set=$8, - encrypted=$9, last_sync=$10, first_event_id=$11, next_batch_id=$12, relay_user_id=$13, expiration_time=$14 - WHERE jid=$15 AND receiver=$16 + encrypted=$9, last_sync=$10, is_parent=$11, parent_group=$12, in_space=$13, + first_event_id=$14, next_batch_id=$15, relay_user_id=$16, expiration_time=$17 + WHERE jid=$18 AND receiver=$19 `, portal.mxidPtr(), portal.Name, portal.NameSet, portal.Topic, portal.TopicSet, portal.Avatar, portal.AvatarURL.String(), - portal.AvatarSet, portal.Encrypted, portal.lastSyncTs(), portal.FirstEventID.String(), portal.NextBatchID.String(), - portal.relayUserPtr(), portal.ExpirationTime, portal.Key.JID, portal.Key.Receiver) + portal.AvatarSet, portal.Encrypted, portal.lastSyncTs(), portal.IsParent, portal.parentGroupPtr(), portal.InSpace, + portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr(), portal.ExpirationTime, + portal.Key.JID, portal.Key.Receiver) if err != nil { portal.log.Warnfln("Failed to update %s: %v", portal.Key, err) } diff --git a/database/upgrades/00-latest-revision.sql b/database/upgrades/00-latest-revision.sql index 1bf27c4..7f7a611 100644 --- a/database/upgrades/00-latest-revision.sql +++ b/database/upgrades/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v51: Latest revision +-- v0 -> v52: Latest revision CREATE TABLE "user" ( mxid TEXT PRIMARY KEY, @@ -29,6 +29,10 @@ CREATE TABLE portal ( encrypted BOOLEAN NOT NULL DEFAULT false, last_sync BIGINT NOT NULL DEFAULT 0, + is_parent BOOLEAN NOT NULL DEFAULT false, + parent_group TEXT, + in_space BOOLEAN NOT NULL DEFAULT false, + first_event_id TEXT, next_batch_id TEXT, relay_user_id TEXT, diff --git a/database/upgrades/52-communities.sql b/database/upgrades/52-communities.sql new file mode 100644 index 0000000..51110e8 --- /dev/null +++ b/database/upgrades/52-communities.sql @@ -0,0 +1,5 @@ +-- v52: Store portal metadata for communities + +ALTER TABLE portal ADD COLUMN is_parent BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE portal ADD COLUMN parent_group TEXT; +ALTER TABLE portal ADD COLUMN in_space BOOLEAN NOT NULL DEFAULT false; diff --git a/go.mod b/go.mod index f23df33..2227865 100644 --- a/go.mod +++ b/go.mod @@ -10,13 +10,13 @@ require ( github.com/mattn/go-sqlite3 v1.14.16 github.com/prometheus/client_golang v1.14.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/tidwall/gjson v1.14.3 - go.mau.fi/whatsmeow v0.0.0-20221126173344-e660988acdbc + github.com/tidwall/gjson v1.14.4 + go.mau.fi/whatsmeow v0.0.0-20221202110551-e067ee7293b0 golang.org/x/image v0.1.0 golang.org/x/net v0.2.0 google.golang.org/protobuf v1.28.1 maunium.net/go/maulogger/v2 v2.3.2 - maunium.net/go/mautrix v0.12.4-0.20221122192554-26c9ef6e7157 + maunium.net/go/mautrix v0.12.4-0.20221201124911-2c57226ad4cd ) require ( diff --git a/go.sum b/go.sum index 2b99f21..34c1802 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= -github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= @@ -66,8 +66,8 @@ github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M= github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mau.fi/libsignal v0.0.0-20221015105917-d970e7c3c9cf h1:mzPxXBgDPHKDHMVV1tIWh7lwCiRpzCsXC0gNRX+K07c= go.mau.fi/libsignal v0.0.0-20221015105917-d970e7c3c9cf/go.mod h1:XCjaU93vl71YNRPn059jMrK0xRDwVO5gKbxoPxow9mQ= -go.mau.fi/whatsmeow v0.0.0-20221126173344-e660988acdbc h1:uZCZs8Ju83OmM1A1+VhpZMXpvVAg5BEQNP0KBXALJBI= -go.mau.fi/whatsmeow v0.0.0-20221126173344-e660988acdbc/go.mod h1:2yweL8nczvtlIxkrvCb0y8xiO13rveX9lJPambwYV/E= +go.mau.fi/whatsmeow v0.0.0-20221202110551-e067ee7293b0 h1:danzDOlj/KiDi8kNsaHOhwJ7IZdo7V7hXelkZXhJhsc= +go.mau.fi/whatsmeow v0.0.0-20221202110551-e067ee7293b0/go.mod h1:2yweL8nczvtlIxkrvCb0y8xiO13rveX9lJPambwYV/E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= @@ -122,5 +122,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0= maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= -maunium.net/go/mautrix v0.12.4-0.20221122192554-26c9ef6e7157 h1:x2SiQnZQeJJ8qYCwEJ/rN0SEpGgT/Ct8kqjB4y/7vM4= -maunium.net/go/mautrix v0.12.4-0.20221122192554-26c9ef6e7157/go.mod h1:uOUjkOjm2C+nQS3mr9B5ATjqemZfnPHvjdd1kZezAwg= +maunium.net/go/mautrix v0.12.4-0.20221201124911-2c57226ad4cd h1:RHe8UuNE3opwiwvj4gRN7o5RYQYy9Gg8IsHxV218ms0= +maunium.net/go/mautrix v0.12.4-0.20221201124911-2c57226ad4cd/go.mod h1:uOUjkOjm2C+nQS3mr9B5ATjqemZfnPHvjdd1kZezAwg= diff --git a/portal.go b/portal.go index 8e3995f..c355eeb 100644 --- a/portal.go +++ b/portal.go @@ -248,6 +248,7 @@ type Portal struct { avatarLock sync.Mutex latestEventBackfillLock sync.Mutex + parentGroupUpdateLock sync.Mutex recentlyHandled [recentlyHandledLength]recentlyHandledWrapper recentlyHandledLock sync.Mutex @@ -262,7 +263,8 @@ type Portal struct { mediaErrorCache map[types.MessageID]*FailedMediaMeta - relayUser *User + relayUser *User + parentPortal *Portal } var ( @@ -1194,6 +1196,27 @@ func (portal *Portal) UpdateTopic(topic string, setBy types.JID, updateInfo bool return false } +func (portal *Portal) UpdateParentGroup(parent types.JID, updateInfo bool) bool { + portal.parentGroupUpdateLock.Lock() + defer portal.parentGroupUpdateLock.Unlock() + if portal.ParentGroup != parent { + portal.log.Debugfln("Updating parent group %v -> %v", portal.ParentGroup, parent) + portal.updateCommunitySpace(false) + portal.ParentGroup = parent + portal.parentPortal = nil + portal.InSpace = false + portal.updateCommunitySpace(true) + if updateInfo { + portal.UpdateBridgeInfo() + portal.Update(nil) + } + return true + } else if !portal.ParentGroup.IsEmpty() && !portal.InSpace { + return portal.updateCommunitySpace(true) + } + return false +} + func (portal *Portal) UpdateMetadata(user *User, groupInfo *types.GroupInfo) bool { if portal.IsPrivateChat() { return false @@ -1230,10 +1253,18 @@ func (portal *Portal) UpdateMetadata(user *User, groupInfo *types.GroupInfo) boo update := false update = portal.UpdateName(groupInfo.Name, groupInfo.NameSetBy, false) || update update = portal.UpdateTopic(groupInfo.Topic, groupInfo.TopicSetBy, false) || update + update = portal.UpdateParentGroup(groupInfo.LinkedParentJID, false) || update if portal.ExpirationTime != groupInfo.DisappearingTimer { update = true portal.ExpirationTime = groupInfo.DisappearingTimer } + if portal.IsParent != groupInfo.IsParent { + if portal.MXID != "" { + portal.log.Warnfln("Existing group changed is_parent from %t to %t", portal.IsParent, groupInfo.IsParent) + } + update = true + portal.IsParent = true + } portal.RestrictMessageSending(groupInfo.IsAnnounce) portal.RestrictMetadataChanges(groupInfo.IsLocked) @@ -1252,7 +1283,7 @@ func (portal *Portal) UpdateMatrixRoom(user *User, groupInfo *types.GroupInfo) b portal.log.Infoln("Syncing portal for", user.MXID) portal.ensureUserInvited(user) - go portal.addToSpace(user) + go portal.addToPersonalSpace(user) update := false update = portal.UpdateMetadata(user, groupInfo) || update @@ -1401,6 +1432,13 @@ func (portal *Portal) getBridgeInfo() (string, event.BridgeEventContent) { AvatarURL: portal.AvatarURL.CUString(), }, } + if parent := portal.GetParentPortal(); parent != nil { + bridgeInfo.Network = &event.BridgeInfoSection{ + ID: parent.Key.JID.String(), + DisplayName: parent.Name, + AvatarURL: parent.AvatarURL.CUString(), + } + } return portal.getBridgeInfoStateKey(), bridgeInfo } @@ -1502,6 +1540,8 @@ func (portal *Portal) CreateMatrixRoom(user *User, groupInfo *types.GroupInfo, i if groupInfo != nil { portal.Name = groupInfo.Name portal.Topic = groupInfo.Topic + portal.IsParent = groupInfo.IsParent + portal.ParentGroup = groupInfo.LinkedParentJID } portal.UpdateAvatar(user, types.EmptyJID, false) } @@ -1552,6 +1592,19 @@ func (portal *Portal) CreateMatrixRoom(user *User, groupInfo *types.GroupInfo, i if !portal.bridge.Config.Bridge.FederateRooms { creationContent["m.federate"] = false } + if portal.IsParent { + creationContent["type"] = event.RoomTypeSpace + } else if parent := portal.GetParentPortal(); parent != nil { + initialState = append(initialState, &event.Event{ + Type: event.StateSpaceParent, + Content: event.Content{ + Parsed: &event.SpaceParentEventContent{ + Via: []string{portal.bridge.Config.Homeserver.Domain}, + Canonical: true, + }, + }, + }) + } autoJoinInvites := portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry if autoJoinInvites { portal.log.Debugfln("Hungryserv mode: adding all group members in create request") @@ -1582,14 +1635,16 @@ func (portal *Portal) CreateMatrixRoom(user *User, groupInfo *types.GroupInfo, i if err != nil { return err } + portal.log.Infoln("Matrix room created:", portal.MXID) + portal.InSpace = false portal.NameSet = len(portal.Name) > 0 portal.TopicSet = len(portal.Topic) > 0 portal.MXID = resp.RoomID portal.bridge.portalsLock.Lock() portal.bridge.portalsByMXID[portal.MXID] = portal portal.bridge.portalsLock.Unlock() + portal.updateCommunitySpace(true) portal.Update(nil) - portal.log.Infoln("Matrix room created:", portal.MXID) // We set the memberships beforehand to make sure the encryption key exchange in initial backfill knows the users are here. inviteMembership := event.MembershipInvite @@ -1605,7 +1660,7 @@ func (portal *Portal) CreateMatrixRoom(user *User, groupInfo *types.GroupInfo, i } user.syncChatDoublePuppetDetails(portal, true) - go portal.addToSpace(user) + go portal.addToPersonalSpace(user) if groupInfo != nil { if groupInfo.IsEphemeral { @@ -1655,7 +1710,7 @@ func (portal *Portal) CreateMatrixRoom(user *User, groupInfo *types.GroupInfo, i return nil } -func (portal *Portal) addToSpace(user *User) { +func (portal *Portal) addToPersonalSpace(user *User) { spaceID := user.GetSpaceRoom() if len(spaceID) == 0 || user.IsInSpace(portal.Key) { return @@ -1671,6 +1726,42 @@ func (portal *Portal) addToSpace(user *User) { } } +func (portal *Portal) updateCommunitySpace(add bool) bool { + if add == portal.InSpace { + return false + } + space := portal.GetParentPortal() + if space == nil || space.MXID == "" { + return false + } + + var action string + var parentContent event.SpaceParentEventContent + var childContent event.SpaceChildEventContent + if add { + parentContent.Canonical = true + parentContent.Via = []string{portal.bridge.Config.Homeserver.Domain} + childContent.Via = []string{portal.bridge.Config.Homeserver.Domain} + action = "add portal to" + portal.log.Debugfln("Adding %s to space %s (%s)", portal.MXID, space.MXID, space.Key.JID) + } else { + action = "remove portal from" + portal.log.Debugfln("Removing %s from space %s (%s)", portal.MXID, space.MXID, space.Key.JID) + } + + _, err := space.MainIntent().SendStateEvent(space.MXID, event.StateSpaceChild, portal.MXID.String(), &childContent) + if err != nil { + portal.log.Errorfln("Failed to send m.space.child event to %s %s: %v", action, space.MXID, err) + return false + } + _, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateSpaceParent, space.MXID.String(), &parentContent) + if err != nil { + portal.log.Warnfln("Failed to send m.space.parent event to %s %s: %v", action, space.MXID, err) + } + portal.InSpace = add + return true +} + func (portal *Portal) IsPrivateChat() bool { return portal.Key.JID.Server == types.DefaultUserServer } @@ -1700,6 +1791,15 @@ func (portal *Portal) GetRelayUser() *User { return portal.relayUser } +func (portal *Portal) GetParentPortal() *Portal { + if portal.ParentGroup.IsEmpty() { + return nil + } else if portal.parentPortal == nil { + portal.parentPortal = portal.bridge.GetPortalByJID(database.NewPortalKey(portal.ParentGroup, portal.ParentGroup)) + } + return portal.parentPortal +} + func (portal *Portal) MainIntent() *appservice.IntentAPI { if portal.IsPrivateChat() { return portal.bridge.GetPuppetByJID(portal.Key.JID).DefaultIntent() diff --git a/user.go b/user.go index 27a8d44..81808ad 100644 --- a/user.go +++ b/user.go @@ -1322,6 +1322,14 @@ func (user *User) handleGroupUpdate(evt *events.GroupInfo) { portal.ChangeAdminStatus(evt.Demote, false) case evt.Ephemeral != nil: portal.UpdateGroupDisappearingMessages(evt.Sender, evt.Timestamp, evt.Ephemeral.DisappearingTimer) + case evt.Link != nil: + if evt.Link.Type == types.GroupLinkChangeTypeParent { + portal.UpdateParentGroup(evt.Link.Group.JID, true) + } + case evt.Unlink != nil: + if evt.Unlink.Type == types.GroupLinkChangeTypeParent && portal.ParentGroup == evt.Unlink.Group.JID { + portal.UpdateParentGroup(types.EmptyJID, true) + } } }