mirror of
				https://github.com/usememos/memos.git
				synced 2025-06-05 22:09:59 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			774 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			774 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package v1
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/labstack/echo/v4"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"github.com/usememos/memos/store"
 | 
						|
)
 | 
						|
 | 
						|
// 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 {
 | 
						|
	switch v {
 | 
						|
	case Public:
 | 
						|
		return "PUBLIC"
 | 
						|
	case Protected:
 | 
						|
		return "PROTECTED"
 | 
						|
	case Private:
 | 
						|
		return "PRIVATE"
 | 
						|
	}
 | 
						|
	return "PRIVATE"
 | 
						|
}
 | 
						|
 | 
						|
type Memo struct {
 | 
						|
	ID int `json:"id"`
 | 
						|
 | 
						|
	// Standard fields
 | 
						|
	RowStatus RowStatus `json:"rowStatus"`
 | 
						|
	CreatorID int       `json:"creatorId"`
 | 
						|
	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
 | 
						|
	CreatorName  string          `json:"creatorName"`
 | 
						|
	ResourceList []*Resource     `json:"resourceList"`
 | 
						|
	RelationList []*MemoRelation `json:"relationList"`
 | 
						|
}
 | 
						|
 | 
						|
type CreateMemoRequest struct {
 | 
						|
	// Standard fields
 | 
						|
	CreatorID int    `json:"-"`
 | 
						|
	CreatedTs *int64 `json:"createdTs"`
 | 
						|
 | 
						|
	// Domain specific fields
 | 
						|
	Visibility Visibility `json:"visibility"`
 | 
						|
	Content    string     `json:"content"`
 | 
						|
 | 
						|
	// Related fields
 | 
						|
	ResourceIDList []int                        `json:"resourceIdList"`
 | 
						|
	RelationList   []*UpsertMemoRelationRequest `json:"relationList"`
 | 
						|
}
 | 
						|
 | 
						|
type PatchMemoRequest struct {
 | 
						|
	ID int `json:"-"`
 | 
						|
 | 
						|
	// 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
 | 
						|
	ResourceIDList []int                        `json:"resourceIdList"`
 | 
						|
	RelationList   []*UpsertMemoRelationRequest `json:"relationList"`
 | 
						|
}
 | 
						|
 | 
						|
type FindMemoRequest struct {
 | 
						|
	ID *int
 | 
						|
 | 
						|
	// Standard fields
 | 
						|
	RowStatus *RowStatus
 | 
						|
	CreatorID *int
 | 
						|
 | 
						|
	// 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) {
 | 
						|
	g.POST("/memo", func(c echo.Context) error {
 | 
						|
		ctx := c.Request().Context()
 | 
						|
		userID, ok := c.Get(getUserIDContextKey()).(int)
 | 
						|
		if !ok {
 | 
						|
			return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
 | 
						|
		}
 | 
						|
 | 
						|
		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")
 | 
						|
		}
 | 
						|
 | 
						|
		if createMemoRequest.Visibility == "" {
 | 
						|
			userMemoVisibilitySetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
 | 
						|
				UserID: &userID,
 | 
						|
				Key:    UserSettingMemoVisibilityKey.String(),
 | 
						|
			})
 | 
						|
			if err != nil {
 | 
						|
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
 | 
						|
			}
 | 
						|
			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
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// 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)
 | 
						|
			if err != nil {
 | 
						|
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
 | 
						|
			}
 | 
						|
			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)
 | 
						|
				}
 | 
						|
				if user == nil {
 | 
						|
					return echo.NewHTTPError(http.StatusNotFound, "User not found")
 | 
						|
				}
 | 
						|
				// Enforce normal user to create private memo if public memos are disabled.
 | 
						|
				if user.Role == store.RoleUser {
 | 
						|
					createMemoRequest.Visibility = Private
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		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)
 | 
						|
		}
 | 
						|
 | 
						|
		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)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		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)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		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))
 | 
						|
		}
 | 
						|
 | 
						|
		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)
 | 
						|
	})
 | 
						|
 | 
						|
	g.PATCH("/memo/:memoId", func(c echo.Context) error {
 | 
						|
		ctx := c.Request().Context()
 | 
						|
		userID, ok := c.Get(getUserIDContextKey()).(int)
 | 
						|
		if !ok {
 | 
						|
			return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
 | 
						|
		}
 | 
						|
 | 
						|
		memoID, err := strconv.Atoi(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,
 | 
						|
		})
 | 
						|
		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")
 | 
						|
		}
 | 
						|
 | 
						|
		currentTs := time.Now().Unix()
 | 
						|
		patchMemoRequest := &PatchMemoRequest{
 | 
						|
			ID:        memoID,
 | 
						|
			UpdatedTs: ¤tTs,
 | 
						|
		}
 | 
						|
		if err := json.NewDecoder(c.Request().Body).Decode(patchMemoRequest); err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		if patchMemoRequest.Content != nil && len(*patchMemoRequest.Content) > maxContentLength {
 | 
						|
			return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB").SetInternal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		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
 | 
						|
		}
 | 
						|
 | 
						|
		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))
 | 
						|
		}
 | 
						|
 | 
						|
		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)
 | 
						|
				}
 | 
						|
			}
 | 
						|
			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)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		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),
 | 
						|
				})
 | 
						|
			}
 | 
						|
			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)
 | 
						|
				}
 | 
						|
			}
 | 
						|
			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)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		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)
 | 
						|
	})
 | 
						|
 | 
						|
	g.GET("/memo", func(c echo.Context) error {
 | 
						|
		ctx := c.Request().Context()
 | 
						|
		findMemoMessage := &store.FindMemo{}
 | 
						|
		if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
 | 
						|
			findMemoMessage.CreatorID = &userID
 | 
						|
		}
 | 
						|
 | 
						|
		currentUserID, ok := c.Get(getUserIDContextKey()).(int)
 | 
						|
		if !ok {
 | 
						|
			if findMemoMessage.CreatorID == nil {
 | 
						|
				return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
 | 
						|
			}
 | 
						|
			findMemoMessage.VisibilityList = []store.Visibility{store.Public}
 | 
						|
		} else {
 | 
						|
			if findMemoMessage.CreatorID == nil {
 | 
						|
				findMemoMessage.CreatorID = ¤tUserID
 | 
						|
			} else {
 | 
						|
				findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		rowStatus := store.RowStatus(c.QueryParam("rowStatus"))
 | 
						|
		if rowStatus != "" {
 | 
						|
			findMemoMessage.RowStatus = &rowStatus
 | 
						|
		}
 | 
						|
		pinnedStr := c.QueryParam("pinned")
 | 
						|
		if pinnedStr != "" {
 | 
						|
			pinned := pinnedStr == "true"
 | 
						|
			findMemoMessage.Pinned = &pinned
 | 
						|
		}
 | 
						|
 | 
						|
		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
 | 
						|
 | 
						|
		visibilityListStr := c.QueryParam("visibility")
 | 
						|
		if visibilityListStr != "" {
 | 
						|
			visibilityList := []store.Visibility{}
 | 
						|
			for _, visibility := range strings.Split(visibilityListStr, ",") {
 | 
						|
				visibilityList = append(visibilityList, store.Visibility(visibility))
 | 
						|
			}
 | 
						|
			findMemoMessage.VisibilityList = visibilityList
 | 
						|
		}
 | 
						|
		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
 | 
						|
		}
 | 
						|
 | 
						|
		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 {
 | 
						|
			memoResponse, err := s.convertMemoFromStore(ctx, memo)
 | 
						|
			if err != nil {
 | 
						|
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
 | 
						|
			}
 | 
						|
			memoResponseList = append(memoResponseList, memoResponse)
 | 
						|
		}
 | 
						|
		return c.JSON(http.StatusOK, memoResponseList)
 | 
						|
	})
 | 
						|
 | 
						|
	g.GET("/memo/:memoId", func(c echo.Context) error {
 | 
						|
		ctx := c.Request().Context()
 | 
						|
		memoID, err := strconv.Atoi(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,
 | 
						|
		})
 | 
						|
		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))
 | 
						|
		}
 | 
						|
 | 
						|
		userID, ok := c.Get(getUserIDContextKey()).(int)
 | 
						|
		if memo.Visibility == store.Private {
 | 
						|
			if !ok || memo.CreatorID != userID {
 | 
						|
				return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
 | 
						|
			}
 | 
						|
		} else if memo.Visibility == store.Protected {
 | 
						|
			if !ok {
 | 
						|
				return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
 | 
						|
			}
 | 
						|
		}
 | 
						|
		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)
 | 
						|
	})
 | 
						|
 | 
						|
	g.GET("/memo/stats", func(c echo.Context) error {
 | 
						|
		ctx := c.Request().Context()
 | 
						|
		normalStatus := store.Normal
 | 
						|
		findMemoMessage := &store.FindMemo{
 | 
						|
			RowStatus: &normalStatus,
 | 
						|
		}
 | 
						|
		if creatorID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
 | 
						|
			findMemoMessage.CreatorID = &creatorID
 | 
						|
		}
 | 
						|
		if findMemoMessage.CreatorID == nil {
 | 
						|
			return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
 | 
						|
		}
 | 
						|
 | 
						|
		currentUserID, ok := c.Get(getUserIDContextKey()).(int)
 | 
						|
		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}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		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 {
 | 
						|
			memoResponse, err := s.convertMemoFromStore(ctx, memo)
 | 
						|
			if err != nil {
 | 
						|
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
 | 
						|
			}
 | 
						|
			memoResponseList = append(memoResponseList, memoResponse)
 | 
						|
		}
 | 
						|
 | 
						|
		displayTsList := []int64{}
 | 
						|
		for _, memo := range memoResponseList {
 | 
						|
			displayTsList = append(displayTsList, memo.DisplayTs)
 | 
						|
		}
 | 
						|
		return c.JSON(http.StatusOK, displayTsList)
 | 
						|
	})
 | 
						|
 | 
						|
	g.GET("/memo/all", func(c echo.Context) error {
 | 
						|
		ctx := c.Request().Context()
 | 
						|
		findMemoMessage := &store.FindMemo{}
 | 
						|
		_, ok := c.Get(getUserIDContextKey()).(int)
 | 
						|
		if !ok {
 | 
						|
			findMemoMessage.VisibilityList = []store.Visibility{store.Public}
 | 
						|
		} else {
 | 
						|
			findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
 | 
						|
		}
 | 
						|
 | 
						|
		pinnedStr := c.QueryParam("pinned")
 | 
						|
		if pinnedStr != "" {
 | 
						|
			pinned := pinnedStr == "true"
 | 
						|
			findMemoMessage.Pinned = &pinned
 | 
						|
		}
 | 
						|
 | 
						|
		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
 | 
						|
 | 
						|
		visibilityListStr := c.QueryParam("visibility")
 | 
						|
		if visibilityListStr != "" {
 | 
						|
			visibilityList := []store.Visibility{}
 | 
						|
			for _, visibility := range strings.Split(visibilityListStr, ",") {
 | 
						|
				visibilityList = append(visibilityList, store.Visibility(visibility))
 | 
						|
			}
 | 
						|
			findMemoMessage.VisibilityList = visibilityList
 | 
						|
		}
 | 
						|
		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
 | 
						|
		}
 | 
						|
 | 
						|
		// Only fetch normal status memos.
 | 
						|
		normalStatus := store.Normal
 | 
						|
		findMemoMessage.RowStatus = &normalStatus
 | 
						|
 | 
						|
		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 fetch all memo list").SetInternal(err)
 | 
						|
		}
 | 
						|
		memoResponseList := []*Memo{}
 | 
						|
		for _, memo := range list {
 | 
						|
			memoResponse, err := s.convertMemoFromStore(ctx, memo)
 | 
						|
			if err != nil {
 | 
						|
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
 | 
						|
			}
 | 
						|
			memoResponseList = append(memoResponseList, memoResponse)
 | 
						|
		}
 | 
						|
		return c.JSON(http.StatusOK, memoResponseList)
 | 
						|
	})
 | 
						|
 | 
						|
	g.DELETE("/memo/:memoId", func(c echo.Context) error {
 | 
						|
		ctx := c.Request().Context()
 | 
						|
		userID, ok := c.Get(getUserIDContextKey()).(int)
 | 
						|
		if !ok {
 | 
						|
			return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
 | 
						|
		}
 | 
						|
		memoID, err := strconv.Atoi(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,
 | 
						|
		})
 | 
						|
		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")
 | 
						|
		}
 | 
						|
 | 
						|
		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)
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
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
 | 
						|
	}
 | 
						|
 | 
						|
	// 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
 | 
						|
}
 | 
						|
 | 
						|
func getIDListDiff(oldList, newList []int) (addedList, removedList []int) {
 | 
						|
	oldMap := map[int]bool{}
 | 
						|
	for _, id := range oldList {
 | 
						|
		oldMap[id] = true
 | 
						|
	}
 | 
						|
	newMap := map[int]bool{}
 | 
						|
	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
 | 
						|
}
 |