diff --git a/doc/api.oa3 b/doc/api.oa3 index dca8c2cf..db6bc0b9 100644 --- a/doc/api.oa3 +++ b/doc/api.oa3 @@ -2434,7 +2434,7 @@ paths: tags: - content description: Get list of assets assigned to an channel. The original assets will only be available to the account holder to provent the accidental sharing of content metadata. Access is granted to the app token of the account holder and the contact token of accounts the channel has been shared with. - operationId: get-channel-assets + operationId: get-channel-topic-assets security: - bearerAuth: [] parameters: @@ -2469,7 +2469,7 @@ paths: tags: - content description: Add an an asset to the to an channel. The original posted asset is referenced in the asset list with a null transform. The transformed assets are referenced accordingly. Transforming the asset strips it of metadata and transcodes it into a specified format. Access is granted to the app token of the account holder. - operationId: add-channel-asset + operationId: add-channel-topic-asset security: - bearerAuth: [] parameters: @@ -2525,7 +2525,7 @@ paths: tags: - content description: Get asset assigned to an channel. The endpoint supports byte-range requests and responds with the content-type set appropriatly. Access granted to the app tokens of the account holder and in the case of non-original assets, the contact token for accounts with which the channel is shared. - operationId: get-channel-asset + operationId: get-channel-topic-asset security: - bearerAuth: [] parameters: @@ -2567,7 +2567,7 @@ paths: tags: - content description: Remove an asset from an channel. Access granted to app tokens of the account holder. - operationId: remove-channel-asset + operationId: remove-channel-topic-asset security: - bearerAuth: [] parameters: @@ -2606,7 +2606,7 @@ paths: tags: - content description: Set confirmed state of the channel. Until the confirmed state has been set to true, the channel will not be visible to contacts with which the channel is shared. Access granted to the app tokens of the acocunt holder. - operationId: set-channel-confirmed + operationId: set-channel-topic-confirmed security: - bearerAuth: [] parameters: @@ -3155,7 +3155,8 @@ components: id: type: string revision: - type: int64 + type: integer + format: int64 data: $ref: '#/components/schemas/ChannelData' @@ -3235,7 +3236,7 @@ components: topicDetail: $ref: '#/components/schemas/TopicDetail' topicTags:: - $ref: '#/components/schemas/TopicSize' + $ref: '#/components/schemas/TopicTags' TopicDetail: type: object @@ -3244,7 +3245,7 @@ components: - dataType - data - created - - modified + - updated - status properties: guid: @@ -3256,14 +3257,14 @@ components: created: type: integer format: int64 - modified: + updated: type: integer format: int64 status: type: string enum: [ unconfirmed, confirmed, complete, error ] - TopicSize: + TopicTags: type: object required: - tagCount diff --git a/net/server/internal/api_addChannelTopic.go b/net/server/internal/api_addChannelTopic.go new file mode 100644 index 00000000..e5c55c1f --- /dev/null +++ b/net/server/internal/api_addChannelTopic.go @@ -0,0 +1,136 @@ +package databag + +import ( + "errors" + "net/http" + "gorm.io/gorm" + "github.com/gorilla/mux" + "github.com/google/uuid" + "databag/internal/store" +) + +func AddChannelTopic(w http.ResponseWriter, r *http.Request) { + + // scan parameters + params := mux.Vars(r) + channelId := params["channelId"] + + var subject Subject + if err := ParseRequest(r, w, &subject); err != nil { + ErrResponse(w, http.StatusBadRequest, err) + return + } + + var guid string + var act *store.Account + tokenType := r.Header.Get("TokenType") + if tokenType == APP_TOKENAPP { + account, code, err := BearerAppToken(r, false); + if err != nil { + ErrResponse(w, code, err) + return + } + act = account + guid = account.Guid + } else if tokenType == APP_TOKENCONTACT { + card, code, err := BearerContactToken(r, true) + if err != nil { + ErrResponse(w, code, err) + return + } + act = &card.Account + guid = card.Guid + } else { + ErrResponse(w, http.StatusBadRequest, errors.New("unknown token type")) + return + } + + // load channel + var channelSlot store.ChannelSlot + if err := store.DB.Preload("Channel.Cards").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_slot_id = ?", act.ID, channelId).First(&channelSlot).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + ErrResponse(w, http.StatusNotFound, err) + } else { + ErrResponse(w, http.StatusInternalServerError, err) + } + return + } + if channelSlot.Channel == nil { + ErrResponse(w, http.StatusNotFound, errors.New("referenced empty channel")) + return + } + + // check if a member + if tokenType == APP_TOKENCONTACT { + if !isMember(guid, channelSlot.Channel.Cards) { + ErrResponse(w, http.StatusUnauthorized, errors.New("not a member of channel")) + return + } + } + + topicSlot := &store.TopicSlot{} + err := store.DB.Transaction(func(tx *gorm.DB) error { + + // add new record + topic := &store.Topic{} + topic.Data = subject.Data + topic.DataType = subject.DataType + topic.Channel = channelSlot.Channel + topic.TagCount = 0 + topic.Guid = guid + topic.DetailRevision = act.ChannelRevision + 1 + topic.TagRevision = act.ChannelRevision + 1 + topic.Status = APP_TOPICUNCONFIRMED + if res := tx.Save(topic).Error; res != nil { + return res + } + topicSlot.TopicSlotId = uuid.New().String() + topicSlot.AccountID = act.ID + topicSlot.TopicID = topic.ID + topicSlot.Revision = act.ChannelRevision + 1 + topicSlot.Topic = topic + if res := tx.Save(topicSlot).Error; res != nil { + return res + } + + // update parent revision + if res := tx.Model(&channelSlot).Update("revision", act.ChannelRevision + 1).Error; res != nil { + return res + } + if res := tx.Model(&act).Update("channel_revision", act.ChannelRevision + 1).Error; res != nil { + return res + } + return nil + }) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + + // determine affected contact list + cards := make(map[string]store.Card) + for _, card := range channelSlot.Channel.Cards { + cards[card.Guid] = card + } + for _, group := range channelSlot.Channel.Groups { + for _, card := range group.Cards { + cards[card.Guid] = card + } + } + + SetStatus(act) + for _, card := range cards { + SetContactChannelNotification(act, &card) + } + WriteResponse(w, getTopicModel(topicSlot, true, true)) +} + +func isMember(guid string, cards []store.Card) bool { + for _, card := range cards { + if guid == card.Guid { + return true + } + } + return false +} + diff --git a/net/server/internal/api_content.go b/net/server/internal/api_content.go index c08b48a9..aedfc0b0 100644 --- a/net/server/internal/api_content.go +++ b/net/server/internal/api_content.go @@ -18,11 +18,6 @@ func AddChannelAsset(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -func AddChannelTopic(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) -} - func AddChannelTopicTag(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) diff --git a/net/server/internal/model b/net/server/internal/model new file mode 100644 index 00000000..e69de29b diff --git a/net/server/internal/modelUtil.go b/net/server/internal/modelUtil.go index a772ed9b..01692905 100644 --- a/net/server/internal/modelUtil.go +++ b/net/server/internal/modelUtil.go @@ -215,3 +215,55 @@ func getChannelModel(slot *store.ChannelSlot, showData bool, showList bool) *Cha +func getTopicRevisionModel(slot *store.TopicSlot, showData bool) *Topic { + + if !showData || slot.Topic == nil { + return &Topic{ + Id: slot.TopicSlotId, + Revision: slot.Revision, + } + } + + return &Topic{ + Id: slot.TopicSlotId, + Revision: slot.Revision, + Data: &TopicData { + DetailRevision: slot.Topic.DetailRevision, + TagRevision: slot.Topic.TagRevision, + }, + } +} + +func getTopicModel(slot *store.TopicSlot, showData bool, showList bool) *Topic { + + if !showData || slot.Topic == nil { + return &Topic{ + Id: slot.TopicSlotId, + Revision: slot.Revision, + } + } + + return &Topic{ + Id: slot.TopicSlotId, + Revision: slot.Revision, + Data: &TopicData { + DetailRevision: slot.Topic.DetailRevision, + TopicDetail: &TopicDetail{ + Guid: slot.Topic.Guid, + DataType: slot.Topic.DataType, + Data: slot.Topic.Data, + Created: slot.Topic.Created, + Updated: slot.Topic.Updated, + Status: slot.Topic.Status, + }, + TagRevision: slot.Topic.TagRevision, + TopicTags: &TopicTags{ + TagCount: slot.Topic.TagCount, + TagUpdated: slot.Topic.TagUpdated, + }, + }, + } +} + + + diff --git a/net/server/internal/models.go b/net/server/internal/models.go index 95480c27..49163873 100644 --- a/net/server/internal/models.go +++ b/net/server/internal/models.go @@ -363,7 +363,7 @@ type Tag struct { Id string `json:"id"` - Revision string `json:"revision"` + Revision int64 `json:"revision"` Data *TagData `json:"data"` } @@ -385,7 +385,7 @@ type Topic struct { Id string `json:"id"` - Revision string `json:"revision"` + Revision int64 `json:"revision"` Data *TopicData `json:"data"` } @@ -398,7 +398,7 @@ type TopicData struct { TopicDetail *TopicDetail `json:"topicDetail,omitempty"` - TopicTags *TopicSize `json:"topicTags:,omitempty"` + TopicTags *TopicTags `json:"topicTags:,omitempty"` } type TopicDetail struct { @@ -411,12 +411,12 @@ type TopicDetail struct { Created int64 `json:"created"` - Modified int64 `json:"modified"` + Updated int64 `json:"updated"` Status string `json:"status"` } -type TopicSize struct { +type TopicTags struct { TagCount int32 `json:"tagCount"` diff --git a/net/server/internal/store/schema.go b/net/server/internal/store/schema.go index afbfae83..5db58d0b 100644 --- a/net/server/internal/store/schema.go +++ b/net/server/internal/store/schema.go @@ -224,9 +224,9 @@ type Topic struct { Status string `gorm:"not null;index"` Created int64 `gorm:"autoCreateTime"` Updated int64 `gorm:"autoUpdateTime"` - SizeRevision int64 `gorm:"not null"` - TagUpdated int64 `gorm:"not null"` - TagRevision uint64 `gorm:"not null"` + TagCount int32 `gorm:"not null"` + TagUpdated int64 `gorm:"autoUpdateTime"` + TagRevision int64 `gorm:"not null"` Channel *Channel Assets []Asset Tags []Tag diff --git a/net/server/internal/ucTopicShare_test.go b/net/server/internal/ucTopicShare_test.go index 354375c2..9e5bb9da 100644 --- a/net/server/internal/ucTopicShare_test.go +++ b/net/server/internal/ucTopicShare_test.go @@ -8,6 +8,7 @@ import ( ) func TestTopicShare(t *testing.T) { + var topic *Topic var channel *Channel var subject *Subject params := make(map[string]string) @@ -68,7 +69,22 @@ func TestTopicShare(t *testing.T) { assert.Zero(t, bytes.Compare(img, data)) + // add a topc + topic = &Topic{} + subject = &Subject{ DataType: "topicdatatype", Data: "subjectfromA" } + assert.NoError(t, ApiTestMsg(AddChannelTopic, "POST", "/content/channels/{channelId}/topics", + ¶ms, subject, APP_TOKENAPP, set.A.Token, topic, nil)) + topic = &Topic{} + subject = &Subject{ DataType: "topicdatatype", Data: "subjectfromB" } + assert.NoError(t, ApiTestMsg(AddChannelTopic, "POST", "/content/channels/{channelId}/topics", + ¶ms, subject, APP_TOKENCONTACT, set.B.A.Token, topic, nil)) + topic = &Topic{} + subject = &Subject{ DataType: "topicdatatype", Data: "subjectfromC" } + assert.NoError(t, ApiTestMsg(AddChannelTopic, "POST", "/content/channels/{channelId}/topics", + ¶ms, subject, APP_TOKENCONTACT, set.C.A.Token, topic, nil)) + + PrintMsg(topic) }