2023-06-26 23:06:53 +08:00
package v1
2023-07-06 21:56:42 +08:00
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
2023-09-17 22:55:13 +08:00
"go.uber.org/zap"
2023-10-26 09:02:50 +08:00
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/internal/util"
2023-11-25 10:31:58 +08:00
"github.com/usememos/memos/plugin/webhook"
2023-10-28 02:43:46 +08:00
storepb "github.com/usememos/memos/proto/gen/store"
2023-10-17 23:44:16 +08:00
"github.com/usememos/memos/server/service/metric"
2023-07-06 21:56:42 +08:00
"github.com/usememos/memos/store"
)
2023-06-26 23:06:53 +08:00
// Visibility is the type of a visibility.
type Visibility string
const (
// Public is the PUBLIC visibility.
Public Visibility = "PUBLIC"
// Protected is the PROTECTED visibility.
Protected Visibility = "PROTECTED"
// Private is the PRIVATE visibility.
Private Visibility = "PRIVATE"
)
func ( v Visibility ) String ( ) string {
2023-07-06 21:56:42 +08:00
switch v {
case Public :
return "PUBLIC"
case Protected :
return "PROTECTED"
case Private :
return "PRIVATE"
}
return "PRIVATE"
}
type Memo struct {
2023-08-04 21:55:07 +08:00
ID int32 ` json:"id" `
2023-07-06 21:56:42 +08:00
// Standard fields
RowStatus RowStatus ` json:"rowStatus" `
2023-08-04 21:55:07 +08:00
CreatorID int32 ` json:"creatorId" `
2023-07-06 21:56:42 +08:00
CreatedTs int64 ` json:"createdTs" `
UpdatedTs int64 ` json:"updatedTs" `
// Domain specific fields
DisplayTs int64 ` json:"displayTs" `
Content string ` json:"content" `
Visibility Visibility ` json:"visibility" `
Pinned bool ` json:"pinned" `
// Related fields
2023-07-20 19:48:39 +08:00
CreatorName string ` json:"creatorName" `
CreatorUsername string ` json:"creatorUsername" `
ResourceList [ ] * Resource ` json:"resourceList" `
RelationList [ ] * MemoRelation ` json:"relationList" `
2023-07-06 21:56:42 +08:00
}
type CreateMemoRequest struct {
// Standard fields
2023-08-04 21:55:07 +08:00
CreatorID int32 ` json:"-" `
2023-07-06 21:56:42 +08:00
CreatedTs * int64 ` json:"createdTs" `
// Domain specific fields
Visibility Visibility ` json:"visibility" `
Content string ` json:"content" `
// Related fields
2023-08-04 21:55:07 +08:00
ResourceIDList [ ] int32 ` json:"resourceIdList" `
2023-07-06 21:56:42 +08:00
RelationList [ ] * UpsertMemoRelationRequest ` json:"relationList" `
}
type PatchMemoRequest struct {
2023-08-04 21:55:07 +08:00
ID int32 ` json:"-" `
2023-07-06 21:56:42 +08:00
// Standard fields
CreatedTs * int64 ` json:"createdTs" `
UpdatedTs * int64
RowStatus * RowStatus ` json:"rowStatus" `
// Domain specific fields
Content * string ` json:"content" `
Visibility * Visibility ` json:"visibility" `
// Related fields
2023-08-04 21:55:07 +08:00
ResourceIDList [ ] int32 ` json:"resourceIdList" `
2023-07-06 21:56:42 +08:00
RelationList [ ] * UpsertMemoRelationRequest ` json:"relationList" `
}
type FindMemoRequest struct {
2023-08-04 21:55:07 +08:00
ID * int32
2023-07-06 21:56:42 +08:00
// Standard fields
RowStatus * RowStatus
2023-08-04 21:55:07 +08:00
CreatorID * int32
2023-07-06 21:56:42 +08:00
// Domain specific fields
Pinned * bool
ContentSearch [ ] string
VisibilityList [ ] Visibility
// Pagination
Limit * int
Offset * int
}
// maxContentLength means the max memo content bytes is 1MB.
const maxContentLength = 1 << 30
func ( s * APIV1Service ) registerMemoRoutes ( g * echo . Group ) {
2023-08-09 22:30:27 +08:00
g . GET ( "/memo" , s . GetMemoList )
g . POST ( "/memo" , s . CreateMemo )
g . GET ( "/memo/all" , s . GetAllMemos )
g . GET ( "/memo/stats" , s . GetMemoStats )
g . GET ( "/memo/:memoId" , s . GetMemo )
g . PATCH ( "/memo/:memoId" , s . UpdateMemo )
g . DELETE ( "/memo/:memoId" , s . DeleteMemo )
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 22:30:27 +08:00
// GetMemoList godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Get a list of memos matching optional filters
// @Tags memo
// @Produce json
// @Param creatorId query int false "Creator ID"
// @Param creatorUsername query string false "Creator username"
// @Param rowStatus query store.RowStatus false "Row status"
// @Param pinned query bool false "Pinned"
// @Param tag query string false "Search for tag. Do not append #"
// @Param content query string false "Search for content"
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} []store.Memo "Memo list"
// @Failure 400 {object} nil "Missing user to find memo"
// @Failure 500 {object} nil "Failed to get memo display with updated ts setting value | Failed to fetch memo list | Failed to compose memo response"
// @Router /api/v1/memo [GET]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) GetMemoList ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-11-19 09:42:59 +08:00
find := & store . FindMemo {
OrderByPinned : true ,
2023-10-08 00:42:02 +08:00
}
2023-08-09 21:53:06 +08:00
if userID , err := util . ConvertStringToInt32 ( c . QueryParam ( "creatorId" ) ) ; err == nil {
2023-11-19 09:42:59 +08:00
find . CreatorID = & userID
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
if username := c . QueryParam ( "creatorUsername" ) ; username != "" {
user , _ := s . Store . GetUser ( ctx , & store . FindUser { Username : & username } )
if user != nil {
2023-11-19 09:42:59 +08:00
find . CreatorID = & user . ID
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-09-14 20:16:17 +08:00
currentUserID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
// Anonymous use should only fetch PUBLIC memos with specified user
2023-11-19 09:42:59 +08:00
if find . CreatorID == nil {
2023-08-09 21:53:06 +08:00
return echo . NewHTTPError ( http . StatusBadRequest , "Missing user to find memo" )
2023-07-06 21:56:42 +08:00
}
2023-11-19 09:42:59 +08:00
find . VisibilityList = [ ] store . Visibility { store . Public }
2023-08-09 21:53:06 +08:00
} else {
// Authorized user can fetch all PUBLIC/PROTECTED memo
visibilityList := [ ] store . Visibility { store . Public , store . Protected }
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
// If Creator is authorized user (as default), PRIVATE memo is OK
2023-11-19 09:42:59 +08:00
if find . CreatorID == nil || * find . CreatorID == currentUserID {
find . CreatorID = & currentUserID
2023-08-09 21:53:06 +08:00
visibilityList = append ( visibilityList , store . Private )
2023-07-06 21:56:42 +08:00
}
2023-11-19 09:42:59 +08:00
find . VisibilityList = visibilityList
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
rowStatus := store . RowStatus ( c . QueryParam ( "rowStatus" ) )
if rowStatus != "" {
2023-11-19 09:42:59 +08:00
find . RowStatus = & rowStatus
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
contentSearch := [ ] string { }
tag := c . QueryParam ( "tag" )
if tag != "" {
contentSearch = append ( contentSearch , "#" + tag )
}
2023-10-19 00:18:07 +08:00
content := c . QueryParam ( "content" )
if content != "" {
contentSearch = append ( contentSearch , content )
2023-08-09 21:53:06 +08:00
}
2023-11-19 09:42:59 +08:00
find . ContentSearch = contentSearch
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
if limit , err := strconv . Atoi ( c . QueryParam ( "limit" ) ) ; err == nil {
2023-11-19 09:42:59 +08:00
find . Limit = & limit
2023-08-09 21:53:06 +08:00
}
if offset , err := strconv . Atoi ( c . QueryParam ( "offset" ) ) ; err == nil {
2023-11-19 09:42:59 +08:00
find . Offset = & offset
2023-08-09 21:53:06 +08:00
}
memoDisplayWithUpdatedTs , err := s . getMemoDisplayWithUpdatedTsSettingValue ( ctx )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to get memo display with updated ts setting value" ) . SetInternal ( err )
}
if memoDisplayWithUpdatedTs {
2023-11-19 09:42:59 +08:00
find . OrderByUpdatedTs = true
2023-08-09 21:53:06 +08:00
}
2023-07-06 22:53:38 +08:00
2023-11-19 09:42:59 +08:00
list , err := s . Store . ListMemos ( ctx , find )
2023-08-09 21:53:06 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to fetch memo list" ) . SetInternal ( err )
}
memoResponseList := [ ] * Memo { }
for _ , memo := range list {
2023-07-06 21:56:42 +08:00
memoResponse , err := s . convertMemoFromStore ( ctx , memo )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compose memo response" ) . SetInternal ( err )
}
2023-08-09 21:53:06 +08:00
memoResponseList = append ( memoResponseList , memoResponse )
}
return c . JSON ( http . StatusOK , memoResponseList )
}
2023-07-06 21:56:42 +08:00
2023-08-09 22:30:27 +08:00
// CreateMemo godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Create a memo
// @Description Visibility can be PUBLIC, PROTECTED or PRIVATE
// @Description *You should omit fields to use their default values
// @Tags memo
// @Accept json
// @Produce json
// @Param body body CreateMemoRequest true "Request object."
// @Success 200 {object} store.Memo "Stored memo"
// @Failure 400 {object} nil "Malformatted post memo request | Content size overflow, up to 1MB"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 404 {object} nil "User not found | Memo not found: %d"
// @Failure 500 {object} nil "Failed to find user setting | Failed to unmarshal user setting value | Failed to find system setting | Failed to unmarshal system setting | Failed to find user | Failed to create memo | Failed to create activity | Failed to upsert memo resource | Failed to upsert memo relation | Failed to compose memo | Failed to compose memo response"
// @Router /api/v1/memo [POST]
//
// NOTES:
// - It's currently possible to create phantom resources and relations. Phantom relations will trigger backend 404's when fetching memo.
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) CreateMemo ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-09-14 20:16:17 +08:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
createMemoRequest := & CreateMemoRequest { }
if err := json . NewDecoder ( c . Request ( ) . Body ) . Decode ( createMemoRequest ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted post memo request" ) . SetInternal ( err )
}
if len ( createMemoRequest . Content ) > maxContentLength {
return echo . NewHTTPError ( http . StatusBadRequest , "Content size overflow, up to 1MB" )
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
if createMemoRequest . Visibility == "" {
2023-12-16 12:18:53 +08:00
userMemoVisibilitySetting , err := s . Store . GetUserSetting ( ctx , & store . FindUserSetting {
2023-08-09 21:53:06 +08:00
UserID : & userID ,
2023-12-08 22:41:47 +08:00
Key : storepb . UserSettingKey_USER_SETTING_MEMO_VISIBILITY ,
2023-07-06 21:56:42 +08:00
} )
if err != nil {
2023-08-09 21:53:06 +08:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find user setting" ) . SetInternal ( err )
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
if userMemoVisibilitySetting != nil {
2023-12-08 22:41:47 +08:00
createMemoRequest . Visibility = Visibility ( userMemoVisibilitySetting . GetMemoVisibility ( ) )
2023-08-09 21:53:06 +08:00
} else {
// Private is the default memo visibility.
createMemoRequest . Visibility = Private
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
// Find disable public memos system setting.
disablePublicMemosSystemSetting , err := s . Store . GetSystemSetting ( ctx , & store . FindSystemSetting {
Name : SystemSettingDisablePublicMemosName . String ( ) ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find system setting" ) . SetInternal ( err )
}
if disablePublicMemosSystemSetting != nil {
disablePublicMemos := false
err = json . Unmarshal ( [ ] byte ( disablePublicMemosSystemSetting . Value ) , & disablePublicMemos )
2023-07-06 21:56:42 +08:00
if err != nil {
2023-08-09 21:53:06 +08:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to unmarshal system setting" ) . SetInternal ( err )
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
if disablePublicMemos {
user , err := s . Store . GetUser ( ctx , & store . FindUser {
ID : & userID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find user" ) . SetInternal ( err )
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
if user == nil {
return echo . NewHTTPError ( http . StatusNotFound , "User not found" )
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
// Enforce normal user to create private memo if public memos are disabled.
if user . Role == store . RoleUser {
createMemoRequest . Visibility = Private
2023-07-06 21:56:42 +08:00
}
}
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
createMemoRequest . CreatorID = userID
memo , err := s . Store . CreateMemo ( ctx , convertCreateMemoRequestToMemoMessage ( createMemoRequest ) )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create memo" ) . SetInternal ( err )
}
2023-07-06 22:53:38 +08:00
2023-08-09 21:53:06 +08:00
for _ , resourceID := range createMemoRequest . ResourceIDList {
2023-09-27 00:40:16 +08:00
if _ , err := s . Store . UpdateResource ( ctx , & store . UpdateResource {
ID : resourceID ,
MemoID : & memo . ID ,
2023-08-09 21:53:06 +08:00
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to upsert memo resource" ) . SetInternal ( err )
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
for _ , memoRelationUpsert := range createMemoRequest . RelationList {
if _ , err := s . Store . UpsertMemoRelation ( ctx , & store . MemoRelation {
MemoID : memo . ID ,
RelatedMemoID : memoRelationUpsert . RelatedMemoID ,
Type : store . MemoRelationType ( memoRelationUpsert . Type ) ,
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to upsert memo relation" ) . SetInternal ( err )
2023-07-06 21:56:42 +08:00
}
2023-10-28 09:02:02 +08:00
if memo . Visibility != store . Private && memoRelationUpsert . Type == MemoRelationComment {
2023-10-27 23:11:56 +08:00
relatedMemo , err := s . Store . GetMemo ( ctx , & store . FindMemo {
ID : & memoRelationUpsert . RelatedMemoID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to get related memo" ) . SetInternal ( err )
}
if relatedMemo . CreatorID != memo . CreatorID {
2023-10-28 02:43:46 +08:00
activity , err := s . Store . CreateActivity ( ctx , & store . Activity {
CreatorID : memo . CreatorID ,
Type : store . ActivityTypeMemoComment ,
Level : store . ActivityLevelInfo ,
Payload : & storepb . ActivityPayload {
MemoComment : & storepb . ActivityMemoCommentPayload {
MemoId : memo . ID ,
RelatedMemoId : memoRelationUpsert . RelatedMemoID ,
} ,
} ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create activity" ) . SetInternal ( err )
}
2023-11-05 21:41:47 +08:00
metric . Enqueue ( "memo comment create" )
2023-10-28 02:43:46 +08:00
if _ , err := s . Store . CreateInbox ( ctx , & store . Inbox {
SenderID : memo . CreatorID ,
ReceiverID : relatedMemo . CreatorID ,
Status : store . UNREAD ,
Message : & storepb . InboxMessage {
Type : storepb . InboxMessage_TYPE_MEMO_COMMENT ,
ActivityId : & activity . ID ,
} ,
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create inbox" ) . SetInternal ( err )
}
2023-10-27 23:11:56 +08:00
}
}
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-09-17 22:55:13 +08:00
composedMemo , err := s . Store . GetMemo ( ctx , & store . FindMemo {
2023-08-09 21:53:06 +08:00
ID : & memo . ID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compose memo" ) . SetInternal ( err )
}
2023-09-17 22:55:13 +08:00
if composedMemo == nil {
2023-08-09 21:53:06 +08:00
return echo . NewHTTPError ( http . StatusNotFound , fmt . Sprintf ( "Memo not found: %d" , memo . ID ) )
}
2023-07-20 19:48:39 +08:00
2023-09-17 22:55:13 +08:00
memoResponse , err := s . convertMemoFromStore ( ctx , composedMemo )
2023-08-09 21:53:06 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compose memo response" ) . SetInternal ( err )
}
2023-09-13 21:36:43 +08:00
2023-11-25 10:31:58 +08:00
// Send notification to telegram if memo is not private.
2023-09-13 21:36:43 +08:00
if memoResponse . Visibility != Private {
// fetch all telegram UserID
2023-12-16 12:18:53 +08:00
userSettings , err := s . Store . ListUserSettings ( ctx , & store . FindUserSetting { Key : storepb . UserSettingKey_USER_SETTING_TELEGRAM_USER_ID } )
2023-09-13 21:36:43 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to ListUserSettings" ) . SetInternal ( err )
}
for _ , userSetting := range userSettings {
2023-12-08 22:41:47 +08:00
tgUserID , err := strconv . ParseInt ( userSetting . GetTelegramUserId ( ) , 10 , 64 )
2023-09-13 21:36:43 +08:00
if err != nil {
log . Error ( "failed to parse Telegram UserID" , zap . Error ( err ) )
continue
}
// send notification to telegram
content := memoResponse . CreatorName + " Says:\n\n" + memoResponse . Content
_ , err = s . telegramBot . SendMessage ( ctx , tgUserID , content )
if err != nil {
log . Error ( "Failed to send Telegram notification" , zap . Error ( err ) )
continue
}
}
}
2023-11-25 10:31:58 +08:00
// Try to dispatch webhook when memo is created.
if err := s . DispatchMemoCreatedWebhook ( ctx , memoResponse ) ; err != nil {
2023-11-28 20:52:48 +08:00
log . Warn ( "Failed to dispatch memo created webhook" , zap . Error ( err ) )
2023-11-25 10:31:58 +08:00
}
2023-10-17 23:44:16 +08:00
metric . Enqueue ( "memo create" )
2023-08-09 21:53:06 +08:00
return c . JSON ( http . StatusOK , memoResponse )
}
2023-07-13 15:20:15 +08:00
2023-08-09 22:30:27 +08:00
// GetAllMemos godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Get a list of public memos matching optional filters
// @Description This should also list protected memos if the user is logged in
// @Description Authentication is optional
// @Tags memo
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} []store.Memo "Memo list"
// @Failure 500 {object} nil "Failed to get memo display with updated ts setting value | Failed to fetch all memo list | Failed to compose memo response"
// @Router /api/v1/memo/all [GET]
//
// NOTES:
// - creatorUsername is listed at ./web/src/helpers/api.ts:82, but it's not present here
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) GetAllMemos ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-12-18 20:47:29 +08:00
memoFind := & store . FindMemo { }
2023-09-14 20:16:17 +08:00
_ , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
2023-12-18 20:47:29 +08:00
memoFind . VisibilityList = [ ] store . Visibility { store . Public }
2023-08-09 21:53:06 +08:00
} else {
2023-12-18 20:47:29 +08:00
memoFind . VisibilityList = [ ] store . Visibility { store . Public , store . Protected }
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
if limit , err := strconv . Atoi ( c . QueryParam ( "limit" ) ) ; err == nil {
2023-12-18 20:47:29 +08:00
memoFind . Limit = & limit
2023-08-09 21:53:06 +08:00
}
if offset , err := strconv . Atoi ( c . QueryParam ( "offset" ) ) ; err == nil {
2023-12-18 20:47:29 +08:00
memoFind . Offset = & offset
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
// Only fetch normal status memos.
normalStatus := store . Normal
2023-12-18 20:47:29 +08:00
memoFind . RowStatus = & normalStatus
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
memoDisplayWithUpdatedTs , err := s . getMemoDisplayWithUpdatedTsSettingValue ( ctx )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to get memo display with updated ts setting value" ) . SetInternal ( err )
}
if memoDisplayWithUpdatedTs {
2023-12-18 20:47:29 +08:00
memoFind . OrderByUpdatedTs = true
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-12-18 20:47:29 +08:00
list , err := s . Store . ListMemos ( ctx , memoFind )
2023-08-09 21:53:06 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to fetch all memo list" ) . SetInternal ( err )
}
memoResponseList := [ ] * Memo { }
for _ , memo := range list {
memoResponse , err := s . convertMemoFromStore ( ctx , memo )
2023-07-06 21:56:42 +08:00
if err != nil {
2023-08-09 21:53:06 +08:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compose memo response" ) . SetInternal ( err )
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
memoResponseList = append ( memoResponseList , memoResponse )
}
return c . JSON ( http . StatusOK , memoResponseList )
}
2023-07-06 21:56:42 +08:00
2023-08-09 22:30:27 +08:00
// GetMemoStats godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Get memo stats by creator ID or username
// @Description Used to generate the heatmap
// @Tags memo
// @Produce json
// @Param creatorId query int false "Creator ID"
// @Param creatorUsername query string false "Creator username"
// @Success 200 {object} []int "Memo createdTs list"
// @Failure 400 {object} nil "Missing user id to find memo"
// @Failure 500 {object} nil "Failed to get memo display with updated ts setting value | Failed to find memo list | Failed to compose memo response"
// @Router /api/v1/memo/stats [GET]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) GetMemoStats ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
normalStatus := store . Normal
findMemoMessage := & store . FindMemo {
2023-10-20 08:48:52 +08:00
RowStatus : & normalStatus ,
ExcludeContent : true ,
2023-08-09 21:53:06 +08:00
}
if creatorID , err := util . ConvertStringToInt32 ( c . QueryParam ( "creatorId" ) ) ; err == nil {
findMemoMessage . CreatorID = & creatorID
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
if username := c . QueryParam ( "creatorUsername" ) ; username != "" {
user , _ := s . Store . GetUser ( ctx , & store . FindUser { Username : & username } )
if user != nil {
findMemoMessage . CreatorID = & user . ID
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
if findMemoMessage . CreatorID == nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Missing user id to find memo" )
}
2023-07-06 21:56:42 +08:00
2023-09-14 20:16:17 +08:00
currentUserID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
findMemoMessage . VisibilityList = [ ] store . Visibility { store . Public }
} else {
if * findMemoMessage . CreatorID != currentUserID {
findMemoMessage . VisibilityList = [ ] store . Visibility { store . Public , store . Protected }
} else {
findMemoMessage . VisibilityList = [ ] store . Visibility { store . Public , store . Protected , store . Private }
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
}
memoDisplayWithUpdatedTs , err := s . getMemoDisplayWithUpdatedTsSettingValue ( ctx )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to get memo display with updated ts setting value" ) . SetInternal ( err )
}
if memoDisplayWithUpdatedTs {
findMemoMessage . OrderByUpdatedTs = true
}
list , err := s . Store . ListMemos ( ctx , findMemoMessage )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find memo list" ) . SetInternal ( err )
}
displayTsList := [ ] int64 { }
2023-09-14 14:18:29 +08:00
if memoDisplayWithUpdatedTs {
for _ , memo := range list {
2023-09-18 13:53:16 +08:00
displayTsList = append ( displayTsList , memo . UpdatedTs )
2023-09-14 14:18:29 +08:00
}
} else {
for _ , memo := range list {
2023-09-18 13:53:16 +08:00
displayTsList = append ( displayTsList , memo . CreatedTs )
2023-09-14 14:18:29 +08:00
}
2023-08-09 21:53:06 +08:00
}
return c . JSON ( http . StatusOK , displayTsList )
}
2023-08-09 22:30:27 +08:00
// GetMemo godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Get memo by ID
// @Tags memo
// @Produce json
// @Param memoId path int true "Memo ID"
// @Success 200 {object} []store.Memo "Memo list"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 403 {object} nil "this memo is private only | this memo is protected, missing user in session
// @Failure 404 {object} nil "Memo not found: %d"
// @Failure 500 {object} nil "Failed to find memo by ID: %v | Failed to compose memo response"
// @Router /api/v1/memo/{memoId} [GET]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) GetMemo ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
memoID , err := util . ConvertStringToInt32 ( c . Param ( "memoId" ) )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "ID is not a number: %s" , c . Param ( "memoId" ) ) ) . SetInternal ( err )
}
memo , err := s . Store . GetMemo ( ctx , & store . FindMemo {
ID : & memoID ,
2023-07-06 21:56:42 +08:00
} )
2023-08-09 21:53:06 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "Failed to find memo by ID: %v" , memoID ) ) . SetInternal ( err )
}
if memo == nil {
return echo . NewHTTPError ( http . StatusNotFound , fmt . Sprintf ( "Memo not found: %d" , memoID ) )
}
2023-07-06 21:56:42 +08:00
2023-09-14 20:16:17 +08:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if memo . Visibility == store . Private {
if ! ok || memo . CreatorID != userID {
return echo . NewHTTPError ( http . StatusForbidden , "this memo is private only" )
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
} else if memo . Visibility == store . Protected {
if ! ok {
return echo . NewHTTPError ( http . StatusForbidden , "this memo is protected, missing user in session" )
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
}
memoResponse , err := s . convertMemoFromStore ( ctx , memo )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compose memo response" ) . SetInternal ( err )
}
return c . JSON ( http . StatusOK , memoResponse )
}
2023-07-20 19:48:39 +08:00
2023-08-09 22:30:27 +08:00
// DeleteMemo godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Delete memo by ID
// @Tags memo
// @Produce json
// @Param memoId path int true "Memo ID to delete"
// @Success 200 {boolean} true "Memo deleted"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 404 {object} nil "Memo not found: %d"
// @Failure 500 {object} nil "Failed to find memo | Failed to delete memo ID: %v"
// @Router /api/v1/memo/{memoId} [DELETE]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) DeleteMemo ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-09-14 20:16:17 +08:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
memoID , err := util . ConvertStringToInt32 ( c . Param ( "memoId" ) )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "ID is not a number: %s" , c . Param ( "memoId" ) ) ) . SetInternal ( err )
}
2023-07-20 19:48:39 +08:00
2023-08-09 21:53:06 +08:00
memo , err := s . Store . GetMemo ( ctx , & store . FindMemo {
ID : & memoID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find memo" ) . SetInternal ( err )
}
if memo == nil {
return echo . NewHTTPError ( http . StatusNotFound , fmt . Sprintf ( "Memo not found: %d" , memoID ) )
}
if memo . CreatorID != userID {
return echo . NewHTTPError ( http . StatusUnauthorized , "Unauthorized" )
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
if err := s . Store . DeleteMemo ( ctx , & store . DeleteMemo {
ID : memoID ,
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "Failed to delete memo ID: %v" , memoID ) ) . SetInternal ( err )
}
return c . JSON ( http . StatusOK , true )
}
2023-07-06 21:56:42 +08:00
2023-08-09 22:30:27 +08:00
// UpdateMemo godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Update a memo
// @Description Visibility can be PUBLIC, PROTECTED or PRIVATE
// @Description *You should omit fields to use their default values
// @Tags memo
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to update"
// @Param body body PatchMemoRequest true "Patched object."
// @Success 200 {object} store.Memo "Stored memo"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted patch memo request | Content size overflow, up to 1MB"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 404 {object} nil "Memo not found: %d"
// @Failure 500 {object} nil "Failed to find memo | Failed to patch memo | Failed to upsert memo resource | Failed to delete memo resource | Failed to compose memo response"
// @Router /api/v1/memo/{memoId} [PATCH]
//
// NOTES:
// - It's currently possible to create phantom resources and relations. Phantom relations will trigger backend 404's when fetching memo.
// - Passing 0 to createdTs and updatedTs will set them to 0 in the database, which is probably unwanted.
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) UpdateMemo ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-09-14 20:16:17 +08:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
memoID , err := util . ConvertStringToInt32 ( c . Param ( "memoId" ) )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "ID is not a number: %s" , c . Param ( "memoId" ) ) ) . SetInternal ( err )
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
memo , err := s . Store . GetMemo ( ctx , & store . FindMemo {
ID : & memoID ,
2023-07-06 21:56:42 +08:00
} )
2023-08-09 21:53:06 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find memo" ) . SetInternal ( err )
}
if memo == nil {
return echo . NewHTTPError ( http . StatusNotFound , fmt . Sprintf ( "Memo not found: %d" , memoID ) )
}
if memo . CreatorID != userID {
return echo . NewHTTPError ( http . StatusUnauthorized , "Unauthorized" )
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
currentTs := time . Now ( ) . Unix ( )
patchMemoRequest := & PatchMemoRequest {
ID : memoID ,
UpdatedTs : & currentTs ,
}
if err := json . NewDecoder ( c . Request ( ) . Body ) . Decode ( patchMemoRequest ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted patch memo request" ) . SetInternal ( err )
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
if patchMemoRequest . Content != nil && len ( * patchMemoRequest . Content ) > maxContentLength {
return echo . NewHTTPError ( http . StatusBadRequest , "Content size overflow, up to 1MB" ) . SetInternal ( err )
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
updateMemoMessage := & store . UpdateMemo {
ID : memoID ,
CreatedTs : patchMemoRequest . CreatedTs ,
UpdatedTs : patchMemoRequest . UpdatedTs ,
Content : patchMemoRequest . Content ,
}
if patchMemoRequest . RowStatus != nil {
rowStatus := store . RowStatus ( patchMemoRequest . RowStatus . String ( ) )
updateMemoMessage . RowStatus = & rowStatus
}
if patchMemoRequest . Visibility != nil {
visibility := store . Visibility ( patchMemoRequest . Visibility . String ( ) )
updateMemoMessage . Visibility = & visibility
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
err = s . Store . UpdateMemo ( ctx , updateMemoMessage )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to patch memo" ) . SetInternal ( err )
}
memo , err = s . Store . GetMemo ( ctx , & store . FindMemo { ID : & memoID } )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find memo" ) . SetInternal ( err )
}
if memo == nil {
return echo . NewHTTPError ( http . StatusNotFound , fmt . Sprintf ( "Memo not found: %d" , memoID ) )
}
2023-07-06 21:56:42 +08:00
2023-12-06 22:44:49 +08:00
memoMessage , err := s . convertMemoFromStore ( ctx , memo )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compose memo" ) . SetInternal ( err )
}
2023-08-09 21:53:06 +08:00
if patchMemoRequest . ResourceIDList != nil {
2023-12-06 22:44:49 +08:00
originResourceIDList := [ ] int32 { }
for _ , resource := range memoMessage . ResourceList {
originResourceIDList = append ( originResourceIDList , resource . ID )
}
addedResourceIDList , removedResourceIDList := getIDListDiff ( originResourceIDList , patchMemoRequest . ResourceIDList )
2023-08-09 21:53:06 +08:00
for _ , resourceID := range addedResourceIDList {
2023-09-27 00:40:16 +08:00
if _ , err := s . Store . UpdateResource ( ctx , & store . UpdateResource {
ID : resourceID ,
MemoID : & memo . ID ,
2023-08-09 21:53:06 +08:00
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to upsert memo resource" ) . SetInternal ( err )
2023-07-06 21:56:42 +08:00
}
}
2023-08-09 21:53:06 +08:00
for _ , resourceID := range removedResourceIDList {
2023-09-27 00:40:16 +08:00
if err := s . Store . DeleteResource ( ctx , & store . DeleteResource {
ID : resourceID ,
2023-08-09 21:53:06 +08:00
} ) ; err != nil {
2023-09-27 00:40:16 +08:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to delete resource" ) . SetInternal ( err )
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
if patchMemoRequest . RelationList != nil {
2023-12-06 22:44:49 +08:00
patchMemoRelationList := make ( [ ] * MemoRelation , 0 )
2023-08-09 21:53:06 +08:00
for _ , memoRelation := range patchMemoRequest . RelationList {
2023-12-06 22:44:49 +08:00
patchMemoRelationList = append ( patchMemoRelationList , & MemoRelation {
2023-08-09 21:53:06 +08:00
MemoID : memo . ID ,
RelatedMemoID : memoRelation . RelatedMemoID ,
2023-12-06 22:44:49 +08:00
Type : memoRelation . Type ,
2023-08-09 21:53:06 +08:00
} )
2023-07-06 21:56:42 +08:00
}
2023-12-06 22:44:49 +08:00
addedMemoRelationList , removedMemoRelationList := getMemoRelationListDiff ( memoMessage . RelationList , patchMemoRelationList )
2023-08-09 21:53:06 +08:00
for _ , memoRelation := range addedMemoRelationList {
if _ , err := s . Store . UpsertMemoRelation ( ctx , memoRelation ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to upsert memo relation" ) . SetInternal ( err )
}
2023-07-06 22:53:38 +08:00
}
2023-08-09 21:53:06 +08:00
for _ , memoRelation := range removedMemoRelationList {
if err := s . Store . DeleteMemoRelation ( ctx , & store . DeleteMemoRelation {
MemoID : & memo . ID ,
RelatedMemoID : & memoRelation . RelatedMemoID ,
Type : & memoRelation . Type ,
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to delete memo relation" ) . SetInternal ( err )
}
2023-07-06 21:56:42 +08:00
}
2023-08-09 21:53:06 +08:00
}
2023-07-06 21:56:42 +08:00
2023-08-09 21:53:06 +08:00
memo , err = s . Store . GetMemo ( ctx , & store . FindMemo { ID : & memoID } )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find memo" ) . SetInternal ( err )
}
if memo == nil {
return echo . NewHTTPError ( http . StatusNotFound , fmt . Sprintf ( "Memo not found: %d" , memoID ) )
}
memoResponse , err := s . convertMemoFromStore ( ctx , memo )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compose memo response" ) . SetInternal ( err )
}
2023-11-28 20:52:48 +08:00
// Try to dispatch webhook when memo is updated.
if err := s . DispatchMemoUpdatedWebhook ( ctx , memoResponse ) ; err != nil {
log . Warn ( "Failed to dispatch memo updated webhook" , zap . Error ( err ) )
2023-11-25 10:31:58 +08:00
}
2023-08-09 21:53:06 +08:00
return c . JSON ( http . StatusOK , memoResponse )
2023-07-06 21:56:42 +08:00
}
func ( s * APIV1Service ) convertMemoFromStore ( ctx context . Context , memo * store . Memo ) ( * Memo , error ) {
2023-12-06 22:44:49 +08:00
memoMessage := & Memo {
2023-07-06 21:56:42 +08:00
ID : memo . ID ,
RowStatus : RowStatus ( memo . RowStatus . String ( ) ) ,
CreatorID : memo . CreatorID ,
CreatedTs : memo . CreatedTs ,
UpdatedTs : memo . UpdatedTs ,
Content : memo . Content ,
Visibility : Visibility ( memo . Visibility . String ( ) ) ,
Pinned : memo . Pinned ,
}
// Compose creator name.
user , err := s . Store . GetUser ( ctx , & store . FindUser {
2023-12-06 22:44:49 +08:00
ID : & memoMessage . CreatorID ,
2023-07-06 21:56:42 +08:00
} )
if err != nil {
return nil , err
}
if user . Nickname != "" {
2023-12-06 22:44:49 +08:00
memoMessage . CreatorName = user . Nickname
2023-07-06 21:56:42 +08:00
} else {
2023-12-06 22:44:49 +08:00
memoMessage . CreatorName = user . Username
2023-07-06 21:56:42 +08:00
}
2023-12-06 22:44:49 +08:00
memoMessage . CreatorUsername = user . Username
2023-07-20 19:48:39 +08:00
2023-07-06 21:56:42 +08:00
// Compose display ts.
2023-12-06 22:44:49 +08:00
memoMessage . DisplayTs = memoMessage . CreatedTs
2023-07-06 21:56:42 +08:00
// Find memo display with updated ts setting.
memoDisplayWithUpdatedTs , err := s . getMemoDisplayWithUpdatedTsSettingValue ( ctx )
if err != nil {
return nil , err
}
if memoDisplayWithUpdatedTs {
2023-12-06 22:44:49 +08:00
memoMessage . DisplayTs = memoMessage . UpdatedTs
2023-07-06 21:56:42 +08:00
}
2023-12-06 22:44:49 +08:00
// Compose related resources.
resourceList , err := s . Store . ListResources ( ctx , & store . FindResource {
MemoID : & memo . ID ,
} )
if err != nil {
return nil , errors . Wrapf ( err , "failed to list resources" )
}
memoMessage . ResourceList = [ ] * Resource { }
for _ , resource := range resourceList {
memoMessage . ResourceList = append ( memoMessage . ResourceList , convertResourceFromStore ( resource ) )
}
// Compose related memo relations.
2023-07-06 21:56:42 +08:00
relationList := [ ] * MemoRelation { }
2023-12-06 22:44:49 +08:00
tempList , err := s . Store . ListMemoRelations ( ctx , & store . FindMemoRelation {
MemoID : & memo . ID ,
} )
if err != nil {
return nil , err
}
for _ , relation := range tempList {
2023-07-06 21:56:42 +08:00
relationList = append ( relationList , convertMemoRelationFromStore ( relation ) )
}
2023-12-06 22:44:49 +08:00
tempList , err = s . Store . ListMemoRelations ( ctx , & store . FindMemoRelation {
RelatedMemoID : & memo . ID ,
} )
if err != nil {
return nil , err
2023-07-06 21:56:42 +08:00
}
2023-12-06 22:44:49 +08:00
for _ , relation := range tempList {
relationList = append ( relationList , convertMemoRelationFromStore ( relation ) )
}
memoMessage . RelationList = relationList
return memoMessage , nil
2023-07-06 21:56:42 +08:00
}
func ( s * APIV1Service ) getMemoDisplayWithUpdatedTsSettingValue ( ctx context . Context ) ( bool , error ) {
memoDisplayWithUpdatedTsSetting , err := s . Store . GetSystemSetting ( ctx , & store . FindSystemSetting {
Name : SystemSettingMemoDisplayWithUpdatedTsName . String ( ) ,
} )
if err != nil {
return false , errors . Wrap ( err , "failed to find system setting" )
}
memoDisplayWithUpdatedTs := false
if memoDisplayWithUpdatedTsSetting != nil {
err = json . Unmarshal ( [ ] byte ( memoDisplayWithUpdatedTsSetting . Value ) , & memoDisplayWithUpdatedTs )
if err != nil {
return false , errors . Wrap ( err , "failed to unmarshal system setting value" )
}
}
return memoDisplayWithUpdatedTs , nil
}
func convertCreateMemoRequestToMemoMessage ( memoCreate * CreateMemoRequest ) * store . Memo {
createdTs := time . Now ( ) . Unix ( )
if memoCreate . CreatedTs != nil {
createdTs = * memoCreate . CreatedTs
}
return & store . Memo {
CreatorID : memoCreate . CreatorID ,
CreatedTs : createdTs ,
Content : memoCreate . Content ,
Visibility : store . Visibility ( memoCreate . Visibility ) ,
}
}
2023-12-06 22:44:49 +08:00
func getMemoRelationListDiff ( oldList , newList [ ] * MemoRelation ) ( addedList , removedList [ ] * store . MemoRelation ) {
2023-07-06 21:56:42 +08:00
oldMap := map [ string ] bool { }
for _ , relation := range oldList {
oldMap [ fmt . Sprintf ( "%d-%s" , relation . RelatedMemoID , relation . Type ) ] = true
}
newMap := map [ string ] bool { }
for _ , relation := range newList {
newMap [ fmt . Sprintf ( "%d-%s" , relation . RelatedMemoID , relation . Type ) ] = true
}
for _ , relation := range oldList {
key := fmt . Sprintf ( "%d-%s" , relation . RelatedMemoID , relation . Type )
if ! newMap [ key ] {
2023-12-06 22:44:49 +08:00
removedList = append ( removedList , & store . MemoRelation {
MemoID : relation . MemoID ,
RelatedMemoID : relation . RelatedMemoID ,
Type : store . MemoRelationType ( relation . Type ) ,
} )
2023-07-06 21:56:42 +08:00
}
}
for _ , relation := range newList {
key := fmt . Sprintf ( "%d-%s" , relation . RelatedMemoID , relation . Type )
if ! oldMap [ key ] {
2023-12-06 22:44:49 +08:00
addedList = append ( addedList , & store . MemoRelation {
MemoID : relation . MemoID ,
RelatedMemoID : relation . RelatedMemoID ,
Type : store . MemoRelationType ( relation . Type ) ,
} )
2023-07-06 21:56:42 +08:00
}
}
return addedList , removedList
}
2023-08-04 21:55:07 +08:00
func getIDListDiff ( oldList , newList [ ] int32 ) ( addedList , removedList [ ] int32 ) {
oldMap := map [ int32 ] bool { }
2023-07-06 21:56:42 +08:00
for _ , id := range oldList {
oldMap [ id ] = true
}
2023-08-04 21:55:07 +08:00
newMap := map [ int32 ] bool { }
2023-07-06 21:56:42 +08:00
for _ , id := range newList {
newMap [ id ] = true
}
for id := range oldMap {
if ! newMap [ id ] {
removedList = append ( removedList , id )
}
}
for id := range newMap {
if ! oldMap [ id ] {
addedList = append ( addedList , id )
}
}
return addedList , removedList
2023-06-26 23:06:53 +08:00
}
2023-11-25 10:31:58 +08:00
// DispatchMemoCreatedWebhook dispatches webhook when memo is created.
func ( s * APIV1Service ) DispatchMemoCreatedWebhook ( ctx context . Context , memo * Memo ) error {
return s . dispatchMemoRelatedWebhook ( ctx , memo , "memos.memo.created" )
}
// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated.
func ( s * APIV1Service ) DispatchMemoUpdatedWebhook ( ctx context . Context , memo * Memo ) error {
return s . dispatchMemoRelatedWebhook ( ctx , memo , "memos.memo.updated" )
}
func ( s * APIV1Service ) dispatchMemoRelatedWebhook ( ctx context . Context , memo * Memo , activityType string ) error {
webhooks , err := s . Store . ListWebhooks ( ctx , & store . FindWebhook {
CreatorID : & memo . CreatorID ,
} )
if err != nil {
return err
}
2023-11-28 21:15:10 +08:00
metric . Enqueue ( "webhook dispatch" )
for _ , hook := range webhooks {
2023-11-25 10:31:58 +08:00
payload := convertMemoToWebhookPayload ( memo )
payload . ActivityType = activityType
2023-11-28 21:15:10 +08:00
payload . URL = hook . Url
2023-11-25 10:31:58 +08:00
err := webhook . Post ( * payload )
if err != nil {
return errors . Wrap ( err , "failed to post webhook" )
}
}
return nil
}
func convertMemoToWebhookPayload ( memo * Memo ) * webhook . WebhookPayload {
return & webhook . WebhookPayload {
CreatorID : memo . CreatorID ,
CreatedTs : time . Now ( ) . Unix ( ) ,
Memo : & webhook . Memo {
ID : memo . ID ,
CreatorID : memo . CreatorID ,
CreatedTs : memo . CreatedTs ,
UpdatedTs : memo . UpdatedTs ,
Content : memo . Content ,
Visibility : memo . Visibility . String ( ) ,
Pinned : memo . Pinned ,
ResourceList : func ( ) [ ] * webhook . Resource {
resources := [ ] * webhook . Resource { }
for _ , resource := range memo . ResourceList {
resources = append ( resources , & webhook . Resource {
ID : resource . ID ,
CreatorID : resource . CreatorID ,
CreatedTs : resource . CreatedTs ,
UpdatedTs : resource . UpdatedTs ,
Filename : resource . Filename ,
InternalPath : resource . InternalPath ,
ExternalLink : resource . ExternalLink ,
Type : resource . Type ,
Size : resource . Size ,
} )
}
return resources
} ( ) ,
RelationList : func ( ) [ ] * webhook . MemoRelation {
relations := [ ] * webhook . MemoRelation { }
for _ , relation := range memo . RelationList {
relations = append ( relations , & webhook . MemoRelation {
MemoID : relation . MemoID ,
RelatedMemoID : relation . RelatedMemoID ,
Type : relation . Type . String ( ) ,
} )
}
return relations
} ( ) ,
} ,
}
}