mirror of
https://github.com/balzack/databag.git
synced 2025-02-11 19:19:16 +00:00
using member table channel membership
This commit is contained in:
parent
ff564c24d6
commit
7fde0558bb
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user