memo.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. package server
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/pkg/errors"
  11. "github.com/usememos/memos/api"
  12. apiv1 "github.com/usememos/memos/api/v1"
  13. "github.com/usememos/memos/common"
  14. "github.com/usememos/memos/store"
  15. "github.com/labstack/echo/v4"
  16. )
  17. // maxContentLength means the max memo content bytes is 1MB.
  18. const maxContentLength = 1 << 30
  19. func (s *Server) registerMemoRoutes(g *echo.Group) {
  20. g.POST("/memo", func(c echo.Context) error {
  21. ctx := c.Request().Context()
  22. userID, ok := c.Get(getUserIDContextKey()).(int)
  23. if !ok {
  24. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  25. }
  26. createMemoRequest := &api.CreateMemoRequest{}
  27. if err := json.NewDecoder(c.Request().Body).Decode(createMemoRequest); err != nil {
  28. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
  29. }
  30. if len(createMemoRequest.Content) > maxContentLength {
  31. return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB")
  32. }
  33. if createMemoRequest.Visibility == "" {
  34. userMemoVisibilitySetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
  35. UserID: &userID,
  36. Key: apiv1.UserSettingMemoVisibilityKey.String(),
  37. })
  38. if err != nil {
  39. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
  40. }
  41. if userMemoVisibilitySetting != nil {
  42. memoVisibility := api.Private
  43. err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
  44. if err != nil {
  45. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
  46. }
  47. createMemoRequest.Visibility = memoVisibility
  48. } else {
  49. // Private is the default memo visibility.
  50. createMemoRequest.Visibility = api.Private
  51. }
  52. }
  53. // Find disable public memos system setting.
  54. disablePublicMemosSystemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
  55. Name: api.SystemSettingDisablePublicMemosName,
  56. })
  57. if err != nil && common.ErrorCode(err) != common.NotFound {
  58. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  59. }
  60. if disablePublicMemosSystemSetting != nil {
  61. disablePublicMemos := false
  62. err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos)
  63. if err != nil {
  64. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
  65. }
  66. if disablePublicMemos {
  67. user, err := s.Store.FindUser(ctx, &api.UserFind{
  68. ID: &userID,
  69. })
  70. if err != nil {
  71. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
  72. }
  73. // Enforce normal user to create private memo if public memos are disabled.
  74. if user.Role == "USER" {
  75. createMemoRequest.Visibility = api.Private
  76. }
  77. }
  78. }
  79. createMemoRequest.CreatorID = userID
  80. memoMessage, err := s.Store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(createMemoRequest))
  81. if err != nil {
  82. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
  83. }
  84. if err := createMemoCreateActivity(c.Request().Context(), s.Store, memoMessage); err != nil {
  85. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
  86. }
  87. for _, resourceID := range createMemoRequest.ResourceIDList {
  88. if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
  89. MemoID: memoMessage.ID,
  90. ResourceID: resourceID,
  91. }); err != nil {
  92. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  93. }
  94. }
  95. for _, memoRelationUpsert := range createMemoRequest.RelationList {
  96. if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelationMessage{
  97. MemoID: memoMessage.ID,
  98. RelatedMemoID: memoRelationUpsert.RelatedMemoID,
  99. Type: store.MemoRelationType(memoRelationUpsert.Type),
  100. }); err != nil {
  101. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
  102. }
  103. }
  104. memoMessage, err = s.Store.GetMemo(ctx, &store.FindMemoMessage{
  105. ID: &memoMessage.ID,
  106. })
  107. if err != nil {
  108. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
  109. }
  110. memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
  111. if err != nil {
  112. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  113. }
  114. return c.JSON(http.StatusOK, composeResponse(memoResponse))
  115. })
  116. g.PATCH("/memo/:memoId", func(c echo.Context) error {
  117. ctx := c.Request().Context()
  118. userID, ok := c.Get(getUserIDContextKey()).(int)
  119. if !ok {
  120. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  121. }
  122. memoID, err := strconv.Atoi(c.Param("memoId"))
  123. if err != nil {
  124. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  125. }
  126. memoMessage, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
  127. ID: &memoID,
  128. })
  129. if err != nil {
  130. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  131. }
  132. if memoMessage.CreatorID != userID {
  133. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  134. }
  135. currentTs := time.Now().Unix()
  136. patchMemoRequest := &api.PatchMemoRequest{
  137. ID: memoID,
  138. UpdatedTs: &currentTs,
  139. }
  140. if err := json.NewDecoder(c.Request().Body).Decode(patchMemoRequest); err != nil {
  141. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
  142. }
  143. if patchMemoRequest.Content != nil && len(*patchMemoRequest.Content) > maxContentLength {
  144. return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB").SetInternal(err)
  145. }
  146. updateMemoMessage := &store.UpdateMemoMessage{
  147. ID: memoID,
  148. CreatedTs: patchMemoRequest.CreatedTs,
  149. UpdatedTs: patchMemoRequest.UpdatedTs,
  150. Content: patchMemoRequest.Content,
  151. }
  152. if patchMemoRequest.RowStatus != nil {
  153. rowStatus := store.RowStatus(patchMemoRequest.RowStatus.String())
  154. updateMemoMessage.RowStatus = &rowStatus
  155. }
  156. if patchMemoRequest.Visibility != nil {
  157. visibility := store.Visibility(patchMemoRequest.Visibility.String())
  158. updateMemoMessage.Visibility = &visibility
  159. }
  160. err = s.Store.UpdateMemo(ctx, updateMemoMessage)
  161. if err != nil {
  162. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
  163. }
  164. memoMessage, err = s.Store.GetMemo(ctx, &store.FindMemoMessage{ID: &memoID})
  165. if err != nil {
  166. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  167. }
  168. if patchMemoRequest.ResourceIDList != nil {
  169. addedResourceIDList, removedResourceIDList := getIDListDiff(memoMessage.ResourceIDList, patchMemoRequest.ResourceIDList)
  170. for _, resourceID := range addedResourceIDList {
  171. if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
  172. MemoID: memoMessage.ID,
  173. ResourceID: resourceID,
  174. }); err != nil {
  175. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  176. }
  177. }
  178. for _, resourceID := range removedResourceIDList {
  179. if err := s.Store.DeleteMemoResource(ctx, &api.MemoResourceDelete{
  180. MemoID: &memoMessage.ID,
  181. ResourceID: &resourceID,
  182. }); err != nil {
  183. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo resource").SetInternal(err)
  184. }
  185. }
  186. }
  187. if patchMemoRequest.RelationList != nil {
  188. patchMemoRelationList := make([]*store.MemoRelationMessage, 0)
  189. for _, memoRelation := range patchMemoRequest.RelationList {
  190. patchMemoRelationList = append(patchMemoRelationList, &store.MemoRelationMessage{
  191. MemoID: memoMessage.ID,
  192. RelatedMemoID: memoRelation.RelatedMemoID,
  193. Type: store.MemoRelationType(memoRelation.Type),
  194. })
  195. }
  196. addedMemoRelationList, removedMemoRelationList := getMemoRelationListDiff(memoMessage.RelationList, patchMemoRelationList)
  197. for _, memoRelation := range addedMemoRelationList {
  198. if _, err := s.Store.UpsertMemoRelation(ctx, memoRelation); err != nil {
  199. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
  200. }
  201. }
  202. for _, memoRelation := range removedMemoRelationList {
  203. if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelationMessage{
  204. MemoID: &memoMessage.ID,
  205. RelatedMemoID: &memoRelation.RelatedMemoID,
  206. Type: &memoRelation.Type,
  207. }); err != nil {
  208. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
  209. }
  210. }
  211. }
  212. memoMessage, err = s.Store.GetMemo(ctx, &store.FindMemoMessage{ID: &memoID})
  213. if err != nil {
  214. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  215. }
  216. memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
  217. if err != nil {
  218. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  219. }
  220. return c.JSON(http.StatusOK, composeResponse(memoResponse))
  221. })
  222. g.GET("/memo", func(c echo.Context) error {
  223. ctx := c.Request().Context()
  224. findMemoMessage := &store.FindMemoMessage{}
  225. if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
  226. findMemoMessage.CreatorID = &userID
  227. }
  228. currentUserID, ok := c.Get(getUserIDContextKey()).(int)
  229. if !ok {
  230. if findMemoMessage.CreatorID == nil {
  231. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
  232. }
  233. findMemoMessage.VisibilityList = []store.Visibility{store.Public}
  234. } else {
  235. if findMemoMessage.CreatorID == nil {
  236. findMemoMessage.CreatorID = &currentUserID
  237. } else {
  238. findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
  239. }
  240. }
  241. rowStatus := store.RowStatus(c.QueryParam("rowStatus"))
  242. if rowStatus != "" {
  243. findMemoMessage.RowStatus = &rowStatus
  244. }
  245. pinnedStr := c.QueryParam("pinned")
  246. if pinnedStr != "" {
  247. pinned := pinnedStr == "true"
  248. findMemoMessage.Pinned = &pinned
  249. }
  250. contentSearch := []string{}
  251. tag := c.QueryParam("tag")
  252. if tag != "" {
  253. contentSearch = append(contentSearch, "#"+tag)
  254. }
  255. contentSlice := c.QueryParams()["content"]
  256. if len(contentSlice) > 0 {
  257. contentSearch = append(contentSearch, contentSlice...)
  258. }
  259. findMemoMessage.ContentSearch = contentSearch
  260. visibilityListStr := c.QueryParam("visibility")
  261. if visibilityListStr != "" {
  262. visibilityList := []store.Visibility{}
  263. for _, visibility := range strings.Split(visibilityListStr, ",") {
  264. visibilityList = append(visibilityList, store.Visibility(visibility))
  265. }
  266. findMemoMessage.VisibilityList = visibilityList
  267. }
  268. if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
  269. findMemoMessage.Limit = &limit
  270. }
  271. if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
  272. findMemoMessage.Offset = &offset
  273. }
  274. memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
  275. if err != nil {
  276. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
  277. }
  278. if memoDisplayWithUpdatedTs {
  279. findMemoMessage.OrderByUpdatedTs = true
  280. }
  281. memoMessageList, err := s.Store.ListMemos(ctx, findMemoMessage)
  282. if err != nil {
  283. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
  284. }
  285. memoResponseList := []*api.MemoResponse{}
  286. for _, memoMessage := range memoMessageList {
  287. memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
  288. if err != nil {
  289. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  290. }
  291. memoResponseList = append(memoResponseList, memoResponse)
  292. }
  293. return c.JSON(http.StatusOK, composeResponse(memoResponseList))
  294. })
  295. g.GET("/memo/:memoId", func(c echo.Context) error {
  296. ctx := c.Request().Context()
  297. memoID, err := strconv.Atoi(c.Param("memoId"))
  298. if err != nil {
  299. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  300. }
  301. memoMessage, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
  302. ID: &memoID,
  303. })
  304. if err != nil {
  305. if common.ErrorCode(err) == common.NotFound {
  306. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
  307. }
  308. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
  309. }
  310. userID, ok := c.Get(getUserIDContextKey()).(int)
  311. if memoMessage.Visibility == store.Private {
  312. if !ok || memoMessage.CreatorID != userID {
  313. return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
  314. }
  315. } else if memoMessage.Visibility == store.Protected {
  316. if !ok {
  317. return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
  318. }
  319. }
  320. memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
  321. if err != nil {
  322. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  323. }
  324. return c.JSON(http.StatusOK, composeResponse(memoResponse))
  325. })
  326. g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
  327. ctx := c.Request().Context()
  328. memoID, err := strconv.Atoi(c.Param("memoId"))
  329. if err != nil {
  330. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  331. }
  332. userID, ok := c.Get(getUserIDContextKey()).(int)
  333. if !ok {
  334. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  335. }
  336. memo, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
  337. ID: &memoID,
  338. })
  339. if err != nil {
  340. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  341. }
  342. if memo.CreatorID != userID {
  343. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  344. }
  345. memoOrganizerUpsert := &api.MemoOrganizerUpsert{}
  346. if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
  347. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
  348. }
  349. memoOrganizerUpsert.MemoID = memoID
  350. memoOrganizerUpsert.UserID = userID
  351. err = s.Store.UpsertMemoOrganizer(ctx, memoOrganizerUpsert)
  352. if err != nil {
  353. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
  354. }
  355. memoMessage, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
  356. ID: &memoID,
  357. })
  358. if err != nil {
  359. if common.ErrorCode(err) == common.NotFound {
  360. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
  361. }
  362. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
  363. }
  364. memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
  365. if err != nil {
  366. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  367. }
  368. return c.JSON(http.StatusOK, composeResponse(memoResponse))
  369. })
  370. g.GET("/memo/stats", func(c echo.Context) error {
  371. ctx := c.Request().Context()
  372. normalStatus := store.Normal
  373. findMemoMessage := &store.FindMemoMessage{
  374. RowStatus: &normalStatus,
  375. }
  376. if creatorID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
  377. findMemoMessage.CreatorID = &creatorID
  378. }
  379. if findMemoMessage.CreatorID == nil {
  380. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
  381. }
  382. currentUserID, ok := c.Get(getUserIDContextKey()).(int)
  383. if !ok {
  384. findMemoMessage.VisibilityList = []store.Visibility{store.Public}
  385. } else {
  386. if *findMemoMessage.CreatorID != currentUserID {
  387. findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
  388. } else {
  389. findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected, store.Private}
  390. }
  391. }
  392. memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
  393. if err != nil {
  394. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
  395. }
  396. if memoDisplayWithUpdatedTs {
  397. findMemoMessage.OrderByUpdatedTs = true
  398. }
  399. memoMessageList, err := s.Store.ListMemos(ctx, findMemoMessage)
  400. if err != nil {
  401. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
  402. }
  403. memoResponseList := []*api.MemoResponse{}
  404. for _, memoMessage := range memoMessageList {
  405. memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
  406. if err != nil {
  407. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  408. }
  409. memoResponseList = append(memoResponseList, memoResponse)
  410. }
  411. displayTsList := []int64{}
  412. for _, memo := range memoResponseList {
  413. displayTsList = append(displayTsList, memo.DisplayTs)
  414. }
  415. return c.JSON(http.StatusOK, composeResponse(displayTsList))
  416. })
  417. g.GET("/memo/all", func(c echo.Context) error {
  418. ctx := c.Request().Context()
  419. findMemoMessage := &store.FindMemoMessage{}
  420. _, ok := c.Get(getUserIDContextKey()).(int)
  421. if !ok {
  422. findMemoMessage.VisibilityList = []store.Visibility{store.Public}
  423. } else {
  424. findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
  425. }
  426. pinnedStr := c.QueryParam("pinned")
  427. if pinnedStr != "" {
  428. pinned := pinnedStr == "true"
  429. findMemoMessage.Pinned = &pinned
  430. }
  431. contentSearch := []string{}
  432. tag := c.QueryParam("tag")
  433. if tag != "" {
  434. contentSearch = append(contentSearch, "#"+tag+" ")
  435. }
  436. contentSlice := c.QueryParams()["content"]
  437. if len(contentSlice) > 0 {
  438. contentSearch = append(contentSearch, contentSlice...)
  439. }
  440. findMemoMessage.ContentSearch = contentSearch
  441. visibilityListStr := c.QueryParam("visibility")
  442. if visibilityListStr != "" {
  443. visibilityList := []store.Visibility{}
  444. for _, visibility := range strings.Split(visibilityListStr, ",") {
  445. visibilityList = append(visibilityList, store.Visibility(visibility))
  446. }
  447. findMemoMessage.VisibilityList = visibilityList
  448. }
  449. if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
  450. findMemoMessage.Limit = &limit
  451. }
  452. if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
  453. findMemoMessage.Offset = &offset
  454. }
  455. // Only fetch normal status memos.
  456. normalStatus := store.Normal
  457. findMemoMessage.RowStatus = &normalStatus
  458. memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
  459. if err != nil {
  460. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
  461. }
  462. if memoDisplayWithUpdatedTs {
  463. findMemoMessage.OrderByUpdatedTs = true
  464. }
  465. memoMessageList, err := s.Store.ListMemos(ctx, findMemoMessage)
  466. if err != nil {
  467. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
  468. }
  469. memoResponseList := []*api.MemoResponse{}
  470. for _, memoMessage := range memoMessageList {
  471. memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
  472. if err != nil {
  473. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
  474. }
  475. memoResponseList = append(memoResponseList, memoResponse)
  476. }
  477. return c.JSON(http.StatusOK, composeResponse(memoResponseList))
  478. })
  479. g.DELETE("/memo/:memoId", func(c echo.Context) error {
  480. ctx := c.Request().Context()
  481. userID, ok := c.Get(getUserIDContextKey()).(int)
  482. if !ok {
  483. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  484. }
  485. memoID, err := strconv.Atoi(c.Param("memoId"))
  486. if err != nil {
  487. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  488. }
  489. memo, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
  490. ID: &memoID,
  491. })
  492. if err != nil {
  493. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  494. }
  495. if memo.CreatorID != userID {
  496. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  497. }
  498. if err := s.Store.DeleteMemo(ctx, &store.DeleteMemoMessage{
  499. ID: memoID,
  500. }); err != nil {
  501. if common.ErrorCode(err) == common.NotFound {
  502. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID))
  503. }
  504. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
  505. }
  506. return c.JSON(http.StatusOK, true)
  507. })
  508. }
  509. func createMemoCreateActivity(ctx context.Context, store *store.Store, memo *store.MemoMessage) error {
  510. payload := api.ActivityMemoCreatePayload{
  511. Content: memo.Content,
  512. Visibility: memo.Visibility.String(),
  513. }
  514. payloadBytes, err := json.Marshal(payload)
  515. if err != nil {
  516. return errors.Wrap(err, "failed to marshal activity payload")
  517. }
  518. activity, err := store.CreateActivity(ctx, &api.ActivityCreate{
  519. CreatorID: memo.CreatorID,
  520. Type: api.ActivityMemoCreate,
  521. Level: api.ActivityInfo,
  522. Payload: string(payloadBytes),
  523. })
  524. if err != nil || activity == nil {
  525. return errors.Wrap(err, "failed to create activity")
  526. }
  527. return err
  528. }
  529. func getIDListDiff(oldList, newList []int) (addedList, removedList []int) {
  530. oldMap := map[int]bool{}
  531. for _, id := range oldList {
  532. oldMap[id] = true
  533. }
  534. newMap := map[int]bool{}
  535. for _, id := range newList {
  536. newMap[id] = true
  537. }
  538. for id := range oldMap {
  539. if !newMap[id] {
  540. removedList = append(removedList, id)
  541. }
  542. }
  543. for id := range newMap {
  544. if !oldMap[id] {
  545. addedList = append(addedList, id)
  546. }
  547. }
  548. return addedList, removedList
  549. }
  550. func getMemoRelationListDiff(oldList, newList []*store.MemoRelationMessage) (addedList, removedList []*store.MemoRelationMessage) {
  551. oldMap := map[string]bool{}
  552. for _, relation := range oldList {
  553. oldMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
  554. }
  555. newMap := map[string]bool{}
  556. for _, relation := range newList {
  557. newMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
  558. }
  559. for _, relation := range oldList {
  560. key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
  561. if !newMap[key] {
  562. removedList = append(removedList, relation)
  563. }
  564. }
  565. for _, relation := range newList {
  566. key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
  567. if !oldMap[key] {
  568. addedList = append(addedList, relation)
  569. }
  570. }
  571. return addedList, removedList
  572. }
  573. func convertCreateMemoRequestToMemoMessage(memoCreate *api.CreateMemoRequest) *store.MemoMessage {
  574. createdTs := time.Now().Unix()
  575. if memoCreate.CreatedTs != nil {
  576. createdTs = *memoCreate.CreatedTs
  577. }
  578. return &store.MemoMessage{
  579. CreatorID: memoCreate.CreatorID,
  580. CreatedTs: createdTs,
  581. Content: memoCreate.Content,
  582. Visibility: store.Visibility(memoCreate.Visibility),
  583. }
  584. }
  585. func (s *Server) composeMemoMessageToMemoResponse(ctx context.Context, memoMessage *store.MemoMessage) (*api.MemoResponse, error) {
  586. memoResponse := &api.MemoResponse{
  587. ID: memoMessage.ID,
  588. RowStatus: api.RowStatus(memoMessage.RowStatus.String()),
  589. CreatorID: memoMessage.CreatorID,
  590. CreatedTs: memoMessage.CreatedTs,
  591. UpdatedTs: memoMessage.UpdatedTs,
  592. Content: memoMessage.Content,
  593. Visibility: api.Visibility(memoMessage.Visibility.String()),
  594. Pinned: memoMessage.Pinned,
  595. }
  596. // Compose creator name.
  597. user, err := s.Store.FindUser(ctx, &api.UserFind{
  598. ID: &memoResponse.CreatorID,
  599. })
  600. if err != nil {
  601. return nil, err
  602. }
  603. if user.Nickname != "" {
  604. memoResponse.CreatorName = user.Nickname
  605. } else {
  606. memoResponse.CreatorName = user.Username
  607. }
  608. // Compose display ts.
  609. memoResponse.DisplayTs = memoResponse.CreatedTs
  610. // Find memo display with updated ts setting.
  611. memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
  612. if err != nil {
  613. return nil, err
  614. }
  615. if memoDisplayWithUpdatedTs {
  616. memoResponse.DisplayTs = memoResponse.UpdatedTs
  617. }
  618. relationList := []*api.MemoRelation{}
  619. for _, relation := range memoMessage.RelationList {
  620. relationList = append(relationList, convertMemoRelationMessageToMemoRelation(relation))
  621. }
  622. memoResponse.RelationList = relationList
  623. resourceList := []*api.Resource{}
  624. for _, resourceID := range memoMessage.ResourceIDList {
  625. resource, err := s.Store.FindResource(ctx, &api.ResourceFind{
  626. ID: &resourceID,
  627. })
  628. if err != nil {
  629. return nil, err
  630. }
  631. resourceList = append(resourceList, resource)
  632. }
  633. memoResponse.ResourceList = resourceList
  634. return memoResponse, nil
  635. }
  636. func (s *Server) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) {
  637. memoDisplayWithUpdatedTsSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
  638. Name: api.SystemSettingMemoDisplayWithUpdatedTsName,
  639. })
  640. if err != nil && common.ErrorCode(err) != common.NotFound {
  641. return false, errors.Wrap(err, "failed to find system setting")
  642. }
  643. memoDisplayWithUpdatedTs := false
  644. if memoDisplayWithUpdatedTsSetting != nil {
  645. err = json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs)
  646. if err != nil {
  647. return false, errors.Wrap(err, "failed to unmarshal system setting value")
  648. }
  649. }
  650. return memoDisplayWithUpdatedTs, nil
  651. }