mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
refactor: migrate memo to apiv1 (#1907)
* refactor: migrate memo to apiv1 * chore: update * chore: update * chore: update * chore: upate * chore: update * chore: update
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
type response struct {
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
func composeResponse(data any) response {
|
||||
return response{
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultGetRequestSkipper(c echo.Context) bool {
|
||||
return c.Request().Method == http.MethodGet
|
||||
}
|
||||
|
||||
func defaultAPIRequestSkipper(c echo.Context) bool {
|
||||
path := c.Path()
|
||||
return common.HasPrefixes(path, "/api")
|
||||
}
|
||||
|
||||
func (s *Server) defaultAuthSkipper(c echo.Context) bool {
|
||||
ctx := c.Request().Context()
|
||||
path := c.Path()
|
||||
|
||||
// Skip auth.
|
||||
if common.HasPrefixes(path, "/api/v1/auth") {
|
||||
return true
|
||||
}
|
||||
|
||||
// If there is openId in query string and related user is found, then skip auth.
|
||||
openID := c.QueryParam("openId")
|
||||
if openID != "" {
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
OpenID: &openID,
|
||||
})
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return false
|
||||
}
|
||||
if user != nil {
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), user.ID)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@@ -1,51 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
getter "github.com/usememos/memos/plugin/http-getter"
|
||||
)
|
||||
|
||||
func registerGetterPublicRoutes(g *echo.Group) {
|
||||
g.GET("/get/httpmeta", func(c echo.Context) error {
|
||||
urlStr := c.QueryParam("url")
|
||||
if urlStr == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing website url")
|
||||
}
|
||||
if _, err := url.Parse(urlStr); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
|
||||
}
|
||||
|
||||
htmlMeta, err := getter.GetHTMLMeta(urlStr)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusNotAcceptable, fmt.Sprintf("Failed to get website meta with url: %s", urlStr)).SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(htmlMeta))
|
||||
})
|
||||
|
||||
g.GET("/get/image", func(c echo.Context) error {
|
||||
urlStr := c.QueryParam("url")
|
||||
if urlStr == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
|
||||
}
|
||||
if _, err := url.Parse(urlStr); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
|
||||
}
|
||||
|
||||
image, err := getter.GetImage(urlStr)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to get image url: %s", urlStr)).SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Writer.WriteHeader(http.StatusOK)
|
||||
c.Response().Writer.Header().Set("Content-Type", image.Mediatype)
|
||||
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
|
||||
if _, err := c.Response().Writer.Write(image.Blob); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
206
server/jwt.go
206
server/jwt.go
@@ -1,206 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/server/auth"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
const (
|
||||
// Context section
|
||||
// The key name used to store user id in the context
|
||||
// user id is extracted from the jwt token subject field.
|
||||
userIDContextKey = "user-id"
|
||||
)
|
||||
|
||||
func getUserIDContextKey() string {
|
||||
return userIDContextKey
|
||||
}
|
||||
|
||||
// Claims creates a struct that will be encoded to a JWT.
|
||||
// We add jwt.RegisteredClaims as an embedded type, to provide fields such as name.
|
||||
type Claims struct {
|
||||
Name string `json:"name"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func extractTokenFromHeader(c echo.Context) (string, error) {
|
||||
authHeader := c.Request().Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
authHeaderParts := strings.Fields(authHeader)
|
||||
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
||||
return "", errors.New("Authorization header format must be Bearer {token}")
|
||||
}
|
||||
|
||||
return authHeaderParts[1], nil
|
||||
}
|
||||
|
||||
func findAccessToken(c echo.Context) string {
|
||||
accessToken := ""
|
||||
cookie, _ := c.Cookie(auth.AccessTokenCookieName)
|
||||
if cookie != nil {
|
||||
accessToken = cookie.Value
|
||||
}
|
||||
if accessToken == "" {
|
||||
accessToken, _ = extractTokenFromHeader(c)
|
||||
}
|
||||
|
||||
return accessToken
|
||||
}
|
||||
|
||||
func audienceContains(audience jwt.ClaimStrings, token string) bool {
|
||||
for _, v := range audience {
|
||||
if v == token {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// JWTMiddleware validates the access token.
|
||||
// If the access token is about to expire or has expired and the request has a valid refresh token, it
|
||||
// will try to generate new access token and refresh token.
|
||||
func JWTMiddleware(server *Server, next echo.HandlerFunc, secret string) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
path := c.Request().URL.Path
|
||||
method := c.Request().Method
|
||||
|
||||
if server.defaultAuthSkipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
token := findAccessToken(c)
|
||||
if token == "" {
|
||||
// Allow the user to access the public endpoints.
|
||||
if common.HasPrefixes(path, "/o") {
|
||||
return next(c)
|
||||
}
|
||||
// When the request is not authenticated, we allow the user to access the memo endpoints for those public memos.
|
||||
if common.HasPrefixes(path, "/api/memo") && method == http.MethodGet {
|
||||
return next(c)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing access token")
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
accessToken, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
|
||||
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
|
||||
return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
|
||||
}
|
||||
if kid, ok := t.Header["kid"].(string); ok {
|
||||
if kid == "v1" {
|
||||
return []byte(secret), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
|
||||
})
|
||||
|
||||
if !accessToken.Valid {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid access token.")
|
||||
}
|
||||
|
||||
if !audienceContains(claims.Audience, auth.AccessTokenAudienceName) {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Invalid access token, audience mismatch, got %q, expected %q.", claims.Audience, auth.AccessTokenAudienceName))
|
||||
}
|
||||
generateToken := time.Until(claims.ExpiresAt.Time) < auth.RefreshThresholdDuration
|
||||
if err != nil {
|
||||
var ve *jwt.ValidationError
|
||||
if errors.As(err, &ve) {
|
||||
// If expiration error is the only error, we will clear the err
|
||||
// and generate new access token and refresh token
|
||||
if ve.Errors == jwt.ValidationErrorExpired {
|
||||
generateToken = true
|
||||
}
|
||||
} else {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, errors.Wrap(err, "Invalid or expired access token"))
|
||||
}
|
||||
}
|
||||
|
||||
// We either have a valid access token or we will attempt to generate new access token and refresh token
|
||||
ctx := c.Request().Context()
|
||||
userID, err := strconv.Atoi(claims.Subject)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Malformed ID in the token.")
|
||||
}
|
||||
|
||||
// Even if there is no error, we still need to make sure the user still exists.
|
||||
user, err := server.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to find user ID: %d", userID)).SetInternal(err)
|
||||
}
|
||||
if user == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Failed to find user ID: %d", userID))
|
||||
}
|
||||
|
||||
if generateToken {
|
||||
generateTokenFunc := func() error {
|
||||
rc, err := c.Cookie(auth.RefreshTokenCookieName)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Failed to generate access token. Missing refresh token.")
|
||||
}
|
||||
|
||||
// Parses token and checks if it's valid.
|
||||
refreshTokenClaims := &Claims{}
|
||||
refreshToken, err := jwt.ParseWithClaims(rc.Value, refreshTokenClaims, func(t *jwt.Token) (any, error) {
|
||||
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
|
||||
return nil, errors.Errorf("unexpected refresh token signing method=%v, expected %v", t.Header["alg"], jwt.SigningMethodHS256)
|
||||
}
|
||||
|
||||
if kid, ok := t.Header["kid"].(string); ok {
|
||||
if kid == "v1" {
|
||||
return []byte(secret), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.Errorf("unexpected refresh token kid=%v", t.Header["kid"])
|
||||
})
|
||||
if err != nil {
|
||||
if err == jwt.ErrSignatureInvalid {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Failed to generate access token. Invalid refresh token signature.")
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to refresh expired token. User Id %d", userID)).SetInternal(err)
|
||||
}
|
||||
|
||||
if !audienceContains(refreshTokenClaims.Audience, auth.RefreshTokenAudienceName) {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized,
|
||||
fmt.Sprintf("Invalid refresh token, audience mismatch, got %q, expected %q. you may send request to the wrong environment",
|
||||
refreshTokenClaims.Audience,
|
||||
auth.RefreshTokenAudienceName,
|
||||
))
|
||||
}
|
||||
|
||||
// If we have a valid refresh token, we will generate new access token and refresh token
|
||||
if refreshToken != nil && refreshToken.Valid {
|
||||
if err := auth.GenerateTokensAndSetCookies(c, user, secret); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to refresh expired token. User Id %d", userID)).SetInternal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// It may happen that we still have a valid access token, but we encounter issue when trying to generate new token
|
||||
// In such case, we won't return the error.
|
||||
if err := generateTokenFunc(); err != nil && !accessToken.Valid {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), userID)
|
||||
return next(c)
|
||||
}
|
||||
}
|
735
server/memo.go
735
server/memo.go
@@ -1,735 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/usememos/memos/api"
|
||||
apiv1 "github.com/usememos/memos/api/v1"
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/store"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// maxContentLength means the max memo content bytes is 1MB.
|
||||
const maxContentLength = 1 << 30
|
||||
|
||||
func (s *Server) 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 := &api.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: apiv1.UserSettingMemoVisibilityKey.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
|
||||
}
|
||||
|
||||
if userMemoVisibilitySetting != nil {
|
||||
memoVisibility := api.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 = api.Private
|
||||
}
|
||||
}
|
||||
|
||||
// Find disable public memos system setting.
|
||||
disablePublicMemosSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
|
||||
Name: apiv1.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)
|
||||
}
|
||||
// Enforce normal user to create private memo if public memos are disabled.
|
||||
if user.Role == store.RoleUser {
|
||||
createMemoRequest.Visibility = api.Private
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createMemoRequest.CreatorID = userID
|
||||
memoMessage, 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, memoMessage); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||
}
|
||||
|
||||
for _, resourceID := range createMemoRequest.ResourceIDList {
|
||||
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
|
||||
MemoID: memoMessage.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.MemoRelationMessage{
|
||||
MemoID: memoMessage.ID,
|
||||
RelatedMemoID: memoRelationUpsert.RelatedMemoID,
|
||||
Type: store.MemoRelationType(memoRelationUpsert.Type),
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
|
||||
}
|
||||
}
|
||||
|
||||
memoMessage, err = s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoMessage.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
|
||||
}
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(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)
|
||||
}
|
||||
|
||||
memoMessage, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memoMessage.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
currentTs := time.Now().Unix()
|
||||
patchMemoRequest := &api.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.UpdateMemoMessage{
|
||||
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)
|
||||
}
|
||||
memoMessage, err = s.Store.GetMemo(ctx, &store.FindMemoMessage{ID: &memoID})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
|
||||
if patchMemoRequest.ResourceIDList != nil {
|
||||
addedResourceIDList, removedResourceIDList := getIDListDiff(memoMessage.ResourceIDList, patchMemoRequest.ResourceIDList)
|
||||
for _, resourceID := range addedResourceIDList {
|
||||
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
|
||||
MemoID: memoMessage.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, &api.MemoResourceDelete{
|
||||
MemoID: &memoMessage.ID,
|
||||
ResourceID: &resourceID,
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo resource").SetInternal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if patchMemoRequest.RelationList != nil {
|
||||
patchMemoRelationList := make([]*store.MemoRelationMessage, 0)
|
||||
for _, memoRelation := range patchMemoRequest.RelationList {
|
||||
patchMemoRelationList = append(patchMemoRelationList, &store.MemoRelationMessage{
|
||||
MemoID: memoMessage.ID,
|
||||
RelatedMemoID: memoRelation.RelatedMemoID,
|
||||
Type: store.MemoRelationType(memoRelation.Type),
|
||||
})
|
||||
}
|
||||
addedMemoRelationList, removedMemoRelationList := getMemoRelationListDiff(memoMessage.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.DeleteMemoRelationMessage{
|
||||
MemoID: &memoMessage.ID,
|
||||
RelatedMemoID: &memoRelation.RelatedMemoID,
|
||||
Type: &memoRelation.Type,
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memoMessage, err = s.Store.GetMemo(ctx, &store.FindMemoMessage{ID: &memoID})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoResponse))
|
||||
})
|
||||
|
||||
g.GET("/memo", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
findMemoMessage := &store.FindMemoMessage{}
|
||||
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
|
||||
}
|
||||
|
||||
memoMessageList, err := s.Store.ListMemos(ctx, findMemoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
|
||||
}
|
||||
memoResponseList := []*api.MemoResponse{}
|
||||
for _, memoMessage := range memoMessageList {
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
memoResponseList = append(memoResponseList, memoResponse)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(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)
|
||||
}
|
||||
|
||||
memoMessage, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
|
||||
}
|
||||
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if memoMessage.Visibility == store.Private {
|
||||
if !ok || memoMessage.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
|
||||
}
|
||||
} else if memoMessage.Visibility == store.Protected {
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
|
||||
}
|
||||
}
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoResponse))
|
||||
})
|
||||
|
||||
g.POST("/memo/:memoId/organizer", 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)
|
||||
}
|
||||
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memo.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
memoOrganizerUpsert := &api.MemoOrganizerUpsert{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
|
||||
}
|
||||
memoOrganizerUpsert.MemoID = memoID
|
||||
memoOrganizerUpsert.UserID = userID
|
||||
|
||||
err = s.Store.UpsertMemoOrganizer(ctx, memoOrganizerUpsert)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
|
||||
}
|
||||
|
||||
memoMessage, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
|
||||
}
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoResponse))
|
||||
})
|
||||
|
||||
g.GET("/memo/stats", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
normalStatus := store.Normal
|
||||
findMemoMessage := &store.FindMemoMessage{
|
||||
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
|
||||
}
|
||||
|
||||
memoMessageList, err := s.Store.ListMemos(ctx, findMemoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
|
||||
}
|
||||
memoResponseList := []*api.MemoResponse{}
|
||||
for _, memoMessage := range memoMessageList {
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
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, composeResponse(displayTsList))
|
||||
})
|
||||
|
||||
g.GET("/memo/all", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
findMemoMessage := &store.FindMemoMessage{}
|
||||
_, 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
|
||||
}
|
||||
|
||||
memoMessageList, err := s.Store.ListMemos(ctx, findMemoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
|
||||
}
|
||||
memoResponseList := []*api.MemoResponse{}
|
||||
for _, memoMessage := range memoMessageList {
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
memoResponseList = append(memoResponseList, memoResponse)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(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.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memo.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
if err := s.Store.DeleteMemo(ctx, &store.DeleteMemoMessage{
|
||||
ID: memoID,
|
||||
}); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) createMemoCreateActivity(ctx context.Context, memo *store.MemoMessage) error {
|
||||
payload := apiv1.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.ActivityMessage{
|
||||
CreatorID: memo.CreatorID,
|
||||
Type: apiv1.ActivityMemoCreate.String(),
|
||||
Level: apiv1.ActivityInfo.String(),
|
||||
Payload: string(payloadBytes),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func getMemoRelationListDiff(oldList, newList []*store.MemoRelationMessage) (addedList, removedList []*store.MemoRelationMessage) {
|
||||
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 convertCreateMemoRequestToMemoMessage(memoCreate *api.CreateMemoRequest) *store.MemoMessage {
|
||||
createdTs := time.Now().Unix()
|
||||
if memoCreate.CreatedTs != nil {
|
||||
createdTs = *memoCreate.CreatedTs
|
||||
}
|
||||
return &store.MemoMessage{
|
||||
CreatorID: memoCreate.CreatorID,
|
||||
CreatedTs: createdTs,
|
||||
Content: memoCreate.Content,
|
||||
Visibility: store.Visibility(memoCreate.Visibility),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) composeMemoMessageToMemoResponse(ctx context.Context, memoMessage *store.MemoMessage) (*api.MemoResponse, error) {
|
||||
memoResponse := &api.MemoResponse{
|
||||
ID: memoMessage.ID,
|
||||
RowStatus: api.RowStatus(memoMessage.RowStatus.String()),
|
||||
CreatorID: memoMessage.CreatorID,
|
||||
CreatedTs: memoMessage.CreatedTs,
|
||||
UpdatedTs: memoMessage.UpdatedTs,
|
||||
Content: memoMessage.Content,
|
||||
Visibility: api.Visibility(memoMessage.Visibility.String()),
|
||||
Pinned: memoMessage.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 := []*api.MemoRelation{}
|
||||
for _, relation := range memoMessage.RelationList {
|
||||
relationList = append(relationList, convertMemoRelationMessageToMemoRelation(relation))
|
||||
}
|
||||
memoResponse.RelationList = relationList
|
||||
|
||||
resourceList := []*api.Resource{}
|
||||
for _, resourceID := range memoMessage.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 *Server) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) {
|
||||
memoDisplayWithUpdatedTsSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
|
||||
Name: apiv1.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 convertResourceFromStore(resource *store.Resource) *api.Resource {
|
||||
return &api.Resource{
|
||||
ID: resource.ID,
|
||||
CreatorID: resource.CreatorID,
|
||||
CreatedTs: resource.CreatedTs,
|
||||
UpdatedTs: resource.UpdatedTs,
|
||||
Filename: resource.Filename,
|
||||
Blob: resource.Blob,
|
||||
InternalPath: resource.InternalPath,
|
||||
ExternalLink: resource.ExternalLink,
|
||||
Type: resource.Type,
|
||||
Size: resource.Size,
|
||||
PublicID: resource.PublicID,
|
||||
LinkedMemoAmount: resource.LinkedMemoAmount,
|
||||
}
|
||||
}
|
@@ -1,84 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/store"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (s *Server) registerMemoRelationRoutes(g *echo.Group) {
|
||||
g.POST("/memo/:memoId/relation", 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)
|
||||
}
|
||||
|
||||
memoRelationUpsert := &api.MemoRelationUpsert{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoRelationUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo relation request").SetInternal(err)
|
||||
}
|
||||
|
||||
memoRelation, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelationMessage{
|
||||
MemoID: memoID,
|
||||
RelatedMemoID: memoRelationUpsert.RelatedMemoID,
|
||||
Type: store.MemoRelationType(memoRelationUpsert.Type),
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoRelation))
|
||||
})
|
||||
|
||||
g.GET("/memo/:memoId/relation", 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)
|
||||
}
|
||||
|
||||
memoRelationList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelationMessage{
|
||||
MemoID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list memo relations").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoRelationList))
|
||||
})
|
||||
|
||||
g.DELETE("/memo/:memoId/relation/:relatedMemoId/type/:relationType", 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("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
|
||||
}
|
||||
relatedMemoID, err := strconv.Atoi(c.Param("relatedMemoId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Related memo ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||
}
|
||||
relationType := store.MemoRelationType(c.Param("relationType"))
|
||||
|
||||
if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelationMessage{
|
||||
MemoID: &memoID,
|
||||
RelatedMemoID: &relatedMemoID,
|
||||
Type: &relationType,
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func convertMemoRelationMessageToMemoRelation(memoRelation *store.MemoRelationMessage) *api.MemoRelation {
|
||||
return &api.MemoRelation{
|
||||
MemoID: memoRelation.MemoID,
|
||||
RelatedMemoID: memoRelation.RelatedMemoID,
|
||||
Type: api.MemoRelationType(memoRelation.Type),
|
||||
}
|
||||
}
|
@@ -1,107 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/store"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (s *Server) registerMemoResourceRoutes(g *echo.Group) {
|
||||
g.POST("/memo/:memoId/resource", 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)
|
||||
}
|
||||
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
memoResourceUpsert := &api.MemoResourceUpsert{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
|
||||
}
|
||||
resource, err := s.Store.GetResource(ctx, &store.FindResource{
|
||||
ID: &memoResourceUpsert.ResourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
|
||||
}
|
||||
if resource == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Resource not found").SetInternal(err)
|
||||
} else if resource.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to bind this resource").SetInternal(err)
|
||||
}
|
||||
|
||||
memoResourceUpsert.MemoID = memoID
|
||||
currentTs := time.Now().Unix()
|
||||
memoResourceUpsert.UpdatedTs = ¤tTs
|
||||
if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
|
||||
g.GET("/memo/:memoId/resource", 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)
|
||||
}
|
||||
|
||||
list, err := s.Store.ListResources(ctx, &store.FindResource{
|
||||
MemoID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
|
||||
}
|
||||
resourceList := []*api.Resource{}
|
||||
for _, resource := range list {
|
||||
resourceList = append(resourceList, convertResourceFromStore(resource))
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(resourceList))
|
||||
})
|
||||
|
||||
g.DELETE("/memo/:memoId/resource/:resourceId", 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("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
|
||||
}
|
||||
resourceID, err := strconv.Atoi(c.Param("resourceId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Resource ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memo.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
memoResourceDelete := &api.MemoResourceDelete{
|
||||
MemoID: &memoID,
|
||||
ResourceID: &resourceID,
|
||||
}
|
||||
if err := s.Store.DeleteMemoResource(ctx, memoResourceDelete); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
@@ -26,7 +26,7 @@ func (s *Server) registerRSSRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
normalStatus := store.Normal
|
||||
memoFind := store.FindMemoMessage{
|
||||
memoFind := store.FindMemo{
|
||||
RowStatus: &normalStatus,
|
||||
VisibilityList: []store.Visibility{store.Public},
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (s *Server) registerRSSRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
normalStatus := store.Normal
|
||||
memoFind := store.FindMemoMessage{
|
||||
memoFind := store.FindMemo{
|
||||
CreatorID: &id,
|
||||
RowStatus: &normalStatus,
|
||||
VisibilityList: []store.Visibility{store.Public},
|
||||
@@ -80,7 +80,7 @@ func (s *Server) registerRSSRoutes(g *echo.Group) {
|
||||
const MaxRSSItemCount = 100
|
||||
const MaxRSSItemTitleLength = 100
|
||||
|
||||
func (s *Server) generateRSSFromMemoList(ctx context.Context, memoList []*store.MemoMessage, baseURL string, profile *apiv1.CustomizedProfile) (string, error) {
|
||||
func (s *Server) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string, profile *apiv1.CustomizedProfile) (string, error) {
|
||||
feed := &feeds.Feed{
|
||||
Title: profile.Name,
|
||||
Link: &feeds.Link{Href: baseURL},
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -87,20 +88,6 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
|
||||
rootGroup := e.Group("")
|
||||
s.registerRSSRoutes(rootGroup)
|
||||
|
||||
publicGroup := e.Group("/o")
|
||||
publicGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return JWTMiddleware(s, next, s.Secret)
|
||||
})
|
||||
registerGetterPublicRoutes(publicGroup)
|
||||
|
||||
apiGroup := e.Group("/api")
|
||||
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return JWTMiddleware(s, next, s.Secret)
|
||||
})
|
||||
s.registerMemoRoutes(apiGroup)
|
||||
s.registerMemoResourceRoutes(apiGroup)
|
||||
s.registerMemoRelationRoutes(apiGroup)
|
||||
|
||||
apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store)
|
||||
apiV1Service.Register(rootGroup)
|
||||
|
||||
@@ -185,7 +172,7 @@ func (s *Server) createServerStartActivity(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &store.ActivityMessage{
|
||||
activity, err := s.Store.CreateActivity(ctx, &store.Activity{
|
||||
CreatorID: apiv1.UnknownID,
|
||||
Type: apiv1.ActivityServerStart.String(),
|
||||
Level: apiv1.ActivityInfo.String(),
|
||||
@@ -196,3 +183,12 @@ func (s *Server) createServerStartActivity(ctx context.Context) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func defaultGetRequestSkipper(c echo.Context) bool {
|
||||
return c.Request().Method == http.MethodGet
|
||||
}
|
||||
|
||||
func defaultAPIRequestSkipper(c echo.Context) bool {
|
||||
path := c.Path()
|
||||
return common.HasPrefixes(path, "/api", "/api/v1")
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/usememos/memos/api"
|
||||
apiv1 "github.com/usememos/memos/api/v1"
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/plugin/telegram"
|
||||
@@ -61,20 +60,19 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
|
||||
return err
|
||||
}
|
||||
|
||||
// create memo
|
||||
memoCreate := api.CreateMemoRequest{
|
||||
create := &store.Memo{
|
||||
CreatorID: creatorID,
|
||||
Visibility: api.Private,
|
||||
Visibility: store.Private,
|
||||
}
|
||||
|
||||
if message.Text != nil {
|
||||
memoCreate.Content = *message.Text
|
||||
create.Content = *message.Text
|
||||
}
|
||||
if blobs != nil && message.Caption != nil {
|
||||
memoCreate.Content = *message.Caption
|
||||
create.Content = *message.Caption
|
||||
}
|
||||
|
||||
memoMessage, err := t.store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(&memoCreate))
|
||||
memoMessage, err := t.store.CreateMemo(ctx, create)
|
||||
if err != nil {
|
||||
_, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("failed to CreateMemo: %s", err), nil)
|
||||
return err
|
||||
@@ -90,7 +88,7 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
|
||||
case ".png":
|
||||
mime = "image/png"
|
||||
}
|
||||
resource, err := t.store.CreateResourceV1(ctx, &store.Resource{
|
||||
resource, err := t.store.CreateResource(ctx, &store.Resource{
|
||||
CreatorID: creatorID,
|
||||
Filename: filename,
|
||||
Type: mime,
|
||||
@@ -103,7 +101,7 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
|
||||
_, err = t.store.UpsertMemoResource(ctx, &store.UpsertMemoResource{
|
||||
MemoID: memoMessage.ID,
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
@@ -126,7 +124,7 @@ func (t *telegramHandler) CallbackQueryHandle(ctx context.Context, bot *telegram
|
||||
return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("fail to parse callbackQuery.Data %s", callbackQuery.Data))
|
||||
}
|
||||
|
||||
update := store.UpdateMemoMessage{
|
||||
update := store.UpdateMemo{
|
||||
ID: memoID,
|
||||
Visibility: &visibility,
|
||||
}
|
||||
|
Reference in New Issue
Block a user