From 8e14f51d5829f5645d281bdbf67437971c37b6cd Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Mon, 9 May 2022 14:08:54 -0700 Subject: [PATCH] adding channel summary method --- doc/api.oa3 | 39 +++++++++++ net/server/internal/api_getChannelDetail.go | 63 ++++++++++++++++++ net/server/internal/api_getChannelSummary.go | 65 +++++++++++++++++++ net/server/internal/api_getChannels.go | 12 ++-- net/server/internal/modelUtil.go | 22 +++++++ net/server/internal/models.go | 9 ++- net/server/internal/routers.go | 7 ++ net/web/src/api/getChannelDetail.js | 8 +++ net/web/src/api/getChannelSummary.js | 8 +++ net/web/src/api/getContactChannelDetail.js | 8 +++ net/web/src/api/getContactChannelSummary.js | 8 +++ net/web/src/context/useCardContext.hook.js | 15 ++++- net/web/src/context/useChannelContext.hook.js | 15 ++++- 13 files changed, 267 insertions(+), 12 deletions(-) create mode 100644 net/server/internal/api_getChannelDetail.go create mode 100644 net/server/internal/api_getChannelSummary.go create mode 100644 net/web/src/api/getChannelDetail.js create mode 100644 net/web/src/api/getChannelSummary.js create mode 100644 net/web/src/api/getContactChannelDetail.js create mode 100644 net/web/src/api/getContactChannelSummary.js diff --git a/doc/api.oa3 b/doc/api.oa3 index 21cca956..b999ad5a 100644 --- a/doc/api.oa3 +++ b/doc/api.oa3 @@ -2144,6 +2144,37 @@ paths: '500': description: internal server error + /content/channels/{channelId}/summary: + get: + tags: + - content + description: Get summary of channel. + operationId: get-channel-summary + security: + - bearerAuth: [] + parameters: + - name: channelId + in: path + description: specified channel id + required: true + schema: + type: string + responses: + '200': + description: success + content: + application/json: + schema: + $ref: '#/components/schemas/ChannelSummary' + '401': + description: invalid password + '404': + description: channel not found + '410': + description: account disabled + '500': + description: internal server error + /content/channels/{channelId}: delete: tags: @@ -3439,6 +3470,8 @@ components: topicRevision: type: integer format: int64 + channelSummary: + $ref: '#/components/schemas/ChannelSummary' channelDetail: $ref: '#/components/schemas/ChannelDetail' @@ -3466,6 +3499,12 @@ components: type: array items: type: string + + ChannelSummary: + type: object + properties: + lastTopic: + $ref: '#/components/schemas/TopicDetail' ChannelContacts: type: object diff --git a/net/server/internal/api_getChannelDetail.go b/net/server/internal/api_getChannelDetail.go new file mode 100644 index 00000000..90fd7602 --- /dev/null +++ b/net/server/internal/api_getChannelDetail.go @@ -0,0 +1,63 @@ +package databag + +import ( + "errors" + "net/http" + "gorm.io/gorm" + "github.com/gorilla/mux" + "databag/internal/store" +) + +func GetChannelDetail(w http.ResponseWriter, r *http.Request) { + + // scan parameters + params := mux.Vars(r) + channelId := params["channelId"] + + var guid string + var act *store.Account + tokenType := ParamTokenType(r) + if tokenType == APP_TOKENAGENT { + account, code, err := ParamAgentToken(r, false); + if err != nil { + ErrResponse(w, code, err) + return + } + act = account + } else if tokenType == APP_TOKENCONTACT { + card, code, err := ParamContactToken(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 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 errors.Is(err, gorm.ErrRecordNotFound) { + ErrResponse(w, http.StatusNotFound, err) + } else { + ErrResponse(w, http.StatusInternalServerError, err) + } + return + } + + // return model data + if guid != "" { + if isChannelShared(guid, slot.Channel) { + WriteResponse(w, getChannelDetailModel(&slot, false)) + } else { + ErrResponse(w, http.StatusNotFound, errors.New("channel not shared with requestor")); + return + } + } else { + WriteResponse(w, getChannelDetailModel(&slot, true)) + } +} + diff --git a/net/server/internal/api_getChannelSummary.go b/net/server/internal/api_getChannelSummary.go new file mode 100644 index 00000000..4b1a638b --- /dev/null +++ b/net/server/internal/api_getChannelSummary.go @@ -0,0 +1,65 @@ +package databag + +import ( + "errors" + "net/http" + "gorm.io/gorm" + "github.com/gorilla/mux" + "databag/internal/store" +) + +func GetChannelSummary(w http.ResponseWriter, r *http.Request) { + + // scan parameters + params := mux.Vars(r) + channelId := params["channelId"] + + var guid string + var act *store.Account + tokenType := ParamTokenType(r) + if tokenType == APP_TOKENAGENT { + account, code, err := ParamAgentToken(r, false); + if err != nil { + ErrResponse(w, code, err) + return + } + act = account + } else if tokenType == APP_TOKENCONTACT { + card, code, err := ParamContactToken(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 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 { + if errors.Is(err, gorm.ErrRecordNotFound) { + ErrResponse(w, http.StatusNotFound, err) + } else { + ErrResponse(w, http.StatusInternalServerError, err) + } + return + } + + // return model data + if guid != "" { + if isChannelShared(guid, slot.Channel) { + WriteResponse(w, getChannelSummaryModel(&slot)) + } else { + ErrResponse(w, http.StatusNotFound, errors.New("channel not shared with requestor")); + return + } + } else { + WriteResponse(w, getChannelSummaryModel(&slot)) + } +} + diff --git a/net/server/internal/api_getChannels.go b/net/server/internal/api_getChannels.go index b5869d98..fefccb7f 100644 --- a/net/server/internal/api_getChannels.go +++ b/net/server/internal/api_getChannels.go @@ -66,7 +66,9 @@ func GetChannels(w http.ResponseWriter, r *http.Request) { return } } else { - if err := store.DB.Preload("Channel.Cards.CardSlot").Preload("Channel.Groups.GroupSlot").Where("account_id = ? AND channel_id != 0", account.ID).Find(&slots).Error; err != nil { + 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.GroupSlot").Where("account_id = ? AND channel_id != 0", account.ID).Find(&slots).Error; err != nil { ErrResponse(w, http.StatusInternalServerError, err) return } @@ -102,14 +104,14 @@ func GetChannels(w http.ResponseWriter, r *http.Request) { account := &card.Account var slots []store.ChannelSlot if channelRevisionSet { - 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").Preload("Channel.Groups.Cards").Where("account_id = ? AND revision > ?", account.ID, channelRevision).Find(&slots).Error; err != nil { + if err := store.DB.Preload("Channel.Cards").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.Cards").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_id != 0", account.ID).Find(&slots).Error; err != nil { + 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").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_id != 0", account.ID).Find(&slots).Error; err != nil { ErrResponse(w, http.StatusInternalServerError, err) return } diff --git a/net/server/internal/modelUtil.go b/net/server/internal/modelUtil.go index 2075824b..baf54094 100644 --- a/net/server/internal/modelUtil.go +++ b/net/server/internal/modelUtil.go @@ -199,6 +199,27 @@ func getChannelDetailModel(slot *store.ChannelSlot, showList bool) *ChannelDetai } } +func getChannelSummaryModel(slot *store.ChannelSlot) *ChannelSummary { + + if slot.Channel == nil { + return nil + } + + topicDetail := TopicDetail{}; + if len(slot.Channel.Topics) > 0 { + topicDetail.Guid = slot.Channel.Topics[0].Guid; + topicDetail.DataType = slot.Channel.Topics[0].DataType; + topicDetail.Data = slot.Channel.Topics[0].Data; + topicDetail.Created = slot.Channel.Topics[0].Created; + topicDetail.Updated = slot.Channel.Topics[0].Updated; + topicDetail.Status = slot.Channel.Topics[0].Status; + } + + return &ChannelSummary{ + LastTopic: &topicDetail, + } +} + func getChannelModel(slot *store.ChannelSlot, showData bool, showList bool) *Channel { if !showData || slot.Channel == nil { @@ -215,6 +236,7 @@ func getChannelModel(slot *store.ChannelSlot, showData bool, showList bool) *Cha DetailRevision: slot.Channel.DetailRevision, TopicRevision: slot.Channel.TopicRevision, ChannelDetail: getChannelDetailModel(slot, showList), + ChannelSummary: getChannelSummaryModel(slot), }, } } diff --git a/net/server/internal/models.go b/net/server/internal/models.go index a7f3673b..dc5ae385 100644 --- a/net/server/internal/models.go +++ b/net/server/internal/models.go @@ -175,6 +175,8 @@ type ChannelData struct { TopicRevision int64 `json:"topicRevision"` + ChannelSummary *ChannelSummary `json:"channelSummary,omitempty"` + ChannelDetail *ChannelDetail `json:"channelDetail,omitempty"` } @@ -193,6 +195,11 @@ type ChannelDetail struct { Members []string `json:"members"` } +type ChannelSummary struct { + + LastTopic *TopicDetail `json:"lastTopic,omitempty"` +} + type ChannelParams struct { DataType string `json:"dataType"` @@ -443,7 +450,7 @@ type TopicDetail struct { Status string `json:"status"` - Transform string `json:"transform"` + Transform string `json:"transform,omitempty"` } type TopicTags struct { diff --git a/net/server/internal/routers.go b/net/server/internal/routers.go index 2bb54e07..bbf1ce7a 100644 --- a/net/server/internal/routers.go +++ b/net/server/internal/routers.go @@ -545,6 +545,13 @@ var routes = Routes{ GetChannelDetail, }, + Route{ + "GetChannelSummary", + strings.ToUpper("Get"), + "/content/channels/{channelId}/summary", + GetChannelSummary, + }, + Route{ "GetChannelSubjectField", strings.ToUpper("Get"), diff --git a/net/web/src/api/getChannelDetail.js b/net/web/src/api/getChannelDetail.js new file mode 100644 index 00000000..cd5b6f04 --- /dev/null +++ b/net/web/src/api/getChannelDetail.js @@ -0,0 +1,8 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; + +export async function getChannelDetail(token, channelId) { + let detail = await fetchWithTimeout(`/content/channels/${channelId}/detail?agent=${token}`, { method: 'GET' }); + checkResponse(detail) + return await detail.json() +} + diff --git a/net/web/src/api/getChannelSummary.js b/net/web/src/api/getChannelSummary.js new file mode 100644 index 00000000..ca9ea8cf --- /dev/null +++ b/net/web/src/api/getChannelSummary.js @@ -0,0 +1,8 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; + +export async function getChannelSummary(token, channelId) { + let summary = await fetchWithTimeout(`/content/channels/${channelId}/summary?agent=${token}`, { method: 'GET' }); + checkResponse(summary) + return await summary.json() +} + diff --git a/net/web/src/api/getContactChannelDetail.js b/net/web/src/api/getContactChannelDetail.js new file mode 100644 index 00000000..69f3eae5 --- /dev/null +++ b/net/web/src/api/getContactChannelDetail.js @@ -0,0 +1,8 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; + +export async function getContactChannelDetail(token, channelId) { + let detail = await fetchWithTimeout(`/content/channels/${channelId}/detail?contact=${token}`, { method: 'GET' }); + checkResponse(detail) + return await detail.json() +} + diff --git a/net/web/src/api/getContactChannelSummary.js b/net/web/src/api/getContactChannelSummary.js new file mode 100644 index 00000000..ccc73e74 --- /dev/null +++ b/net/web/src/api/getContactChannelSummary.js @@ -0,0 +1,8 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; + +export async function getContactChannelSummary(token, channelId) { + let summary = await fetchWithTimeout(`/content/channels/${channelId}/summary?contact=${token}`, { method: 'GET' }); + checkResponse(summary) + return await summary.json() +} + diff --git a/net/web/src/context/useCardContext.hook.js b/net/web/src/context/useCardContext.hook.js index a02aa2ce..f4589b0c 100644 --- a/net/web/src/context/useCardContext.hook.js +++ b/net/web/src/context/useCardContext.hook.js @@ -1,6 +1,7 @@ import { useEffect, useState, useRef } from 'react'; import { getContactChannels } from 'api/getContactChannels'; import { getContactChannelDetail } from 'api/getContactChannelDetail'; +import { getContactChannelSummary } from 'api/getContactChannelSummary'; import { getContactProfile } from 'api/getContactProfile'; import { setCardProfile } from 'api/setCardProfile'; import { getCards } from 'api/getCards'; @@ -116,15 +117,23 @@ export function useCardContext() { if (cur.data.detailRevision != channel.data.detailRevision) { if (channel.data.channelDetail != null) { cur.data.channelDetail = channel.data.channelDetail; - cur.data.detailRevision = channel.data.detailRevision; } else { let detail = await getContactChannelDetail(guid + "." + token, channel.id); cur.data.channelDetail = detail; - cur.data.detailRevision = channel.data.detailRevision; } + cur.data.detailRevision = channel.data.detailRevision; + } + if (cur.data.detailRevision != channel.data.detailRevision) { + if (channel.data.channelSummary != null) { + cur.data.channelSummary = channel.data.channelSummary; + } + else { + let summary = await getContactChannelSummary(guid + "." + token, channel.id); + cur.data.channelSummary = summary; + } + cur.data.topicRevision = channel.data.topicRevision; } - cur.data.topicRevision = channel.data.topicRevision; cur.revision = channel.revision; channelMap.set(channel.id, cur); } diff --git a/net/web/src/context/useChannelContext.hook.js b/net/web/src/context/useChannelContext.hook.js index 3ecf8004..b3c5861a 100644 --- a/net/web/src/context/useChannelContext.hook.js +++ b/net/web/src/context/useChannelContext.hook.js @@ -1,6 +1,7 @@ import { useEffect, useState, useRef } from 'react'; import { getChannels } from 'api/getChannels'; import { getChannelDetail } from 'api/getChannelDetail'; +import { getChannelSummary } from 'api/getChannelSummary'; import { addChannel } from 'api/addChannel'; import { addChannelTopic } from 'api/addChannelTopic'; import { getChannelTopics } from 'api/getChannelTopics'; @@ -32,15 +33,23 @@ export function useChannelContext() { if (cur.data.detailRevision != channel.data.detailRevision) { if (channel.data.channelDetail != null) { cur.data.channelDetail = channel.data.channelDetail; - cur.data.detailRevision = channel.data.detailRevision; } else { let detail = await getChannelDetail(access.current, channel.id); cur.data.channelDetail = detail; - cur.data.detailRevision = channel.data.detailRevision; } + cur.data.detailRevision = channel.data.detailRevision; + } + if (cur.data.topicRevision != channel.data.topicRevision) { + if (channel.data.channelDetail != null) { + cur.data.channelDetail = channel.data.channelDetail; + } + else { + let summary = await getChannelSummary(access.current, channel.id); + cur.data.channelSummary = summary; + } + cur.data.topicRevision = channel.data.topicRevision; } - cur.data.topicRevision = channel.data.topicRevision; cur.revision = channel.revision; channels.current.set(channel.id, cur); }