memo.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. package server
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/pkg/errors"
  10. "github.com/usememos/memos/api"
  11. "github.com/usememos/memos/common"
  12. metric "github.com/usememos/memos/plugin/metrics"
  13. "github.com/labstack/echo/v4"
  14. )
  15. func (s *Server) registerMemoRoutes(g *echo.Group) {
  16. g.POST("/memo", func(c echo.Context) error {
  17. ctx := c.Request().Context()
  18. userID, ok := c.Get(getUserIDContextKey()).(int)
  19. if !ok {
  20. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  21. }
  22. memoCreate := &api.MemoCreate{}
  23. if err := json.NewDecoder(c.Request().Body).Decode(memoCreate); err != nil {
  24. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
  25. }
  26. if memoCreate.Visibility == "" {
  27. userSettingMemoVisibilityKey := api.UserSettingMemoVisibilityKey
  28. userMemoVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{
  29. UserID: userID,
  30. Key: &userSettingMemoVisibilityKey,
  31. })
  32. if err != nil {
  33. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
  34. }
  35. if userMemoVisibilitySetting != nil {
  36. memoVisibility := api.Private
  37. err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
  38. if err != nil {
  39. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
  40. }
  41. memoCreate.Visibility = memoVisibility
  42. } else {
  43. // Private is the default memo visibility.
  44. memoCreate.Visibility = api.Private
  45. }
  46. }
  47. // Find system settings
  48. disablePublicMemosSystemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
  49. Name: api.SystemSettingDisablePublicMemosName,
  50. })
  51. if err != nil && common.ErrorCode(err) != common.NotFound {
  52. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  53. }
  54. if disablePublicMemosSystemSetting != nil {
  55. disablePublicMemos := false
  56. err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos)
  57. if err != nil {
  58. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
  59. }
  60. if disablePublicMemos {
  61. memoCreate.Visibility = api.Private
  62. }
  63. }
  64. if len(memoCreate.Content) > api.MaxContentLength {
  65. return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB").SetInternal(err)
  66. }
  67. memoCreate.CreatorID = userID
  68. memo, err := s.Store.CreateMemo(ctx, memoCreate)
  69. if err != nil {
  70. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
  71. }
  72. if err := s.createMemoCreateActivity(c, memo); err != nil {
  73. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
  74. }
  75. for _, resourceID := range memoCreate.ResourceIDList {
  76. if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
  77. MemoID: memo.ID,
  78. ResourceID: resourceID,
  79. }); err != nil {
  80. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  81. }
  82. }
  83. memo, err = s.Store.ComposeMemo(ctx, memo)
  84. if err != nil {
  85. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
  86. }
  87. return c.JSON(http.StatusOK, composeResponse(memo))
  88. })
  89. g.PATCH("/memo/:memoId", func(c echo.Context) error {
  90. ctx := c.Request().Context()
  91. userID, ok := c.Get(getUserIDContextKey()).(int)
  92. if !ok {
  93. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  94. }
  95. memoID, err := strconv.Atoi(c.Param("memoId"))
  96. if err != nil {
  97. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  98. }
  99. memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
  100. ID: &memoID,
  101. })
  102. if err != nil {
  103. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  104. }
  105. if memo.CreatorID != userID {
  106. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  107. }
  108. currentTs := time.Now().Unix()
  109. memoPatch := &api.MemoPatch{
  110. ID: memoID,
  111. UpdatedTs: &currentTs,
  112. }
  113. if err := json.NewDecoder(c.Request().Body).Decode(memoPatch); err != nil {
  114. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
  115. }
  116. if memoPatch.Content != nil && len(*memoPatch.Content) > api.MaxContentLength {
  117. return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB").SetInternal(err)
  118. }
  119. memo, err = s.Store.PatchMemo(ctx, memoPatch)
  120. if err != nil {
  121. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
  122. }
  123. for _, resourceID := range memoPatch.ResourceIDList {
  124. if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
  125. MemoID: memo.ID,
  126. ResourceID: resourceID,
  127. }); err != nil {
  128. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  129. }
  130. }
  131. memo, err = s.Store.ComposeMemo(ctx, memo)
  132. if err != nil {
  133. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
  134. }
  135. return c.JSON(http.StatusOK, composeResponse(memo))
  136. })
  137. g.GET("/memo", func(c echo.Context) error {
  138. ctx := c.Request().Context()
  139. memoFind := &api.MemoFind{}
  140. if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
  141. memoFind.CreatorID = &userID
  142. }
  143. currentUserID, ok := c.Get(getUserIDContextKey()).(int)
  144. if !ok {
  145. if memoFind.CreatorID == nil {
  146. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
  147. }
  148. memoFind.VisibilityList = []api.Visibility{api.Public}
  149. } else {
  150. if memoFind.CreatorID == nil {
  151. memoFind.CreatorID = &currentUserID
  152. } else {
  153. memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
  154. }
  155. }
  156. rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
  157. if rowStatus != "" {
  158. memoFind.RowStatus = &rowStatus
  159. }
  160. pinnedStr := c.QueryParam("pinned")
  161. if pinnedStr != "" {
  162. pinned := pinnedStr == "true"
  163. memoFind.Pinned = &pinned
  164. }
  165. tag := c.QueryParam("tag")
  166. if tag != "" {
  167. contentSearch := "#" + tag
  168. memoFind.ContentSearch = &contentSearch
  169. }
  170. visibilityListStr := c.QueryParam("visibility")
  171. if visibilityListStr != "" {
  172. visibilityList := []api.Visibility{}
  173. for _, visibility := range strings.Split(visibilityListStr, ",") {
  174. visibilityList = append(visibilityList, api.Visibility(visibility))
  175. }
  176. memoFind.VisibilityList = visibilityList
  177. }
  178. if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
  179. memoFind.Limit = &limit
  180. }
  181. if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
  182. memoFind.Offset = &offset
  183. }
  184. list, err := s.Store.FindMemoList(ctx, memoFind)
  185. if err != nil {
  186. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
  187. }
  188. return c.JSON(http.StatusOK, composeResponse(list))
  189. })
  190. g.GET("/memo/:memoId", func(c echo.Context) error {
  191. ctx := c.Request().Context()
  192. memoID, err := strconv.Atoi(c.Param("memoId"))
  193. if err != nil {
  194. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  195. }
  196. memoFind := &api.MemoFind{
  197. ID: &memoID,
  198. }
  199. memo, err := s.Store.FindMemo(ctx, memoFind)
  200. if err != nil {
  201. if common.ErrorCode(err) == common.NotFound {
  202. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
  203. }
  204. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
  205. }
  206. userID, ok := c.Get(getUserIDContextKey()).(int)
  207. if memo.Visibility == api.Private {
  208. if !ok || memo.CreatorID != userID {
  209. return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
  210. }
  211. } else if memo.Visibility == api.Protected {
  212. if !ok {
  213. return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
  214. }
  215. }
  216. return c.JSON(http.StatusOK, composeResponse(memo))
  217. })
  218. g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
  219. ctx := c.Request().Context()
  220. memoID, err := strconv.Atoi(c.Param("memoId"))
  221. if err != nil {
  222. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  223. }
  224. userID, ok := c.Get(getUserIDContextKey()).(int)
  225. if !ok {
  226. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  227. }
  228. memoOrganizerUpsert := &api.MemoOrganizerUpsert{}
  229. if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
  230. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
  231. }
  232. memoOrganizerUpsert.MemoID = memoID
  233. memoOrganizerUpsert.UserID = userID
  234. err = s.Store.UpsertMemoOrganizer(ctx, memoOrganizerUpsert)
  235. if err != nil {
  236. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
  237. }
  238. memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
  239. ID: &memoID,
  240. })
  241. if err != nil {
  242. if common.ErrorCode(err) == common.NotFound {
  243. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
  244. }
  245. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
  246. }
  247. return c.JSON(http.StatusOK, composeResponse(memo))
  248. })
  249. g.POST("/memo/:memoId/resource", func(c echo.Context) error {
  250. ctx := c.Request().Context()
  251. memoID, err := strconv.Atoi(c.Param("memoId"))
  252. if err != nil {
  253. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  254. }
  255. userID, ok := c.Get(getUserIDContextKey()).(int)
  256. if !ok {
  257. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  258. }
  259. memoResourceUpsert := &api.MemoResourceUpsert{}
  260. if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil {
  261. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
  262. }
  263. resourceFind := &api.ResourceFind{
  264. ID: &memoResourceUpsert.ResourceID,
  265. }
  266. resource, err := s.Store.FindResource(ctx, resourceFind)
  267. if err != nil {
  268. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
  269. }
  270. if resource == nil {
  271. return echo.NewHTTPError(http.StatusBadRequest, "Resource not found").SetInternal(err)
  272. } else if resource.CreatorID != userID {
  273. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to bind this resource").SetInternal(err)
  274. }
  275. memoResourceUpsert.MemoID = memoID
  276. currentTs := time.Now().Unix()
  277. memoResourceUpsert.UpdatedTs = &currentTs
  278. if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil {
  279. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  280. }
  281. return c.JSON(http.StatusOK, composeResponse(resource))
  282. })
  283. g.GET("/memo/:memoId/resource", func(c echo.Context) error {
  284. ctx := c.Request().Context()
  285. memoID, err := strconv.Atoi(c.Param("memoId"))
  286. if err != nil {
  287. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  288. }
  289. resourceFind := &api.ResourceFind{
  290. MemoID: &memoID,
  291. }
  292. resourceList, err := s.Store.FindResourceList(ctx, resourceFind)
  293. if err != nil {
  294. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
  295. }
  296. return c.JSON(http.StatusOK, composeResponse(resourceList))
  297. })
  298. g.GET("/memo/stats", func(c echo.Context) error {
  299. ctx := c.Request().Context()
  300. normalStatus := api.Normal
  301. memoFind := &api.MemoFind{
  302. RowStatus: &normalStatus,
  303. }
  304. if creatorID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
  305. memoFind.CreatorID = &creatorID
  306. }
  307. if memoFind.CreatorID == nil {
  308. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
  309. }
  310. currentUserID, ok := c.Get(getUserIDContextKey()).(int)
  311. if !ok {
  312. memoFind.VisibilityList = []api.Visibility{api.Public}
  313. } else {
  314. if *memoFind.CreatorID != currentUserID {
  315. memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
  316. } else {
  317. memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected, api.Private}
  318. }
  319. }
  320. list, err := s.Store.FindMemoList(ctx, memoFind)
  321. if err != nil {
  322. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
  323. }
  324. createdTsList := []int64{}
  325. for _, memo := range list {
  326. createdTsList = append(createdTsList, memo.CreatedTs)
  327. }
  328. return c.JSON(http.StatusOK, composeResponse(createdTsList))
  329. })
  330. g.GET("/memo/all", func(c echo.Context) error {
  331. ctx := c.Request().Context()
  332. memoFind := &api.MemoFind{}
  333. _, ok := c.Get(getUserIDContextKey()).(int)
  334. if !ok {
  335. memoFind.VisibilityList = []api.Visibility{api.Public}
  336. } else {
  337. memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
  338. }
  339. pinnedStr := c.QueryParam("pinned")
  340. if pinnedStr != "" {
  341. pinned := pinnedStr == "true"
  342. memoFind.Pinned = &pinned
  343. }
  344. tag := c.QueryParam("tag")
  345. if tag != "" {
  346. contentSearch := "#" + tag + " "
  347. memoFind.ContentSearch = &contentSearch
  348. }
  349. visibilityListStr := c.QueryParam("visibility")
  350. if visibilityListStr != "" {
  351. visibilityList := []api.Visibility{}
  352. for _, visibility := range strings.Split(visibilityListStr, ",") {
  353. visibilityList = append(visibilityList, api.Visibility(visibility))
  354. }
  355. memoFind.VisibilityList = visibilityList
  356. }
  357. if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
  358. memoFind.Limit = &limit
  359. }
  360. if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
  361. memoFind.Offset = &offset
  362. }
  363. // Only fetch normal status memos.
  364. normalStatus := api.Normal
  365. memoFind.RowStatus = &normalStatus
  366. list, err := s.Store.FindMemoList(ctx, memoFind)
  367. if err != nil {
  368. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
  369. }
  370. return c.JSON(http.StatusOK, composeResponse(list))
  371. })
  372. g.DELETE("/memo/:memoId", func(c echo.Context) error {
  373. ctx := c.Request().Context()
  374. userID, ok := c.Get(getUserIDContextKey()).(int)
  375. if !ok {
  376. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  377. }
  378. memoID, err := strconv.Atoi(c.Param("memoId"))
  379. if err != nil {
  380. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  381. }
  382. memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
  383. ID: &memoID,
  384. })
  385. if err != nil {
  386. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  387. }
  388. if memo.CreatorID != userID {
  389. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  390. }
  391. memoDelete := &api.MemoDelete{
  392. ID: memoID,
  393. }
  394. if err := s.Store.DeleteMemo(ctx, memoDelete); err != nil {
  395. if common.ErrorCode(err) == common.NotFound {
  396. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID))
  397. }
  398. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
  399. }
  400. return c.JSON(http.StatusOK, true)
  401. })
  402. g.DELETE("/memo/:memoId/resource/:resourceId", func(c echo.Context) error {
  403. ctx := c.Request().Context()
  404. userID, ok := c.Get(getUserIDContextKey()).(int)
  405. if !ok {
  406. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  407. }
  408. memoID, err := strconv.Atoi(c.Param("memoId"))
  409. if err != nil {
  410. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  411. }
  412. resourceID, err := strconv.Atoi(c.Param("resourceId"))
  413. if err != nil {
  414. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Resource ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  415. }
  416. memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
  417. ID: &memoID,
  418. })
  419. if err != nil {
  420. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  421. }
  422. if memo.CreatorID != userID {
  423. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  424. }
  425. memoResourceDelete := &api.MemoResourceDelete{
  426. MemoID: &memoID,
  427. ResourceID: &resourceID,
  428. }
  429. if err := s.Store.DeleteMemoResource(ctx, memoResourceDelete); err != nil {
  430. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
  431. }
  432. return c.JSON(http.StatusOK, true)
  433. })
  434. }
  435. func (s *Server) createMemoCreateActivity(c echo.Context, memo *api.Memo) error {
  436. ctx := c.Request().Context()
  437. payload := api.ActivityMemoCreatePayload{
  438. Content: memo.Content,
  439. Visibility: memo.Visibility.String(),
  440. }
  441. payloadBytes, err := json.Marshal(payload)
  442. if err != nil {
  443. return errors.Wrap(err, "failed to marshal activity payload")
  444. }
  445. activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
  446. CreatorID: memo.CreatorID,
  447. Type: api.ActivityMemoCreate,
  448. Level: api.ActivityInfo,
  449. Payload: string(payloadBytes),
  450. })
  451. if err != nil || activity == nil {
  452. return errors.Wrap(err, "failed to create activity")
  453. }
  454. s.Collector.Collect(ctx, &metric.Metric{
  455. Name: string(activity.Type),
  456. })
  457. return err
  458. }