2023-06-26 18:06:53 +03:00
package v1
2023-07-06 16:56:42 +03:00
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
2023-07-31 15:55:40 +03:00
"github.com/usememos/memos/api/auth"
2023-08-04 16:55:07 +03:00
"github.com/usememos/memos/common/util"
2023-07-06 16:56:42 +03:00
"github.com/usememos/memos/store"
)
2023-06-26 18:06:53 +03: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 16:56:42 +03:00
switch v {
case Public :
return "PUBLIC"
case Protected :
return "PROTECTED"
case Private :
return "PRIVATE"
}
return "PRIVATE"
}
type Memo struct {
2023-08-04 16:55:07 +03:00
ID int32 ` json:"id" `
2023-07-06 16:56:42 +03:00
// Standard fields
RowStatus RowStatus ` json:"rowStatus" `
2023-08-04 16:55:07 +03:00
CreatorID int32 ` json:"creatorId" `
2023-07-06 16:56:42 +03: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 14:48:39 +03:00
CreatorName string ` json:"creatorName" `
CreatorUsername string ` json:"creatorUsername" `
ResourceList [ ] * Resource ` json:"resourceList" `
RelationList [ ] * MemoRelation ` json:"relationList" `
2023-07-06 16:56:42 +03:00
}
type CreateMemoRequest struct {
// Standard fields
2023-08-04 16:55:07 +03:00
CreatorID int32 ` json:"-" `
2023-07-06 16:56:42 +03:00
CreatedTs * int64 ` json:"createdTs" `
// Domain specific fields
Visibility Visibility ` json:"visibility" `
Content string ` json:"content" `
// Related fields
2023-08-04 16:55:07 +03:00
ResourceIDList [ ] int32 ` json:"resourceIdList" `
2023-07-06 16:56:42 +03:00
RelationList [ ] * UpsertMemoRelationRequest ` json:"relationList" `
}
type PatchMemoRequest struct {
2023-08-04 16:55:07 +03:00
ID int32 ` json:"-" `
2023-07-06 16:56:42 +03: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 16:55:07 +03:00
ResourceIDList [ ] int32 ` json:"resourceIdList" `
2023-07-06 16:56:42 +03:00
RelationList [ ] * UpsertMemoRelationRequest ` json:"relationList" `
}
type FindMemoRequest struct {
2023-08-04 16:55:07 +03:00
ID * int32
2023-07-06 16:56:42 +03:00
// Standard fields
RowStatus * RowStatus
2023-08-04 16:55:07 +03:00
CreatorID * int32
2023-07-06 16:56:42 +03: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 17:30:27 +03: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 16:53:06 +03:00
}
2023-07-06 16:56:42 +03:00
2023-08-09 17:30:27 +03:00
// GetMemoList godoc
2023-08-09 16:53:06 +03: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"
// @Security ApiKeyAuth
// @Router /api/v1/memo [GET]
2023-08-09 17:30:27 +03:00
func ( s * APIV1Service ) GetMemoList ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
findMemoMessage := & store . FindMemo { }
if userID , err := util . ConvertStringToInt32 ( c . QueryParam ( "creatorId" ) ) ; err == nil {
findMemoMessage . CreatorID = & userID
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
currentUserID , ok := c . Get ( auth . UserIDContextKey ) . ( int32 )
if ! ok {
// Anonymous use should only fetch PUBLIC memos with specified user
if findMemoMessage . CreatorID == nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Missing user to find memo" )
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
findMemoMessage . VisibilityList = [ ] store . Visibility { store . Public }
} else {
// Authorized user can fetch all PUBLIC/PROTECTED memo
visibilityList := [ ] store . Visibility { store . Public , store . Protected }
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
// If Creator is authorized user (as default), PRIVATE memo is OK
if findMemoMessage . CreatorID == nil || * findMemoMessage . CreatorID == currentUserID {
findMemoMessage . CreatorID = & currentUserID
visibilityList = append ( visibilityList , store . Private )
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
findMemoMessage . VisibilityList = visibilityList
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
rowStatus := store . RowStatus ( c . QueryParam ( "rowStatus" ) )
if rowStatus != "" {
findMemoMessage . RowStatus = & rowStatus
}
pinnedStr := c . QueryParam ( "pinned" )
if pinnedStr != "" {
pinned := pinnedStr == "true"
findMemoMessage . Pinned = & pinned
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
contentSearch := [ ] string { }
tag := c . QueryParam ( "tag" )
if tag != "" {
contentSearch = append ( contentSearch , "#" + tag )
}
contentSlice := c . QueryParams ( ) [ "content" ]
if len ( contentSlice ) > 0 {
contentSearch = append ( contentSearch , contentSlice ... )
}
findMemoMessage . ContentSearch = contentSearch
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
if limit , err := strconv . Atoi ( c . QueryParam ( "limit" ) ) ; err == nil {
findMemoMessage . Limit = & limit
}
if offset , err := strconv . Atoi ( c . QueryParam ( "offset" ) ) ; err == nil {
findMemoMessage . Offset = & offset
}
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
}
2023-07-06 17:53:38 +03:00
2023-08-09 16:53:06 +03:00
list , err := s . Store . ListMemos ( ctx , findMemoMessage )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to fetch memo list" ) . SetInternal ( err )
}
memoResponseList := [ ] * Memo { }
for _ , memo := range list {
2023-07-06 16:56:42 +03: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 16:53:06 +03:00
memoResponseList = append ( memoResponseList , memoResponse )
}
return c . JSON ( http . StatusOK , memoResponseList )
}
2023-07-06 16:56:42 +03:00
2023-08-09 17:30:27 +03:00
// CreateMemo godoc
2023-08-09 16:53:06 +03: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"
// @Security ApiKeyAuth
// @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 17:30:27 +03:00
func ( s * APIV1Service ) CreateMemo ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
userID , ok := c . Get ( auth . UserIDContextKey ) . ( int32 )
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
if createMemoRequest . Visibility == "" {
userMemoVisibilitySetting , err := s . Store . GetUserSetting ( ctx , & store . FindUserSetting {
UserID : & userID ,
Key : UserSettingMemoVisibilityKey . String ( ) ,
2023-07-06 16:56:42 +03:00
} )
if err != nil {
2023-08-09 16:53:06 +03:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find user setting" ) . SetInternal ( err )
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
if userMemoVisibilitySetting != nil {
memoVisibility := Private
err := json . Unmarshal ( [ ] byte ( userMemoVisibilitySetting . Value ) , & memoVisibility )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to unmarshal user setting value" ) . SetInternal ( err )
}
createMemoRequest . Visibility = memoVisibility
} else {
// Private is the default memo visibility.
createMemoRequest . Visibility = Private
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
if err != nil {
2023-08-09 16:53:06 +03:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to unmarshal system setting" ) . SetInternal ( err )
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
if user == nil {
return echo . NewHTTPError ( http . StatusNotFound , "User not found" )
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
// Enforce normal user to create private memo if public memos are disabled.
if user . Role == store . RoleUser {
createMemoRequest . Visibility = Private
2023-07-06 16:56:42 +03:00
}
}
2023-08-09 16:53:06 +03:00
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 )
}
if err := s . createMemoCreateActivity ( ctx , memo ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create activity" ) . SetInternal ( err )
}
2023-07-06 17:53:38 +03:00
2023-08-09 16:53:06 +03:00
for _ , resourceID := range createMemoRequest . ResourceIDList {
if _ , err := s . Store . UpsertMemoResource ( ctx , & store . UpsertMemoResource {
MemoID : memo . ID ,
ResourceID : resourceID ,
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to upsert memo resource" ) . SetInternal ( err )
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
memo , err = s . Store . GetMemo ( ctx , & store . FindMemo {
ID : & memo . ID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compose memo" ) . SetInternal ( err )
}
if memo == nil {
return echo . NewHTTPError ( http . StatusNotFound , fmt . Sprintf ( "Memo not found: %d" , memo . ID ) )
}
2023-07-20 14:48:39 +03:00
2023-08-09 16:53:06 +03: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-13 10:20:15 +03:00
2023-08-09 17:30:27 +03:00
// GetAllMemos godoc
2023-08-09 16:53:06 +03: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"
// @Security ApiKeyAuth
// @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 17:30:27 +03:00
func ( s * APIV1Service ) GetAllMemos ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
findMemoMessage := & store . FindMemo { }
_ , ok := c . Get ( auth . UserIDContextKey ) . ( int32 )
if ! ok {
findMemoMessage . VisibilityList = [ ] store . Visibility { store . Public }
} else {
findMemoMessage . VisibilityList = [ ] store . Visibility { store . Public , store . Protected }
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
if limit , err := strconv . Atoi ( c . QueryParam ( "limit" ) ) ; err == nil {
findMemoMessage . Limit = & limit
}
if offset , err := strconv . Atoi ( c . QueryParam ( "offset" ) ) ; err == nil {
findMemoMessage . Offset = & offset
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
// Only fetch normal status memos.
normalStatus := store . Normal
findMemoMessage . RowStatus = & normalStatus
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03: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
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
list , err := s . Store . ListMemos ( ctx , findMemoMessage )
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 16:56:42 +03:00
if err != nil {
2023-08-09 16:53:06 +03:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compose memo response" ) . SetInternal ( err )
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
memoResponseList = append ( memoResponseList , memoResponse )
}
return c . JSON ( http . StatusOK , memoResponseList )
}
2023-07-06 16:56:42 +03:00
2023-08-09 17:30:27 +03:00
// GetMemoStats godoc
2023-08-09 16:53:06 +03: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 17:30:27 +03:00
func ( s * APIV1Service ) GetMemoStats ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
normalStatus := store . Normal
findMemoMessage := & store . FindMemo {
RowStatus : & normalStatus ,
}
if creatorID , err := util . ConvertStringToInt32 ( c . QueryParam ( "creatorId" ) ) ; err == nil {
findMemoMessage . CreatorID = & creatorID
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
if findMemoMessage . CreatorID == nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Missing user id to find memo" )
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
currentUserID , ok := c . Get ( auth . UserIDContextKey ) . ( int32 )
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 16:56:42 +03:00
}
2023-08-09 16:53:06 +03: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 )
}
memoResponseList := [ ] * Memo { }
for _ , memo := range list {
2023-07-06 16:56:42 +03: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 16:53:06 +03:00
memoResponseList = append ( memoResponseList , memoResponse )
}
displayTsList := [ ] int64 { }
for _ , memo := range memoResponseList {
displayTsList = append ( displayTsList , memo . DisplayTs )
}
return c . JSON ( http . StatusOK , displayTsList )
}
2023-08-09 17:30:27 +03:00
// GetMemo godoc
2023-08-09 16:53:06 +03: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 17:30:27 +03:00
func ( s * APIV1Service ) GetMemo ( c echo . Context ) error {
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
} )
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
userID , ok := c . Get ( auth . UserIDContextKey ) . ( int32 )
if memo . Visibility == store . Private {
if ! ok || memo . CreatorID != userID {
return echo . NewHTTPError ( http . StatusForbidden , "this memo is private only" )
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
}
2023-08-09 16:53:06 +03: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 14:48:39 +03:00
2023-08-09 17:30:27 +03:00
// DeleteMemo godoc
2023-08-09 16:53:06 +03: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"
// @Security ApiKeyAuth
// @Router /api/v1/memo/{memoId} [DELETE]
2023-08-09 17:30:27 +03:00
func ( s * APIV1Service ) DeleteMemo ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
userID , ok := c . Get ( auth . UserIDContextKey ) . ( int32 )
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 14:48:39 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 17:30:27 +03:00
// UpdateMemo godoc
2023-08-09 16:53:06 +03: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"
// @Security ApiKeyAuth
// @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 17:30:27 +03:00
func ( s * APIV1Service ) UpdateMemo ( c echo . Context ) error {
2023-08-09 16:53:06 +03:00
ctx := c . Request ( ) . Context ( )
userID , ok := c . Get ( auth . UserIDContextKey ) . ( int32 )
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
memo , err := s . Store . GetMemo ( ctx , & store . FindMemo {
ID : & memoID ,
2023-07-06 16:56:42 +03:00
} )
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
if patchMemoRequest . ResourceIDList != nil {
addedResourceIDList , removedResourceIDList := getIDListDiff ( memo . ResourceIDList , patchMemoRequest . ResourceIDList )
for _ , resourceID := range addedResourceIDList {
if _ , err := s . Store . UpsertMemoResource ( ctx , & store . UpsertMemoResource {
MemoID : memo . ID ,
ResourceID : resourceID ,
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to upsert memo resource" ) . SetInternal ( err )
2023-07-06 16:56:42 +03:00
}
}
2023-08-09 16:53:06 +03:00
for _ , resourceID := range removedResourceIDList {
if err := s . Store . DeleteMemoResource ( ctx , & store . DeleteMemoResource {
MemoID : & memo . ID ,
ResourceID : & resourceID ,
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to delete memo resource" ) . SetInternal ( err )
}
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03:00
if patchMemoRequest . RelationList != nil {
patchMemoRelationList := make ( [ ] * store . MemoRelation , 0 )
for _ , memoRelation := range patchMemoRequest . RelationList {
patchMemoRelationList = append ( patchMemoRelationList , & store . MemoRelation {
MemoID : memo . ID ,
RelatedMemoID : memoRelation . RelatedMemoID ,
Type : store . MemoRelationType ( memoRelation . Type ) ,
} )
2023-07-06 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
addedMemoRelationList , removedMemoRelationList := getMemoRelationListDiff ( memo . RelationList , patchMemoRelationList )
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 17:53:38 +03:00
}
2023-08-09 16:53:06 +03: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 16:56:42 +03:00
}
2023-08-09 16:53:06 +03:00
}
2023-07-06 16:56:42 +03:00
2023-08-09 16:53:06 +03: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 )
}
return c . JSON ( http . StatusOK , memoResponse )
2023-07-06 16:56:42 +03:00
}
func ( s * APIV1Service ) createMemoCreateActivity ( ctx context . Context , memo * store . Memo ) error {
payload := ActivityMemoCreatePayload {
Content : memo . Content ,
Visibility : memo . Visibility . String ( ) ,
}
payloadBytes , err := json . Marshal ( payload )
if err != nil {
return errors . Wrap ( err , "failed to marshal activity payload" )
}
activity , err := s . Store . CreateActivity ( ctx , & store . Activity {
CreatorID : memo . CreatorID ,
Type : ActivityMemoCreate . String ( ) ,
Level : ActivityInfo . String ( ) ,
Payload : string ( payloadBytes ) ,
} )
if err != nil || activity == nil {
return errors . Wrap ( err , "failed to create activity" )
}
return err
}
func ( s * APIV1Service ) convertMemoFromStore ( ctx context . Context , memo * store . Memo ) ( * Memo , error ) {
memoResponse := & Memo {
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 {
ID : & memoResponse . CreatorID ,
} )
if err != nil {
return nil , err
}
if user . Nickname != "" {
memoResponse . CreatorName = user . Nickname
} else {
memoResponse . CreatorName = user . Username
}
2023-07-20 14:48:39 +03:00
memoResponse . CreatorUsername = user . Username
2023-07-06 16:56:42 +03:00
// Compose display ts.
memoResponse . DisplayTs = memoResponse . CreatedTs
// Find memo display with updated ts setting.
memoDisplayWithUpdatedTs , err := s . getMemoDisplayWithUpdatedTsSettingValue ( ctx )
if err != nil {
return nil , err
}
if memoDisplayWithUpdatedTs {
memoResponse . DisplayTs = memoResponse . UpdatedTs
}
relationList := [ ] * MemoRelation { }
for _ , relation := range memo . RelationList {
relationList = append ( relationList , convertMemoRelationFromStore ( relation ) )
}
memoResponse . RelationList = relationList
resourceList := [ ] * Resource { }
for _ , resourceID := range memo . ResourceIDList {
resource , err := s . Store . GetResource ( ctx , & store . FindResource {
ID : & resourceID ,
} )
if err != nil {
return nil , err
}
if resource != nil {
resourceList = append ( resourceList , convertResourceFromStore ( resource ) )
}
}
memoResponse . ResourceList = resourceList
return memoResponse , nil
}
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 ) ,
}
}
func getMemoRelationListDiff ( oldList , newList [ ] * store . MemoRelation ) ( addedList , removedList [ ] * store . MemoRelation ) {
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 ] {
removedList = append ( removedList , relation )
}
}
for _ , relation := range newList {
key := fmt . Sprintf ( "%d-%s" , relation . RelatedMemoID , relation . Type )
if ! oldMap [ key ] {
addedList = append ( addedList , relation )
}
}
return addedList , removedList
}
2023-08-04 16:55:07 +03:00
func getIDListDiff ( oldList , newList [ ] int32 ) ( addedList , removedList [ ] int32 ) {
oldMap := map [ int32 ] bool { }
2023-07-06 16:56:42 +03:00
for _ , id := range oldList {
oldMap [ id ] = true
}
2023-08-04 16:55:07 +03:00
newMap := map [ int32 ] bool { }
2023-07-06 16:56:42 +03: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 18:06:53 +03:00
}