Add support for communities

This commit is contained in:
Tulir Asokan 2022-12-02 15:36:19 +02:00
parent 357f165581
commit a1192bd0a4
10 changed files with 182 additions and 31 deletions

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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)
}

View file

@ -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,

View file

@ -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;

6
go.mod
View file

@ -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 (

12
go.sum
View file

@ -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=

110
portal.go
View file

@ -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()

View file

@ -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)
}
}
}