diff --git a/net/server/internal/api_addChannel.go b/net/server/internal/api_addChannel.go index 92684043..4259934b 100644 --- a/net/server/internal/api_addChannel.go +++ b/net/server/internal/api_addChannel.go @@ -49,7 +49,13 @@ func AddChannel(w http.ResponseWriter, r *http.Request) { if res := tx.Preload("Card").Where("account_id = ? AND card_slot_id = ?", account.ID, cardID).First(&cardSlot).Error; res != nil { return res } - if res := tx.Model(&slot.Channel).Association("Cards").Append(cardSlot.Card); res != nil { + member := &store.Member{} + member.ChannelID = channel.ID + member.CardID = cardSlot.Card.ID + member.Card = *cardSlot.Card + member.Channel = channel + member.PushEnabled = true + if res := tx.Save(member).Error; res != nil { return res } cards = append(cards, cardSlot.Card) diff --git a/net/server/internal/api_addChannelTopic.go b/net/server/internal/api_addChannelTopic.go index 13458c3b..b938e21f 100644 --- a/net/server/internal/api_addChannelTopic.go +++ b/net/server/internal/api_addChannelTopic.go @@ -75,8 +75,8 @@ func AddChannelTopic(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/api_addChannelTopicAsset.go b/net/server/internal/api_addChannelTopicAsset.go index 06548ae6..196f0f3d 100644 --- a/net/server/internal/api_addChannelTopicAsset.go +++ b/net/server/internal/api_addChannelTopicAsset.go @@ -153,8 +153,8 @@ func AddChannelTopicAsset(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/api_addChannelTopicTag.go b/net/server/internal/api_addChannelTopicTag.go index 06d039f9..c0794095 100644 --- a/net/server/internal/api_addChannelTopicTag.go +++ b/net/server/internal/api_addChannelTopicTag.go @@ -94,8 +94,8 @@ func AddChannelTopicTag(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/api_clearChannelCard.go b/net/server/internal/api_clearChannelCard.go index 5e7a8296..6e951da0 100644 --- a/net/server/internal/api_clearChannelCard.go +++ b/net/server/internal/api_clearChannelCard.go @@ -24,7 +24,7 @@ func ClearChannelCard(w http.ResponseWriter, r *http.Request) { // load referenced channel var channelSlot store.ChannelSlot - if err := store.DB.Preload("Channel.Cards.CardSlot").Preload("Channel.Groups.GroupSlot").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_slot_id = ?", account.ID, channelID).First(&channelSlot).Error; err != nil { + if err := store.DB.Preload("Channel.Members.Card.CardSlot").Preload("Channel.Groups.GroupSlot").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_slot_id = ?", account.ID, channelID).First(&channelSlot).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { ErrResponse(w, http.StatusInternalServerError, err) } else { @@ -54,8 +54,8 @@ func ClearChannelCard(w http.ResponseWriter, r *http.Request) { // determine contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { @@ -65,13 +65,13 @@ func ClearChannelCard(w http.ResponseWriter, r *http.Request) { // save and update contact revision err = store.DB.Transaction(func(tx *gorm.DB) error { - if res := tx.Model(&channelSlot.Channel).Association("Cards").Delete(cardSlot.Card); res != nil { + if res := tx.Where("channel_id = ? AND card_id = ?", channelSlot.Channel.ID, cardSlot.Card.ID).Delete(&store.Member{}).Error; res != nil { return res } - if res := tx.Model(&channelSlot.Channel).Update("detail_revision", account.ChannelRevision+1).Error; res != nil { + if res := tx.Model(&store.Channel{}).Where("id = ?", channelSlot.Channel.ID).Update("detail_revision", account.ChannelRevision+1).Error; res != nil { return res } - if res := tx.Model(&channelSlot).Update("revision", account.ChannelRevision+1).Error; res != nil { + if res := tx.Model(&store.ChannelSlot{}).Where("id = ?", channelSlot.ID).Update("revision", account.ChannelRevision+1).Error; res != nil { return res } if res := tx.Model(&account).Update("channel_revision", account.ChannelRevision+1).Error; res != nil { diff --git a/net/server/internal/api_getChannelDetail.go b/net/server/internal/api_getChannelDetail.go index 9e646e04..54f1dc9d 100644 --- a/net/server/internal/api_getChannelDetail.go +++ b/net/server/internal/api_getChannelDetail.go @@ -40,7 +40,7 @@ func GetChannelDetail(w http.ResponseWriter, r *http.Request) { // load channel var slot store.ChannelSlot - if err := store.DB.Preload("Channel.Cards.CardSlot").Preload("Channel.Groups.Cards").Preload("Channel.Groups.GroupSlot").Where("account_id = ? AND channel_slot_id = ?", act.ID, channelID).First(&slot).Error; err != nil { + if err := store.DB.Preload("Channel.Members.Card.CardSlot").Preload("Channel.Groups.Cards").Preload("Channel.Groups.GroupSlot").Where("account_id = ? AND channel_slot_id = ?", act.ID, channelID).First(&slot).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { ErrResponse(w, http.StatusNotFound, err) } else { diff --git a/net/server/internal/api_getChannelSummary.go b/net/server/internal/api_getChannelSummary.go index e4fa9b83..c12a2232 100644 --- a/net/server/internal/api_getChannelSummary.go +++ b/net/server/internal/api_getChannelSummary.go @@ -42,7 +42,7 @@ func GetChannelSummary(w http.ResponseWriter, r *http.Request) { var slot store.ChannelSlot if err := store.DB.Preload("Channel.Topics", func(db *gorm.DB) *gorm.DB { return store.DB.Order("topics.id DESC").Limit(1) - }).Preload("Channel.Cards.CardSlot").Preload("Channel.Groups.Cards").Preload("Channel.Groups.GroupSlot").Where("account_id = ? AND channel_slot_id = ?", act.ID, channelID).First(&slot).Error; err != nil { + }).Preload("Channel.Members.Card").Preload("Channel.Groups.Cards").Preload("Channel.Groups.GroupSlot").Where("account_id = ? AND channel_slot_id = ?", act.ID, channelID).First(&slot).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { ErrResponse(w, http.StatusNotFound, err) } else { diff --git a/net/server/internal/api_getChannelTopic.go b/net/server/internal/api_getChannelTopic.go index 1ddd35a7..1e88a450 100644 --- a/net/server/internal/api_getChannelTopic.go +++ b/net/server/internal/api_getChannelTopic.go @@ -36,9 +36,9 @@ func GetChannelTopic(w http.ResponseWriter, r *http.Request) { WriteResponse(w, getTopicModel(&topicSlot)) } -func isMember(guid string, cards []store.Card) bool { - for _, card := range cards { - if guid == card.GUID { +func isMember(guid string, members []store.Member) bool { + for _, member := range members { + if guid == member.Card.GUID { return true } } @@ -86,7 +86,7 @@ func getChannelSlot(r *http.Request, member bool) (slot store.ChannelSlot, guid } // load channel - if err = store.DB.Preload("Account").Preload("Channel.Cards").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_slot_id = ?", account.ID, channelID).First(&slot).Error; err != nil { + if err = store.DB.Preload("Account").Preload("Channel.Members.Card").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_slot_id = ?", account.ID, channelID).First(&slot).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { code = http.StatusNotFound } else { @@ -102,11 +102,11 @@ func getChannelSlot(r *http.Request, member bool) (slot store.ChannelSlot, guid // validate access to channel if tokenType == APPTokenContact { - if member && !isMember(guid, slot.Channel.Cards) { + if member && !isMember(guid, slot.Channel.Members) { err = errors.New("contact is not a channel member") code = http.StatusUnauthorized return - } else if !isViewer(guid, slot.Channel.Groups) && !isMember(guid, slot.Channel.Cards) { + } else if !isViewer(guid, slot.Channel.Groups) && !isMember(guid, slot.Channel.Members) { err = errors.New("contact is not a channel viewer") code = http.StatusUnauthorized return diff --git a/net/server/internal/api_getChannels.go b/net/server/internal/api_getChannels.go index 8029bb5d..69690885 100644 --- a/net/server/internal/api_getChannels.go +++ b/net/server/internal/api_getChannels.go @@ -69,7 +69,7 @@ func GetChannels(w http.ResponseWriter, r *http.Request) { } else { if err := store.DB.Preload("Channel.Topics", func(db *gorm.DB) *gorm.DB { return store.DB.Order("topics.id DESC") - }).Preload("Channel.Cards.CardSlot").Preload("Channel.Groups.GroupSlot").Where("account_id = ? AND channel_id != 0", account.ID).Find(&slots).Error; err != nil { + }).Preload("Channel.Members.Card.CardSlot").Preload("Channel.Groups.GroupSlot").Where("account_id = ? AND channel_id != 0", account.ID).Find(&slots).Error; err != nil { ErrResponse(w, http.StatusInternalServerError, err) return } @@ -108,14 +108,14 @@ func GetChannels(w http.ResponseWriter, r *http.Request) { account := &card.Account var slots []store.ChannelSlot if channelRevisionSet { - if err := store.DB.Preload("Channel.Cards").Preload("Channel.Groups.Cards").Where("account_id = ? AND revision > ?", account.ID, channelRevision).Find(&slots).Error; err != nil { + if err := store.DB.Preload("Channel.Members.Card").Preload("Channel.Groups.Cards").Where("account_id = ? AND revision > ?", account.ID, channelRevision).Find(&slots).Error; err != nil { ErrResponse(w, http.StatusInternalServerError, err) return } } else { if err := store.DB.Preload("Channel.Topics", func(db *gorm.DB) *gorm.DB { return store.DB.Order("topics.id DESC") - }).Preload("Channel.Cards").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_id != 0", account.ID).Find(&slots).Error; err != nil { + }).Preload("Channel.Members.Card").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_id != 0", account.ID).Find(&slots).Error; err != nil { ErrResponse(w, http.StatusInternalServerError, err) return } @@ -150,8 +150,8 @@ func isChannelShared(guid string, channel *store.Channel) bool { if channel == nil { return false } - for _, card := range channel.Cards { - if guid == card.GUID { + for _, member := range channel.Members { + if guid == member.Card.GUID { return true } } diff --git a/net/server/internal/api_removeCard.go b/net/server/internal/api_removeCard.go index fc7ba44e..01f6094f 100644 --- a/net/server/internal/api_removeCard.go +++ b/net/server/internal/api_removeCard.go @@ -51,8 +51,8 @@ func RemoveCard(w http.ResponseWriter, r *http.Request) { if res := tx.Model(&channel.ChannelSlot).Update("revision", account.ChannelRevision+1).Error; res != nil { return res } - for _, card := range channel.Cards { - cards[card.GUID] = &card + for _, member := range channel.Members { + cards[member.Card.GUID] = &member.Card } } if res := tx.Model(&slot.Card).Association("Groups").Clear(); res != nil { diff --git a/net/server/internal/api_removeChannel.go b/net/server/internal/api_removeChannel.go index 9f1661f2..1ba4a081 100644 --- a/net/server/internal/api_removeChannel.go +++ b/net/server/internal/api_removeChannel.go @@ -43,7 +43,7 @@ func RemoveChannel(w http.ResponseWriter, r *http.Request) { // load channel var slot store.ChannelSlot - if err = store.DB.Preload("Channel.Cards").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_slot_id = ?", account.ID, channelID).First(&slot).Error; err != nil { + if err = store.DB.Preload("Channel.Members.Card").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_slot_id = ?", account.ID, channelID).First(&slot).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { ErrResponse(w, http.StatusNotFound, err) } else { @@ -58,8 +58,8 @@ func RemoveChannel(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range slot.Channel.Cards { - cards[card.GUID] = card + for _, member := range slot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range slot.Channel.Groups { for _, card := range group.Cards { @@ -76,7 +76,7 @@ func RemoveChannel(w http.ResponseWriter, r *http.Request) { if res := tx.Model(&slot.Channel).Association("Cards").Clear(); res != nil { return res } - slot.Channel.Cards = []store.Card{} + slot.Channel.Members = []store.Member{} if res := tx.Where("channel_id = ?", slot.Channel.ID).Delete(&store.Tag{}).Error; res != nil { return res } @@ -121,15 +121,15 @@ func RemoveChannel(w http.ResponseWriter, r *http.Request) { return } err = store.DB.Transaction(func(tx *gorm.DB) error { - if res := tx.Model(&slot.Channel).Association("Cards").Delete(contact); res != nil { - return res - } - if res := tx.Model(&slot.Channel).Update("detail_revision", account.ChannelRevision+1).Error; res != nil { - return res - } - if res := tx.Model(&slot).Update("revision", account.ChannelRevision+1).Error; res != nil { + if res := tx.Where("channel_id = ? AND card_id = ?", slot.Channel.ID, contact.ID).Delete(&store.Member{}).Error; res != nil { return res } + if res := tx.Model(&store.Channel{}).Where("id = ?", slot.Channel.ID).Update("detail_revision", account.ChannelRevision+1).Error; res != nil { + return res + } + if res := tx.Model(&store.ChannelSlot{}).Where("id = ?", slot.ID).Update("revision", account.ChannelRevision+1).Error; res != nil { + return res + } if res := tx.Model(&account).Update("channel_revision", account.ChannelRevision+1).Error; res != nil { return res } diff --git a/net/server/internal/api_removeChannelTopic.go b/net/server/internal/api_removeChannelTopic.go index 789a2a2c..5800fcff 100644 --- a/net/server/internal/api_removeChannelTopic.go +++ b/net/server/internal/api_removeChannelTopic.go @@ -86,8 +86,8 @@ func RemoveChannelTopic(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/api_removeChannelTopicAsset.go b/net/server/internal/api_removeChannelTopicAsset.go index 92841153..6b89301d 100644 --- a/net/server/internal/api_removeChannelTopicAsset.go +++ b/net/server/internal/api_removeChannelTopicAsset.go @@ -76,8 +76,8 @@ func RemoveChannelTopicAsset(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/api_removeChannelTopicTag.go b/net/server/internal/api_removeChannelTopicTag.go index 106b6b1d..b43870a2 100644 --- a/net/server/internal/api_removeChannelTopicTag.go +++ b/net/server/internal/api_removeChannelTopicTag.go @@ -98,8 +98,8 @@ func RemoveChannelTopicTag(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/api_setChannelCard.go b/net/server/internal/api_setChannelCard.go index cf573aa8..3e33ea79 100644 --- a/net/server/internal/api_setChannelCard.go +++ b/net/server/internal/api_setChannelCard.go @@ -54,8 +54,8 @@ func SetChannelCard(w http.ResponseWriter, r *http.Request) { // determine contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { @@ -66,13 +66,19 @@ func SetChannelCard(w http.ResponseWriter, r *http.Request) { // save and update contact revision err = store.DB.Transaction(func(tx *gorm.DB) error { - if res := tx.Model(&channelSlot.Channel).Association("Cards").Append(cardSlot.Card); res != nil { + member := &store.Member{} + member.ChannelID = channelSlot.Channel.ID + member.CardID = cardSlot.Card.ID + member.Channel = channelSlot.Channel + member.Card = *cardSlot.Card + member.PushEnabled = true + if res := tx.Save(member).Error; res != nil { return res } - if res := tx.Model(&channelSlot.Channel).Update("detail_revision", account.ChannelRevision+1).Error; res != nil { + if res := tx.Model(&store.Channel{}).Where("id = ?", channelSlot.Channel.ID).Update("detail_revision", account.ChannelRevision+1).Error; res != nil { return res } - if res := tx.Model(&channelSlot).Update("revision", account.ChannelRevision+1).Error; res != nil { + if res := tx.Model(&store.ChannelSlot{}).Where("id = ?", channelSlot.ID).Update("revision", account.ChannelRevision+1).Error; res != nil { return res } if res := tx.Model(&account).Update("channel_revision", account.ChannelRevision+1).Error; res != nil { diff --git a/net/server/internal/api_setChannelSubject.go b/net/server/internal/api_setChannelSubject.go index d12ff3b8..0be79677 100644 --- a/net/server/internal/api_setChannelSubject.go +++ b/net/server/internal/api_setChannelSubject.go @@ -44,8 +44,8 @@ func SetChannelSubject(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range slot.Channel.Cards { - cards[card.GUID] = card + for _, member := range slot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range slot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/api_setChannelTopicConfirmed.go b/net/server/internal/api_setChannelTopicConfirmed.go index ef8cb278..a90ab4ad 100644 --- a/net/server/internal/api_setChannelTopicConfirmed.go +++ b/net/server/internal/api_setChannelTopicConfirmed.go @@ -75,8 +75,8 @@ func SetChannelTopicConfirmed(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/api_setChannelTopicSubject.go b/net/server/internal/api_setChannelTopicSubject.go index 4e06ec65..29da825e 100644 --- a/net/server/internal/api_setChannelTopicSubject.go +++ b/net/server/internal/api_setChannelTopicSubject.go @@ -88,8 +88,8 @@ func SetChannelTopicSubject(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/api_setChannelTopicTagSubject.go b/net/server/internal/api_setChannelTopicTagSubject.go index 52019078..6d10ea65 100644 --- a/net/server/internal/api_setChannelTopicTagSubject.go +++ b/net/server/internal/api_setChannelTopicTagSubject.go @@ -92,8 +92,8 @@ func SetChannelTopicTagSubject(w http.ResponseWriter, r *http.Request) { // determine affected contact list cards := make(map[string]store.Card) - for _, card := range channelSlot.Channel.Cards { - cards[card.GUID] = card + for _, member := range channelSlot.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range channelSlot.Channel.Groups { for _, card := range group.Cards { diff --git a/net/server/internal/modelUtil.go b/net/server/internal/modelUtil.go index 50e66d44..10c59de1 100644 --- a/net/server/internal/modelUtil.go +++ b/net/server/internal/modelUtil.go @@ -179,15 +179,15 @@ func getChannelDetailModel(slot *store.ChannelSlot, showList bool, image bool, a groups = append(groups, group.GroupSlot.GroupSlotID) } var cards []string - for _, card := range slot.Channel.Cards { - cards = append(cards, card.CardSlot.CardSlotID) + for _, member := range slot.Channel.Members { + cards = append(cards, member.Card.CardSlot.CardSlotID) } contacts = &ChannelContacts{Groups: groups, Cards: cards} } members := []string{} - for _, card := range slot.Channel.Cards { - members = append(members, card.GUID) + for _, member := range slot.Channel.Members { + members = append(members, member.Card.GUID) } return &ChannelDetail{ diff --git a/net/server/internal/store/schema.go b/net/server/internal/store/schema.go index 1ba4208f..b6d09a89 100644 --- a/net/server/internal/store/schema.go +++ b/net/server/internal/store/schema.go @@ -103,7 +103,7 @@ type Session struct { AppVersion string Platform string PushEnabled bool - PushToken string + PushToken string Created int64 `gorm:"autoCreateTime"` Account Account `gorm:"references:GUID"` Token string `gorm:"not null;index:sessguid,unique"` @@ -243,7 +243,6 @@ type Channel struct { Created int64 `gorm:"autoCreateTime"` Updated int64 `gorm:"autoUpdateTime"` Groups []Group `gorm:"many2many:channel_groups;"` - Cards []Card `gorm:"many2many:channel_cards;"` Members []Member Topics []Topic ChannelSlot ChannelSlot @@ -254,6 +253,8 @@ type Member struct { ChannelID uint CardID uint PushEnabled bool + Card Card + Channel *Channel } type TopicSlot struct { diff --git a/net/server/internal/transcodeUtil.go b/net/server/internal/transcodeUtil.go index 5ca4554a..e5f176be 100644 --- a/net/server/internal/transcodeUtil.go +++ b/net/server/internal/transcodeUtil.go @@ -173,8 +173,8 @@ func updateAsset(asset *store.Asset, status string, crc uint32, size int64) (err // determine affected contact list cards := make(map[string]store.Card) - for _, card := range topic.Channel.Cards { - cards[card.GUID] = card + for _, member := range topic.Channel.Members { + cards[member.Card.GUID] = member.Card } for _, group := range topic.Channel.Groups { for _, card := range group.Cards {