memo.go 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  1. package v1
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "strconv"
  8. "time"
  9. "github.com/labstack/echo/v4"
  10. "github.com/lithammer/shortuuid/v4"
  11. "github.com/pkg/errors"
  12. "go.uber.org/zap"
  13. "github.com/usememos/memos/internal/log"
  14. "github.com/usememos/memos/internal/util"
  15. "github.com/usememos/memos/plugin/webhook"
  16. storepb "github.com/usememos/memos/proto/gen/store"
  17. "github.com/usememos/memos/store"
  18. )
  19. // Visibility is the type of a visibility.
  20. type Visibility string
  21. const (
  22. // Public is the PUBLIC visibility.
  23. Public Visibility = "PUBLIC"
  24. // Protected is the PROTECTED visibility.
  25. Protected Visibility = "PROTECTED"
  26. // Private is the PRIVATE visibility.
  27. Private Visibility = "PRIVATE"
  28. )
  29. func (v Visibility) String() string {
  30. switch v {
  31. case Public:
  32. return "PUBLIC"
  33. case Protected:
  34. return "PROTECTED"
  35. case Private:
  36. return "PRIVATE"
  37. }
  38. return "PRIVATE"
  39. }
  40. type Memo struct {
  41. ID int32 `json:"id"`
  42. Name string `json:"name"`
  43. // Standard fields
  44. RowStatus RowStatus `json:"rowStatus"`
  45. CreatorID int32 `json:"creatorId"`
  46. CreatedTs int64 `json:"createdTs"`
  47. UpdatedTs int64 `json:"updatedTs"`
  48. // Domain specific fields
  49. DisplayTs int64 `json:"displayTs"`
  50. Content string `json:"content"`
  51. Visibility Visibility `json:"visibility"`
  52. Pinned bool `json:"pinned"`
  53. // Related fields
  54. CreatorName string `json:"creatorName"`
  55. CreatorUsername string `json:"creatorUsername"`
  56. ResourceList []*Resource `json:"resourceList"`
  57. RelationList []*MemoRelation `json:"relationList"`
  58. }
  59. type CreateMemoRequest struct {
  60. // Standard fields
  61. CreatorID int32 `json:"-"`
  62. CreatedTs *int64 `json:"createdTs"`
  63. // Domain specific fields
  64. Visibility Visibility `json:"visibility"`
  65. Content string `json:"content"`
  66. // Related fields
  67. ResourceIDList []int32 `json:"resourceIdList"`
  68. RelationList []*UpsertMemoRelationRequest `json:"relationList"`
  69. }
  70. type PatchMemoRequest struct {
  71. ID int32 `json:"-"`
  72. // Standard fields
  73. CreatedTs *int64 `json:"createdTs"`
  74. UpdatedTs *int64
  75. RowStatus *RowStatus `json:"rowStatus"`
  76. // Domain specific fields
  77. Content *string `json:"content"`
  78. Visibility *Visibility `json:"visibility"`
  79. // Related fields
  80. ResourceIDList []int32 `json:"resourceIdList"`
  81. RelationList []*UpsertMemoRelationRequest `json:"relationList"`
  82. }
  83. type FindMemoRequest struct {
  84. ID *int32
  85. // Standard fields
  86. RowStatus *RowStatus
  87. CreatorID *int32
  88. // Domain specific fields
  89. Pinned *bool
  90. ContentSearch []string
  91. VisibilityList []Visibility
  92. // Pagination
  93. Limit *int
  94. Offset *int
  95. }
  96. // maxContentLength means the max memo content bytes is 1MB.
  97. const maxContentLength = 1 << 30
  98. func (s *APIV1Service) registerMemoRoutes(g *echo.Group) {
  99. g.GET("/memo", s.GetMemoList)
  100. g.POST("/memo", s.CreateMemo)
  101. g.GET("/memo/all", s.GetAllMemos)
  102. g.GET("/memo/stats", s.GetMemoStats)
  103. g.GET("/memo/:memoId", s.GetMemo)
  104. g.PATCH("/memo/:memoId", s.UpdateMemo)
  105. g.DELETE("/memo/:memoId", s.DeleteMemo)
  106. }
  107. // GetMemoList godoc
  108. //
  109. // @Summary Get a list of memos matching optional filters
  110. // @Tags memo
  111. // @Produce json
  112. // @Param creatorId query int false "Creator ID"
  113. // @Param creatorUsername query string false "Creator username"
  114. // @Param rowStatus query store.RowStatus false "Row status"
  115. // @Param pinned query bool false "Pinned"
  116. // @Param tag query string false "Search for tag. Do not append #"
  117. // @Param content query string false "Search for content"
  118. // @Param limit query int false "Limit"
  119. // @Param offset query int false "Offset"
  120. // @Success 200 {object} []store.Memo "Memo list"
  121. // @Failure 400 {object} nil "Missing user to find memo"
  122. // @Failure 500 {object} nil "Failed to get memo display with updated ts setting value | Failed to fetch memo list | Failed to compose memo response"
  123. // @Router /api/v1/memo [GET]
  124. func (s *APIV1Service) GetMemoList(c echo.Context) error {
  125. ctx := c.Request().Context()
  126. find := &store.FindMemo{
  127. OrderByPinned: true,
  128. }
  129. if userID, err := util.ConvertStringToInt32(c.QueryParam("creatorId")); err == nil {
  130. find.CreatorID = &userID
  131. }
  132. if username := c.QueryParam("creatorUsername"); username != "" {
  133. user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
  134. if user != nil {
  135. find.CreatorID = &user.ID
  136. }
  137. }
  138. currentUserID, ok := c.Get(userIDContextKey).(int32)
  139. if !ok {
  140. // Anonymous use should only fetch PUBLIC memos with specified user
  141. if find.CreatorID == nil {
  142. return echo.NewHTTPError(http.StatusBadRequest, "Missing user to find memo")
  143. }
  144. find.VisibilityList = []store.Visibility{store.Public}
  145. } else {
  146. // Authorized user can fetch all PUBLIC/PROTECTED memo
  147. visibilityList := []store.Visibility{store.Public, store.Protected}
  148. // If Creator is authorized user (as default), PRIVATE memo is OK
  149. if find.CreatorID == nil || *find.CreatorID == currentUserID {
  150. find.CreatorID = &currentUserID
  151. visibilityList = append(visibilityList, store.Private)
  152. }
  153. find.VisibilityList = visibilityList
  154. }
  155. rowStatus := store.RowStatus(c.QueryParam("rowStatus"))
  156. if rowStatus != "" {
  157. find.RowStatus = &rowStatus
  158. }
  159. contentSearch := []string{}
  160. tag := c.QueryParam("tag")
  161. if tag != "" {
  162. contentSearch = append(contentSearch, "#"+tag)
  163. }
  164. content := c.QueryParam("content")
  165. if content != "" {
  166. contentSearch = append(contentSearch, content)
  167. }
  168. find.ContentSearch = contentSearch
  169. if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
  170. find.Limit = &limit
  171. }
  172. if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
  173. find.Offset = &offset
  174. }
  175. memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
  176. if err != nil {
  177. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
  178. }
  179. if memoDisplayWithUpdatedTs {
  180. find.OrderByUpdatedTs = true
  181. }
  182. list, err := s.Store.ListMemos(ctx, find)
  183. if err != nil {
  184. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
  185. }
  186. memoResponseList := []*Memo{}
  187. for _, memo := range list {
  188. memoResponse, err := s.convertMemoFromStore(ctx, memo)
  189. if err != nil {
  190. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  191. }
  192. memoResponseList = append(memoResponseList, memoResponse)
  193. }
  194. return c.JSON(http.StatusOK, memoResponseList)
  195. }
  196. // CreateMemo godoc
  197. //
  198. // @Summary Create a memo
  199. // @Description Visibility can be PUBLIC, PROTECTED or PRIVATE
  200. // @Description *You should omit fields to use their default values
  201. // @Tags memo
  202. // @Accept json
  203. // @Produce json
  204. // @Param body body CreateMemoRequest true "Request object."
  205. // @Success 200 {object} store.Memo "Stored memo"
  206. // @Failure 400 {object} nil "Malformatted post memo request | Content size overflow, up to 1MB"
  207. // @Failure 401 {object} nil "Missing user in session"
  208. // @Failure 404 {object} nil "User not found | Memo not found: %d"
  209. // @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"
  210. // @Router /api/v1/memo [POST]
  211. //
  212. // NOTES:
  213. // - It's currently possible to create phantom resources and relations. Phantom relations will trigger backend 404's when fetching memo.
  214. func (s *APIV1Service) CreateMemo(c echo.Context) error {
  215. ctx := c.Request().Context()
  216. userID, ok := c.Get(userIDContextKey).(int32)
  217. if !ok {
  218. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  219. }
  220. createMemoRequest := &CreateMemoRequest{}
  221. if err := json.NewDecoder(c.Request().Body).Decode(createMemoRequest); err != nil {
  222. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
  223. }
  224. if len(createMemoRequest.Content) > maxContentLength {
  225. return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB")
  226. }
  227. if createMemoRequest.Visibility == "" {
  228. userMemoVisibilitySetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
  229. UserID: &userID,
  230. Key: storepb.UserSettingKey_USER_SETTING_MEMO_VISIBILITY,
  231. })
  232. if err != nil {
  233. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
  234. }
  235. if userMemoVisibilitySetting != nil {
  236. createMemoRequest.Visibility = Visibility(userMemoVisibilitySetting.GetMemoVisibility())
  237. } else {
  238. // Private is the default memo visibility.
  239. createMemoRequest.Visibility = Private
  240. }
  241. }
  242. // Find disable public memos system setting.
  243. disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
  244. Name: SystemSettingDisablePublicMemosName.String(),
  245. })
  246. if err != nil {
  247. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  248. }
  249. if disablePublicMemosSystemSetting != nil {
  250. disablePublicMemos := false
  251. err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos)
  252. if err != nil {
  253. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
  254. }
  255. if disablePublicMemos {
  256. user, err := s.Store.GetUser(ctx, &store.FindUser{
  257. ID: &userID,
  258. })
  259. if err != nil {
  260. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
  261. }
  262. if user == nil {
  263. return echo.NewHTTPError(http.StatusNotFound, "User not found")
  264. }
  265. // Enforce normal user to create private memo if public memos are disabled.
  266. if user.Role == store.RoleUser {
  267. createMemoRequest.Visibility = Private
  268. }
  269. }
  270. }
  271. createMemoRequest.CreatorID = userID
  272. memo, err := s.Store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(createMemoRequest))
  273. if err != nil {
  274. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
  275. }
  276. for _, resourceID := range createMemoRequest.ResourceIDList {
  277. if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
  278. ID: resourceID,
  279. MemoID: &memo.ID,
  280. }); err != nil {
  281. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  282. }
  283. }
  284. for _, memoRelationUpsert := range createMemoRequest.RelationList {
  285. if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
  286. MemoID: memo.ID,
  287. RelatedMemoID: memoRelationUpsert.RelatedMemoID,
  288. Type: store.MemoRelationType(memoRelationUpsert.Type),
  289. }); err != nil {
  290. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
  291. }
  292. if memo.Visibility != store.Private && memoRelationUpsert.Type == MemoRelationComment {
  293. relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  294. ID: &memoRelationUpsert.RelatedMemoID,
  295. })
  296. if err != nil {
  297. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get related memo").SetInternal(err)
  298. }
  299. if relatedMemo.CreatorID != memo.CreatorID {
  300. activity, err := s.Store.CreateActivity(ctx, &store.Activity{
  301. CreatorID: memo.CreatorID,
  302. Type: store.ActivityTypeMemoComment,
  303. Level: store.ActivityLevelInfo,
  304. Payload: &storepb.ActivityPayload{
  305. MemoComment: &storepb.ActivityMemoCommentPayload{
  306. MemoId: memo.ID,
  307. RelatedMemoId: memoRelationUpsert.RelatedMemoID,
  308. },
  309. },
  310. })
  311. if err != nil {
  312. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
  313. }
  314. if _, err := s.Store.CreateInbox(ctx, &store.Inbox{
  315. SenderID: memo.CreatorID,
  316. ReceiverID: relatedMemo.CreatorID,
  317. Status: store.UNREAD,
  318. Message: &storepb.InboxMessage{
  319. Type: storepb.InboxMessage_TYPE_MEMO_COMMENT,
  320. ActivityId: &activity.ID,
  321. },
  322. }); err != nil {
  323. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create inbox").SetInternal(err)
  324. }
  325. }
  326. }
  327. }
  328. composedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  329. ID: &memo.ID,
  330. })
  331. if err != nil {
  332. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
  333. }
  334. if composedMemo == nil {
  335. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memo.ID))
  336. }
  337. memoResponse, err := s.convertMemoFromStore(ctx, composedMemo)
  338. if err != nil {
  339. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  340. }
  341. // Send notification to telegram if memo is not private.
  342. if memoResponse.Visibility != Private {
  343. // fetch all telegram UserID
  344. userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{Key: storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID})
  345. if err != nil {
  346. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to ListUserSettings").SetInternal(err)
  347. }
  348. for _, userSetting := range userSettings {
  349. tgUserID, err := strconv.ParseInt(userSetting.GetTelegramUserId(), 10, 64)
  350. if err != nil {
  351. log.Error("failed to parse Telegram UserID", zap.Error(err))
  352. continue
  353. }
  354. // send notification to telegram
  355. content := memoResponse.CreatorName + " Says:\n\n" + memoResponse.Content
  356. _, err = s.telegramBot.SendMessage(ctx, tgUserID, content)
  357. if err != nil {
  358. log.Error("Failed to send Telegram notification", zap.Error(err))
  359. continue
  360. }
  361. }
  362. }
  363. // Try to dispatch webhook when memo is created.
  364. if err := s.DispatchMemoCreatedWebhook(ctx, memoResponse); err != nil {
  365. log.Warn("Failed to dispatch memo created webhook", zap.Error(err))
  366. }
  367. return c.JSON(http.StatusOK, memoResponse)
  368. }
  369. // GetAllMemos godoc
  370. //
  371. // @Summary Get a list of public memos matching optional filters
  372. // @Description This should also list protected memos if the user is logged in
  373. // @Description Authentication is optional
  374. // @Tags memo
  375. // @Produce json
  376. // @Param limit query int false "Limit"
  377. // @Param offset query int false "Offset"
  378. // @Success 200 {object} []store.Memo "Memo list"
  379. // @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"
  380. // @Router /api/v1/memo/all [GET]
  381. //
  382. // NOTES:
  383. // - creatorUsername is listed at ./web/src/helpers/api.ts:82, but it's not present here
  384. func (s *APIV1Service) GetAllMemos(c echo.Context) error {
  385. ctx := c.Request().Context()
  386. memoFind := &store.FindMemo{}
  387. _, ok := c.Get(userIDContextKey).(int32)
  388. if !ok {
  389. memoFind.VisibilityList = []store.Visibility{store.Public}
  390. } else {
  391. memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
  392. }
  393. if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
  394. memoFind.Limit = &limit
  395. }
  396. if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
  397. memoFind.Offset = &offset
  398. }
  399. // Only fetch normal status memos.
  400. normalStatus := store.Normal
  401. memoFind.RowStatus = &normalStatus
  402. memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
  403. if err != nil {
  404. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
  405. }
  406. if memoDisplayWithUpdatedTs {
  407. memoFind.OrderByUpdatedTs = true
  408. }
  409. list, err := s.Store.ListMemos(ctx, memoFind)
  410. if err != nil {
  411. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
  412. }
  413. memoResponseList := []*Memo{}
  414. for _, memo := range list {
  415. memoResponse, err := s.convertMemoFromStore(ctx, memo)
  416. if err != nil {
  417. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  418. }
  419. memoResponseList = append(memoResponseList, memoResponse)
  420. }
  421. return c.JSON(http.StatusOK, memoResponseList)
  422. }
  423. // GetMemoStats godoc
  424. //
  425. // @Summary Get memo stats by creator ID or username
  426. // @Description Used to generate the heatmap
  427. // @Tags memo
  428. // @Produce json
  429. // @Param creatorId query int false "Creator ID"
  430. // @Param creatorUsername query string false "Creator username"
  431. // @Success 200 {object} []int "Memo createdTs list"
  432. // @Failure 400 {object} nil "Missing user id to find memo"
  433. // @Failure 500 {object} nil "Failed to get memo display with updated ts setting value | Failed to find memo list | Failed to compose memo response"
  434. // @Router /api/v1/memo/stats [GET]
  435. func (s *APIV1Service) GetMemoStats(c echo.Context) error {
  436. ctx := c.Request().Context()
  437. normalStatus := store.Normal
  438. findMemoMessage := &store.FindMemo{
  439. RowStatus: &normalStatus,
  440. ExcludeContent: true,
  441. }
  442. if creatorID, err := util.ConvertStringToInt32(c.QueryParam("creatorId")); err == nil {
  443. findMemoMessage.CreatorID = &creatorID
  444. }
  445. if username := c.QueryParam("creatorUsername"); username != "" {
  446. user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
  447. if user != nil {
  448. findMemoMessage.CreatorID = &user.ID
  449. }
  450. }
  451. if findMemoMessage.CreatorID == nil {
  452. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
  453. }
  454. currentUserID, ok := c.Get(userIDContextKey).(int32)
  455. if !ok {
  456. findMemoMessage.VisibilityList = []store.Visibility{store.Public}
  457. } else {
  458. if *findMemoMessage.CreatorID != currentUserID {
  459. findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
  460. } else {
  461. findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected, store.Private}
  462. }
  463. }
  464. memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
  465. if err != nil {
  466. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
  467. }
  468. if memoDisplayWithUpdatedTs {
  469. findMemoMessage.OrderByUpdatedTs = true
  470. }
  471. list, err := s.Store.ListMemos(ctx, findMemoMessage)
  472. if err != nil {
  473. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
  474. }
  475. displayTsList := []int64{}
  476. if memoDisplayWithUpdatedTs {
  477. for _, memo := range list {
  478. displayTsList = append(displayTsList, memo.UpdatedTs)
  479. }
  480. } else {
  481. for _, memo := range list {
  482. displayTsList = append(displayTsList, memo.CreatedTs)
  483. }
  484. }
  485. return c.JSON(http.StatusOK, displayTsList)
  486. }
  487. // GetMemo godoc
  488. //
  489. // @Summary Get memo by ID
  490. // @Tags memo
  491. // @Produce json
  492. // @Param memoId path int true "Memo ID"
  493. // @Success 200 {object} []store.Memo "Memo list"
  494. // @Failure 400 {object} nil "ID is not a number: %s"
  495. // @Failure 401 {object} nil "Missing user in session"
  496. // @Failure 403 {object} nil "this memo is private only | this memo is protected, missing user in session
  497. // @Failure 404 {object} nil "Memo not found: %d"
  498. // @Failure 500 {object} nil "Failed to find memo by ID: %v | Failed to compose memo response"
  499. // @Router /api/v1/memo/{memoId} [GET]
  500. func (s *APIV1Service) GetMemo(c echo.Context) error {
  501. ctx := c.Request().Context()
  502. memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
  503. if err != nil {
  504. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  505. }
  506. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  507. ID: &memoID,
  508. })
  509. if err != nil {
  510. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
  511. }
  512. if memo == nil {
  513. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
  514. }
  515. userID, ok := c.Get(userIDContextKey).(int32)
  516. if memo.Visibility == store.Private {
  517. if !ok || memo.CreatorID != userID {
  518. return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
  519. }
  520. } else if memo.Visibility == store.Protected {
  521. if !ok {
  522. return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
  523. }
  524. }
  525. memoResponse, err := s.convertMemoFromStore(ctx, memo)
  526. if err != nil {
  527. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  528. }
  529. return c.JSON(http.StatusOK, memoResponse)
  530. }
  531. // DeleteMemo godoc
  532. //
  533. // @Summary Delete memo by ID
  534. // @Tags memo
  535. // @Produce json
  536. // @Param memoId path int true "Memo ID to delete"
  537. // @Success 200 {boolean} true "Memo deleted"
  538. // @Failure 400 {object} nil "ID is not a number: %s"
  539. // @Failure 401 {object} nil "Missing user in session | Unauthorized"
  540. // @Failure 404 {object} nil "Memo not found: %d"
  541. // @Failure 500 {object} nil "Failed to find memo | Failed to delete memo ID: %v"
  542. // @Router /api/v1/memo/{memoId} [DELETE]
  543. func (s *APIV1Service) DeleteMemo(c echo.Context) error {
  544. ctx := c.Request().Context()
  545. userID, ok := c.Get(userIDContextKey).(int32)
  546. if !ok {
  547. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  548. }
  549. memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
  550. if err != nil {
  551. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  552. }
  553. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  554. ID: &memoID,
  555. })
  556. if err != nil {
  557. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  558. }
  559. if memo == nil {
  560. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
  561. }
  562. if memo.CreatorID != userID {
  563. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  564. }
  565. if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil {
  566. // Try to dispatch webhook when memo is deleted.
  567. if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil {
  568. log.Warn("Failed to dispatch memo deleted webhook", zap.Error(err))
  569. }
  570. }
  571. if err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{
  572. ID: memoID,
  573. }); err != nil {
  574. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
  575. }
  576. return c.JSON(http.StatusOK, true)
  577. }
  578. // UpdateMemo godoc
  579. //
  580. // @Summary Update a memo
  581. // @Description Visibility can be PUBLIC, PROTECTED or PRIVATE
  582. // @Description *You should omit fields to use their default values
  583. // @Tags memo
  584. // @Accept json
  585. // @Produce json
  586. // @Param memoId path int true "ID of memo to update"
  587. // @Param body body PatchMemoRequest true "Patched object."
  588. // @Success 200 {object} store.Memo "Stored memo"
  589. // @Failure 400 {object} nil "ID is not a number: %s | Malformatted patch memo request | Content size overflow, up to 1MB"
  590. // @Failure 401 {object} nil "Missing user in session | Unauthorized"
  591. // @Failure 404 {object} nil "Memo not found: %d"
  592. // @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"
  593. // @Router /api/v1/memo/{memoId} [PATCH]
  594. //
  595. // NOTES:
  596. // - It's currently possible to create phantom resources and relations. Phantom relations will trigger backend 404's when fetching memo.
  597. // - Passing 0 to createdTs and updatedTs will set them to 0 in the database, which is probably unwanted.
  598. func (s *APIV1Service) UpdateMemo(c echo.Context) error {
  599. ctx := c.Request().Context()
  600. userID, ok := c.Get(userIDContextKey).(int32)
  601. if !ok {
  602. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  603. }
  604. memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
  605. if err != nil {
  606. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  607. }
  608. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  609. ID: &memoID,
  610. })
  611. if err != nil {
  612. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  613. }
  614. if memo == nil {
  615. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
  616. }
  617. if memo.CreatorID != userID {
  618. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  619. }
  620. currentTs := time.Now().Unix()
  621. patchMemoRequest := &PatchMemoRequest{
  622. ID: memoID,
  623. UpdatedTs: &currentTs,
  624. }
  625. if err := json.NewDecoder(c.Request().Body).Decode(patchMemoRequest); err != nil {
  626. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
  627. }
  628. if patchMemoRequest.Content != nil && len(*patchMemoRequest.Content) > maxContentLength {
  629. return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB").SetInternal(err)
  630. }
  631. updateMemoMessage := &store.UpdateMemo{
  632. ID: memoID,
  633. CreatedTs: patchMemoRequest.CreatedTs,
  634. UpdatedTs: patchMemoRequest.UpdatedTs,
  635. Content: patchMemoRequest.Content,
  636. }
  637. if patchMemoRequest.RowStatus != nil {
  638. rowStatus := store.RowStatus(patchMemoRequest.RowStatus.String())
  639. updateMemoMessage.RowStatus = &rowStatus
  640. }
  641. if patchMemoRequest.Visibility != nil {
  642. visibility := store.Visibility(patchMemoRequest.Visibility.String())
  643. updateMemoMessage.Visibility = &visibility
  644. // Find disable public memos system setting.
  645. disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
  646. Name: SystemSettingDisablePublicMemosName.String(),
  647. })
  648. if err != nil {
  649. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  650. }
  651. if disablePublicMemosSystemSetting != nil {
  652. disablePublicMemos := false
  653. err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos)
  654. if err != nil {
  655. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
  656. }
  657. if disablePublicMemos {
  658. user, err := s.Store.GetUser(ctx, &store.FindUser{
  659. ID: &userID,
  660. })
  661. if err != nil {
  662. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
  663. }
  664. if user == nil {
  665. return echo.NewHTTPError(http.StatusNotFound, "User not found")
  666. }
  667. // Enforce normal user to save as private memo if public memos are disabled.
  668. if user.Role == store.RoleUser {
  669. visibility = store.Visibility("PRIVATE")
  670. updateMemoMessage.Visibility = &visibility
  671. }
  672. }
  673. }
  674. }
  675. err = s.Store.UpdateMemo(ctx, updateMemoMessage)
  676. if err != nil {
  677. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
  678. }
  679. memo, err = s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoID})
  680. if err != nil {
  681. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  682. }
  683. if memo == nil {
  684. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
  685. }
  686. memoMessage, err := s.convertMemoFromStore(ctx, memo)
  687. if err != nil {
  688. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
  689. }
  690. if patchMemoRequest.ResourceIDList != nil {
  691. originResourceIDList := []int32{}
  692. for _, resource := range memoMessage.ResourceList {
  693. originResourceIDList = append(originResourceIDList, resource.ID)
  694. }
  695. addedResourceIDList, removedResourceIDList := getIDListDiff(originResourceIDList, patchMemoRequest.ResourceIDList)
  696. for _, resourceID := range addedResourceIDList {
  697. if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
  698. ID: resourceID,
  699. MemoID: &memo.ID,
  700. }); err != nil {
  701. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  702. }
  703. }
  704. for _, resourceID := range removedResourceIDList {
  705. if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
  706. ID: resourceID,
  707. }); err != nil {
  708. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
  709. }
  710. }
  711. }
  712. if patchMemoRequest.RelationList != nil {
  713. patchMemoRelationList := make([]*MemoRelation, 0)
  714. for _, memoRelation := range patchMemoRequest.RelationList {
  715. patchMemoRelationList = append(patchMemoRelationList, &MemoRelation{
  716. MemoID: memo.ID,
  717. RelatedMemoID: memoRelation.RelatedMemoID,
  718. Type: memoRelation.Type,
  719. })
  720. }
  721. addedMemoRelationList, removedMemoRelationList := getMemoRelationListDiff(memoMessage.RelationList, patchMemoRelationList)
  722. for _, memoRelation := range addedMemoRelationList {
  723. if _, err := s.Store.UpsertMemoRelation(ctx, memoRelation); err != nil {
  724. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
  725. }
  726. }
  727. for _, memoRelation := range removedMemoRelationList {
  728. if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{
  729. MemoID: &memo.ID,
  730. RelatedMemoID: &memoRelation.RelatedMemoID,
  731. Type: &memoRelation.Type,
  732. }); err != nil {
  733. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
  734. }
  735. }
  736. }
  737. memo, err = s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoID})
  738. if err != nil {
  739. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  740. }
  741. if memo == nil {
  742. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
  743. }
  744. memoResponse, err := s.convertMemoFromStore(ctx, memo)
  745. if err != nil {
  746. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  747. }
  748. // Try to dispatch webhook when memo is updated.
  749. if err := s.DispatchMemoUpdatedWebhook(ctx, memoResponse); err != nil {
  750. log.Warn("Failed to dispatch memo updated webhook", zap.Error(err))
  751. }
  752. return c.JSON(http.StatusOK, memoResponse)
  753. }
  754. func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*Memo, error) {
  755. memoMessage := &Memo{
  756. ID: memo.ID,
  757. Name: memo.ResourceName,
  758. RowStatus: RowStatus(memo.RowStatus.String()),
  759. CreatorID: memo.CreatorID,
  760. CreatedTs: memo.CreatedTs,
  761. UpdatedTs: memo.UpdatedTs,
  762. Content: memo.Content,
  763. Visibility: Visibility(memo.Visibility.String()),
  764. Pinned: memo.Pinned,
  765. }
  766. // Compose creator name.
  767. user, err := s.Store.GetUser(ctx, &store.FindUser{
  768. ID: &memoMessage.CreatorID,
  769. })
  770. if err != nil {
  771. return nil, err
  772. }
  773. if user.Nickname != "" {
  774. memoMessage.CreatorName = user.Nickname
  775. } else {
  776. memoMessage.CreatorName = user.Username
  777. }
  778. memoMessage.CreatorUsername = user.Username
  779. // Compose display ts.
  780. memoMessage.DisplayTs = memoMessage.CreatedTs
  781. // Find memo display with updated ts setting.
  782. memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
  783. if err != nil {
  784. return nil, err
  785. }
  786. if memoDisplayWithUpdatedTs {
  787. memoMessage.DisplayTs = memoMessage.UpdatedTs
  788. }
  789. // Compose related resources.
  790. resourceList, err := s.Store.ListResources(ctx, &store.FindResource{
  791. MemoID: &memo.ID,
  792. })
  793. if err != nil {
  794. return nil, errors.Wrapf(err, "failed to list resources")
  795. }
  796. memoMessage.ResourceList = []*Resource{}
  797. for _, resource := range resourceList {
  798. memoMessage.ResourceList = append(memoMessage.ResourceList, convertResourceFromStore(resource))
  799. }
  800. // Compose related memo relations.
  801. relationList := []*MemoRelation{}
  802. tempList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
  803. MemoID: &memo.ID,
  804. })
  805. if err != nil {
  806. return nil, err
  807. }
  808. for _, relation := range tempList {
  809. relationList = append(relationList, convertMemoRelationFromStore(relation))
  810. }
  811. tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
  812. RelatedMemoID: &memo.ID,
  813. })
  814. if err != nil {
  815. return nil, err
  816. }
  817. for _, relation := range tempList {
  818. relationList = append(relationList, convertMemoRelationFromStore(relation))
  819. }
  820. memoMessage.RelationList = relationList
  821. return memoMessage, nil
  822. }
  823. func (s *APIV1Service) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) {
  824. memoDisplayWithUpdatedTsSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
  825. Name: SystemSettingMemoDisplayWithUpdatedTsName.String(),
  826. })
  827. if err != nil {
  828. return false, errors.Wrap(err, "failed to find system setting")
  829. }
  830. memoDisplayWithUpdatedTs := false
  831. if memoDisplayWithUpdatedTsSetting != nil {
  832. err = json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs)
  833. if err != nil {
  834. return false, errors.Wrap(err, "failed to unmarshal system setting value")
  835. }
  836. }
  837. return memoDisplayWithUpdatedTs, nil
  838. }
  839. func convertCreateMemoRequestToMemoMessage(memoCreate *CreateMemoRequest) *store.Memo {
  840. createdTs := time.Now().Unix()
  841. if memoCreate.CreatedTs != nil {
  842. createdTs = *memoCreate.CreatedTs
  843. }
  844. return &store.Memo{
  845. ResourceName: shortuuid.New(),
  846. CreatorID: memoCreate.CreatorID,
  847. CreatedTs: createdTs,
  848. Content: memoCreate.Content,
  849. Visibility: store.Visibility(memoCreate.Visibility),
  850. }
  851. }
  852. func getMemoRelationListDiff(oldList, newList []*MemoRelation) (addedList, removedList []*store.MemoRelation) {
  853. oldMap := map[string]bool{}
  854. for _, relation := range oldList {
  855. oldMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
  856. }
  857. newMap := map[string]bool{}
  858. for _, relation := range newList {
  859. newMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
  860. }
  861. for _, relation := range oldList {
  862. key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
  863. if !newMap[key] {
  864. removedList = append(removedList, &store.MemoRelation{
  865. MemoID: relation.MemoID,
  866. RelatedMemoID: relation.RelatedMemoID,
  867. Type: store.MemoRelationType(relation.Type),
  868. })
  869. }
  870. }
  871. for _, relation := range newList {
  872. key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
  873. if !oldMap[key] {
  874. addedList = append(addedList, &store.MemoRelation{
  875. MemoID: relation.MemoID,
  876. RelatedMemoID: relation.RelatedMemoID,
  877. Type: store.MemoRelationType(relation.Type),
  878. })
  879. }
  880. }
  881. return addedList, removedList
  882. }
  883. func getIDListDiff(oldList, newList []int32) (addedList, removedList []int32) {
  884. oldMap := map[int32]bool{}
  885. for _, id := range oldList {
  886. oldMap[id] = true
  887. }
  888. newMap := map[int32]bool{}
  889. for _, id := range newList {
  890. newMap[id] = true
  891. }
  892. for id := range oldMap {
  893. if !newMap[id] {
  894. removedList = append(removedList, id)
  895. }
  896. }
  897. for id := range newMap {
  898. if !oldMap[id] {
  899. addedList = append(addedList, id)
  900. }
  901. }
  902. return addedList, removedList
  903. }
  904. // DispatchMemoCreatedWebhook dispatches webhook when memo is created.
  905. func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *Memo) error {
  906. return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created")
  907. }
  908. // DispatchMemoUpdatedWebhook dispatches webhook when memo is updated.
  909. func (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *Memo) error {
  910. return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated")
  911. }
  912. // DispatchMemoDeletedWebhook dispatches webhook when memo is deletedd.
  913. func (s *APIV1Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *Memo) error {
  914. return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted")
  915. }
  916. func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *Memo, activityType string) error {
  917. webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
  918. CreatorID: &memo.CreatorID,
  919. })
  920. if err != nil {
  921. return err
  922. }
  923. for _, hook := range webhooks {
  924. payload := convertMemoToWebhookPayload(memo)
  925. payload.ActivityType = activityType
  926. payload.URL = hook.Url
  927. err := webhook.Post(*payload)
  928. if err != nil {
  929. return errors.Wrap(err, "failed to post webhook")
  930. }
  931. }
  932. return nil
  933. }
  934. func convertMemoToWebhookPayload(memo *Memo) *webhook.WebhookPayload {
  935. return &webhook.WebhookPayload{
  936. CreatorID: memo.CreatorID,
  937. CreatedTs: time.Now().Unix(),
  938. Memo: &webhook.Memo{
  939. ID: memo.ID,
  940. CreatorID: memo.CreatorID,
  941. CreatedTs: memo.CreatedTs,
  942. UpdatedTs: memo.UpdatedTs,
  943. Content: memo.Content,
  944. Visibility: memo.Visibility.String(),
  945. Pinned: memo.Pinned,
  946. ResourceList: func() []*webhook.Resource {
  947. resources := []*webhook.Resource{}
  948. for _, resource := range memo.ResourceList {
  949. resources = append(resources, &webhook.Resource{
  950. ID: resource.ID,
  951. CreatorID: resource.CreatorID,
  952. CreatedTs: resource.CreatedTs,
  953. UpdatedTs: resource.UpdatedTs,
  954. Filename: resource.Filename,
  955. InternalPath: resource.InternalPath,
  956. ExternalLink: resource.ExternalLink,
  957. Type: resource.Type,
  958. Size: resource.Size,
  959. })
  960. }
  961. return resources
  962. }(),
  963. RelationList: func() []*webhook.MemoRelation {
  964. relations := []*webhook.MemoRelation{}
  965. for _, relation := range memo.RelationList {
  966. relations = append(relations, &webhook.MemoRelation{
  967. MemoID: relation.MemoID,
  968. RelatedMemoID: relation.RelatedMemoID,
  969. Type: relation.Type.String(),
  970. })
  971. }
  972. return relations
  973. }(),
  974. },
  975. }
  976. }