memo.go 37 KB

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