Compare commits

..

200 Commits

Author SHA1 Message Date
Steven
b0f52ade7a chore: remove metric service 2024-02-24 23:32:39 +08:00
Steven
222d04fb22 chore: add fuse to get tag suggestions 2024-02-24 11:02:46 +08:00
Steven
68468927dd chore: tweak setting styles 2024-02-24 11:01:57 +08:00
Steven
dfe29ec766 chore: tweak route layout 2024-02-24 10:18:53 +08:00
Steven
db56e1b575 chore: fix user banner dropdown 2024-02-23 23:47:26 +08:00
Steven
5b92ac1775 chore: fix setting migrator 2024-02-23 09:16:34 +08:00
Steven
f2eb9f1b8f chore: fix get workspace setting 2024-02-23 09:11:39 +08:00
Steven
e602aeecc1 fix: update workspace general setting 2024-02-23 09:08:49 +08:00
Steven
ce133ad69b chore: retire unused workspace settings 2024-02-23 08:40:33 +08:00
Steven
e585578553 chore: tweak readme 2024-02-23 08:25:54 +08:00
Steven
4d9c929c32 chore: upgrade gomark 2024-02-23 08:22:39 +08:00
Steven
39bf850591 fix: embed memos callback 2024-02-22 23:17:15 +08:00
Steven
9cd835b979 chore: upgrade gomark wasm 2024-02-22 23:11:32 +08:00
Steven
0afdbe3332 chore: remove animation of spoiler 2024-02-22 19:25:24 +08:00
Steven
6b14d87521 chore: fix linter 2024-02-21 23:45:12 +08:00
Steven
51d58d3982 chore: add workspace setting migrator 2024-02-21 23:43:18 +08:00
Steven
4378816e44 chore: tweak i18n 2024-02-21 23:02:18 +08:00
Steven
e7bbd850b2 chore: tweak spoiler animation 2024-02-21 21:31:34 +08:00
Steven
c6162d3f38 chore: update dependencies 2024-02-21 21:24:31 +08:00
Steven
ce32206677 chore: remove unused system setting 2024-02-21 21:15:28 +08:00
Steven
50a3af3b29 chore: fix get general setting 2024-02-21 20:44:35 +08:00
Steven
80b64c02fd chore: tweak workspace setting seeds 2024-02-21 20:25:25 +08:00
Steven
13b911ebf0 chore: add spoiler node 2024-02-20 23:55:54 +08:00
Steven
4a6da91719 chore: fix serve frontend flag 2024-02-20 23:24:13 +08:00
Steven
fa62e8b59a chore: tweak linter errors 2024-02-20 23:07:42 +08:00
Steven
8e11826db1 chore: update workspace setting service 2024-02-20 23:02:01 +08:00
David Nguyen
e6d0c00cf6 chore: update vi.json (#2980) 2024-02-20 17:57:06 +08:00
Kristián
03d67d5a00 feat: add mermaid support in codeblock (#2971) 2024-02-19 15:10:58 +08:00
Kazuki H
a86117f613 feat: add new line if the cursor is on a character when adding a tag (#2960) 2024-02-19 08:54:47 +08:00
Søm
fc1a2cf2fc chore: update ja.json (#2966) 2024-02-17 09:31:52 +08:00
Steven
d22b772232 chore: add memo actions to memo detail page 2024-02-15 11:16:51 +08:00
Steven
f1ec5775a7 chore: update inbox props 2024-02-14 09:44:35 +08:00
Bryan
4aa4417d91 chore: allow all 20x response status code in webhook (#2947) 2024-02-13 09:30:48 +08:00
Steven
606e574e19 chore: update enum type 2024-02-13 09:30:28 +08:00
Brilliant Hanabi
ebe3678288 feat: add visibility select in ShareMemoDialog (#2941)
In ShareMemoDialog, user can change the visibility
of the memo, so that the memo can be set to public
to be viewed by anyone with the link.
2024-02-13 09:28:16 +08:00
Steven
b3ca9969c4 chore: tweak linter 2024-02-09 22:18:55 +08:00
Steven
3dddd3ec4c chore: tweak reaction store 2024-02-09 21:59:45 +08:00
Brilliant Hanabi
81aa9b107f feat: add notice when sharing private links in MemoDetail (#2942) 2024-02-09 09:30:01 +08:00
Steven
60efd3ac32 chore: tweak memo view 2024-02-08 22:52:49 +08:00
Steven
4081a6f5ad chore: add more reactions 2024-02-08 21:20:51 +08:00
Lincoln Nogueira
334e489867 chore: improve docker-compose.dev (#2938) 2024-02-08 16:28:43 +08:00
Steven
c7822515a1 chore: tweak view checks 2024-02-08 13:37:38 +08:00
Steven
d86f0bac8c chore: implement reaction frontend 2024-02-08 13:25:15 +08:00
Steven
e5f244cb50 chore: fix tests 2024-02-08 11:58:23 +08:00
Steven
3a5bc82d39 chore: implement reaction service 2024-02-08 11:54:59 +08:00
Steven
a4fa67cd18 chore: update dependencies 2024-02-08 08:13:42 +08:00
Steven
43a2d6ce09 chore: tweak user setting 2024-02-08 08:06:55 +08:00
Mehad Nadeem
d2434111b4 chore: impl compact mode setting (#2935)
* chore: backend/DB related files for compact view setting.

* fix: passing lint errors

* fix2: passing linter errors
2024-02-08 08:05:56 +08:00
Steven
559e427c50 chore: implement reaction store 2024-02-07 23:40:23 +08:00
Steven
99568236a3 chore: run buf generate 2024-02-07 21:51:30 +08:00
Mehad Nadeem
06fb2174c3 feat: add compact mode setting (Proto) (#2934)
* chore: proto related files for compact view setting.

* fix: pasing lint errors
2024-02-07 21:45:11 +08:00
Kazuki H
5ac17fc012 fix: displaying archived memos (#2933)
* fix: web: Archived: Show displayTime instead of updateTime

Archiving a memo is considered "updating" it, so the time it was
archived will be displayed, instead of the time it was created.

* fix: web: Archived: Add an option to fetch more memos

Just like on other pages, add a button to fetch more memos. Otherwise,
the user would only be able to load the latest 10 memos, as defined in
DEFAULT_MEMO_LIMIT.
2024-02-07 17:53:23 +08:00
Steven
a76b86f18a chore: fix highlight code 2024-02-06 20:59:17 +08:00
Steven
ded8001735 chore: fix v2 routes 2024-02-06 20:55:27 +08:00
Steven
185ec2ad2a chore: update inbox service 2024-02-06 19:46:25 +08:00
Steven
6b59c7670c chore: fix linter warning 2024-02-05 23:32:01 +08:00
Steven
434ef44f8c chore: add cookie builder 2024-02-05 23:28:29 +08:00
Steven
46ea16ef7e chore: fix cookie attrs 2024-02-05 22:14:58 +08:00
Steven
8f15e8773a chore: update cookie attrs 2024-02-05 21:42:23 +08:00
Steven
25efc33b24 chore: tweak timeline styles 2024-02-05 21:37:43 +08:00
Steven
ba460382b0 chore: remove type-gen script 2024-02-05 20:42:01 +08:00
Steven
e35225ff24 chore: fix resource url 2024-02-05 19:35:23 +08:00
Steven
397a7f00ef chore: add postinstall script 2024-02-05 19:29:47 +08:00
Steven
06eff151e7 chore: tweak memo find builder 2024-02-05 06:40:55 +08:00
Steven
c30d7ab8f3 chore: update cors middleware 2024-02-05 06:10:10 +08:00
Steven
ab4a670bec chore: add env example 2024-02-05 00:33:07 +08:00
Steven
ce663efc14 chore: update cookie attrs 2024-02-05 00:11:36 +08:00
Steven
9e72432f19 chore: tweak cookie attrs 2024-02-05 00:10:54 +08:00
Steven
b056c59dea chore: add vercel.json 2024-02-05 00:01:31 +08:00
Steven
15c90871d9 chore: update request base url 2024-02-04 23:48:26 +08:00
Steven
be899cd027 chore: update eslint config 2024-02-04 22:52:47 +08:00
Steven
8773a3d2c1 chore: tweak assets folder 2024-02-04 22:45:51 +08:00
Steven
d2603ee67b chore: upgrade frontend dependencies 2024-02-04 21:36:11 +08:00
Steven
c92507728a chore: tweak filter checks 2024-02-04 21:07:14 +08:00
Steven
eb4f7b47b7 chore: update memo find builder 2024-02-04 20:54:17 +08:00
Steven
1e07b70d23 chore: fix export memos 2024-02-04 20:20:14 +08:00
Athurg Gooth
b8a9783db5 fix: signin error notification is not shown (#2908)
fix signin error notification is not shown
2024-02-04 14:25:51 +08:00
Ikko Eltociear Ashimine
82e72813f9 chore: fix typo in About.tsx (#2899) 2024-02-04 08:33:13 +08:00
Steven
57510ddee5 chore: update readme with used resources 2024-02-03 23:42:24 +08:00
Steven
00c47a0673 chore: fix menu z-index 2024-02-03 23:17:03 +08:00
Steven
374f3f7d96 chore: fix filter initial state 2024-02-03 22:03:56 +08:00
Steven
8340e6b247 chore: upgrade gomark 2024-02-03 21:52:54 +08:00
Steven
7f5148d490 chore: clean dropdown 2024-02-01 21:56:08 +08:00
Steven
c522e1450a chore: update codeblock style 2024-02-01 21:38:28 +08:00
Steven
c342c464a2 chore: update services comment 2024-02-01 21:26:09 +08:00
Steven
f6f193af2d chore: format proto 2024-02-01 21:15:56 +08:00
Steven
dd06278692 feat: add batch upsert tags 2024-02-01 21:13:42 +08:00
Steven
fdd17ce849 chore: update memo content key 2024-02-01 20:40:43 +08:00
Elliot Chen
7cd3fcbc61 fix: wrong order of the timeline in the resource page & add webhook when create memos using Telegram bot (#2886)
* fix: wrong order in resource page timeline

* feat: add webhook when create memos using Telegram bot

* rename variables and fix typos for static checks
2024-02-01 20:24:58 +08:00
Steven
e78311b3af fix: initial gomark wasm 2024-02-01 19:39:41 +08:00
Steven
e3afad74ce fix: update initial wasm 2024-02-01 19:27:54 +08:00
Steven
554f93eccc fix: move initial wasm into app 2024-02-01 19:06:44 +08:00
Steven
79227021f5 chore: bump version 2024-01-31 23:08:33 +08:00
Steven
b4f2a3bd14 chore: remove migrator 2024-01-31 22:58:43 +08:00
Steven
0b4914d880 chore: update generated node types 2024-01-31 22:42:51 +08:00
Steven
2f0b0e0071 chore: remove node definition 2024-01-31 22:32:09 +08:00
Steven
8ce6a32aac feat: use gomark wasm in frontend 2024-01-31 22:25:24 +08:00
Wen Sun
3158c4b8b5 fix: role error in api/v2 when the first user registers (#2875)
Fix role error in api/v2 when the first user registers
2024-01-31 19:55:52 +08:00
Steven
30ae4140f3 chore: update gomark source 2024-01-31 19:01:08 +08:00
Lincoln Nogueira
279cba0e6b chore: greatly speed up migrator and lower memory usage (#2874)
* chore: add en-GB language

* chore: remove en-GB contents

* chore: prevent visitors from breaking demo
- prevent disabling password login
- prevent updating `memos-demo` user
- prevent setting additional style
- prevent setting additional script
- add some error feedback to system settings UI

* Revert "chore: add en-GB language"

This reverts commit 2716377b04.

* chore: speed-up migrator and lower memory usage
- remove all Store indirections
- query database directly with prepared statements

* chore: fix golangci-lint warnings
2024-01-31 16:45:21 +08:00
Lincoln Nogueira
52539fc130 chore: prevent visitors from breaking demo (#2869)
* chore: add en-GB language

* chore: remove en-GB contents

* chore: prevent visitors from breaking demo
- prevent disabling password login
- prevent updating `memos-demo` user
- prevent setting additional style
- prevent setting additional script
- add some error feedback to system settings UI

* Revert "chore: add en-GB language"

This reverts commit 2716377b04.
2024-01-31 13:16:31 +08:00
Steven
49e3eb107c chore: update gomark wasm 2024-01-31 00:25:01 +08:00
Lincoln Nogueira
e7d5dfe515 chore: add en-GB language (#2865)
* chore: add en-GB language

* chore: remove en-GB contents
2024-01-30 23:39:13 +08:00
Steven
28c7a75ea2 chore: fix import nodes 2024-01-30 22:15:05 +08:00
Steven
59d69a05fa feat: initial gomark wasm importer 2024-01-30 22:12:44 +08:00
Steven
ad2d492dec chore: move preview memo content 2024-01-30 22:10:17 +08:00
Elliot Chen
bee6f278ba fix: the same-storage check in the new pre-sign feature (#2860)
* fix: error check for the same oss-storage

* fix: conflict error2 variable in code refactor in s3.go

* chore: rename endpointUrl to endpointURL
2024-01-30 19:07:16 +08:00
Brilliant Hanabi
1bad0543d0 feat: add notice when sharing private links (#2809)
Co-authored-by: boojack <stevenlgtm@gmail.com>
2024-01-30 19:06:30 +08:00
Noah Alderton
73337331cb feat: export all user Memos as a .zip of Markdown files (#2854)
* Add gRPC Memos Export

* Update code style

* Add URL.revokeObjectURL

* Rename protobuf and ESLint fix

* Change MemosExport to ExportMemos
2024-01-30 16:42:54 +08:00
Wen Sun
50f7f131ea fix: month grouping error in timeline page (#2861) 2024-01-30 07:56:03 +08:00
Steven
a16bde23f7 chore: tweak variable name 2024-01-29 23:15:47 +08:00
Steven
c5a5f67fdb refactor: migrate auth service 2024-01-29 23:12:02 +08:00
Steven
de8db63811 chore: rename workspace setting service 2024-01-29 22:43:40 +08:00
Steven
dd9ee44a1f docs: regenerate swagger 2024-01-29 22:05:33 +08:00
Aleksandr Baryshnikov
fa17dce046 feat: pre-signed URL for S3 storage (#2855)
Adds automatically background refresh of all external links if they are belongs to the current blob (S3) storage. The feature is disabled by default in order to keep backward compatibility.

The background go-routine spawns once during startup and periodically signs and updates external links if that links belongs to current S3 storage.

The original idea was to sign external links on-demand, however, with current architecture it will require duplicated code in plenty of places. If do it, the changes will be quite invasive and in the end pointless: I believe, the architecture will be eventually updated to give more scalable way for pluggable storage. For example - Upload/Download interface without hard dependency on external link. There are stubs already, but I don't feel confident enough to change significant part of the application architecture.
2024-01-29 21:12:29 +08:00
Steven
cbcec80c5d chore: fix import order 2024-01-29 21:08:30 +08:00
Steven
2b7bd47b44 fix: rss routes 2024-01-29 21:04:35 +08:00
Steven
54c5039db3 chore: fix golang linter 2024-01-29 19:17:25 +08:00
Steven
af646ce2de refactor: move gomark 2024-01-29 19:14:46 +08:00
Steven
f4ac7ff529 chore: update memo resource url 2024-01-28 23:02:38 +08:00
Steven
55ecdae509 chore: fix auto link matcher 2024-01-28 22:13:19 +08:00
Steven
ef73299340 chore: update resource name migrator 2024-01-28 21:40:24 +08:00
Steven
8c6292925e chore: update code block styles 2024-01-28 15:41:11 +08:00
Steven
f05a89315c chore: fix list memos 2024-01-28 08:38:29 +08:00
Steven
a4452d8a2f chore: update linter rules 2024-01-28 08:17:11 +08:00
Steven
5e74394643 chore: add resource name migrator 2024-01-28 07:58:53 +08:00
Steven
f4e722c516 chore: remove latest tag 2024-01-28 07:46:08 +08:00
Steven
12275c6a34 chore: fix linter warning 2024-01-28 07:38:01 +08:00
Steven
21ef5a9bc0 chore: tweak workspace service 2024-01-28 07:35:42 +08:00
Steven
87b23940a6 chore: upgrade backend dependencies 2024-01-28 07:13:11 +08:00
Steven
11dd23f59b chore: tweak link checks 2024-01-28 07:04:35 +08:00
Lincoln Nogueira
887903b66b feat: add buf plugin to generate openapiv2 spec (#2843) 2024-01-28 06:16:37 +08:00
Steven
309fab222e chore: implement nested blockquote 2024-01-27 21:38:07 +08:00
Steven
1dc4f02b64 chore: update memo requests 2024-01-27 20:35:48 +08:00
Steven
8db90a040c chore: remove unused dependencies 2024-01-27 20:20:36 +08:00
Steven
932f636d84 chore: update codeblock renderer 2024-01-27 19:09:10 +08:00
Steven
ed32b20c9e chore: update frontend dependencies 2024-01-27 17:28:06 +08:00
Steven
10d709c167 chore: fix highlight cursor 2024-01-27 12:35:01 +08:00
Steven
8455114eef chore: fix list memos request 2024-01-27 11:17:59 +08:00
Steven
c26109cd36 chore: update list memos request 2024-01-27 11:14:17 +08:00
Steven
4b223c1e4c chore: update collapse sidebar 2024-01-27 05:27:42 +08:00
Steven
b9cbe6626f chore: update tag rename 2024-01-27 05:26:32 +08:00
Steven
566171783d chore: tweak embedded memo style 2024-01-26 23:33:40 +08:00
Steven
7edb3598ea chore: update default request limit 2024-01-26 23:03:10 +08:00
Steven
bc2d2d0cde feat: support set embedded content in UI 2024-01-26 22:51:57 +08:00
Steven
e1977df14b chore: remove check underscores 2024-01-26 21:23:36 +08:00
Wei Zhang
ddc89029b7 fix: use mysql to parse dsn (#2838)
Signed-off-by: Zhang Wei <kweizh@gmail.com>
2024-01-26 13:43:48 +08:00
Steven
f8b9a83d4a chore: tweak default value 2024-01-26 09:22:47 +08:00
Steven
2f16b7065a chore: tweak scripts 2024-01-26 09:15:27 +08:00
Steven
e5ff1829a5 chore: add Hungarian locale 2024-01-26 08:44:12 +08:00
Steven
d7889d9903 chore: tweak url filters 2024-01-26 08:39:53 +08:00
Steven
db3457e081 chore: bump version 2024-01-26 08:30:22 +08:00
Steven
4f2b00b4f3 chore: add migration scripts 2024-01-26 08:29:11 +08:00
Steven
79558028c0 feat: implement rename tag 2024-01-25 23:09:35 +08:00
Steven
70d1301dc3 chore: use filter in url params 2024-01-25 20:05:47 +08:00
Steven
6d5e1def76 chore: update member section 2024-01-25 19:49:39 +08:00
boojack
a5bc2d0ed6 chore: update i18n from Weblate (#2832)
* Added translation using Weblate (Hungarian)

* Translated using Weblate (Hungarian)

Currently translated at 0.3% (1 of 317 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/hu/

* Translated using Weblate (Hungarian)

Currently translated at 71.2% (226 of 317 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/hu/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (316 of 316 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (316 of 316 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hant/

---------

Co-authored-by: Vermunds <com.github@weylus.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
Co-authored-by: ti777777 <ti12272198686@yahoo.com.tw>
2024-01-25 19:36:02 +08:00
Steven
08ac60cc70 chore: update memo relation dialog 2024-01-25 08:59:10 +08:00
Mudkip
f654d3c90e fix: encode filename when using url prefix for resources (#2829)
* fix: encode filename when using url prefix for resources

* fix: only encode the last parts of filename

* fix: encode all parts in filepath
2024-01-24 11:28:26 +08:00
Steven
1b69b73eb9 chore: update calendar styles 2024-01-23 23:15:54 +08:00
Steven
3dbb254aeb chore: update referenced memo title from params 2024-01-23 21:54:15 +08:00
Steven
fdb1779a59 chore: implement referenced content renderer 2024-01-23 21:47:17 +08:00
Steven
a316e239ce chore: implement referenced content node 2024-01-23 21:40:59 +08:00
Steven
d7f02b94e5 chore: fix linter 2024-01-23 21:27:05 +08:00
Steven
d165d87288 refactor: markdown parser matchers 2024-01-23 21:23:40 +08:00
Steven
bf905bba86 chore: remove unused date picker 2024-01-22 22:50:27 +08:00
Brilliant Hanabi
3a129d5cfb fix: avoid making memos public when disabled (#2816)
* fix: avoid making memos public when disabled in v2

* fix: avoid making memos public when disabled in v1
2024-01-22 20:51:33 +08:00
ti777777
024a818e91 chore: update zh-Hant (#2815) 2024-01-21 23:50:44 +08:00
Steven
54a24833a7 chore: fix resource seeds 2024-01-21 21:36:31 +08:00
Steven
a620d140c5 chore: update embedded content renderers 2024-01-21 21:27:04 +08:00
Steven
370054e040 chore: implement collapsed navigation 2024-01-21 21:02:55 +08:00
Steven
fae0b4e900 chore: update memo relation style 2024-01-21 11:22:25 +08:00
Steven
c38404b5d5 chore: tweak get memo by name 2024-01-21 10:57:53 +08:00
Steven
4d48f50815 chore: use resource name in frontend 2024-01-21 10:55:49 +08:00
Steven
582cc6609c feat: add user-defined id to resource 2024-01-21 10:49:30 +08:00
Steven
40bd75c725 fix: create memo with resource name 2024-01-21 10:33:31 +08:00
Steven
b2fc3076f6 chore: update memo store 2024-01-21 01:23:55 +08:00
Steven
288527914b chore: migrate memo route 2024-01-21 01:23:44 +08:00
Steven
a2aea3747c chore: remove server tests 2024-01-20 23:51:24 +08:00
Steven
8382354ef7 feat: add user-defined name to memo 2024-01-20 23:48:35 +08:00
Steven
264e6e6e9c chore: tweak file name 2024-01-20 23:47:04 +08:00
Steven
eb72609ea3 chore: update memo editor cache key 2024-01-20 20:52:11 +08:00
Brilliant Hanabi
776785ac90 chore: update zh-Hans & zh-Hant translations (#2804) 2024-01-20 20:27:20 +08:00
Steven
d5f874e185 chore: handle resource not found 2024-01-20 12:47:43 +08:00
Steven
89d940d9b7 feat: implement params field for embedded content node 2024-01-20 12:41:08 +08:00
Mark Zhao
bd1cf62761 feat: enable iframe rendering in markdown code block (#2799)
* enable iframe rendering in code block

* fix eslint issue
2024-01-20 11:36:45 +08:00
Steven
196facfacd feat: implement embedded resource renderer 2024-01-20 09:17:31 +08:00
Steven
afe75fd9f2 chore: fix tokens split tests 2024-01-20 02:09:33 +08:00
Steven
8a34013558 feat: implement embedded memo renderer 2024-01-20 01:56:10 +08:00
Steven
67f5ac3657 feat: implement subscript and superscript renderer 2024-01-19 23:10:16 +08:00
Steven
7236552b6c feat: implement subscript and superscript parsers 2024-01-19 23:06:22 +08:00
Steven
1f5899d238 chore: update dependencies 2024-01-19 19:20:59 +08:00
Wen Sun
ec4884ea04 fix: incorrect timeline month display (#2792)
Fix incorrect timeline month display
2024-01-19 18:07:08 +08:00
Elliot Chen
2e0619b4dc feat: add a webhook action for deleted memos (#2791) 2024-01-19 09:56:00 +08:00
Steven
c9146bc749 chore: update code style 2024-01-19 07:06:28 +08:00
Steven
f5b5bd64bc chore: tweak datetime.ts 2024-01-18 22:18:56 +08:00
Steven
d31d9eb71c chore: remove unused nil checks 2024-01-18 19:23:45 +08:00
Steven
f28b654057 chore: fix setting nil checks 2024-01-18 19:20:48 +08:00
Steven
8738b68a44 chore: tweak readme 2024-01-18 19:06:49 +08:00
Steven
42381fa154 chore: add stable build action 2024-01-18 18:49:06 +08:00
409 changed files with 24306 additions and 18461 deletions

View File

@@ -19,12 +19,12 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: 1.22
check-latest: true
cache: true
- name: Verify go.mod is tidy
run: |
go mod tidy -go=1.21
go mod tidy -go=1.22
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
@@ -39,7 +39,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: 1.22
check-latest: true
cache: true
- name: Run all tests

View File

@@ -52,7 +52,6 @@ jobs:
neosmemo/memos
ghcr.io/usememos/memos
tags: |
type=raw,value=latest
type=semver,pattern={{version}},value=${{ env.VERSION }}
type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }}

View File

@@ -0,0 +1,61 @@
name: build-and-push-stable-image
on:
push:
branches:
- "stable"
jobs:
build-and-push-release-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: neosmemo
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
install: true
version: v0.9.1
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
neosmemo/memos
ghcr.io/usememos/memos
tags: |
type=raw,value=stable
flavor: |
latest=true
- name: Build and Push
id: docker_build
uses: docker/build-push-action@v5
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -25,8 +25,6 @@ jobs:
cache-dependency-path: "web/pnpm-lock.yaml"
- run: pnpm install
working-directory: web
- run: pnpm type-gen
working-directory: web
- name: Run eslint check
run: pnpm lint
working-directory: web
@@ -45,8 +43,6 @@ jobs:
cache-dependency-path: "web/pnpm-lock.yaml"
- run: pnpm install
working-directory: web
- run: pnpm type-gen
working-directory: web
- name: Run frontend build
run: pnpm build
working-directory: web

View File

@@ -73,6 +73,8 @@ linters-settings:
disabled: true
- name: unhandled-error
disabled: true
- name: if-return
disabled: true
gocritic:
disabled-checks:
- ifElseChain

View File

@@ -6,12 +6,12 @@ COPY . .
WORKDIR /frontend-build/web
RUN corepack enable && pnpm i --frozen-lockfile && pnpm type-gen
RUN corepack enable && pnpm i --frozen-lockfile
RUN pnpm build
# Build backend exec file.
FROM golang:1.21-alpine AS backend
FROM golang:1.22-alpine AS backend
WORKDIR /backend-build
COPY . .

View File

@@ -8,13 +8,12 @@ A privacy-first, lightweight note-taking service. Easily capture and share your
<a href="https://demo.usememos.com/">Live Demo</a>
<p>
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos?logo=github" /></a>
<a href="https://hub.docker.com/r/neosmemo/memos"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg"/></a>
<a href="https://hosted.weblate.org/engage/memos-i18n/"><img src="https://hosted.weblate.org/widget/memos-i18n/english/svg-badge.svg" alt="Translation status" /></a>
<a href="https://discord.gg/tfPJa4UmAv"><img alt="Discord" src="https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5" /></a>
</p>
![demo](https://www.usememos.com/demo.webp)
![demo](https://www.usememos.com/demo.png)
## Key points
@@ -27,7 +26,7 @@ A privacy-first, lightweight note-taking service. Easily capture and share your
## Deploy with Docker in seconds
```bash
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos ghcr.io/usememos/memos:latest
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:stable
```
> The `~/.memos/` directory will be used as the data directory on your local machine, while `/var/opt/memos` is the directory of the volume in Docker and should not be modified.
@@ -42,21 +41,11 @@ Contributions are what make the open-source community such an amazing place to l
<img src="https://contri-graphy.yourselfhosted.com/graph?repo=usememos/memos&format=svg" />
</a>
---
- [Moe Memos](https://memos.moe/) - Third party client for iOS and Android
- [lmm214/memos-bber](https://github.com/lmm214/memos-bber) - Chrome extension
- [Rabithua/memos_wmp](https://github.com/Rabithua/memos_wmp) - WeChat MiniProgram
- [qazxcdswe123/telegramMemoBot](https://github.com/qazxcdswe123/telegramMemoBot) - Telegram bot
- [eallion/memos.top](https://github.com/eallion/memos.top) - Static page rendered with the Memos API
- [eindex/logseq-memos-sync](https://github.com/EINDEX/logseq-memos-sync) - Logseq plugin
- [quanru/obsidian-periodic-para](https://github.com/quanru/obsidian-periodic-para#daily-record) - Obsidian plugin
- [JakeLaoyu/memos-import-from-flomo](https://github.com/JakeLaoyu/memos-import-from-flomo) - Import data. Support from flomo, wechat reading
- [Quick Memo](https://www.icloud.com/shortcuts/1eaef307112843ed9f91d256f5ee7ad9) - Shortcuts (iOS, iPadOS or macOS)
- [Memos Raycast Extension](https://www.raycast.com/JakeYu/memos) - Raycast extension
- [Memos Desktop](https://github.com/xudaolong/memos-desktop) - Third party client for MacOS and Windows
- [MemosGallery](https://github.com/BarryYangi/MemosGallery) - A static Gallery rendered with the Memos API
## Star history
[![Star History Chart](https://api.star-history.com/svg?repos=usememos/memos&type=Date)](https://star-history.com/#usememos/memos&Date)
## Other projects
- [**Slash**](https://github.com/yourselfhosted/slash): An open source, self-hosted bookmarks and link sharing platform. Save and share your links very easily.
- [**Gomark**](https://github.com/yourselfhosted/gomark): A markdown parser written in Go for Memos. And its [WebAssembly version](https://github.com/yourselfhosted/gomark-wasm) is also available.

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
)
const (

View File

@@ -30,39 +30,35 @@ const (
thumbnailImagePath = ".thumbnail_cache"
)
type Service struct {
type ResourceService struct {
Profile *profile.Profile
Store *store.Store
}
func NewService(profile *profile.Profile, store *store.Store) *Service {
return &Service{
func NewResourceService(profile *profile.Profile, store *store.Store) *ResourceService {
return &ResourceService{
Profile: profile,
Store: store,
}
}
func (s *Service) RegisterResourcePublicRoutes(g *echo.Group) {
g.GET("/r/:resourceId", s.streamResource)
g.GET("/r/:resourceId/*", s.streamResource)
func (s *ResourceService) RegisterRoutes(g *echo.Group) {
g.GET("/r/:resourceName", s.streamResource)
g.GET("/r/:resourceName/*", s.streamResource)
}
func (s *Service) streamResource(c echo.Context) error {
func (s *ResourceService) streamResource(c echo.Context) error {
ctx := c.Request().Context()
resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resourceName := c.Param("resourceName")
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID,
GetBlob: true,
ResourceName: &resourceName,
GetBlob: true,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by id: %s", resourceName)).SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %s", resourceName))
}
// Check the related memo visibility.
if resource.MemoID != nil {

169
api/rss/rss.go Normal file
View File

@@ -0,0 +1,169 @@
package rss
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/feeds"
"github.com/labstack/echo/v4"
"github.com/yourselfhosted/gomark"
"github.com/yourselfhosted/gomark/ast"
"github.com/yourselfhosted/gomark/renderer"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
)
const (
maxRSSItemCount = 100
maxRSSItemTitleLength = 128
)
type RSSService struct {
Profile *profile.Profile
Store *store.Store
}
func NewRSSService(profile *profile.Profile, store *store.Store) *RSSService {
return &RSSService{
Profile: profile,
Store: store,
}
}
func (s *RSSService) RegisterRoutes(g *echo.Group) {
g.GET("/explore/rss.xml", s.GetExploreRSS)
g.GET("/u/:username/rss.xml", s.GetUserRSS)
}
func (s *RSSService) GetExploreRSS(c echo.Context) error {
ctx := c.Request().Context()
normalStatus := store.Normal
memoFind := store.FindMemo{
RowStatus: &normalStatus,
VisibilityList: []store.Visibility{store.Public},
}
memoList, err := s.Store.ListMemos(ctx, &memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
baseURL := c.Scheme() + "://" + c.Request().Host
rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
return c.String(http.StatusOK, rss)
}
func (s *RSSService) GetUserRSS(c echo.Context) error {
ctx := c.Request().Context()
username := c.Param("username")
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &username,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
normalStatus := store.Normal
memoFind := store.FindMemo{
CreatorID: &user.ID,
RowStatus: &normalStatus,
VisibilityList: []store.Visibility{store.Public},
}
memoList, err := s.Store.ListMemos(ctx, &memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
baseURL := c.Scheme() + "://" + c.Request().Host
rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
return c.String(http.StatusOK, rss)
}
func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string) (string, error) {
feed := &feeds.Feed{
Title: "Memos",
Link: &feeds.Link{Href: baseURL},
Description: "An open source, lightweight note-taking service. Easily capture and share your great thoughts.",
Created: time.Now(),
}
var itemCountLimit = util.Min(len(memoList), maxRSSItemCount)
feed.Items = make([]*feeds.Item, itemCountLimit)
for i := 0; i < itemCountLimit; i++ {
memo := memoList[i]
description, err := getRSSItemDescription(memo.Content)
if err != nil {
return "", err
}
feed.Items[i] = &feeds.Item{
Title: getRSSItemTitle(memo.Content),
Link: &feeds.Link{Href: baseURL + "/m/" + memo.ResourceName},
Description: description,
Created: time.Unix(memo.CreatedTs, 0),
}
resources, err := s.Store.ListResources(ctx, &store.FindResource{
MemoID: &memo.ID,
})
if err != nil {
return "", err
}
if len(resources) > 0 {
resource := resources[0]
enclosure := feeds.Enclosure{}
if resource.ExternalLink != "" {
enclosure.Url = resource.ExternalLink
} else {
enclosure.Url = baseURL + "/o/r/" + resource.ResourceName
}
enclosure.Length = strconv.Itoa(int(resource.Size))
enclosure.Type = resource.Type
feed.Items[i].Enclosure = &enclosure
}
}
rss, err := feed.ToRss()
if err != nil {
return "", err
}
return rss, nil
}
func getRSSItemTitle(content string) string {
nodes, _ := gomark.Parse(content)
if len(nodes) > 0 {
firstNode := nodes[0]
title := renderer.NewStringRenderer().Render([]ast.Node{firstNode})
return title
}
title := strings.Split(content, "\n")[0]
var titleLengthLimit = util.Min(len(title), maxRSSItemTitleLength)
if titleLengthLimit < len(title) {
title = title[:titleLengthLimit] + "..."
}
return title
}
func getRSSItemDescription(content string) (string, error) {
nodes, err := gomark.Parse(content)
if err != nil {
return "", err
}
result := renderer.NewHTMLRenderer().Render(nodes)
return result, nil
}

View File

@@ -21,10 +21,6 @@ import (
"github.com/usememos/memos/store"
)
var (
usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$")
)
type SignIn struct {
Username string `json:"username"`
Password string `json:"password"`
@@ -64,25 +60,18 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
// @Router /api/v1/auth/signin [POST]
func (s *APIV1Service) SignIn(c echo.Context) error {
ctx := c.Request().Context()
signin := &SignIn{}
disablePasswordLoginSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingDisablePasswordLoginName.String(),
})
workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
if disablePasswordLoginSystemSetting != nil {
disablePasswordLogin := false
err = json.Unmarshal([]byte(disablePasswordLoginSystemSetting.Value), &disablePasswordLogin)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
}
if disablePasswordLogin {
return echo.NewHTTPError(http.StatusUnauthorized, "Password login is deactivated")
}
if workspaceGeneralSetting.DisallowSignup {
return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
}
if workspaceGeneralSetting.DisallowPasswordLogin {
return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").SetInternal(err)
}
signin := &SignIn{}
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
}
@@ -190,21 +179,11 @@ func (s *APIV1Service) SignInSSO(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
}
if user == nil {
allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingAllowSignUpName.String(),
})
workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
allowSignUpSettingValue := true
if allowSignUpSetting != nil {
err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
}
}
if !allowSignUpSettingValue {
if workspaceGeneralSetting.DisallowSignup {
return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
}
@@ -293,7 +272,7 @@ func (s *APIV1Service) SignUp(c echo.Context) error {
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
}
if !usernameMatcher.MatchString(strings.ToLower(signup.Username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(signup.Username)) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err)
}
@@ -307,39 +286,15 @@ func (s *APIV1Service) SignUp(c echo.Context) error {
// Change the default role to host if there is no host user.
userCreate.Role = store.RoleHost
} else {
allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingAllowSignUpName.String(),
})
workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
allowSignUpSettingValue := true
if allowSignUpSetting != nil {
err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
}
}
if !allowSignUpSettingValue {
if workspaceGeneralSetting.DisallowSignup {
return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
}
disablePasswordLoginSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingDisablePasswordLoginName.String(),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
if disablePasswordLoginSystemSetting != nil {
disablePasswordLogin := false
err = json.Unmarshal([]byte(disablePasswordLoginSystemSetting.Value), &disablePasswordLogin)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
}
if disablePasswordLogin {
return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated")
}
if workspaceGeneralSetting.DisallowPasswordLogin {
return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").SetInternal(err)
}
}

View File

@@ -199,7 +199,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProvider"
"$ref": "#/definitions/api_v1.IdentityProvider"
}
}
},
@@ -226,7 +226,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/github_com_usememos_memos_api_v1.CreateIdentityProviderRequest"
"$ref": "#/definitions/api_v1.CreateIdentityProviderRequest"
}
}
],
@@ -354,7 +354,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest"
"$ref": "#/definitions/api_v1.UpdateIdentityProviderRequest"
}
}
],
@@ -838,7 +838,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.UpsertMemoRelationRequest"
"$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest"
}
}
],
@@ -992,7 +992,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/github_com_usememos_memos_api_v1.CreateResourceRequest"
"$ref": "#/definitions/api_v1.CreateResourceRequest"
}
}
],
@@ -1116,7 +1116,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/github_com_usememos_memos_api_v1.UpdateResourceRequest"
"$ref": "#/definitions/api_v1.UpdateResourceRequest"
}
}
],
@@ -1155,7 +1155,7 @@ const docTemplate = `{
"200": {
"description": "System GetSystemStatus",
"schema": {
"$ref": "#/definitions/github_com_usememos_memos_api_v1.SystemStatus"
"$ref": "#/definitions/api_v1.SystemStatus"
}
},
"401": {
@@ -1366,12 +1366,6 @@ const docTemplate = `{
}
],
"responses": {
"200": {
"description": "Created system setting",
"schema": {
"$ref": "#/definitions/store.SystemSetting"
}
},
"400": {
"description": "Malformatted post system setting request | invalid system setting"
},
@@ -1592,7 +1586,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/github_com_usememos_memos_api_v1.CreateUserRequest"
"$ref": "#/definitions/api_v1.CreateUserRequest"
}
}
],
@@ -1773,7 +1767,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/github_com_usememos_memos_api_v1.UpdateUserRequest"
"$ref": "#/definitions/api_v1.UpdateUserRequest"
}
}
],
@@ -1799,25 +1793,6 @@ const docTemplate = `{
}
}
},
"/explore/rss.xml": {
"get": {
"produces": [
"text/xml"
],
"tags": [
"rss"
],
"summary": "Get RSS",
"responses": {
"200": {
"description": "RSS"
},
"500": {
"description": "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
}
}
}
},
"/o/get/GetImage": {
"get": {
"produces": [
@@ -1848,37 +1823,6 @@ const docTemplate = `{
}
}
}
},
"/u/{id}/rss.xml": {
"get": {
"produces": [
"text/xml"
],
"tags": [
"rss"
],
"summary": "Get RSS for a user",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "RSS"
},
"400": {
"description": "User id is not a number"
},
"500": {
"description": "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
}
}
}
}
},
"definitions": {
@@ -1990,10 +1934,6 @@ const docTemplate = `{
"description": "Description is the server description.",
"type": "string"
},
"externalUrl": {
"description": "ExternalURL is the external url of server. e.g. https://usermemos.com",
"type": "string"
},
"locale": {
"description": "Locale is the server default locale.",
"type": "string"
@@ -2228,6 +2168,9 @@ const docTemplate = `{
"path": {
"type": "string"
},
"presign": {
"type": "boolean"
},
"region": {
"type": "string"
},
@@ -2271,34 +2214,24 @@ const docTemplate = `{
"enum": [
"server-id",
"secret-session",
"allow-signup",
"disable-password-login",
"disable-public-memos",
"max-upload-size-mib",
"additional-style",
"additional-script",
"customized-profile",
"storage-service-id",
"local-storage-path",
"telegram-bot-token",
"memo-display-with-updated-ts",
"instance-url"
"memo-display-with-updated-ts"
],
"x-enum-varnames": [
"SystemSettingServerIDName",
"SystemSettingSecretSessionName",
"SystemSettingAllowSignUpName",
"SystemSettingDisablePasswordLoginName",
"SystemSettingDisablePublicMemosName",
"SystemSettingMaxUploadSizeMiBName",
"SystemSettingAdditionalStyleName",
"SystemSettingAdditionalScriptName",
"SystemSettingCustomizedProfileName",
"SystemSettingStorageServiceIDName",
"SystemSettingLocalStoragePathName",
"SystemSettingTelegramBotTokenName",
"SystemSettingMemoDisplayWithUpdatedTsName",
"SystemSettingInstanceURLName"
"SystemSettingMemoDisplayWithUpdatedTsName"
]
},
"api_v1.SystemStatus": {
@@ -2621,10 +2554,6 @@ const docTemplate = `{
"description": "Description is the server description.",
"type": "string"
},
"externalUrl": {
"description": "ExternalURL is the external url of server. e.g. https://usermemos.com",
"type": "string"
},
"locale": {
"description": "Locale is the server default locale.",
"type": "string"
@@ -2859,6 +2788,9 @@ const docTemplate = `{
"path": {
"type": "string"
},
"presign": {
"type": "boolean"
},
"region": {
"type": "string"
},
@@ -2902,34 +2834,24 @@ const docTemplate = `{
"enum": [
"server-id",
"secret-session",
"allow-signup",
"disable-password-login",
"disable-public-memos",
"max-upload-size-mib",
"additional-style",
"additional-script",
"customized-profile",
"storage-service-id",
"local-storage-path",
"telegram-bot-token",
"memo-display-with-updated-ts",
"instance-url"
"memo-display-with-updated-ts"
],
"x-enum-varnames": [
"SystemSettingServerIDName",
"SystemSettingSecretSessionName",
"SystemSettingAllowSignUpName",
"SystemSettingDisablePasswordLoginName",
"SystemSettingDisablePublicMemosName",
"SystemSettingMaxUploadSizeMiBName",
"SystemSettingAdditionalStyleName",
"SystemSettingAdditionalScriptName",
"SystemSettingCustomizedProfileName",
"SystemSettingStorageServiceIDName",
"SystemSettingLocalStoragePathName",
"SystemSettingTelegramBotTokenName",
"SystemSettingMemoDisplayWithUpdatedTsName",
"SystemSettingInstanceURLName"
"SystemSettingMemoDisplayWithUpdatedTsName"
]
},
"github_com_usememos_memos_api_v1.SystemStatus": {
@@ -3253,10 +3175,16 @@ const docTemplate = `{
"id": {
"type": "integer"
},
"parentID": {
"type": "integer"
},
"pinned": {
"description": "Composed fields",
"type": "boolean"
},
"resourceName": {
"type": "string"
},
"rowStatus": {
"description": "Standard fields",
"allOf": [
@@ -3330,6 +3258,9 @@ const docTemplate = `{
"memoID": {
"type": "integer"
},
"resourceName": {
"type": "string"
},
"size": {
"type": "integer"
},
@@ -3382,20 +3313,6 @@ const docTemplate = `{
}
}
},
"store.SystemSetting": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"store.User": {
"type": "object",
"properties": {

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"strings"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"go.uber.org/zap"

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/labstack/echo/v4"
"github.com/lithammer/shortuuid/v4"
"github.com/pkg/errors"
"go.uber.org/zap"
@@ -16,7 +17,6 @@ import (
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/webhook"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store"
)
@@ -45,7 +45,8 @@ func (v Visibility) String() string {
}
type Memo struct {
ID int32 `json:"id"`
ID int32 `json:"id"`
Name string `json:"name"`
// Standard fields
RowStatus RowStatus `json:"rowStatus"`
@@ -275,7 +276,7 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
}
// Find disable public memos system setting.
disablePublicMemosSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: SystemSettingDisablePublicMemosName.String(),
})
if err != nil {
@@ -349,7 +350,6 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
metric.Enqueue("memo comment create")
if _, err := s.Store.CreateInbox(ctx, &store.Inbox{
SenderID: memo.CreatorID,
ReceiverID: relatedMemo.CreatorID,
@@ -408,7 +408,6 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
log.Warn("Failed to dispatch memo created webhook", zap.Error(err))
}
metric.Enqueue("memo create")
return c.JSON(http.StatusOK, memoResponse)
}
@@ -625,6 +624,13 @@ func (s *APIV1Service) DeleteMemo(c echo.Context) error {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil {
// Try to dispatch webhook when memo is deleted.
if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil {
log.Warn("Failed to dispatch memo deleted webhook", zap.Error(err))
}
}
if err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{
ID: memoID,
}); err != nil {
@@ -704,6 +710,36 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error {
if patchMemoRequest.Visibility != nil {
visibility := store.Visibility(patchMemoRequest.Visibility.String())
updateMemoMessage.Visibility = &visibility
// Find disable public memos system setting.
disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: SystemSettingDisablePublicMemosName.String(),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
if disablePublicMemosSystemSetting != nil {
disablePublicMemos := false
err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
}
if disablePublicMemos {
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
// Enforce normal user to save as private memo if public memos are disabled.
if user.Role == store.RoleUser {
visibility = store.Visibility("PRIVATE")
updateMemoMessage.Visibility = &visibility
}
}
}
}
err = s.Store.UpdateMemo(ctx, updateMemoMessage)
@@ -794,6 +830,7 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error {
func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*Memo, error) {
memoMessage := &Memo{
ID: memo.ID,
Name: memo.ResourceName,
RowStatus: RowStatus(memo.RowStatus.String()),
CreatorID: memo.CreatorID,
CreatedTs: memo.CreatedTs,
@@ -865,7 +902,7 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
}
func (s *APIV1Service) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) {
memoDisplayWithUpdatedTsSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
memoDisplayWithUpdatedTsSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: SystemSettingMemoDisplayWithUpdatedTsName.String(),
})
if err != nil {
@@ -887,10 +924,11 @@ func convertCreateMemoRequestToMemoMessage(memoCreate *CreateMemoRequest) *store
createdTs = *memoCreate.CreatedTs
}
return &store.Memo{
CreatorID: memoCreate.CreatorID,
CreatedTs: createdTs,
Content: memoCreate.Content,
Visibility: store.Visibility(memoCreate.Visibility),
ResourceName: shortuuid.New(),
CreatorID: memoCreate.CreatorID,
CreatedTs: createdTs,
Content: memoCreate.Content,
Visibility: store.Visibility(memoCreate.Visibility),
}
}
@@ -958,6 +996,11 @@ func (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *Mem
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated")
}
// DispatchMemoDeletedWebhook dispatches webhook when memo is deletedd.
func (s *APIV1Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *Memo) error {
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted")
}
func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *Memo, activityType string) error {
webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
CreatorID: &memo.CreatorID,
@@ -965,7 +1008,6 @@ func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *Mem
if err != nil {
return err
}
metric.Enqueue("webhook dispatch")
for _, hook := range webhooks {
payload := convertMemoToWebhookPayload(memo)
payload.ActivityType = activityType

View File

@@ -15,18 +15,19 @@ import (
"time"
"github.com/labstack/echo/v4"
"github.com/lithammer/shortuuid/v4"
"github.com/pkg/errors"
"go.uber.org/zap"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/storage/s3"
"github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store"
)
type Resource struct {
ID int32 `json:"id"`
ID int32 `json:"id"`
Name string `json:"name"`
// Standard fields
CreatorID int32 `json:"creatorId"`
@@ -139,6 +140,7 @@ func (s *APIV1Service) CreateResource(c echo.Context) error {
}
create := &store.Resource{
ResourceName: shortuuid.New(),
CreatorID: userID,
Filename: request.Filename,
ExternalLink: request.ExternalLink,
@@ -159,7 +161,6 @@ func (s *APIV1Service) CreateResource(c echo.Context) error {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
}
metric.Enqueue("resource create")
return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}
@@ -182,14 +183,21 @@ func (s *APIV1Service) UploadResource(c echo.Context) error {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
// This is the backend default max upload size limit.
maxUploadSetting := s.Store.GetSystemSettingValueWithDefault(ctx, SystemSettingMaxUploadSizeMiBName.String(), "32")
maxUploadSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingMaxUploadSizeMiBName.String()})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get max upload size").SetInternal(err)
}
var settingMaxUploadSizeBytes int
if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting); err == nil {
settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte
if maxUploadSetting != nil {
if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting.Value); err == nil {
settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte
} else {
log.Warn("Failed to parse max upload size", zap.Error(err))
settingMaxUploadSizeBytes = 0
}
} else {
log.Warn("Failed to parse max upload size", zap.Error(err))
settingMaxUploadSizeBytes = 0
// Default to 32 MiB.
settingMaxUploadSizeBytes = 32 * MebiByte
}
file, err := c.FormFile("file")
@@ -215,10 +223,11 @@ func (s *APIV1Service) UploadResource(c echo.Context) error {
defer sourceFile.Close()
create := &store.Resource{
CreatorID: userID,
Filename: file.Filename,
Type: file.Header.Get("Content-Type"),
Size: file.Size,
ResourceName: shortuuid.New(),
CreatorID: userID,
Filename: file.Filename,
Type: file.Header.Get("Content-Type"),
Size: file.Size,
}
err = SaveResourceBlob(ctx, s.Store, create, sourceFile)
if err != nil {
@@ -365,6 +374,7 @@ func replacePathTemplate(path, filename string) string {
func convertResourceFromStore(resource *store.Resource) *Resource {
return &Resource{
ID: resource.ID,
Name: resource.ResourceName,
CreatorID: resource.CreatorID,
CreatedTs: resource.CreatedTs,
UpdatedTs: resource.UpdatedTs,
@@ -384,7 +394,7 @@ func convertResourceFromStore(resource *store.Resource) *Resource {
// 2. *LocalStorage*: `create.InternalPath`.
// 3. Others( external service): `create.ExternalLink`.
func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resource, r io.Reader) error {
systemSettingStorageServiceID, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()})
systemSettingStorageServiceID, err := s.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingStorageServiceIDName.String()})
if err != nil {
return errors.Wrap(err, "Failed to find SystemSettingStorageServiceIDName")
}
@@ -407,7 +417,7 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc
return nil
} else if storageServiceID == LocalStorage {
// `LocalStorage` means save blob into local disk
systemSettingLocalStoragePath, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingLocalStoragePathName.String()})
systemSettingLocalStoragePath, err := s.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingLocalStoragePathName.String()})
if err != nil {
return errors.Wrap(err, "Failed to find SystemSettingLocalStoragePathName")
}
@@ -474,6 +484,7 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc
Bucket: s3Config.Bucket,
URLPrefix: s3Config.URLPrefix,
URLSuffix: s3Config.URLSuffix,
PreSign: s3Config.PreSign,
})
if err != nil {
return errors.Wrap(err, "Failed to create s3 client")

View File

@@ -1,201 +0,0 @@
package v1
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/feeds"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/renderer"
"github.com/usememos/memos/store"
)
const (
maxRSSItemCount = 100
maxRSSItemTitleLength = 128
)
func (s *APIV1Service) registerRSSRoutes(g *echo.Group) {
g.GET("/explore/rss.xml", s.GetExploreRSS)
g.GET("/u/:id/rss.xml", s.GetUserRSS)
}
// GetExploreRSS godoc
//
// @Summary Get RSS
// @Tags rss
// @Produce xml
// @Success 200 {object} nil "RSS"
// @Failure 500 {object} nil "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
// @Router /explore/rss.xml [GET]
func (s *APIV1Service) GetExploreRSS(c echo.Context) error {
ctx := c.Request().Context()
systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
}
normalStatus := store.Normal
memoFind := store.FindMemo{
RowStatus: &normalStatus,
VisibilityList: []store.Visibility{store.Public},
}
memoList, err := s.Store.ListMemos(ctx, &memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
baseURL := c.Scheme() + "://" + c.Request().Host
rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, systemCustomizedProfile)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
return c.String(http.StatusOK, rss)
}
// GetUserRSS godoc
//
// @Summary Get RSS for a user
// @Tags rss
// @Produce xml
// @Param id path int true "User ID"
// @Success 200 {object} nil "RSS"
// @Failure 400 {object} nil "User id is not a number"
// @Failure 500 {object} nil "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
// @Router /u/{id}/rss.xml [GET]
func (s *APIV1Service) GetUserRSS(c echo.Context) error {
ctx := c.Request().Context()
id, err := util.ConvertStringToInt32(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "User id is not a number").SetInternal(err)
}
systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
}
normalStatus := store.Normal
memoFind := store.FindMemo{
CreatorID: &id,
RowStatus: &normalStatus,
VisibilityList: []store.Visibility{store.Public},
}
memoList, err := s.Store.ListMemos(ctx, &memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
baseURL := c.Scheme() + "://" + c.Request().Host
rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, systemCustomizedProfile)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
return c.String(http.StatusOK, rss)
}
func (s *APIV1Service) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string, profile *CustomizedProfile) (string, error) {
feed := &feeds.Feed{
Title: profile.Name,
Link: &feeds.Link{Href: baseURL},
Description: profile.Description,
Created: time.Now(),
}
var itemCountLimit = util.Min(len(memoList), maxRSSItemCount)
feed.Items = make([]*feeds.Item, itemCountLimit)
for i := 0; i < itemCountLimit; i++ {
memoMessage, err := s.convertMemoFromStore(ctx, memoList[i])
if err != nil {
return "", err
}
description, err := getRSSItemDescription(memoMessage.Content)
if err != nil {
return "", err
}
feed.Items[i] = &feeds.Item{
Title: getRSSItemTitle(memoMessage.Content),
Link: &feeds.Link{Href: baseURL + "/m/" + fmt.Sprintf("%d", memoMessage.ID)},
Description: description,
Created: time.Unix(memoMessage.CreatedTs, 0),
Enclosure: &feeds.Enclosure{Url: baseURL + "/m/" + fmt.Sprintf("%d", memoMessage.ID) + "/image"},
}
if len(memoMessage.ResourceList) > 0 {
resource := memoMessage.ResourceList[0]
enclosure := feeds.Enclosure{}
if resource.ExternalLink != "" {
enclosure.Url = resource.ExternalLink
} else {
enclosure.Url = baseURL + "/o/r/" + fmt.Sprintf("%d", resource.ID)
}
enclosure.Length = strconv.Itoa(int(resource.Size))
enclosure.Type = resource.Type
feed.Items[i].Enclosure = &enclosure
}
}
rss, err := feed.ToRss()
if err != nil {
return "", err
}
return rss, nil
}
func (s *APIV1Service) getSystemCustomizedProfile(ctx context.Context) (*CustomizedProfile, error) {
systemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingCustomizedProfileName.String(),
})
if err != nil {
return nil, err
}
customizedProfile := &CustomizedProfile{
Name: "Memos",
Locale: "en",
Appearance: "system",
}
if systemSetting != nil {
if err := json.Unmarshal([]byte(systemSetting.Value), customizedProfile); err != nil {
return nil, err
}
}
return customizedProfile, nil
}
func getRSSItemTitle(content string) string {
tokens := tokenizer.Tokenize(content)
nodes, _ := parser.Parse(tokens)
if len(nodes) > 0 {
firstNode := nodes[0]
title := renderer.NewStringRenderer().Render([]ast.Node{firstNode})
return title
}
title := strings.Split(content, "\n")[0]
var titleLengthLimit = util.Min(len(title), maxRSSItemTitleLength)
if titleLengthLimit < len(title) {
title = title[:titleLengthLimit] + "..."
}
return title
}
func getRSSItemDescription(content string) (string, error) {
tokens := tokenizer.Tokenize(content)
nodes, err := parser.Parse(tokens)
if err != nil {
return "", err
}
result := renderer.NewHTMLRenderer().Render(nodes)
return result, nil
}

View File

@@ -43,6 +43,7 @@ type StorageS3Config struct {
Bucket string `json:"bucket"`
URLPrefix string `json:"urlPrefix"`
URLSuffix string `json:"urlSuffix"`
PreSign bool `json:"presign"`
}
type Storage struct {
@@ -208,7 +209,7 @@ func (s *APIV1Service) DeleteStorage(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
}
systemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()})
systemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingStorageServiceIDName.String()})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
}

1708
api/v1/swagger.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -70,9 +70,6 @@ definitions:
description:
description: Description is the server description.
type: string
externalUrl:
description: ExternalURL is the external url of server. e.g. https://usermemos.com
type: string
locale:
description: Locale is the server default locale.
type: string
@@ -230,6 +227,8 @@ definitions:
type: string
path:
type: string
presign:
type: boolean
region:
type: string
secretKey:
@@ -259,34 +258,24 @@ definitions:
enum:
- server-id
- secret-session
- allow-signup
- disable-password-login
- disable-public-memos
- max-upload-size-mib
- additional-style
- additional-script
- customized-profile
- storage-service-id
- local-storage-path
- telegram-bot-token
- memo-display-with-updated-ts
- instance-url
type: string
x-enum-varnames:
- SystemSettingServerIDName
- SystemSettingSecretSessionName
- SystemSettingAllowSignUpName
- SystemSettingDisablePasswordLoginName
- SystemSettingDisablePublicMemosName
- SystemSettingMaxUploadSizeMiBName
- SystemSettingAdditionalStyleName
- SystemSettingAdditionalScriptName
- SystemSettingCustomizedProfileName
- SystemSettingStorageServiceIDName
- SystemSettingLocalStoragePathName
- SystemSettingTelegramBotTokenName
- SystemSettingMemoDisplayWithUpdatedTsName
- SystemSettingInstanceURLName
api_v1.SystemStatus:
properties:
additionalScript:
@@ -500,9 +489,6 @@ definitions:
description:
description: Description is the server description.
type: string
externalUrl:
description: ExternalURL is the external url of server. e.g. https://usermemos.com
type: string
locale:
description: Locale is the server default locale.
type: string
@@ -660,6 +646,8 @@ definitions:
type: string
path:
type: string
presign:
type: boolean
region:
type: string
secretKey:
@@ -689,34 +677,24 @@ definitions:
enum:
- server-id
- secret-session
- allow-signup
- disable-password-login
- disable-public-memos
- max-upload-size-mib
- additional-style
- additional-script
- customized-profile
- storage-service-id
- local-storage-path
- telegram-bot-token
- memo-display-with-updated-ts
- instance-url
type: string
x-enum-varnames:
- SystemSettingServerIDName
- SystemSettingSecretSessionName
- SystemSettingAllowSignUpName
- SystemSettingDisablePasswordLoginName
- SystemSettingDisablePublicMemosName
- SystemSettingMaxUploadSizeMiBName
- SystemSettingAdditionalStyleName
- SystemSettingAdditionalScriptName
- SystemSettingCustomizedProfileName
- SystemSettingStorageServiceIDName
- SystemSettingLocalStoragePathName
- SystemSettingTelegramBotTokenName
- SystemSettingMemoDisplayWithUpdatedTsName
- SystemSettingInstanceURLName
github_com_usememos_memos_api_v1.SystemStatus:
properties:
additionalScript:
@@ -932,9 +910,13 @@ definitions:
type: integer
id:
type: integer
parentID:
type: integer
pinned:
description: Composed fields
type: boolean
resourceName:
type: string
rowStatus:
allOf:
- $ref: '#/definitions/store.RowStatus'
@@ -983,6 +965,8 @@ definitions:
type: string
memoID:
type: integer
resourceName:
type: string
size:
type: integer
type:
@@ -1019,15 +1003,6 @@ definitions:
type:
type: string
type: object
store.SystemSetting:
properties:
description:
type: string
name:
type: string
value:
type: string
type: object
store.User:
properties:
avatarURL:
@@ -1201,7 +1176,7 @@ paths:
description: List of available identity providers
schema:
items:
$ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProvider'
$ref: '#/definitions/api_v1.IdentityProvider'
type: array
"500":
description: Failed to find identity provider list | Failed to find user
@@ -1217,7 +1192,7 @@ paths:
name: body
required: true
schema:
$ref: '#/definitions/github_com_usememos_memos_api_v1.CreateIdentityProviderRequest'
$ref: '#/definitions/api_v1.CreateIdentityProviderRequest'
produces:
- application/json
responses:
@@ -1302,7 +1277,7 @@ paths:
name: body
required: true
schema:
$ref: '#/definitions/github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest'
$ref: '#/definitions/api_v1.UpdateIdentityProviderRequest'
produces:
- application/json
responses:
@@ -1583,7 +1558,7 @@ paths:
name: body
required: true
schema:
$ref: '#/definitions/api_v1.UpsertMemoRelationRequest'
$ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest'
produces:
- application/json
responses:
@@ -1743,7 +1718,7 @@ paths:
name: body
required: true
schema:
$ref: '#/definitions/github_com_usememos_memos_api_v1.CreateResourceRequest'
$ref: '#/definitions/api_v1.CreateResourceRequest'
produces:
- application/json
responses:
@@ -1801,7 +1776,7 @@ paths:
name: patch
required: true
schema:
$ref: '#/definitions/github_com_usememos_memos_api_v1.UpdateResourceRequest'
$ref: '#/definitions/api_v1.UpdateResourceRequest'
produces:
- application/json
responses:
@@ -1856,7 +1831,7 @@ paths:
"200":
description: System GetSystemStatus
schema:
$ref: '#/definitions/github_com_usememos_memos_api_v1.SystemStatus'
$ref: '#/definitions/api_v1.SystemStatus'
"401":
description: Missing user in session | Unauthorized
"500":
@@ -1997,10 +1972,6 @@ paths:
produces:
- application/json
responses:
"200":
description: Created system setting
schema:
$ref: '#/definitions/store.SystemSetting'
"400":
description: Malformatted post system setting request | invalid system setting
"401":
@@ -2142,7 +2113,7 @@ paths:
name: body
required: true
schema:
$ref: '#/definitions/github_com_usememos_memos_api_v1.CreateUserRequest'
$ref: '#/definitions/api_v1.CreateUserRequest'
produces:
- application/json
responses:
@@ -2224,7 +2195,7 @@ paths:
name: patch
required: true
schema:
$ref: '#/definitions/github_com_usememos_memos_api_v1.UpdateUserRequest'
$ref: '#/definitions/api_v1.UpdateUserRequest'
produces:
- application/json
responses:
@@ -2283,19 +2254,6 @@ paths:
summary: Get user by username
tags:
- user
/explore/rss.xml:
get:
produces:
- text/xml
responses:
"200":
description: RSS
"500":
description: Failed to get system customized profile | Failed to find memo
list | Failed to generate rss
summary: Get RSS
tags:
- rss
/o/get/GetImage:
get:
parameters:
@@ -2317,25 +2275,4 @@ paths:
summary: Get GetImage from URL
tags:
- image-url
/u/{id}/rss.xml:
get:
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- text/xml
responses:
"200":
description: RSS
"400":
description: User id is not a number
"500":
description: Failed to get system customized profile | Failed to find memo
list | Failed to generate rss
summary: Get RSS for a user
tags:
- rss
swagger: "2.0"

View File

@@ -5,9 +5,7 @@ import (
"net/http"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
)
@@ -18,18 +16,12 @@ type SystemStatus struct {
DBSize int64 `json:"dbSize"`
// System settings
// Allow sign up.
AllowSignUp bool `json:"allowSignUp"`
// Disable password login.
DisablePasswordLogin bool `json:"disablePasswordLogin"`
// Disable public memos.
DisablePublicMemos bool `json:"disablePublicMemos"`
// Max upload size.
MaxUploadSizeMiB int `json:"maxUploadSizeMiB"`
// Additional style.
AdditionalStyle string `json:"additionalStyle"`
// Additional script.
AdditionalScript string `json:"additionalScript"`
// Customized server profile, including server name and external url.
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
// Storage service ID.
@@ -74,8 +66,6 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
Mode: s.Profile.Mode,
Version: s.Profile.Version,
},
// Allow sign up by default.
AllowSignUp: true,
MaxUploadSizeMiB: 32,
CustomizedProfile: CustomizedProfile{
Name: "Memos",
@@ -97,12 +87,18 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
systemStatus.Host = &User{ID: hostUser.ID}
}
systemSettingList, err := s.Store.ListSystemSettings(ctx, &store.FindSystemSetting{})
workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace general setting").SetInternal(err)
}
systemStatus.DisablePasswordLogin = workspaceGeneralSetting.DisallowPasswordLogin
systemSettingList, err := s.Store.ListWorkspaceSettings(ctx, &store.FindWorkspaceSetting{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
for _, systemSetting := range systemSettingList {
if systemSetting.Name == SystemSettingServerIDName.String() || systemSetting.Name == SystemSettingSecretSessionName.String() || systemSetting.Name == SystemSettingTelegramBotTokenName.String() || systemSetting.Name == SystemSettingInstanceURLName.String() {
if systemSetting.Name == SystemSettingServerIDName.String() || systemSetting.Name == SystemSettingSecretSessionName.String() || systemSetting.Name == SystemSettingTelegramBotTokenName.String() {
continue
}
@@ -114,18 +110,10 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
}
switch systemSetting.Name {
case SystemSettingAllowSignUpName.String():
systemStatus.AllowSignUp = baseValue.(bool)
case SystemSettingDisablePasswordLoginName.String():
systemStatus.DisablePasswordLogin = baseValue.(bool)
case SystemSettingDisablePublicMemosName.String():
systemStatus.DisablePublicMemos = baseValue.(bool)
case SystemSettingMaxUploadSizeMiBName.String():
systemStatus.MaxUploadSizeMiB = int(baseValue.(float64))
case SystemSettingAdditionalStyleName.String():
systemStatus.AdditionalStyle = baseValue.(string)
case SystemSettingAdditionalScriptName.String():
systemStatus.AdditionalScript = baseValue.(string)
case SystemSettingCustomizedProfileName.String():
customizedProfile := CustomizedProfile{}
if err := json.Unmarshal([]byte(systemSetting.Value), &customizedProfile); err != nil {
@@ -139,7 +127,7 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
case SystemSettingMemoDisplayWithUpdatedTsName.String():
systemStatus.MemoDisplayWithUpdatedTs = baseValue.(bool)
default:
log.Warn("Unknown system setting name", zap.String("setting name", systemSetting.Name))
// Skip unknown system setting.
}
}

View File

@@ -19,18 +19,10 @@ const (
SystemSettingServerIDName SystemSettingName = "server-id"
// SystemSettingSecretSessionName is the name of secret session.
SystemSettingSecretSessionName SystemSettingName = "secret-session"
// SystemSettingAllowSignUpName is the name of allow signup setting.
SystemSettingAllowSignUpName SystemSettingName = "allow-signup"
// SystemSettingDisablePasswordLoginName is the name of disable password login setting.
SystemSettingDisablePasswordLoginName SystemSettingName = "disable-password-login"
// SystemSettingDisablePublicMemosName is the name of disable public memos setting.
SystemSettingDisablePublicMemosName SystemSettingName = "disable-public-memos"
// SystemSettingMaxUploadSizeMiBName is the name of max upload size setting.
SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib"
// SystemSettingAdditionalStyleName is the name of additional style.
SystemSettingAdditionalStyleName SystemSettingName = "additional-style"
// SystemSettingAdditionalScriptName is the name of additional script.
SystemSettingAdditionalScriptName SystemSettingName = "additional-script"
// SystemSettingCustomizedProfileName is the name of customized server profile.
SystemSettingCustomizedProfileName SystemSettingName = "customized-profile"
// SystemSettingStorageServiceIDName is the name of storage service ID.
@@ -41,8 +33,6 @@ const (
SystemSettingTelegramBotTokenName SystemSettingName = "telegram-bot-token"
// SystemSettingMemoDisplayWithUpdatedTsName is the name of memo display with updated ts.
SystemSettingMemoDisplayWithUpdatedTsName SystemSettingName = "memo-display-with-updated-ts"
// SystemSettingInstanceURLName is the name of instance url setting.
SystemSettingInstanceURLName SystemSettingName = "instance-url"
)
const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"`
@@ -58,8 +48,6 @@ type CustomizedProfile struct {
Locale string `json:"locale"`
// Appearance is the server default appearance.
Appearance string `json:"appearance"`
// ExternalURL is the external url of server. e.g. https://usermemos.com
ExternalURL string `json:"externalUrl"`
}
func (key SystemSettingName) String() string {
@@ -110,7 +98,7 @@ func (s *APIV1Service) GetSystemSettingList(c echo.Context) error {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
list, err := s.Store.ListSystemSettings(ctx, &store.FindSystemSetting{})
list, err := s.Store.ListWorkspaceSettings(ctx, &store.FindWorkspaceSetting{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
@@ -129,7 +117,6 @@ func (s *APIV1Service) GetSystemSettingList(c echo.Context) error {
// @Accept json
// @Produce json
// @Param body body UpsertSystemSettingRequest true "Request object."
// @Success 200 {object} store.SystemSetting "Created system setting"
// @Failure 400 {object} nil "Malformatted post system setting request | invalid system setting"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 403 {object} nil "Cannot disable passwords if no SSO identity provider is configured."
@@ -159,22 +146,8 @@ func (s *APIV1Service) CreateSystemSetting(c echo.Context) error {
if err := systemSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
}
if systemSettingUpsert.Name == SystemSettingDisablePasswordLoginName {
var disablePasswordLogin bool
if err := json.Unmarshal([]byte(systemSettingUpsert.Value), &disablePasswordLogin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
}
identityProviderList, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProvider{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
if disablePasswordLogin && len(identityProviderList) == 0 {
return echo.NewHTTPError(http.StatusForbidden, "Cannot disable passwords if no SSO identity provider is configured.")
}
}
systemSetting, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
systemSetting, err := s.Store.UpsertWorkspaceSetting(ctx, &store.WorkspaceSetting{
Name: systemSettingUpsert.Name.String(),
Value: systemSettingUpsert.Value,
Description: systemSettingUpsert.Description,
@@ -189,16 +162,6 @@ func (upsert UpsertSystemSettingRequest) Validate() error {
switch settingName := upsert.Name; settingName {
case SystemSettingServerIDName:
return errors.Errorf("updating %v is not allowed", settingName)
case SystemSettingAllowSignUpName:
var value bool
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingDisablePasswordLoginName:
var value bool
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingDisablePublicMemosName:
var value bool
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
@@ -209,24 +172,13 @@ func (upsert UpsertSystemSettingRequest) Validate() error {
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingAdditionalStyleName:
var value string
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingAdditionalScriptName:
var value string
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingCustomizedProfileName:
customizedProfile := CustomizedProfile{
Name: "memos",
Name: "Memos",
LogoURL: "",
Description: "",
Locale: "en",
Appearance: "system",
ExternalURL: "",
}
if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
@@ -282,14 +234,13 @@ func (upsert UpsertSystemSettingRequest) Validate() error {
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingInstanceURLName:
default:
return errors.New("invalid system setting name")
}
return nil
}
func convertSystemSettingFromStore(systemSetting *store.SystemSetting) *SystemSetting {
func convertSystemSettingFromStore(systemSetting *store.WorkspaceSetting) *SystemSetting {
return &SystemSetting{
Name: SystemSettingName(systemSetting.Name),
Value: systemSetting.Value,

View File

@@ -12,7 +12,6 @@ import (
"golang.org/x/crypto/bcrypt"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store"
)
@@ -158,7 +157,7 @@ func (s *APIV1Service) CreateUser(c echo.Context) error {
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
}
if !usernameMatcher.MatchString(strings.ToLower(userCreate.Username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(userCreate.Username)) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", userCreate.Username)).SetInternal(err)
}
// Disallow host user to be created.
@@ -183,7 +182,6 @@ func (s *APIV1Service) CreateUser(c echo.Context) error {
}
userMessage := convertUserFromStore(user)
metric.Enqueue("user create")
return c.JSON(http.StatusOK, userMessage)
}
@@ -316,6 +314,14 @@ func (s *APIV1Service) DeleteUser(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Cannot delete current user")
}
findUser, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if s.Profile.Mode == "demo" && findUser.Username == "memos-demo" {
return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to delete this user in demo mode")
}
if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
ID: userID,
}); err != nil {
@@ -366,6 +372,10 @@ func (s *APIV1Service) UpdateUser(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid update user request").SetInternal(err)
}
if s.Profile.Mode == "demo" && *request.Username == "memos-demo" {
return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to update user in demo mode")
}
currentTs := time.Now().Unix()
userUpdate := &store.UpdateUser{
ID: userID,
@@ -379,7 +389,7 @@ func (s *APIV1Service) UpdateUser(c echo.Context) error {
}
}
if request.Username != nil {
if !usernameMatcher.MatchString(strings.ToLower(*request.Username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(*request.Username)) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", *request.Username)).SetInternal(err)
}
userUpdate.Username = request.Username

View File

@@ -8,6 +8,7 @@ import (
"github.com/labstack/echo/v4/middleware"
"github.com/usememos/memos/api/resource"
"github.com/usememos/memos/api/rss"
"github.com/usememos/memos/plugin/telegram"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
@@ -44,9 +45,6 @@ func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store
}
func (s *APIV1Service) Register(rootGroup *echo.Group) {
// Register RSS routes.
s.registerRSSRoutes(rootGroup)
// Register API v1 routes.
apiV1Group := rootGroup.Group("/api/v1")
apiV1Group.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
@@ -85,9 +83,12 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) {
return JWTMiddleware(s, next, s.Secret)
})
s.registerGetterPublicRoutes(publicGroup)
// Create and register resource public routes.
resourceService := resource.NewService(s.Profile, s.Store)
resourceService.RegisterResourcePublicRoutes(publicGroup)
resource.NewResourceService(s.Profile, s.Store).RegisterRoutes(publicGroup)
// Create and register rss public routes.
rss.NewRSSService(s.Profile, s.Store).RegisterRoutes(rootGroup)
// programmatically set API version same as the server version
SwaggerInfo.Version = s.Profile.Version

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"strings"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/pkg/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"

View File

@@ -3,15 +3,20 @@ package v2
import "strings"
var authenticationAllowlistMethods = map[string]bool{
"/memos.api.v2.SystemService/GetSystemInfo": true,
"/memos.api.v2.AuthService/GetAuthStatus": true,
"/memos.api.v2.UserService/GetUser": true,
"/memos.api.v2.MemoService/ListMemos": true,
"/memos.api.v2.MemoService/GetMemo": true,
"/memos.api.v2.MemoService/ListMemoResources": true,
"/memos.api.v2.MemoService/ListMemoRelations": true,
"/memos.api.v2.MemoService/ListMemoComments": true,
"/memos.api.v2.MarkdownService/ParseMarkdown": true,
"/memos.api.v2.WorkspaceService/GetWorkspaceProfile": true,
"/memos.api.v2.WorkspaceSettingService/GetWorkspaceSetting": true,
"/memos.api.v2.AuthService/GetAuthStatus": true,
"/memos.api.v2.AuthService/SignIn": true,
"/memos.api.v2.AuthService/SignInWithSSO": true,
"/memos.api.v2.AuthService/SignOut": true,
"/memos.api.v2.AuthService/SignUp": true,
"/memos.api.v2.UserService/GetUser": true,
"/memos.api.v2.MemoService/ListMemos": true,
"/memos.api.v2.MemoService/GetMemo": true,
"/memos.api.v2.MemoService/GetMemoByName": true,
"/memos.api.v2.MemoService/ListMemoResources": true,
"/memos.api.v2.MemoService/ListMemoRelations": true,
"/memos.api.v2.MemoService/ListMemoComments": true,
}
// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication.

1731
api/v2/apidocs.swagger.md Normal file

File diff suppressed because it is too large Load Diff

2249
api/v2/apidocs.swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,15 +3,23 @@ package v2
import (
"context"
"fmt"
"regexp"
"strings"
"time"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"github.com/usememos/memos/api/auth"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/plugin/idp/oauth2"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) GetAuthStatus(ctx context.Context, _ *apiv2pb.GetAuthStatusRequest) (*apiv2pb.GetAuthStatusResponse, error) {
@@ -21,7 +29,7 @@ func (s *APIV2Service) GetAuthStatus(ctx context.Context, _ *apiv2pb.GetAuthStat
}
if user == nil {
// Set the cookie header to expire access token.
if err := clearAccessTokenCookie(ctx); err != nil {
if err := s.clearAccessTokenCookie(ctx); err != nil {
return nil, status.Errorf(codes.Internal, "failed to set grpc header")
}
return nil, status.Errorf(codes.Unauthenticated, "user not found")
@@ -31,11 +39,224 @@ func (s *APIV2Service) GetAuthStatus(ctx context.Context, _ *apiv2pb.GetAuthStat
}, nil
}
func clearAccessTokenCookie(ctx context.Context) error {
func (s *APIV2Service) SignIn(ctx context.Context, request *apiv2pb.SignInRequest) (*apiv2pb.SignInResponse, error) {
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &request.Username,
})
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to find user by username %s", request.Username))
}
if user == nil {
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("user not found with username %s", request.Username))
} else if user.RowStatus == store.Archived {
return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("user has been archived with username %s", request.Username))
}
// Compare the stored hashed password, with the hashed version of the password that was received.
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(request.Password)); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmatched email and password")
}
expireTime := time.Now().Add(auth.AccessTokenDuration)
if request.NeverExpire {
// Set the expire time to 100 years.
expireTime = time.Now().Add(100 * 365 * 24 * time.Hour)
}
if err := s.doSignIn(ctx, user, expireTime); err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to sign in, err: %s", err))
}
return &apiv2pb.SignInResponse{
User: convertUserFromStore(user),
}, nil
}
func (s *APIV2Service) SignInWithSSO(ctx context.Context, request *apiv2pb.SignInWithSSORequest) (*apiv2pb.SignInWithSSOResponse, error) {
identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
ID: &request.IdpId,
})
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get identity provider, err: %s", err))
}
if identityProvider == nil {
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("identity provider not found with id %d", request.IdpId))
}
var userInfo *idp.IdentityProviderUserInfo
if identityProvider.Type == store.IdentityProviderOAuth2Type {
oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.OAuth2Config)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create oauth2 identity provider, err: %s", err))
}
token, err := oauth2IdentityProvider.ExchangeToken(ctx, request.RedirectUri, request.Code)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to exchange token, err: %s", err))
}
userInfo, err = oauth2IdentityProvider.UserInfo(token)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get user info, err: %s", err))
}
}
identifierFilter := identityProvider.IdentifierFilter
if identifierFilter != "" {
identifierFilterRegex, err := regexp.Compile(identifierFilter)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to compile identifier filter regex, err: %s", err))
}
if !identifierFilterRegex.MatchString(userInfo.Identifier) {
return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("identifier %s is not allowed", userInfo.Identifier))
}
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &userInfo.Identifier,
})
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to find user by username %s", userInfo.Identifier))
}
if user == nil {
userCreate := &store.User{
Username: userInfo.Identifier,
// The new signup user should be normal user by default.
Role: store.RoleUser,
Nickname: userInfo.DisplayName,
Email: userInfo.Email,
}
password, err := util.RandomString(20)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to generate random password, err: %s", err))
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to generate password hash, err: %s", err))
}
userCreate.PasswordHash = string(passwordHash)
user, err = s.Store.CreateUser(ctx, userCreate)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create user, err: %s", err))
}
}
if user.RowStatus == store.Archived {
return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("user has been archived with username %s", userInfo.Identifier))
}
if err := s.doSignIn(ctx, user, time.Now().Add(auth.AccessTokenDuration)); err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to sign in, err: %s", err))
}
return &apiv2pb.SignInWithSSOResponse{
User: convertUserFromStore(user),
}, nil
}
func (s *APIV2Service) doSignIn(ctx context.Context, user *store.User, expireTime time.Time) error {
accessToken, err := auth.GenerateAccessToken(user.Email, user.ID, expireTime, []byte(s.Secret))
if err != nil {
return status.Errorf(codes.Internal, fmt.Sprintf("failed to generate tokens, err: %s", err))
}
if err := s.UpsertAccessTokenToStore(ctx, user, accessToken, "user login"); err != nil {
return status.Errorf(codes.Internal, fmt.Sprintf("failed to upsert access token to store, err: %s", err))
}
cookie, err := s.buildAccessTokenCookie(ctx, accessToken, expireTime)
if err != nil {
return status.Errorf(codes.Internal, fmt.Sprintf("failed to build access token cookie, err: %s", err))
}
if err := grpc.SetHeader(ctx, metadata.New(map[string]string{
"Set-Cookie": fmt.Sprintf("%s=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Strict", auth.AccessTokenCookieName),
"Set-Cookie": cookie,
})); err != nil {
return status.Errorf(codes.Internal, "failed to set grpc header, error: %v", err)
}
return nil
}
func (s *APIV2Service) SignUp(ctx context.Context, request *apiv2pb.SignUpRequest) (*apiv2pb.SignUpResponse, error) {
workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get workspace setting, err: %s", err))
}
if workspaceGeneralSetting.DisallowSignup || workspaceGeneralSetting.DisallowPasswordLogin {
return nil, status.Errorf(codes.PermissionDenied, "sign up is not allowed")
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to generate password hash, err: %s", err))
}
create := &store.User{
Username: request.Username,
Nickname: request.Username,
PasswordHash: string(passwordHash),
}
hostUserType := store.RoleHost
existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{
Role: &hostUserType,
})
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to list users, err: %s", err))
}
if len(existedHostUsers) == 0 {
// Change the default role to host if there is no host user.
create.Role = store.RoleHost
} else {
create.Role = store.RoleUser
}
user, err := s.Store.CreateUser(ctx, create)
if err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create user, err: %s", err))
}
if err := s.doSignIn(ctx, user, time.Now().Add(auth.AccessTokenDuration)); err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to sign in, err: %s", err))
}
return &apiv2pb.SignUpResponse{
User: convertUserFromStore(user),
}, nil
}
func (s *APIV2Service) SignOut(ctx context.Context, _ *apiv2pb.SignOutRequest) (*apiv2pb.SignOutResponse, error) {
if err := s.clearAccessTokenCookie(ctx); err != nil {
return nil, status.Errorf(codes.Internal, "failed to set grpc header, error: %v", err)
}
return &apiv2pb.SignOutResponse{}, nil
}
func (s *APIV2Service) clearAccessTokenCookie(ctx context.Context) error {
cookie, err := s.buildAccessTokenCookie(ctx, "", time.Time{})
if err != nil {
return errors.Wrap(err, "failed to build access token cookie")
}
if err := grpc.SetHeader(ctx, metadata.New(map[string]string{
"Set-Cookie": cookie,
})); err != nil {
return errors.Wrap(err, "failed to set grpc header")
}
return nil
}
func (s *APIV2Service) buildAccessTokenCookie(ctx context.Context, accessToken string, expireTime time.Time) (string, error) {
attrs := []string{
fmt.Sprintf("%s=%s", auth.AccessTokenCookieName, accessToken),
"Path=/",
"HttpOnly",
}
if expireTime.IsZero() {
attrs = append(attrs, "Expires=Thu, 01 Jan 1970 00:00:00 GMT")
} else {
attrs = append(attrs, "Expires="+expireTime.Format(time.RFC1123))
}
workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to get workspace setting")
}
if strings.HasPrefix(workspaceGeneralSetting.InstanceUrl, "https://") {
attrs = append(attrs, "SameSite=None")
attrs = append(attrs, "Secure")
} else {
attrs = append(attrs, "SameSite=Strict")
}
return strings.Join(attrs, "; "), nil
}

View File

@@ -2,6 +2,10 @@ package v2
import (
"context"
"encoding/base64"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
@@ -42,3 +46,29 @@ func getCurrentUser(ctx context.Context, s *store.Store) (*store.User, error) {
}
return user, nil
}
func getPageToken(limit int, offset int) (string, error) {
return marshalPageToken(&apiv2pb.PageToken{
Limit: int32(limit),
Offset: int32(offset),
})
}
func marshalPageToken(pageToken *apiv2pb.PageToken) (string, error) {
b, err := proto.Marshal(pageToken)
if err != nil {
return "", errors.Wrapf(err, "failed to marshal page token")
}
return base64.StdEncoding.EncodeToString(b), nil
}
func unmarshalPageToken(s string, pageToken *apiv2pb.PageToken) error {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return errors.Wrapf(err, "failed to decode page token")
}
if err := proto.Unmarshal(b, pageToken); err != nil {
return errors.Wrapf(err, "failed to unmarshal page token")
}
return nil
}

View File

@@ -1,215 +0,0 @@
package v2
import (
"context"
"github.com/pkg/errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
)
func (*APIV2Service) ParseMarkdown(_ context.Context, request *apiv2pb.ParseMarkdownRequest) (*apiv2pb.ParseMarkdownResponse, error) {
rawNodes, err := parser.Parse(tokenizer.Tokenize(request.Markdown))
if err != nil {
return nil, errors.Wrap(err, "failed to parse memo content")
}
nodes := convertFromASTNodes(rawNodes)
return &apiv2pb.ParseMarkdownResponse{
Nodes: nodes,
}, nil
}
func convertFromASTNodes(rawNodes []ast.Node) []*apiv2pb.Node {
nodes := []*apiv2pb.Node{}
for _, rawNode := range rawNodes {
node := convertFromASTNode(rawNode)
nodes = append(nodes, node)
}
return nodes
}
func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node {
node := &apiv2pb.Node{
Type: apiv2pb.NodeType(rawNode.Type()),
}
switch n := rawNode.(type) {
case *ast.LineBreak:
node.Node = &apiv2pb.Node_LineBreakNode{}
case *ast.Paragraph:
children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_ParagraphNode{ParagraphNode: &apiv2pb.ParagraphNode{Children: children}}
case *ast.CodeBlock:
node.Node = &apiv2pb.Node_CodeBlockNode{CodeBlockNode: &apiv2pb.CodeBlockNode{Language: n.Language, Content: n.Content}}
case *ast.Heading:
children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_HeadingNode{HeadingNode: &apiv2pb.HeadingNode{Level: int32(n.Level), Children: children}}
case *ast.HorizontalRule:
node.Node = &apiv2pb.Node_HorizontalRuleNode{HorizontalRuleNode: &apiv2pb.HorizontalRuleNode{Symbol: n.Symbol}}
case *ast.Blockquote:
children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_BlockquoteNode{BlockquoteNode: &apiv2pb.BlockquoteNode{Children: children}}
case *ast.OrderedList:
children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_OrderedListNode{OrderedListNode: &apiv2pb.OrderedListNode{Number: n.Number, Indent: int32(n.Indent), Children: children}}
case *ast.UnorderedList:
children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_UnorderedListNode{UnorderedListNode: &apiv2pb.UnorderedListNode{Symbol: n.Symbol, Indent: int32(n.Indent), Children: children}}
case *ast.TaskList:
children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_TaskListNode{TaskListNode: &apiv2pb.TaskListNode{Symbol: n.Symbol, Indent: int32(n.Indent), Complete: n.Complete, Children: children}}
case *ast.MathBlock:
node.Node = &apiv2pb.Node_MathBlockNode{MathBlockNode: &apiv2pb.MathBlockNode{Content: n.Content}}
case *ast.Table:
node.Node = &apiv2pb.Node_TableNode{TableNode: convertTableFromASTNode(n)}
case *ast.Text:
node.Node = &apiv2pb.Node_TextNode{TextNode: &apiv2pb.TextNode{Content: n.Content}}
case *ast.Bold:
children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_BoldNode{BoldNode: &apiv2pb.BoldNode{Symbol: n.Symbol, Children: children}}
case *ast.Italic:
node.Node = &apiv2pb.Node_ItalicNode{ItalicNode: &apiv2pb.ItalicNode{Symbol: n.Symbol, Content: n.Content}}
case *ast.BoldItalic:
node.Node = &apiv2pb.Node_BoldItalicNode{BoldItalicNode: &apiv2pb.BoldItalicNode{Symbol: n.Symbol, Content: n.Content}}
case *ast.Code:
node.Node = &apiv2pb.Node_CodeNode{CodeNode: &apiv2pb.CodeNode{Content: n.Content}}
case *ast.Image:
node.Node = &apiv2pb.Node_ImageNode{ImageNode: &apiv2pb.ImageNode{AltText: n.AltText, Url: n.URL}}
case *ast.Link:
node.Node = &apiv2pb.Node_LinkNode{LinkNode: &apiv2pb.LinkNode{Text: n.Text, Url: n.URL}}
case *ast.AutoLink:
node.Node = &apiv2pb.Node_AutoLinkNode{AutoLinkNode: &apiv2pb.AutoLinkNode{Url: n.URL, IsRawText: n.IsRawText}}
case *ast.Tag:
node.Node = &apiv2pb.Node_TagNode{TagNode: &apiv2pb.TagNode{Content: n.Content}}
case *ast.Strikethrough:
node.Node = &apiv2pb.Node_StrikethroughNode{StrikethroughNode: &apiv2pb.StrikethroughNode{Content: n.Content}}
case *ast.EscapingCharacter:
node.Node = &apiv2pb.Node_EscapingCharacterNode{EscapingCharacterNode: &apiv2pb.EscapingCharacterNode{Symbol: n.Symbol}}
case *ast.Math:
node.Node = &apiv2pb.Node_MathNode{MathNode: &apiv2pb.MathNode{Content: n.Content}}
case *ast.Highlight:
node.Node = &apiv2pb.Node_HighlightNode{HighlightNode: &apiv2pb.HighlightNode{Content: n.Content}}
default:
node.Node = &apiv2pb.Node_TextNode{TextNode: &apiv2pb.TextNode{}}
}
return node
}
func convertToASTNodes(nodes []*apiv2pb.Node) []ast.Node {
rawNodes := []ast.Node{}
for _, node := range nodes {
rawNode := convertToASTNode(node)
rawNodes = append(rawNodes, rawNode)
}
return rawNodes
}
func convertToASTNode(node *apiv2pb.Node) ast.Node {
switch n := node.Node.(type) {
case *apiv2pb.Node_LineBreakNode:
return &ast.LineBreak{}
case *apiv2pb.Node_ParagraphNode:
children := convertToASTNodes(n.ParagraphNode.Children)
return &ast.Paragraph{Children: children}
case *apiv2pb.Node_CodeBlockNode:
return &ast.CodeBlock{Language: n.CodeBlockNode.Language, Content: n.CodeBlockNode.Content}
case *apiv2pb.Node_HeadingNode:
children := convertToASTNodes(n.HeadingNode.Children)
return &ast.Heading{Level: int(n.HeadingNode.Level), Children: children}
case *apiv2pb.Node_HorizontalRuleNode:
return &ast.HorizontalRule{Symbol: n.HorizontalRuleNode.Symbol}
case *apiv2pb.Node_BlockquoteNode:
children := convertToASTNodes(n.BlockquoteNode.Children)
return &ast.Blockquote{Children: children}
case *apiv2pb.Node_OrderedListNode:
children := convertToASTNodes(n.OrderedListNode.Children)
return &ast.OrderedList{Number: n.OrderedListNode.Number, Indent: int(n.OrderedListNode.Indent), Children: children}
case *apiv2pb.Node_UnorderedListNode:
children := convertToASTNodes(n.UnorderedListNode.Children)
return &ast.UnorderedList{Symbol: n.UnorderedListNode.Symbol, Indent: int(n.UnorderedListNode.Indent), Children: children}
case *apiv2pb.Node_TaskListNode:
children := convertToASTNodes(n.TaskListNode.Children)
return &ast.TaskList{Symbol: n.TaskListNode.Symbol, Indent: int(n.TaskListNode.Indent), Complete: n.TaskListNode.Complete, Children: children}
case *apiv2pb.Node_MathBlockNode:
return &ast.MathBlock{Content: n.MathBlockNode.Content}
case *apiv2pb.Node_TableNode:
return convertTableToASTNode(node)
case *apiv2pb.Node_TextNode:
return &ast.Text{Content: n.TextNode.Content}
case *apiv2pb.Node_BoldNode:
children := convertToASTNodes(n.BoldNode.Children)
return &ast.Bold{Symbol: n.BoldNode.Symbol, Children: children}
case *apiv2pb.Node_ItalicNode:
return &ast.Italic{Symbol: n.ItalicNode.Symbol, Content: n.ItalicNode.Content}
case *apiv2pb.Node_BoldItalicNode:
return &ast.BoldItalic{Symbol: n.BoldItalicNode.Symbol, Content: n.BoldItalicNode.Content}
case *apiv2pb.Node_CodeNode:
return &ast.Code{Content: n.CodeNode.Content}
case *apiv2pb.Node_ImageNode:
return &ast.Image{AltText: n.ImageNode.AltText, URL: n.ImageNode.Url}
case *apiv2pb.Node_LinkNode:
return &ast.Link{Text: n.LinkNode.Text, URL: n.LinkNode.Url}
case *apiv2pb.Node_AutoLinkNode:
return &ast.AutoLink{URL: n.AutoLinkNode.Url, IsRawText: n.AutoLinkNode.IsRawText}
case *apiv2pb.Node_TagNode:
return &ast.Tag{Content: n.TagNode.Content}
case *apiv2pb.Node_StrikethroughNode:
return &ast.Strikethrough{Content: n.StrikethroughNode.Content}
case *apiv2pb.Node_EscapingCharacterNode:
return &ast.EscapingCharacter{Symbol: n.EscapingCharacterNode.Symbol}
case *apiv2pb.Node_MathNode:
return &ast.Math{Content: n.MathNode.Content}
case *apiv2pb.Node_HighlightNode:
return &ast.Highlight{Content: n.HighlightNode.Content}
default:
return &ast.Text{}
}
}
func convertTableToASTNode(node *apiv2pb.Node) *ast.Table {
table := &ast.Table{
Header: node.GetTableNode().Header,
Delimiter: node.GetTableNode().Delimiter,
}
for _, row := range node.GetTableNode().Rows {
table.Rows = append(table.Rows, row.Cells)
}
return table
}
func convertTableFromASTNode(node *ast.Table) *apiv2pb.TableNode {
table := &apiv2pb.TableNode{
Header: node.Header,
Delimiter: node.Delimiter,
}
for _, row := range node.Rows {
table.Rows = append(table.Rows, &apiv2pb.TableNode_Row{Cells: row})
}
return table
}
func traverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
for _, node := range nodes {
fn(node)
switch n := node.(type) {
case *ast.Paragraph:
traverseASTNodes(n.Children, fn)
case *ast.Heading:
traverseASTNodes(n.Children, fn)
case *ast.Blockquote:
traverseASTNodes(n.Children, fn)
case *ast.OrderedList:
traverseASTNodes(n.Children, fn)
case *ast.UnorderedList:
traverseASTNodes(n.Children, fn)
case *ast.TaskList:
traverseASTNodes(n.Children, fn)
case *ast.Bold:
traverseASTNodes(n.Children, fn)
}
}
}

View File

@@ -1,30 +0,0 @@
package v2
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
)
func TestConvertFromASTNodes(t *testing.T) {
tests := []struct {
name string
rawNodes []ast.Node
want []*apiv2pb.Node
}{
{
name: "empty",
want: []*apiv2pb.Node{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := convertFromASTNodes(tt.rawNodes)
require.Equal(t, tt.want, got)
})
}
}

View File

@@ -1,12 +1,15 @@
package v2
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/cel-go/cel"
"github.com/lithammer/shortuuid/v4"
"github.com/pkg/errors"
"go.uber.org/zap"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
@@ -16,19 +19,17 @@ import (
apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/webhook"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store"
)
const (
DefaultPageSize = 10
MaxContentLength = 8 * 1024
ChunkSize = 64 * 1024 // 64 KiB
)
func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMemoRequest) (*apiv2pb.CreateMemoResponse, error) {
@@ -43,15 +44,11 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe
return nil, status.Errorf(codes.InvalidArgument, "content too long")
}
nodes, err := parser.Parse(tokenizer.Tokenize(request.Content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse memo content")
}
create := &store.Memo{
CreatorID: user.ID,
Content: request.Content,
Visibility: store.Visibility(request.Visibility.String()),
ResourceName: shortuuid.New(),
CreatorID: user.ID,
Content: request.Content,
Visibility: convertVisibilityToStore(request.Visibility),
}
// Find disable public memos system setting.
disablePublicMemosSystem, err := s.getDisablePublicMemosSystemSettingValue(ctx)
@@ -66,19 +63,6 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe
if err != nil {
return nil, err
}
metric.Enqueue("memo create")
// Dynamically upsert tags from memo content.
traverseASTNodes(nodes, func(node ast.Node) {
if tag, ok := node.(*ast.Tag); ok {
if _, err := s.Store.UpsertTag(ctx, &store.Tag{
Name: tag.Content,
CreatorID: user.ID,
}); err != nil {
log.Warn("Failed to create tag", zap.Error(err))
}
}
})
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
@@ -100,103 +84,52 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos
// Exclude comments by default.
ExcludeComments: true,
}
if request.Filter != "" {
filter, err := parseListMemosFilter(request.Filter)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
}
if len(filter.ContentSearch) > 0 {
memoFind.ContentSearch = filter.ContentSearch
}
if len(filter.Visibilities) > 0 {
memoFind.VisibilityList = filter.Visibilities
}
if filter.OrderByPinned {
memoFind.OrderByPinned = filter.OrderByPinned
}
if filter.DisplayTimeAfter != nil {
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
if displayWithUpdatedTs {
memoFind.UpdatedTsAfter = filter.DisplayTimeAfter
} else {
memoFind.CreatedTsAfter = filter.DisplayTimeAfter
}
}
if filter.DisplayTimeBefore != nil {
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
if displayWithUpdatedTs {
memoFind.UpdatedTsBefore = filter.DisplayTimeBefore
} else {
memoFind.CreatedTsBefore = filter.DisplayTimeBefore
}
}
if filter.Creator != nil {
username, err := ExtractUsernameFromName(*filter.Creator)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid creator name")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &username,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
memoFind.CreatorID = &user.ID
}
if filter.RowStatus != nil {
memoFind.RowStatus = filter.RowStatus
if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter")
}
var limit, offset int
if request.PageToken != "" {
var pageToken apiv2pb.PageToken
if err := unmarshalPageToken(request.PageToken, &pageToken); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err)
}
limit = int(pageToken.Limit)
offset = int(pageToken.Offset)
} else {
return nil, status.Errorf(codes.InvalidArgument, "filter is required")
limit = int(request.PageSize)
}
user, _ := getCurrentUser(ctx, s.Store)
// If the user is not authenticated, only public memos are visible.
if user == nil {
memoFind.VisibilityList = []store.Visibility{store.Public}
}
if user != nil && memoFind.CreatorID != nil && *memoFind.CreatorID != user.ID {
memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
}
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
if displayWithUpdatedTs {
memoFind.OrderByUpdatedTs = true
}
if request.Limit != 0 {
offset, limit := int(request.Offset), int(request.Limit)
memoFind.Offset = &offset
memoFind.Limit = &limit
if limit <= 0 {
limit = DefaultPageSize
}
limitPlusOne := limit + 1
memoFind.Limit = &limitPlusOne
memoFind.Offset = &offset
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, err
return nil, status.Errorf(codes.Internal, "failed to list memos")
}
memoMessages := make([]*apiv2pb.Memo, len(memos))
for i, memo := range memos {
memoMessages := []*apiv2pb.Memo{}
nextPageToken := ""
if len(memos) == limitPlusOne {
memos = memos[:limit]
nextPageToken, err = getPageToken(limit, offset+limit)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get next page token, error: %v", err)
}
}
for _, memo := range memos {
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
memoMessages[i] = memoMessage
memoMessages = append(memoMessages, memoMessage)
}
response := &apiv2pb.ListMemosResponse{
Memos: memoMessages,
Memos: memoMessages,
NextPageToken: nextPageToken,
}
return response, nil
}
@@ -234,13 +167,46 @@ func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv2pb.GetMemoRequ
return response, nil
}
func (s *APIV2Service) GetMemoByName(ctx context.Context, request *apiv2pb.GetMemoByNameRequest) (*apiv2pb.GetMemoByNameResponse, error) {
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ResourceName: &request.Name,
})
if err != nil {
return nil, err
}
if memo == nil {
return nil, status.Errorf(codes.NotFound, "memo not found")
}
if memo.Visibility != store.Public {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if memo.Visibility == store.Private && memo.CreatorID != user.ID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
response := &apiv2pb.GetMemoByNameResponse{
Memo: memoMessage,
}
return response, nil
}
func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMemoRequest) (*apiv2pb.UpdateMemoResponse, error) {
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
}
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &request.Id,
ID: &request.Memo.Id,
})
if err != nil {
return nil, err
@@ -256,45 +222,37 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
currentTs := time.Now().Unix()
update := &store.UpdateMemo{
ID: request.Id,
ID: request.Memo.Id,
UpdatedTs: &currentTs,
}
for _, path := range request.UpdateMask.Paths {
if path == "content" {
update.Content = &request.Memo.Content
nodes, err := parser.Parse(tokenizer.Tokenize(*update.Content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse memo content")
} else if path == "resource_name" {
update.ResourceName = &request.Memo.Name
if !util.ResourceNameMatcher.MatchString(*update.ResourceName) {
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name")
}
// Dynamically upsert tags from memo content.
traverseASTNodes(nodes, func(node ast.Node) {
if tag, ok := node.(*ast.Tag); ok {
if _, err := s.Store.UpsertTag(ctx, &store.Tag{
Name: tag.Content,
CreatorID: user.ID,
}); err != nil {
log.Warn("Failed to create tag", zap.Error(err))
}
}
})
} else if path == "nodes" {
nodes := convertToASTNodes(request.Memo.Nodes)
content := restore.Restore(nodes)
update.Content = &content
} else if path == "visibility" {
visibility := convertVisibilityToStore(request.Memo.Visibility)
// Find disable public memos system setting.
disablePublicMemosSystem, err := s.getDisablePublicMemosSystemSettingValue(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get system setting")
}
if disablePublicMemosSystem && visibility == store.Public {
return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled")
}
update.Visibility = &visibility
} else if path == "row_status" {
rowStatus := convertRowStatusToStore(request.Memo.RowStatus)
println("rowStatus", rowStatus)
update.RowStatus = &rowStatus
} else if path == "created_ts" {
createdTs := request.Memo.CreateTime.AsTime().Unix()
update.CreatedTs = &createdTs
} else if path == "pinned" {
if _, err := s.Store.UpsertMemoOrganizer(ctx, &store.MemoOrganizer{
MemoID: request.Id,
MemoID: request.Memo.Id,
UserID: user.ID,
Pinned: request.Memo.Pinned,
}); err != nil {
@@ -311,7 +269,7 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
}
memo, err = s.Store.GetMemo(ctx, &store.FindMemo{
ID: &request.Id,
ID: &request.Memo.Id,
})
if err != nil {
return nil, errors.Wrap(err, "failed to get memo")
@@ -346,6 +304,13 @@ func (s *APIV2Service) DeleteMemo(ctx context.Context, request *apiv2pb.DeleteMe
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil {
// Try to dispatch webhook when memo is deleted.
if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil {
log.Warn("Failed to dispatch memo deleted webhook", zap.Error(err))
}
}
if err = s.Store.DeleteMemo(ctx, &store.DeleteMemo{
ID: request.Id,
}); err != nil {
@@ -404,7 +369,6 @@ func (s *APIV2Service) CreateMemoComment(ctx context.Context, request *apiv2pb.C
return nil, status.Errorf(codes.Internal, "failed to create inbox")
}
}
metric.Enqueue("memo comment create")
response := &apiv2pb.CreateMemoCommentResponse{
Memo: memo,
@@ -467,52 +431,8 @@ func (s *APIV2Service) GetUserMemosStats(ctx context.Context, request *apiv2pb.G
ExcludeComments: true,
ExcludeContent: true,
}
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
if displayWithUpdatedTs {
memoFind.OrderByUpdatedTs = true
}
if request.Filter != "" {
filter, err := parseListMemosFilter(request.Filter)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
}
if len(filter.ContentSearch) > 0 {
memoFind.ContentSearch = filter.ContentSearch
}
if len(filter.Visibilities) > 0 {
memoFind.VisibilityList = filter.Visibilities
}
if filter.OrderByPinned {
memoFind.OrderByPinned = filter.OrderByPinned
}
if filter.DisplayTimeAfter != nil {
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
if displayWithUpdatedTs {
memoFind.UpdatedTsAfter = filter.DisplayTimeAfter
} else {
memoFind.CreatedTsAfter = filter.DisplayTimeAfter
}
}
if filter.DisplayTimeBefore != nil {
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
if displayWithUpdatedTs {
memoFind.UpdatedTsBefore = filter.DisplayTimeBefore
} else {
memoFind.CreatedTsBefore = filter.DisplayTimeBefore
}
}
if filter.RowStatus != nil {
memoFind.RowStatus = filter.RowStatus
}
if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter")
}
memos, err := s.Store.ListMemos(ctx, memoFind)
@@ -525,6 +445,10 @@ func (s *APIV2Service) GetUserMemosStats(ctx context.Context, request *apiv2pb.G
return nil, status.Errorf(codes.Internal, "invalid timezone location")
}
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
stats := make(map[string]int32)
for _, memo := range memos {
displayTs := memo.CreatedTs
@@ -540,11 +464,48 @@ func (s *APIV2Service) GetUserMemosStats(ctx context.Context, request *apiv2pb.G
return response, nil
}
func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*apiv2pb.Memo, error) {
rawNodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse memo content")
func (s *APIV2Service) ExportMemos(ctx context.Context, request *apiv2pb.ExportMemosRequest) (*apiv2pb.ExportMemosResponse, error) {
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
RowStatus: &normalRowStatus,
// Exclude comments by default.
ExcludeComments: true,
}
if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil {
return nil, status.Errorf(codes.Internal, "failed to build find memos with filter")
}
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos")
}
buf := new(bytes.Buffer)
writer := zip.NewWriter(buf)
for _, memo := range memos {
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
file, err := writer.Create(time.Unix(memo.CreatedTs, 0).Format(time.RFC3339) + ".md")
if err != nil {
return nil, status.Errorf(codes.Internal, "Failed to create memo file")
}
_, err = file.Write([]byte(memoMessage.Content))
if err != nil {
return nil, status.Errorf(codes.Internal, "Failed to write to memo file")
}
}
if err := writer.Close(); err != nil {
return nil, status.Errorf(codes.Internal, "Failed to close zip file writer")
}
return &apiv2pb.ExportMemosResponse{
Content: buf.Bytes(),
}, nil
}
func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*apiv2pb.Memo, error) {
displayTs := memo.CreatedTs
if displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx); err == nil && displayWithUpdatedTs {
displayTs = memo.UpdatedTs
@@ -565,8 +526,14 @@ func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
return nil, errors.Wrap(err, "failed to list memo resources")
}
listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &apiv2pb.ListMemoReactionsRequest{Id: memo.ID})
if err != nil {
return nil, errors.Wrap(err, "failed to list memo reactions")
}
return &apiv2pb.Memo{
Id: int32(memo.ID),
Name: memo.ResourceName,
RowStatus: convertRowStatusFromStore(memo.RowStatus),
Creator: fmt.Sprintf("%s%s", UserNamePrefix, creator.Username),
CreatorId: int32(memo.CreatorID),
@@ -574,42 +541,46 @@ func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
DisplayTime: timestamppb.New(time.Unix(displayTs, 0)),
Content: memo.Content,
Nodes: convertFromASTNodes(rawNodes),
Visibility: convertVisibilityFromStore(memo.Visibility),
Pinned: memo.Pinned,
ParentId: memo.ParentID,
Relations: listMemoRelationsResponse.Relations,
Resources: listMemoResourcesResponse.Resources,
Reactions: listMemoReactionsResponse.Reactions,
}, nil
}
func (s *APIV2Service) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) {
memoDisplayWithUpdatedTsSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
memoDisplayWithUpdatedTsSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: apiv1.SystemSettingMemoDisplayWithUpdatedTsName.String(),
})
if err != nil {
return false, errors.Wrap(err, "failed to find system setting")
}
if memoDisplayWithUpdatedTsSetting == nil {
return false, nil
}
memoDisplayWithUpdatedTs := false
if memoDisplayWithUpdatedTsSetting != nil {
err = json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs)
if err != nil {
return false, errors.Wrap(err, "failed to unmarshal system setting value")
}
if err := json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs); err != nil {
return false, errors.Wrap(err, "failed to unmarshal system setting value")
}
return memoDisplayWithUpdatedTs, nil
}
func (s *APIV2Service) getDisablePublicMemosSystemSettingValue(ctx context.Context) (bool, error) {
disablePublicMemosSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: apiv1.SystemSettingDisablePublicMemosName.String(),
})
if err != nil {
return false, errors.Wrap(err, "failed to find system setting")
}
if disablePublicMemosSystemSetting == nil {
return false, nil
}
disablePublicMemos := false
err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos)
if err != nil {
if err := json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos); err != nil {
return false, errors.Wrap(err, "failed to unmarshal system setting value")
}
return disablePublicMemos, nil
@@ -641,6 +612,90 @@ func convertVisibilityToStore(visibility apiv2pb.Visibility) store.Visibility {
}
}
func (s *APIV2Service) buildMemoFindWithFilter(ctx context.Context, find *store.FindMemo, filter string) error {
user, _ := getCurrentUser(ctx, s.Store)
if find == nil {
find = &store.FindMemo{}
}
if filter != "" {
filter, err := parseListMemosFilter(filter)
if err != nil {
return status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
}
if len(filter.ContentSearch) > 0 {
find.ContentSearch = filter.ContentSearch
}
if len(filter.Visibilities) > 0 {
find.VisibilityList = filter.Visibilities
}
if filter.OrderByPinned {
find.OrderByPinned = filter.OrderByPinned
}
if filter.DisplayTimeAfter != nil {
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
if displayWithUpdatedTs {
find.UpdatedTsAfter = filter.DisplayTimeAfter
} else {
find.CreatedTsAfter = filter.DisplayTimeAfter
}
}
if filter.DisplayTimeBefore != nil {
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
if displayWithUpdatedTs {
find.UpdatedTsBefore = filter.DisplayTimeBefore
} else {
find.CreatedTsBefore = filter.DisplayTimeBefore
}
}
if filter.Creator != nil {
username, err := ExtractUsernameFromName(*filter.Creator)
if err != nil {
return status.Errorf(codes.InvalidArgument, "invalid creator name")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &username,
})
if err != nil {
return status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return status.Errorf(codes.NotFound, "user not found")
}
find.CreatorID = &user.ID
}
if filter.RowStatus != nil {
find.RowStatus = filter.RowStatus
}
}
// If the user is not authenticated, only public memos are visible.
if user == nil {
if filter == "" {
// If no filter is provided, return an error.
return status.Errorf(codes.InvalidArgument, "filter is required")
}
find.VisibilityList = []store.Visibility{store.Public}
} else if find.CreatorID != nil && *find.CreatorID != user.ID {
find.VisibilityList = []store.Visibility{store.Public, store.Protected}
}
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value")
}
if displayWithUpdatedTs {
find.OrderByUpdatedTs = true
}
return nil
}
// ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter.
var ListMemosFilterCELAttributes = []cel.EnvOption{
cel.Variable("content_search", cel.ListType(cel.StringType)),
@@ -736,6 +791,11 @@ func (s *APIV2Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *api
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated")
}
// DispatchMemoDeletedWebhook dispatches webhook when memo is deleted.
func (s *APIV2Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *apiv2pb.Memo) error {
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted")
}
func (s *APIV2Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *apiv2pb.Memo, activityType string) error {
webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
CreatorID: &memo.CreatorId,
@@ -743,7 +803,6 @@ func (s *APIV2Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *api
if err != nil {
return err
}
metric.Enqueue("webhook dispatch")
for _, hook := range webhooks {
payload := convertMemoToWebhookPayload(memo)
payload.ActivityType = activityType

View File

@@ -0,0 +1,83 @@
package v2
import (
"context"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) ListMemoReactions(ctx context.Context, request *apiv2pb.ListMemoReactionsRequest) (*apiv2pb.ListMemoReactionsResponse, error) {
contentID := fmt.Sprintf("memos/%d", request.Id)
reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{
ContentID: &contentID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list reactions")
}
response := &apiv2pb.ListMemoReactionsResponse{
Reactions: []*apiv2pb.Reaction{},
}
for _, reaction := range reactions {
reactionMessage, err := s.convertReactionFromStore(ctx, reaction)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert reaction")
}
response.Reactions = append(response.Reactions, reactionMessage)
}
return response, nil
}
func (s *APIV2Service) UpsertMemoReaction(ctx context.Context, request *apiv2pb.UpsertMemoReactionRequest) (*apiv2pb.UpsertMemoReactionResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
reaction, err := s.Store.UpsertReaction(ctx, &storepb.Reaction{
CreatorId: user.ID,
ContentId: request.Reaction.ContentId,
ReactionType: storepb.Reaction_Type(request.Reaction.ReactionType),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert reaction")
}
reactionMessage, err := s.convertReactionFromStore(ctx, reaction)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert reaction")
}
return &apiv2pb.UpsertMemoReactionResponse{
Reaction: reactionMessage,
}, nil
}
func (s *APIV2Service) DeleteMemoReaction(ctx context.Context, request *apiv2pb.DeleteMemoReactionRequest) (*apiv2pb.DeleteMemoReactionResponse, error) {
if err := s.Store.DeleteReaction(ctx, &store.DeleteReaction{
ID: request.Id,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete reaction")
}
return &apiv2pb.DeleteMemoReactionResponse{}, nil
}
func (s *APIV2Service) convertReactionFromStore(ctx context.Context, reaction *storepb.Reaction) (*apiv2pb.Reaction, error) {
creator, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &reaction.CreatorId,
})
if err != nil {
return nil, err
}
return &apiv2pb.Reaction{
Id: reaction.Id,
Creator: fmt.Sprintf("%s%s", UserNamePrefix, creator.Username),
ContentId: reaction.ContentId,
ReactionType: apiv2pb.Reaction_Type(reaction.ReactionType),
}, nil
}

View File

@@ -10,8 +10,9 @@ import (
)
const (
UserNamePrefix = "users/"
InboxNamePrefix = "inboxes/"
WorkspaceSettingNamePrefix = "settings/"
UserNamePrefix = "users/"
InboxNamePrefix = "inboxes/"
)
// GetNameParentTokens returns the tokens from a resource name.
@@ -34,6 +35,14 @@ func GetNameParentTokens(name string, tokenPrefixes ...string) ([]string, error)
return tokens, nil
}
func ExtractWorkspaceSettingKeyFromName(name string) (string, error) {
tokens, err := GetNameParentTokens(name, WorkspaceSettingNamePrefix)
if err != nil {
return "", err
}
return tokens[0], nil
}
// ExtractUsernameFromName returns the username from a resource name.
func ExtractUsernameFromName(name string) (string, error) {
tokens, err := GetNameParentTokens(name, UserNamePrefix)

View File

@@ -5,6 +5,7 @@ import (
"net/url"
"time"
"github.com/lithammer/shortuuid/v4"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -30,6 +31,7 @@ func (s *APIV2Service) CreateResource(ctx context.Context, request *apiv2pb.Crea
}
create := &store.Resource{
ResourceName: shortuuid.New(),
CreatorID: user.ID,
Filename: request.Filename,
ExternalLink: request.ExternalLink,
@@ -42,6 +44,7 @@ func (s *APIV2Service) CreateResource(ctx context.Context, request *apiv2pb.Crea
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create resource: %v", err)
}
return &apiv2pb.CreateResourceResponse{
Resource: s.convertResourceFromStore(ctx, resource),
}, nil
@@ -66,6 +69,38 @@ func (s *APIV2Service) ListResources(ctx context.Context, _ *apiv2pb.ListResourc
return response, nil
}
func (s *APIV2Service) GetResource(ctx context.Context, request *apiv2pb.GetResourceRequest) (*apiv2pb.GetResourceResponse, error) {
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &request.Id,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err)
}
if resource == nil {
return nil, status.Errorf(codes.NotFound, "resource not found")
}
return &apiv2pb.GetResourceResponse{
Resource: s.convertResourceFromStore(ctx, resource),
}, nil
}
func (s *APIV2Service) GetResourceByName(ctx context.Context, request *apiv2pb.GetResourceByNameRequest) (*apiv2pb.GetResourceByNameResponse, error) {
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ResourceName: &request.Name,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err)
}
if resource == nil {
return nil, status.Errorf(codes.NotFound, "resource not found")
}
return &apiv2pb.GetResourceByNameResponse{
Resource: s.convertResourceFromStore(ctx, resource),
}, nil
}
func (s *APIV2Service) UpdateResource(ctx context.Context, request *apiv2pb.UpdateResourceRequest) (*apiv2pb.UpdateResourceResponse, error) {
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
@@ -130,6 +165,7 @@ func (s *APIV2Service) convertResourceFromStore(ctx context.Context, resource *s
return &apiv2pb.Resource{
Id: resource.ID,
Name: resource.ResourceName,
CreateTime: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
Filename: resource.Filename,
ExternalLink: resource.ExternalLink,

View File

@@ -1,92 +0,0 @@
package v2
import (
"context"
"strconv"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) GetSystemInfo(ctx context.Context, _ *apiv2pb.GetSystemInfoRequest) (*apiv2pb.GetSystemInfoResponse, error) {
defaultSystemInfo := &apiv2pb.SystemInfo{}
// Get the database size if the user is a host.
currentUser, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser != nil && currentUser.Role == store.RoleHost {
size, err := s.Store.GetCurrentDBSize(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get db size: %v", err)
}
defaultSystemInfo.DbSize = size
}
response := &apiv2pb.GetSystemInfoResponse{
SystemInfo: defaultSystemInfo,
}
return response, nil
}
func (s *APIV2Service) UpdateSystemInfo(ctx context.Context, request *apiv2pb.UpdateSystemInfoRequest) (*apiv2pb.UpdateSystemInfoResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if user.Role != store.RoleHost {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
}
// Update system settings.
for _, field := range request.UpdateMask.Paths {
if field == "allow_registration" {
_, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: "allow-signup",
Value: strconv.FormatBool(request.SystemInfo.AllowRegistration),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update allow_registration system setting: %v", err)
}
} else if field == "disable_password_login" {
_, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: "disable-password-login",
Value: strconv.FormatBool(request.SystemInfo.DisablePasswordLogin),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update disable_password_login system setting: %v", err)
}
} else if field == "additional_script" {
_, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: "additional-script",
Value: request.SystemInfo.AdditionalScript,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update additional_script system setting: %v", err)
}
} else if field == "additional_style" {
_, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: "additional-style",
Value: request.SystemInfo.AdditionalStyle,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update additional_style system setting: %v", err)
}
}
}
systemInfo, err := s.GetSystemInfo(ctx, &apiv2pb.GetSystemInfoRequest{})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get system info: %v", err)
}
return &apiv2pb.UpdateSystemInfoResponse{
SystemInfo: systemInfo.SystemInfo,
}, nil
}

View File

@@ -3,11 +3,14 @@ package v2
import (
"context"
"fmt"
"regexp"
"slices"
"sort"
"github.com/pkg/errors"
"golang.org/x/exp/slices"
"github.com/yourselfhosted/gomark/ast"
"github.com/yourselfhosted/gomark/parser"
"github.com/yourselfhosted/gomark/parser/tokenizer"
"github.com/yourselfhosted/gomark/restore"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -38,6 +41,15 @@ func (s *APIV2Service) UpsertTag(ctx context.Context, request *apiv2pb.UpsertTag
}, nil
}
func (s *APIV2Service) BatchUpsertTag(ctx context.Context, request *apiv2pb.BatchUpsertTagRequest) (*apiv2pb.BatchUpsertTagResponse, error) {
for _, r := range request.Requests {
if _, err := s.UpsertTag(ctx, r); err != nil {
return nil, status.Errorf(codes.Internal, "failed to batch upsert tags: %v", err)
}
}
return &apiv2pb.BatchUpsertTagResponse{}, nil
}
func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRequest) (*apiv2pb.ListTagsResponse, error) {
username, err := ExtractUsernameFromName(request.User)
if err != nil {
@@ -70,6 +82,71 @@ func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRe
return response, nil
}
func (s *APIV2Service) RenameTag(ctx context.Context, request *apiv2pb.RenameTagRequest) (*apiv2pb.RenameTagResponse, error) {
username, err := ExtractUsernameFromName(request.User)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &username,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
// Find all related memos.
memos, err := s.Store.ListMemos(ctx, &store.FindMemo{
CreatorID: &user.ID,
ContentSearch: []string{fmt.Sprintf("#%s", request.OldName)},
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
// Replace tag name in memo content.
for _, memo := range memos {
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to parse memo: %v", err)
}
traverseASTNodes(nodes, func(node ast.Node) {
if tag, ok := node.(*ast.Tag); ok && tag.Content == request.OldName {
tag.Content = request.NewName
}
})
content := restore.Restore(nodes)
if err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
Content: &content,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update memo: %v", err)
}
}
// Delete old tag and create new tag.
if err := s.Store.DeleteTag(ctx, &store.DeleteTag{
CreatorID: user.ID,
Name: request.OldName,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err)
}
tag, err := s.Store.UpsertTag(ctx, &store.Tag{
CreatorID: user.ID,
Name: request.NewName,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err)
}
tagMessage, err := s.convertTagFromStore(ctx, tag)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err)
}
return &apiv2pb.RenameTagResponse{Tag: tagMessage}, nil
}
func (s *APIV2Service) DeleteTag(ctx context.Context, request *apiv2pb.DeleteTagRequest) (*apiv2pb.DeleteTagResponse, error) {
username, err := ExtractUsernameFromName(request.Tag.Creator)
if err != nil {
@@ -114,7 +191,7 @@ func (s *APIV2Service) GetTagSuggestions(ctx context.Context, request *apiv2pb.G
ContentSearch: []string{"#"},
RowStatus: &normalRowStatus,
}
memoList, err := s.Store.ListMemos(ctx, memoFind)
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
@@ -131,12 +208,21 @@ func (s *APIV2Service) GetTagSuggestions(ctx context.Context, request *apiv2pb.G
tagNameList = append(tagNameList, tag.Name)
}
tagMapSet := make(map[string]bool)
for _, memo := range memoList {
for _, tag := range findTagListFromMemoContent(memo.Content) {
if !slices.Contains(tagNameList, tag) {
tagMapSet[tag] = true
}
for _, memo := range memos {
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse memo content")
}
// Dynamically upsert tags from memo content.
traverseASTNodes(nodes, func(node ast.Node) {
if tagNode, ok := node.(*ast.Tag); ok {
tag := tagNode.Content
if !slices.Contains(tagNameList, tag) {
tagMapSet[tag] = true
}
}
})
}
suggestions := []string{}
for tag := range tagMapSet {
@@ -162,20 +248,24 @@ func (s *APIV2Service) convertTagFromStore(ctx context.Context, tag *store.Tag)
}, nil
}
var tagRegexp = regexp.MustCompile(`#([^\s#,]+)`)
func findTagListFromMemoContent(memoContent string) []string {
tagMapSet := make(map[string]bool)
matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
for _, v := range matches {
tagName := v[1]
tagMapSet[tagName] = true
func traverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
for _, node := range nodes {
fn(node)
switch n := node.(type) {
case *ast.Paragraph:
traverseASTNodes(n.Children, fn)
case *ast.Heading:
traverseASTNodes(n.Children, fn)
case *ast.Blockquote:
traverseASTNodes(n.Children, fn)
case *ast.OrderedList:
traverseASTNodes(n.Children, fn)
case *ast.UnorderedList:
traverseASTNodes(n.Children, fn)
case *ast.TaskList:
traverseASTNodes(n.Children, fn)
case *ast.Bold:
traverseASTNodes(n.Children, fn)
}
}
tagList := []string{}
for tag := range tagMapSet {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
return tagList
}

View File

@@ -4,11 +4,10 @@ import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
@@ -18,15 +17,12 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/usememos/memos/api/auth"
"github.com/usememos/memos/internal/util"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
var (
usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$")
)
func (s *APIV2Service) ListUsers(ctx context.Context, _ *apiv2pb.ListUsersRequest) (*apiv2pb.ListUsersResponse, error) {
currentUser, err := getCurrentUser(ctx, s.Store)
if err != nil {
@@ -85,7 +81,7 @@ func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUs
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "name is required")
}
if !usernameMatcher.MatchString(strings.ToLower(username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(username)) {
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", username)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
@@ -134,6 +130,10 @@ func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUs
return nil, status.Errorf(codes.NotFound, "user not found")
}
if s.Profile.Mode == "demo" && user.Username == "memos-demo" {
return nil, status.Errorf(codes.PermissionDenied, "unauthorized to update user in demo mode")
}
currentTs := time.Now().Unix()
update := &store.UpdateUser{
ID: user.ID,
@@ -141,7 +141,7 @@ func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUs
}
for _, field := range request.UpdateMask.Paths {
if field == "username" {
if !usernameMatcher.MatchString(strings.ToLower(request.User.Username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(request.User.Username)) {
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username)
}
update.Username = &request.User.Username
@@ -201,6 +201,10 @@ func (s *APIV2Service) DeleteUser(ctx context.Context, request *apiv2pb.DeleteUs
return nil, status.Errorf(codes.NotFound, "user not found")
}
if s.Profile.Mode == "demo" && user.Username == "memos-demo" {
return nil, status.Errorf(codes.PermissionDenied, "unauthorized to delete this user in demo mode")
}
if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
ID: user.ID,
}); err != nil {
@@ -215,6 +219,7 @@ func getDefaultUserSetting() *apiv2pb.UserSetting {
Locale: "en",
Appearance: "system",
MemoVisibility: "PRIVATE",
CompactView: false,
}
}
@@ -240,6 +245,8 @@ func (s *APIV2Service) GetUserSetting(ctx context.Context, _ *apiv2pb.GetUserSet
userSettingMessage.MemoVisibility = setting.GetMemoVisibility()
} else if setting.Key == storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID {
userSettingMessage.TelegramUserId = setting.GetTelegramUserId()
} else if setting.Key == storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW {
userSettingMessage.CompactView = setting.GetCompactView()
}
}
return &apiv2pb.GetUserSettingResponse{
@@ -298,6 +305,16 @@ func (s *APIV2Service) UpdateUserSetting(ctx context.Context, request *apiv2pb.U
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
}
} else if field == "compact_view" {
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW,
Value: &storepb.UserSetting_CompactView{
CompactView: request.Setting.CompactView,
},
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
}
} else {
return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
}

View File

@@ -21,7 +21,8 @@ import (
)
type APIV2Service struct {
apiv2pb.UnimplementedSystemServiceServer
apiv2pb.UnimplementedWorkspaceServiceServer
apiv2pb.UnimplementedWorkspaceSettingServiceServer
apiv2pb.UnimplementedAuthServiceServer
apiv2pb.UnimplementedUserServiceServer
apiv2pb.UnimplementedMemoServiceServer
@@ -30,7 +31,6 @@ type APIV2Service struct {
apiv2pb.UnimplementedInboxServiceServer
apiv2pb.UnimplementedActivityServiceServer
apiv2pb.UnimplementedWebhookServiceServer
apiv2pb.UnimplementedMarkdownServiceServer
Secret string
Profile *profile.Profile
@@ -56,7 +56,8 @@ func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store
grpcServerPort: grpcServerPort,
}
apiv2pb.RegisterSystemServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterWorkspaceServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterWorkspaceSettingServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterAuthServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterUserServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterMemoServiceServer(grpcServer, apiv2Service)
@@ -65,7 +66,6 @@ func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store
apiv2pb.RegisterInboxServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterActivityServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterWebhookServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterMarkdownServiceServer(grpcServer, apiv2Service)
reflection.Register(grpcServer)
return apiv2Service
@@ -89,7 +89,10 @@ func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error
}
gwMux := runtime.NewServeMux()
if err := apiv2pb.RegisterSystemServiceHandler(context.Background(), gwMux, conn); err != nil {
if err := apiv2pb.RegisterWorkspaceServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterWorkspaceSettingServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterAuthServiceHandler(context.Background(), gwMux, conn); err != nil {
@@ -116,9 +119,6 @@ func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error
if err := apiv2pb.RegisterWebhookServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterMarkdownServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
e.Any("/api/v2/*", echo.WrapHandler(gwMux))
// GRPC web proxy.

View File

@@ -0,0 +1,17 @@
package v2
import (
"context"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
)
func (s *APIV2Service) GetWorkspaceProfile(_ context.Context, _ *apiv2pb.GetWorkspaceProfileRequest) (*apiv2pb.GetWorkspaceProfileResponse, error) {
workspaceProfile := &apiv2pb.WorkspaceProfile{
Version: s.Profile.Version,
Mode: s.Profile.Mode,
}
return &apiv2pb.GetWorkspaceProfileResponse{
WorkspaceProfile: workspaceProfile,
}, nil
}

View File

@@ -0,0 +1,95 @@
package v2
import (
"context"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) GetWorkspaceSetting(ctx context.Context, request *apiv2pb.GetWorkspaceSettingRequest) (*apiv2pb.GetWorkspaceSettingResponse, error) {
settingKeyString, err := ExtractWorkspaceSettingKeyFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid workspace setting name: %v", err)
}
settingKey := storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[settingKeyString])
workspaceSetting, err := s.Store.GetWorkspaceSettingV1(ctx, &store.FindWorkspaceSettingV1{
Key: settingKey,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get workspace setting: %v", err)
}
if workspaceSetting == nil {
return nil, status.Errorf(codes.NotFound, "workspace setting not found")
}
return &apiv2pb.GetWorkspaceSettingResponse{
Setting: convertWorkspaceSettingFromStore(workspaceSetting),
}, nil
}
func (s *APIV2Service) SetWorkspaceSetting(ctx context.Context, request *apiv2pb.SetWorkspaceSettingRequest) (*apiv2pb.SetWorkspaceSettingResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if user.Role != store.RoleHost {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if _, err := s.Store.UpsertWorkspaceSettingV1(ctx, convertWorkspaceSettingToStore(request.Setting)); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert workspace setting: %v", err)
}
return &apiv2pb.SetWorkspaceSettingResponse{}, nil
}
func convertWorkspaceSettingFromStore(setting *storepb.WorkspaceSetting) *apiv2pb.WorkspaceSetting {
return &apiv2pb.WorkspaceSetting{
Name: fmt.Sprintf("%s%s", WorkspaceSettingNamePrefix, setting.Key.String()),
Value: &apiv2pb.WorkspaceSetting_GeneralSetting{
GeneralSetting: convertWorkspaceGeneralSettingFromStore(setting.GetGeneral()),
},
}
}
func convertWorkspaceSettingToStore(setting *apiv2pb.WorkspaceSetting) *storepb.WorkspaceSetting {
settingKeyString, _ := ExtractWorkspaceSettingKeyFromName(setting.Name)
return &storepb.WorkspaceSetting{
Key: storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[settingKeyString]),
Value: &storepb.WorkspaceSetting_General{
General: convertWorkspaceGeneralSettingToStore(setting.GetGeneralSetting()),
},
}
}
func convertWorkspaceGeneralSettingFromStore(setting *storepb.WorkspaceGeneralSetting) *apiv2pb.WorkspaceGeneralSetting {
if setting == nil {
return nil
}
return &apiv2pb.WorkspaceGeneralSetting{
InstanceUrl: setting.InstanceUrl,
DisallowSignup: setting.DisallowSignup,
DisallowPasswordLogin: setting.DisallowPasswordLogin,
AdditionalScript: setting.AdditionalScript,
AdditionalStyle: setting.AdditionalStyle,
}
}
func convertWorkspaceGeneralSettingToStore(setting *apiv2pb.WorkspaceGeneralSetting) *storepb.WorkspaceGeneralSetting {
if setting == nil {
return nil
}
return &storepb.WorkspaceGeneralSetting{
InstanceUrl: setting.InstanceUrl,
DisallowSignup: setting.DisallowSignup,
DisallowPasswordLogin: setting.DisallowPasswordLogin,
AdditionalScript: setting.AdditionalScript,
AdditionalStyle: setting.AdditionalStyle,
}
}

View File

@@ -12,10 +12,10 @@ import (
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/usememos/memos/internal/jobs"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/server"
_profile "github.com/usememos/memos/server/profile"
"github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store/db"
)
@@ -32,14 +32,14 @@ const (
)
var (
profile *_profile.Profile
mode string
addr string
port int
data string
driver string
dsn string
enableMetric bool
profile *_profile.Profile
mode string
addr string
port int
data string
driver string
dsn string
serveFrontend bool
rootCmd = &cobra.Command{
Use: "memos",
@@ -58,28 +58,20 @@ var (
return
}
store := store.New(dbDriver, profile)
storeInstance := store.New(dbDriver, profile)
if err := storeInstance.MigrateManually(ctx); err != nil {
cancel()
log.Error("failed to migrate manually", zap.Error(err))
return
}
go func() {
if err := store.MigrateResourceInternalPath(ctx); err != nil {
cancel()
log.Error("failed to migrate resource internal path", zap.Error(err))
return
}
}()
s, err := server.NewServer(ctx, profile, store)
s, err := server.NewServer(ctx, profile, storeInstance)
if err != nil {
cancel()
log.Error("failed to create server", zap.Error(err))
return
}
if profile.Metric {
// nolint
metric.NewMetricClient(s.ID, *profile)
}
c := make(chan os.Signal, 1)
// Trigger graceful shutdown on SIGINT or SIGTERM.
// The default signal sent by the `kill` command is SIGTERM,
@@ -94,6 +86,9 @@ var (
printGreetings()
// update (pre-sign) object storage links if applicable
go jobs.RunPreSignLinks(ctx, storeInstance)
if err := s.Start(ctx); err != nil {
if err != http.ErrServerClosed {
log.Error("failed to start server", zap.Error(err))
@@ -121,7 +116,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&data, "data", "d", "", "data directory")
rootCmd.PersistentFlags().StringVarP(&driver, "driver", "", "", "database driver")
rootCmd.PersistentFlags().StringVarP(&dsn, "dsn", "", "", "database source name(aka. DSN)")
rootCmd.PersistentFlags().BoolVarP(&enableMetric, "metric", "", true, "allow metric collection")
rootCmd.PersistentFlags().BoolVarP(&serveFrontend, "frontend", "", true, "serve frontend files")
err := viper.BindPFlag("mode", rootCmd.PersistentFlags().Lookup("mode"))
if err != nil {
@@ -147,7 +142,7 @@ func init() {
if err != nil {
panic(err)
}
err = viper.BindPFlag("metric", rootCmd.PersistentFlags().Lookup("metric"))
err = viper.BindPFlag("frontend", rootCmd.PersistentFlags().Lookup("frontend"))
if err != nil {
panic(err)
}
@@ -156,7 +151,7 @@ func init() {
viper.SetDefault("driver", "sqlite")
viper.SetDefault("addr", "")
viper.SetDefault("port", 8081)
viper.SetDefault("metric", true)
viper.SetDefault("frontend", true)
viper.SetEnvPrefix("memos")
}
@@ -169,17 +164,18 @@ func initConfig() {
return
}
println("---")
println("Server profile")
println("data:", profile.Data)
println("dsn:", profile.DSN)
println("addr:", profile.Addr)
println("port:", profile.Port)
println("mode:", profile.Mode)
println("driver:", profile.Driver)
println("version:", profile.Version)
println("metric:", profile.Metric)
println("---")
fmt.Printf(`---
Server profile
version: %s
data: %s
dsn: %s
addr: %s
port: %d
mode: %s
driver: %s
frontend: %t
---
`, profile.Version, profile.Data, profile.DSN, profile.Addr, profile.Port, profile.Mode, profile.Driver, profile.Frontend)
}
func printGreetings() {
@@ -189,11 +185,12 @@ func printGreetings() {
} else {
fmt.Printf("Version %s has been started on address '%s' and port %d\n", profile.Version, profile.Addr, profile.Port)
}
println("---")
println("See more in:")
fmt.Printf("👉Website: %s\n", "https://usememos.com")
fmt.Printf("👉GitHub: %s\n", "https://github.com/usememos/memos")
println("---")
fmt.Printf(`---
See more in:
👉Website: %s
👉GitHub: %s
---
`, "https://usememos.com", "https://github.com/usememos/memos")
}
func main() {

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@ Memos is built with a curated tech stack. It is optimized for developer experien
3. Install frontend dependencies and generate TypeScript code from protobuf
```
cd web && pnpm i && pnpm type-gen
cd web && pnpm i
```
4. Start the dev server of frontend

89
go.mod
View File

@@ -1,38 +1,40 @@
module github.com/usememos/memos
go 1.21
go 1.22
require (
github.com/aws/aws-sdk-go-v2 v1.24.1
github.com/aws/aws-sdk-go-v2/config v1.26.3
github.com/aws/aws-sdk-go-v2/credentials v1.16.14
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0
github.com/aws/aws-sdk-go-v2 v1.25.0
github.com/aws/aws-sdk-go-v2/config v1.27.0
github.com/aws/aws-sdk-go-v2/credentials v1.17.0
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1
github.com/disintegration/imaging v1.6.2
github.com/go-sql-driver/mysql v1.7.1
github.com/google/cel-go v0.18.2
github.com/google/uuid v1.5.0
github.com/google/cel-go v0.20.0
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.1.2
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
github.com/improbable-eng/grpc-web v0.15.0
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.11.4
github.com/lib/pq v1.10.9
github.com/lithammer/shortuuid/v4 v4.0.0
github.com/microcosm-cc/bluemonday v1.0.26
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4
github.com/swaggo/swag v1.16.2
github.com/swaggo/swag v1.16.3
github.com/yourselfhosted/gomark v0.0.0-20240222150908-75b2b15a7837
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.18.0
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3
golang.org/x/mod v0.14.0
golang.org/x/net v0.20.0
golang.org/x/oauth2 v0.16.0
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1
google.golang.org/grpc v1.60.1
modernc.org/sqlite v1.28.0
golang.org/x/crypto v0.19.0
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
golang.org/x/mod v0.15.0
golang.org/x/net v0.21.0
golang.org/x/oauth2 v0.17.0
google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9
google.golang.org/grpc v1.61.1
modernc.org/sqlite v1.29.1
)
require (
@@ -45,11 +47,12 @@ require (
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/spec v0.20.14 // indirect
github.com/go-openapi/swag v0.22.7 // indirect
github.com/go-openapi/swag v0.22.9 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/cors v1.10.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
@@ -57,40 +60,37 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/tools v0.17.0 // indirect
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.15 // indirect
modernc.org/libc v1.40.1 // indirect
golang.org/x/tools v0.18.0 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.41.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
nhooyr.io/websocket v1.8.10 // indirect
)
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 // indirect
github.com/aws/smithy-go v1.20.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -102,7 +102,6 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/posthog/posthog-go v0.0.0-20240110105835-f2ee529330e9
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
@@ -110,7 +109,7 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect

185
go.sum
View File

@@ -26,44 +26,44 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo=
github.com/aws/aws-sdk-go-v2/config v1.26.3 h1:dKuc2jdp10y13dEEvPqWxqLoc0vF3Z9FC45MvuQSxOA=
github.com/aws/aws-sdk-go-v2/config v1.26.3/go.mod h1:Bxgi+DeeswYofcYO0XyGClwlrq3DZEXli0kLf4hkGA0=
github.com/aws/aws-sdk-go-v2/credentials v1.16.14 h1:mMDTwwYO9A0/JbOCOG7EOZHtYM+o7OfGWfu0toa23VE=
github.com/aws/aws-sdk-go-v2/credentials v1.16.14/go.mod h1:cniAUh3ErQPHtCQGPT5ouvSAQ0od8caTO9OOuufZOAE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11 h1:I6lAa3wBWfCz/cKkOpAcumsETRkFAl70sWi8ItcMEsM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11/go.mod h1:be1NIO30kJA23ORBLqPo1LttEM6tPNSEcjkd1eKzNW0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10 h1:5oE2WzJE56/mVveuDZPJESKlg/00AaS2pY2QZcnxg4M=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10/go.mod h1:FHbKWQtRBYUz4vO5WBWjzMD2by126ny5y/1EoaWoLfI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 h1:L0ai8WICYHozIKK+OtPzVJBugL7culcuM4E4JOpIEm8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10/go.mod h1:byqfyxJBshFk0fF9YmK0M0ugIO8OWjzH2T3bPG4eGuA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 h1:KOxnQeWy5sXyS37fdKEvAsGHOr9fa/qvwxfJurR/BzE=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10/go.mod h1:jMx5INQFYFYB3lQD9W0D8Ohgq6Wnl7NYOJ2TQndbulI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 h1:PJTdBMsyvra6FtED7JZtDpQrIAflYDHFoZAu/sKYkwU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0/go.mod h1:4qXHrG1Ne3VGIMZPCB8OjH/pLFO94sKABIusjh0KWPU=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 h1:dGrs+Q/WzhsiUKh82SfTVN66QzyulXuMDTV/G8ZxOac=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 h1:Yf2MIo9x+0tyv76GljxzqA3WtC5mw7NmazD2chwjxE4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/aws/aws-sdk-go-v2 v1.25.0 h1:sv7+1JVJxOu/dD/sz/csHX7jFqmP001TIY7aytBWDSQ=
github.com/aws/aws-sdk-go-v2 v1.25.0/go.mod h1:G104G1Aho5WqF+SR3mDIobTABQzpYV0WxMsKxlMggOA=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 h1:2UO6/nT1lCZq1LqM67Oa4tdgP1CvL1sLSxvuD+VrOeE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0/go.mod h1:5zGj2eA85ClyedTDK+Whsu+w9yimnVIZvhvBKrDquM8=
github.com/aws/aws-sdk-go-v2/config v1.27.0 h1:J5sdGCAHuWKIXLeXiqr8II/adSvetkx0qdZwdbXXpb0=
github.com/aws/aws-sdk-go-v2/config v1.27.0/go.mod h1:cfh8v69nuSUohNFMbIISP2fhmblGmYEOKs5V53HiHnk=
github.com/aws/aws-sdk-go-v2/credentials v1.17.0 h1:lMW2x6sKBsiAJrpi1doOXqWFyEPoE886DTb1X0wb7So=
github.com/aws/aws-sdk-go-v2/credentials v1.17.0/go.mod h1:uT41FIH8cCIxOdUYIL0PYyHlL1NoneDuDSCwg5VE/5o=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 h1:xWCwjjvVz2ojYTP4kBKUuUh9ZrXfcAXpflhOUUeXg1k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0/go.mod h1:j3fACuqXg4oMTQOR2yY7m0NmJY0yBK4L4sLsRXq1Ins=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2 h1:VEekE/fJWqAWYozxFQ07B+h8NdvTPAYhV13xIBenuO0=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2/go.mod h1:8vozqAHmDNmoD4YbuDKIfpnLbByzngczL4My1RELLVo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0 h1:NPs/EqVO+ajwOoq56EfcGKa3L3ruWuazkIw1BqxwOPw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0/go.mod h1:D+duLy2ylgatV+yTlQ8JTuLfDD0BnFvnQRc+o6tbZ4M=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0 h1:ks7KGMVUMoDzcxNWUlEdI+/lokMFD136EL6DWmUOV80=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0/go.mod h1:hL6BWM/d/qz113fVitZjbXR0E+RCTU1+x+1Idyn5NgE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.0 h1:TkbRExyKSVHELwG9gz2+gql37jjec2R5vus9faTomwE=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.0/go.mod h1:T3/9xMKudHhnj8it5EqIrhvv11tVZqWYkKcot+BFStc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 h1:a33HuFlO0KsveiP90IUJh8Xr/cx9US2PqkSroaLc+o8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0/go.mod h1:SxIkWpByiGbhbHYTo9CMTUnx2G4p4ZQMrDPcRRy//1c=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.0 h1:UiSyK6ent6OKpkMJN3+k5HZ4sk4UfchEaaW5wv7SblQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.0/go.mod h1:l7kzl8n8DXoRyFz5cIMG70HnPauWa649TUhgw8Rq6lo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 h1:SHN/umDLTmFTmYfI+gkanz6da3vK8Kvj/5wkqnTHbuA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0/go.mod h1:l8gPU5RYGOFHJqWEpPMoRTP0VoaWQSkJdKo+hwWnnDA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0 h1:l5puwOHr7IxECuPMIuZG7UKOzAnF24v6t4l+Z5Moay4=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0/go.mod h1:Oov79flWa/n7Ni+lQC3z+VM7PoRM47omRqbJU9B5Y7E=
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1 h1:bjpWJEXch7moIt3PX2r5XpGROsletl7enqG1Q3Te1Dc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1/go.mod h1:1o/W6JFUuREj2ExoQ21vHJgO7wakvjhol91M9eknFgs=
github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 h1:u6OkVDxtBPnxPkZ9/63ynEe+8kHbtS5IfaC4PzVxzWM=
github.com/aws/aws-sdk-go-v2/service/sso v1.19.0/go.mod h1:YqbU3RS/pkDVu+v+Nwxvn0i1WB0HkNWEePWbmODEbbs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 h1:6DL0qu5+315wbsAEEmzK+P9leRwNbkp+lGjPC+CEvb8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0/go.mod h1:olUAyg+FaoFaL/zFaeQQONjOZ9HXoxgvI/c7mQTYz7M=
github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 h1:cjTRjh700H36MQ8M0LnDn33W3JmwC77mdxIIyPWCdpM=
github.com/aws/aws-sdk-go-v2/service/sts v1.27.0/go.mod h1:nXfOBMWPokIbOY+Gi7a1psWMSvskUCemZzI+SMB7Akc=
github.com/aws/smithy-go v1.20.0 h1:6+kZsCXZwKxZS9RfISnPc4EXlHoyAkm2hPuM8X2BrrQ=
github.com/aws/smithy-go v1.20.0/go.mod h1:uo5RKksAl4PzhqaAbjd4rLgFoq5koTsQKYuGe7dklGc=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -133,8 +133,8 @@ github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdX
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8=
github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
@@ -152,8 +152,8 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -178,8 +178,8 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/cel-go v0.18.2 h1:L0B6sNBSVmt0OyECi8v6VOS74KOc9W/tLiWKfZABvf4=
github.com/google/cel-go v0.18.2/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/cel-go v0.20.0 h1:h4n6DOCppEMpWERzllyNkntl7JrDyxoE543KWS6BLpc=
github.com/google/cel-go v0.20.0/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -194,8 +194,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S3
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -210,8 +210,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -228,6 +228,8 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
@@ -261,8 +263,6 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
@@ -286,6 +286,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
@@ -331,6 +333,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
@@ -363,8 +367,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posthog/posthog-go v0.0.0-20240110105835-f2ee529330e9 h1:KAKskYPB1yqqx1LpRtHnJSH1A65ttD+eD68sfjtDQps=
github.com/posthog/posthog-go v0.0.0-20240110105835-f2ee529330e9/go.mod h1:migYMxlAqcnQy+3eN8mcL0b2tpKy6R+8Zc0lxwk4dKM=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -449,19 +451,20 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04=
github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E=
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yourselfhosted/gomark v0.0.0-20240222150908-75b2b15a7837 h1:TAFqMn/ey7NykzAtE0rJCy/4f2OIp8uAJZti7WfVSpo=
github.com/yourselfhosted/gomark v0.0.0-20240222150908-75b2b15a7837/go.mod h1:dfl9FHGIw1oISjPc16u8n6/H/dngiVfdVRtS5+WJ4Js=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
@@ -490,13 +493,13 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -512,8 +515,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -535,12 +538,12 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -548,8 +551,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -582,8 +583,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -614,8 +615,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -634,12 +635,12 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs=
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo=
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 h1:4++qSzdWBUy9/2x8L5KZgwZw+mjJZ2yDSCGMVM0YzRs=
google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:PVreiBMirk8ypES6aw9d4p6iiBNSIfZEBqr3UGoAi2E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@@ -653,8 +654,8 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -701,34 +702,20 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.40.1 h1:ZhRylEBcj3GyQbPVC8JxIg7SdrT4JOxIDJoUon0NfF8=
modernc.org/libc v1.40.1/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA=
modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=

View File

@@ -0,0 +1,140 @@
package jobs
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/plugin/storage/s3"
"github.com/usememos/memos/store"
)
// RunPreSignLinks is a background job that pre-signs external links stored in the database.
// It uses S3 client to generate presigned URLs and updates the corresponding resources in the store.
func RunPreSignLinks(ctx context.Context, dataStore *store.Store) {
for {
started := time.Now()
if err := signExternalLinks(ctx, dataStore); err != nil {
log.Warn("failed sign external links", zap.Error(err))
} else {
log.Info("links pre-signed", zap.Duration("duration", time.Since(started)))
}
select {
case <-time.After(s3.LinkLifetime / 2):
case <-ctx.Done():
return
}
}
}
func signExternalLinks(ctx context.Context, dataStore *store.Store) error {
const pageSize = 32
objectStore, err := findObjectStorage(ctx, dataStore)
if err != nil {
return errors.Wrapf(err, "find object storage")
}
if objectStore == nil || !objectStore.Config.PreSign {
// object storage not set or not supported
return nil
}
var offset int
var limit = pageSize
for {
resources, err := dataStore.ListResources(ctx, &store.FindResource{
GetBlob: false,
Limit: &limit,
Offset: &offset,
})
if err != nil {
return errors.Wrapf(err, "list resources, offset %d", offset)
}
for _, res := range resources {
if res.ExternalLink == "" {
// not for object store
continue
}
if strings.Contains(res.ExternalLink, "?") && time.Since(time.Unix(res.UpdatedTs, 0)) < s3.LinkLifetime/2 {
// resource not signed (hack for migration)
// resource was recently updated - skipping
continue
}
newLink, err := objectStore.PreSignLink(ctx, res.ExternalLink)
if err != nil {
log.Warn("failed pre-sign link", zap.Int32("resource", res.ID), zap.String("link", res.ExternalLink), zap.Error(err))
continue // do not fail - we may want update left over links too
}
now := time.Now().Unix()
// we may want to use here transaction and batch update in the future
_, err = dataStore.UpdateResource(ctx, &store.UpdateResource{
ID: res.ID,
UpdatedTs: &now,
ExternalLink: &newLink,
})
if err != nil {
// something with DB - better to stop here
return errors.Wrapf(err, "update resource %d link to %q", res.ID, newLink)
}
}
offset += limit
if len(resources) < limit {
break
}
}
return nil
}
// findObjectStorage returns current default storage if it's S3-compatible or nil otherwise.
// Returns error only in case of internal problems (ie: database or configuration issues).
// May return nil client and nil error.
func findObjectStorage(ctx context.Context, dataStore *store.Store) (*s3.Client, error) {
systemSettingStorageServiceID, err := dataStore.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: apiv1.SystemSettingStorageServiceIDName.String()})
if err != nil {
return nil, errors.Wrap(err, "Failed to find SystemSettingStorageServiceIDName")
}
storageServiceID := apiv1.DefaultStorage
if systemSettingStorageServiceID != nil {
err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
if err != nil {
return nil, errors.Wrap(err, "Failed to unmarshal storage service id")
}
}
storage, err := dataStore.GetStorage(ctx, &store.FindStorage{ID: &storageServiceID})
if err != nil {
return nil, errors.Wrap(err, "Failed to find StorageServiceID")
}
if storage == nil {
return nil, nil // storage not configured - not an error, just return empty ref
}
storageMessage, err := apiv1.ConvertStorageFromStore(storage)
if err != nil {
return nil, errors.Wrap(err, "Failed to ConvertStorageFromStore")
}
if storageMessage.Type != apiv1.StorageS3 {
return nil, nil
}
s3Config := storageMessage.Config.S3Config
return s3.NewClient(ctx, &s3.Config{
AccessKey: s3Config.AccessKey,
SecretKey: s3Config.SecretKey,
EndPoint: s3Config.EndPoint,
Region: s3Config.Region,
Bucket: s3Config.Bucket,
URLPrefix: s3Config.URLPrefix,
URLSuffix: s3Config.URLSuffix,
PreSign: s3Config.PreSign,
})
}

View File

@@ -0,0 +1,7 @@
package util
import "regexp"
var (
ResourceNameMatcher = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9-]{1,30}[a-zA-Z0-9])$")
)

View File

@@ -1,3 +0,0 @@
# gomark
A markdown parser for memos. WIP

View File

@@ -1,84 +0,0 @@
package ast
type NodeType uint32
const (
UnknownNode NodeType = iota
// Block nodes.
LineBreakNode
ParagraphNode
CodeBlockNode
HeadingNode
HorizontalRuleNode
BlockquoteNode
OrderedListNode
UnorderedListNode
TaskListNode
MathBlockNode
TableNode
// Inline nodes.
TextNode
BoldNode
ItalicNode
BoldItalicNode
CodeNode
ImageNode
LinkNode
AutoLinkNode
TagNode
StrikethroughNode
EscapingCharacterNode
MathNode
HighlightNode
)
type Node interface {
// Type returns a node type.
Type() NodeType
// Restore returns a string representation of this node.
Restore() string
// PrevSibling returns a previous sibling node of this node.
PrevSibling() Node
// NextSibling returns a next sibling node of this node.
NextSibling() Node
// SetPrevSibling sets a previous sibling node to this node.
SetPrevSibling(Node)
// SetNextSibling sets a next sibling node to this node.
SetNextSibling(Node)
}
type BaseNode struct {
prevSibling Node
nextSibling Node
}
func (n *BaseNode) PrevSibling() Node {
return n.prevSibling
}
func (n *BaseNode) NextSibling() Node {
return n.nextSibling
}
func (n *BaseNode) SetPrevSibling(node Node) {
n.prevSibling = node
}
func (n *BaseNode) SetNextSibling(node Node) {
n.nextSibling = node
}
func IsBlockNode(node Node) bool {
switch node.Type() {
case ParagraphNode, CodeBlockNode, HeadingNode, HorizontalRuleNode, BlockquoteNode, OrderedListNode, UnorderedListNode, TaskListNode, MathBlockNode:
return true
default:
return false
}
}

View File

@@ -1,230 +0,0 @@
package ast
import (
"fmt"
"strings"
)
type BaseBlock struct {
BaseNode
}
type LineBreak struct {
BaseBlock
}
func (*LineBreak) Type() NodeType {
return LineBreakNode
}
func (*LineBreak) Restore() string {
return "\n"
}
type Paragraph struct {
BaseBlock
Children []Node
}
func (*Paragraph) Type() NodeType {
return ParagraphNode
}
func (n *Paragraph) Restore() string {
var result string
for _, child := range n.Children {
result += child.Restore()
}
return result
}
type CodeBlock struct {
BaseBlock
Language string
Content string
}
func (*CodeBlock) Type() NodeType {
return CodeBlockNode
}
func (n *CodeBlock) Restore() string {
return fmt.Sprintf("```%s\n%s\n```", n.Language, n.Content)
}
type Heading struct {
BaseBlock
Level int
Children []Node
}
func (*Heading) Type() NodeType {
return HeadingNode
}
func (n *Heading) Restore() string {
var result string
for _, child := range n.Children {
result += child.Restore()
}
symbol := ""
for i := 0; i < n.Level; i++ {
symbol += "#"
}
return fmt.Sprintf("%s %s", symbol, result)
}
type HorizontalRule struct {
BaseBlock
// Symbol is "*" or "-" or "_".
Symbol string
}
func (*HorizontalRule) Type() NodeType {
return HorizontalRuleNode
}
func (n *HorizontalRule) Restore() string {
return n.Symbol + n.Symbol + n.Symbol
}
type Blockquote struct {
BaseBlock
Children []Node
}
func (*Blockquote) Type() NodeType {
return BlockquoteNode
}
func (n *Blockquote) Restore() string {
var result string
for _, child := range n.Children {
result += child.Restore()
}
return fmt.Sprintf("> %s", result)
}
type OrderedList struct {
BaseBlock
// Number is the number of the list.
Number string
// Indent is the number of spaces.
Indent int
Children []Node
}
func (*OrderedList) Type() NodeType {
return OrderedListNode
}
func (n *OrderedList) Restore() string {
var result string
for _, child := range n.Children {
result += child.Restore()
}
return fmt.Sprintf("%s%s. %s", strings.Repeat(" ", n.Indent), n.Number, result)
}
type UnorderedList struct {
BaseBlock
// Symbol is "*" or "-" or "+".
Symbol string
// Indent is the number of spaces.
Indent int
Children []Node
}
func (*UnorderedList) Type() NodeType {
return UnorderedListNode
}
func (n *UnorderedList) Restore() string {
var result string
for _, child := range n.Children {
result += child.Restore()
}
return fmt.Sprintf("%s%s %s", strings.Repeat(" ", n.Indent), n.Symbol, result)
}
type TaskList struct {
BaseBlock
// Symbol is "*" or "-" or "+".
Symbol string
// Indent is the number of spaces.
Indent int
Complete bool
Children []Node
}
func (*TaskList) Type() NodeType {
return TaskListNode
}
func (n *TaskList) Restore() string {
var result string
for _, child := range n.Children {
result += child.Restore()
}
complete := " "
if n.Complete {
complete = "x"
}
return fmt.Sprintf("%s%s [%s] %s", strings.Repeat(" ", n.Indent), n.Symbol, complete, result)
}
type MathBlock struct {
BaseBlock
Content string
}
func (*MathBlock) Type() NodeType {
return MathBlockNode
}
func (n *MathBlock) Restore() string {
return fmt.Sprintf("$$\n%s\n$$", n.Content)
}
type Table struct {
BaseBlock
Header []string
Delimiter []string
Rows [][]string
}
func (*Table) Type() NodeType {
return TableNode
}
func (n *Table) Restore() string {
var result string
for _, header := range n.Header {
result += fmt.Sprintf("| %s ", header)
}
result += "|\n"
for _, d := range n.Delimiter {
result += fmt.Sprintf("| %s ", d)
}
result += "|\n"
for index, row := range n.Rows {
for _, cell := range row {
result += fmt.Sprintf("| %s ", cell)
}
result += "|"
if index != len(n.Rows)-1 {
result += "\n"
}
}
return result
}

View File

@@ -1,207 +0,0 @@
package ast
import "fmt"
type BaseInline struct {
BaseNode
}
type Text struct {
BaseInline
Content string
}
func (*Text) Type() NodeType {
return TextNode
}
func (n *Text) Restore() string {
return n.Content
}
type Bold struct {
BaseInline
// Symbol is "*" or "_".
Symbol string
Children []Node
}
func (*Bold) Type() NodeType {
return BoldNode
}
func (n *Bold) Restore() string {
symbol := n.Symbol + n.Symbol
children := ""
for _, child := range n.Children {
children += child.Restore()
}
return fmt.Sprintf("%s%s%s", symbol, children, symbol)
}
type Italic struct {
BaseInline
// Symbol is "*" or "_".
Symbol string
Content string
}
func (*Italic) Type() NodeType {
return ItalicNode
}
func (n *Italic) Restore() string {
return fmt.Sprintf("%s%s%s", n.Symbol, n.Content, n.Symbol)
}
type BoldItalic struct {
BaseInline
// Symbol is "*" or "_".
Symbol string
Content string
}
func (*BoldItalic) Type() NodeType {
return BoldItalicNode
}
func (n *BoldItalic) Restore() string {
symbol := n.Symbol + n.Symbol + n.Symbol
return fmt.Sprintf("%s%s%s", symbol, n.Content, symbol)
}
type Code struct {
BaseInline
Content string
}
func (*Code) Type() NodeType {
return CodeNode
}
func (n *Code) Restore() string {
return fmt.Sprintf("`%s`", n.Content)
}
type Image struct {
BaseInline
AltText string
URL string
}
func (*Image) Type() NodeType {
return ImageNode
}
func (n *Image) Restore() string {
return fmt.Sprintf("![%s](%s)", n.AltText, n.URL)
}
type Link struct {
BaseInline
Text string
URL string
}
func (*Link) Type() NodeType {
return LinkNode
}
func (n *Link) Restore() string {
return fmt.Sprintf("[%s](%s)", n.Text, n.URL)
}
type AutoLink struct {
BaseInline
URL string
IsRawText bool
}
func (*AutoLink) Type() NodeType {
return AutoLinkNode
}
func (n *AutoLink) Restore() string {
if n.IsRawText {
return n.URL
}
return fmt.Sprintf("<%s>", n.URL)
}
type Tag struct {
BaseInline
Content string
}
func (*Tag) Type() NodeType {
return TagNode
}
func (n *Tag) Restore() string {
return fmt.Sprintf("#%s", n.Content)
}
type Strikethrough struct {
BaseInline
Content string
}
func (*Strikethrough) Type() NodeType {
return StrikethroughNode
}
func (n *Strikethrough) Restore() string {
return fmt.Sprintf("~~%s~~", n.Content)
}
type EscapingCharacter struct {
BaseInline
Symbol string
}
func (*EscapingCharacter) Type() NodeType {
return EscapingCharacterNode
}
func (n *EscapingCharacter) Restore() string {
return fmt.Sprintf("\\%s", n.Symbol)
}
type Math struct {
BaseInline
Content string
}
func (*Math) Type() NodeType {
return MathNode
}
func (n *Math) Restore() string {
return fmt.Sprintf("$%s$", n.Content)
}
type Highlight struct {
BaseInline
Content string
}
func (*Highlight) Type() NodeType {
return HighlightNode
}
func (n *Highlight) Restore() string {
return fmt.Sprintf("==%s==", n.Content)
}

View File

@@ -1,23 +0,0 @@
package ast
func FindPrevSiblingExceptLineBreak(node Node) Node {
if node == nil {
return nil
}
prev := node.PrevSibling()
if prev != nil && prev.Type() == LineBreakNode && prev.PrevSibling() != nil && prev.PrevSibling().Type() != LineBreakNode {
return FindPrevSiblingExceptLineBreak(prev)
}
return prev
}
func FindNextSiblingExceptLineBreak(node Node) Node {
if node == nil {
return nil
}
next := node.NextSibling()
if next != nil && next.Type() == LineBreakNode && next.NextSibling() != nil && next.NextSibling().Type() != LineBreakNode {
return FindNextSiblingExceptLineBreak(next)
}
return next
}

View File

@@ -1 +0,0 @@
package gomark

View File

@@ -1,69 +0,0 @@
package parser
import (
"errors"
"net/url"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type AutoLinkParser struct{}
func NewAutoLinkParser() *AutoLinkParser {
return &AutoLinkParser{}
}
func (*AutoLinkParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 3 {
return 0, false
}
hasAngleBrackets := false
if tokens[0].Type == tokenizer.LessThan {
hasAngleBrackets = true
}
contentTokens := []*tokenizer.Token{}
for _, token := range tokens {
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space {
break
}
contentTokens = append(contentTokens, token)
if hasAngleBrackets && token.Type == tokenizer.GreaterThan {
break
}
}
if hasAngleBrackets && contentTokens[len(contentTokens)-1].Type != tokenizer.GreaterThan {
return 0, false
}
content := tokenizer.Stringify(contentTokens)
if !hasAngleBrackets {
u, err := url.Parse(content)
if err != nil || u.Scheme == "" || u.Host == "" {
return 0, false
}
}
return len(contentTokens), true
}
func (p *AutoLinkParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
url := tokenizer.Stringify(tokens[:size])
isRawText := true
if tokens[0].Type == tokenizer.LessThan && tokens[size-1].Type == tokenizer.GreaterThan {
isRawText = false
url = tokenizer.Stringify(tokens[1 : size-1])
}
return &ast.AutoLink{
URL: url,
IsRawText: isRawText,
}, nil
}

View File

@@ -1,42 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestAutoLinkParser(t *testing.T) {
tests := []struct {
text string
link ast.Node
}{
{
text: "<https://example.com)",
link: nil,
},
{
text: "<https://example.com>",
link: &ast.AutoLink{
URL: "https://example.com",
},
},
{
text: "https://example.com",
link: &ast.AutoLink{
URL: "https://example.com",
IsRawText: true,
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewAutoLinkParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.link}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,52 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type BlockquoteParser struct{}
func NewBlockquoteParser() *BlockquoteParser {
return &BlockquoteParser{}
}
func (*BlockquoteParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 3 {
return 0, false
}
if tokens[0].Type != tokenizer.GreaterThan || tokens[1].Type != tokenizer.Space {
return 0, false
}
contentTokens := []*tokenizer.Token{}
for _, token := range tokens[2:] {
if token.Type == tokenizer.Newline {
break
}
contentTokens = append(contentTokens, token)
}
if len(contentTokens) == 0 {
return 0, false
}
return len(contentTokens) + 2, true
}
func (p *BlockquoteParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
contentTokens := tokens[2:size]
children, err := ParseBlockWithParsers(contentTokens, []BlockParser{NewParagraphParser(), NewLineBreakParser()})
if err != nil {
return nil, err
}
return &ast.Blockquote{
Children: children,
}, nil
}

View File

@@ -1,71 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestBlockquoteParser(t *testing.T) {
tests := []struct {
text string
blockquote ast.Node
}{
{
text: "> Hello world",
blockquote: &ast.Blockquote{
Children: []ast.Node{
&ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Hello world",
},
},
},
},
},
},
{
text: "> 你好",
blockquote: &ast.Blockquote{
Children: []ast.Node{
&ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "你好",
},
},
},
},
},
},
{
text: "> Hello\nworld",
blockquote: &ast.Blockquote{
Children: []ast.Node{
&ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Hello",
},
},
},
},
},
},
{
text: ">Hello\nworld",
blockquote: nil,
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewBlockquoteParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.blockquote}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,64 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type BoldParser struct{}
func NewBoldParser() InlineParser {
return &BoldParser{}
}
func (*BoldParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 5 {
return 0, false
}
prefixTokens := tokens[:2]
if prefixTokens[0].Type != prefixTokens[1].Type {
return 0, false
}
prefixTokenType := prefixTokens[0].Type
if prefixTokenType != tokenizer.Asterisk && prefixTokenType != tokenizer.Underscore {
return 0, false
}
cursor, matched := 2, false
for ; cursor < len(tokens)-1; cursor++ {
token, nextToken := tokens[cursor], tokens[cursor+1]
if token.Type == tokenizer.Newline || nextToken.Type == tokenizer.Newline {
return 0, false
}
if token.Type == prefixTokenType && nextToken.Type == prefixTokenType {
matched = true
break
}
}
if !matched {
return 0, false
}
return cursor + 2, true
}
func (p *BoldParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
prefixTokenType := tokens[0].Type
contentTokens := tokens[2 : size-2]
children, err := ParseInlineWithParsers(contentTokens, []InlineParser{NewLinkParser(), NewTextParser()})
if err != nil {
return nil, err
}
return &ast.Bold{
Symbol: prefixTokenType,
Children: children,
}, nil
}

View File

@@ -1,60 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type BoldItalicParser struct{}
func NewBoldItalicParser() InlineParser {
return &BoldItalicParser{}
}
func (*BoldItalicParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 7 {
return 0, false
}
prefixTokens := tokens[:3]
if prefixTokens[0].Type != prefixTokens[1].Type || prefixTokens[0].Type != prefixTokens[2].Type || prefixTokens[1].Type != prefixTokens[2].Type {
return 0, false
}
prefixTokenType := prefixTokens[0].Type
if prefixTokenType != tokenizer.Asterisk && prefixTokenType != tokenizer.Underscore {
return 0, false
}
cursor, matched := 3, false
for ; cursor < len(tokens)-2; cursor++ {
token, nextToken, endToken := tokens[cursor], tokens[cursor+1], tokens[cursor+2]
if token.Type == tokenizer.Newline || nextToken.Type == tokenizer.Newline || endToken.Type == tokenizer.Newline {
return 0, false
}
if token.Type == prefixTokenType && nextToken.Type == prefixTokenType && endToken.Type == prefixTokenType {
matched = true
break
}
}
if !matched {
return 0, false
}
return cursor + 3, true
}
func (p *BoldItalicParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
prefixTokenType := tokens[0].Type
contentTokens := tokens[3 : size-3]
return &ast.BoldItalic{
Symbol: prefixTokenType,
Content: tokenizer.Stringify(contentTokens),
}, nil
}

View File

@@ -1,51 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestBoldItalicParser(t *testing.T) {
tests := []struct {
text string
boldItalic ast.Node
}{
{
text: "*Hello world!",
boldItalic: nil,
},
{
text: "***Hello***",
boldItalic: &ast.BoldItalic{
Symbol: "*",
Content: "Hello",
},
},
{
text: "*** Hello ***",
boldItalic: &ast.BoldItalic{
Symbol: "*",
Content: " Hello ",
},
},
{
text: "*** Hello * *",
boldItalic: nil,
},
{
text: "*** Hello **",
boldItalic: nil,
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewBoldItalicParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.boldItalic}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,59 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestBoldParser(t *testing.T) {
tests := []struct {
text string
bold ast.Node
}{
{
text: "*Hello world!",
bold: nil,
},
{
text: "**Hello**",
bold: &ast.Bold{
Symbol: "*",
Children: []ast.Node{
&ast.Text{
Content: "Hello",
},
},
},
},
{
text: "** Hello **",
bold: &ast.Bold{
Symbol: "*",
Children: []ast.Node{
&ast.Text{
Content: " Hello ",
},
},
},
},
{
text: "** Hello * *",
bold: nil,
},
{
text: "* * Hello **",
bold: nil,
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewBoldParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.bold}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,51 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type CodeParser struct{}
func NewCodeParser() *CodeParser {
return &CodeParser{}
}
func (*CodeParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 3 {
return 0, false
}
if tokens[0].Type != tokenizer.Backtick {
return 0, false
}
contentTokens, matched := []*tokenizer.Token{}, false
for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline {
return 0, false
}
if token.Type == tokenizer.Backtick {
matched = true
break
}
contentTokens = append(contentTokens, token)
}
if !matched || len(contentTokens) == 0 {
return 0, false
}
return len(contentTokens) + 2, true
}
func (p *CodeParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
contentTokens := tokens[1 : size-1]
return &ast.Code{
Content: tokenizer.Stringify(contentTokens),
}, nil
}

View File

@@ -1,76 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type CodeBlockParser struct {
Language string
Content string
}
func NewCodeBlockParser() *CodeBlockParser {
return &CodeBlockParser{}
}
func (*CodeBlockParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 9 {
return 0, false
}
if tokens[0].Type != tokenizer.Backtick || tokens[1].Type != tokenizer.Backtick || tokens[2].Type != tokenizer.Backtick {
return 0, false
}
if tokens[3].Type != tokenizer.Newline && tokens[4].Type != tokenizer.Newline {
return 0, false
}
cursor := 4
if tokens[3].Type != tokenizer.Newline {
cursor = 5
}
matched := false
for ; cursor < len(tokens)-3; cursor++ {
if tokens[cursor].Type == tokenizer.Newline && tokens[cursor+1].Type == tokenizer.Backtick && tokens[cursor+2].Type == tokenizer.Backtick && tokens[cursor+3].Type == tokenizer.Backtick {
if cursor+3 == len(tokens)-1 {
cursor += 4
matched = true
break
} else if tokens[cursor+4].Type == tokenizer.Newline {
cursor += 4
matched = true
break
}
}
}
if !matched {
return 0, false
}
return cursor, true
}
func (p *CodeBlockParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
languageToken := tokens[3]
contentStart, contentEnd := 5, size-4
if languageToken.Type == tokenizer.Newline {
languageToken = nil
contentStart = 4
}
codeBlock := &ast.CodeBlock{
Content: tokenizer.Stringify(tokens[contentStart:contentEnd]),
}
if languageToken != nil {
codeBlock.Language = languageToken.String()
}
return codeBlock, nil
}

View File

@@ -1,65 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestCodeBlockParser(t *testing.T) {
tests := []struct {
text string
codeBlock ast.Node
}{
{
text: "```Hello world!```",
codeBlock: nil,
},
{
text: "```\nHello\n```",
codeBlock: &ast.CodeBlock{
Language: "",
Content: "Hello",
},
},
{
text: "```\nHello world!\n```",
codeBlock: &ast.CodeBlock{
Language: "",
Content: "Hello world!",
},
},
{
text: "```java\nHello \n world!\n```",
codeBlock: &ast.CodeBlock{
Language: "java",
Content: "Hello \n world!",
},
},
{
text: "```java\nHello \n world!\n```111",
codeBlock: nil,
},
{
text: "```java\nHello \n world!\n``` 111",
codeBlock: nil,
},
{
text: "```java\nHello \n world!\n```\n123123",
codeBlock: &ast.CodeBlock{
Language: "java",
Content: "Hello \n world!",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewCodeBlockParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.codeBlock}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,39 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestCodeParser(t *testing.T) {
tests := []struct {
text string
code ast.Node
}{
{
text: "`Hello world!",
code: nil,
},
{
text: "`Hello world!`",
code: &ast.Code{
Content: "Hello world!",
},
},
{
text: "`Hello \nworld!`",
code: nil,
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewCodeParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.code}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,41 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type EscapingCharacterParser struct{}
func NewEscapingCharacterParser() *EscapingCharacterParser {
return &EscapingCharacterParser{}
}
func (*EscapingCharacterParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) == 0 {
return 0, false
}
if tokens[0].Type != tokenizer.Backslash {
return 0, false
}
if len(tokens) == 1 {
return 0, false
}
if tokens[1].Type == tokenizer.Newline || tokens[1].Type == tokenizer.Space || tokens[1].Type == tokenizer.Text || tokens[1].Type == tokenizer.Number {
return 0, false
}
return 2, true
}
func (p *EscapingCharacterParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
return &ast.EscapingCharacter{
Symbol: tokens[1].Value,
}, nil
}

View File

@@ -1,31 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestEscapingCharacterParser(t *testing.T) {
tests := []struct {
text string
node ast.Node
}{
{
text: `\# 123`,
node: &ast.EscapingCharacter{
Symbol: "#",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewEscapingCharacterParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.node}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,73 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type HeadingParser struct{}
func NewHeadingParser() *HeadingParser {
return &HeadingParser{}
}
func (*HeadingParser) Match(tokens []*tokenizer.Token) (int, bool) {
level := 0
for _, token := range tokens {
if token.Type == tokenizer.PoundSign {
level++
} else {
break
}
}
if len(tokens) <= level+1 {
return 0, false
}
if tokens[level].Type != tokenizer.Space {
return 0, false
}
if level == 0 || level > 6 {
return 0, false
}
contentTokens := []*tokenizer.Token{}
for _, token := range tokens[level+1:] {
if token.Type == tokenizer.Newline {
break
}
contentTokens = append(contentTokens, token)
}
if len(contentTokens) == 0 {
return 0, false
}
return len(contentTokens) + level + 1, true
}
func (p *HeadingParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
level := 0
for _, token := range tokens {
if token.Type == tokenizer.PoundSign {
level++
} else {
break
}
}
contentTokens := tokens[level+1 : size]
children, err := ParseInline(contentTokens)
if err != nil {
return nil, err
}
return &ast.Heading{
Level: level,
Children: children,
}, nil
}

View File

@@ -1,86 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestHeadingParser(t *testing.T) {
tests := []struct {
text string
heading ast.Node
}{
{
text: "*Hello world",
heading: nil,
},
{
text: "## Hello World\n123",
heading: &ast.Heading{
Level: 2,
Children: []ast.Node{
&ast.Text{
Content: "Hello World",
},
},
},
},
{
text: "# # Hello World",
heading: &ast.Heading{
Level: 1,
Children: []ast.Node{
&ast.Text{
Content: "# Hello World",
},
},
},
},
{
text: " # 123123 Hello World",
heading: nil,
},
{
text: `# 123
Hello World`,
heading: &ast.Heading{
Level: 1,
Children: []ast.Node{
&ast.Text{
Content: "123 ",
},
},
},
},
{
text: "### **Hello** World",
heading: &ast.Heading{
Level: 3,
Children: []ast.Node{
&ast.Bold{
Symbol: "*",
Children: []ast.Node{
&ast.Text{
Content: "Hello",
},
},
},
&ast.Text{
Content: " World",
},
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewHeadingParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.heading}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,58 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type HighlightParser struct{}
func NewHighlightParser() InlineParser {
return &HighlightParser{}
}
func (*HighlightParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 5 {
return 0, false
}
prefixTokens := tokens[:2]
if prefixTokens[0].Type != prefixTokens[1].Type {
return 0, false
}
prefixTokenType := prefixTokens[0].Type
if prefixTokenType != tokenizer.EqualSign {
return 0, false
}
cursor, matched := 2, false
for ; cursor < len(tokens)-1; cursor++ {
token, nextToken := tokens[cursor], tokens[cursor+1]
if token.Type == tokenizer.Newline || nextToken.Type == tokenizer.Newline {
return 0, false
}
if token.Type == prefixTokenType && nextToken.Type == prefixTokenType {
matched = true
break
}
}
if !matched {
return 0, false
}
return cursor + 2, true
}
func (p *HighlightParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
contentTokens := tokens[2 : size-2]
return &ast.Highlight{
Content: tokenizer.Stringify(contentTokens),
}, nil
}

View File

@@ -1,41 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestHighlightParser(t *testing.T) {
tests := []struct {
text string
bold ast.Node
}{
{
text: "==Hello world!",
bold: nil,
},
{
text: "==Hello==",
bold: &ast.Highlight{
Content: "Hello",
},
},
{
text: "==Hello world==",
bold: &ast.Highlight{
Content: "Hello world",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewHighlightParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.bold}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,41 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type HorizontalRuleParser struct{}
func NewHorizontalRuleParser() *HorizontalRuleParser {
return &HorizontalRuleParser{}
}
func (*HorizontalRuleParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 3 {
return 0, false
}
if tokens[0].Type != tokens[1].Type || tokens[0].Type != tokens[2].Type || tokens[1].Type != tokens[2].Type {
return 0, false
}
if tokens[0].Type != tokenizer.Hyphen && tokens[0].Type != tokenizer.Underscore && tokens[0].Type != tokenizer.Asterisk {
return 0, false
}
if len(tokens) > 3 && tokens[3].Type != tokenizer.Newline {
return 0, false
}
return 3, true
}
func (p *HorizontalRuleParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
return &ast.HorizontalRule{
Symbol: tokens[0].Type,
}, nil
}

View File

@@ -1,57 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestHorizontalRuleParser(t *testing.T) {
tests := []struct {
text string
horizontalRule ast.Node
}{
{
text: "---",
horizontalRule: &ast.HorizontalRule{
Symbol: "-",
},
},
{
text: "---\naaa",
horizontalRule: &ast.HorizontalRule{
Symbol: "-",
},
},
{
text: "****",
horizontalRule: nil,
},
{
text: "***",
horizontalRule: &ast.HorizontalRule{
Symbol: "*",
},
},
{
text: "-*-",
horizontalRule: nil,
},
{
text: "___",
horizontalRule: &ast.HorizontalRule{
Symbol: "_",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewHorizontalRuleParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.horizontalRule}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,75 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type ImageParser struct{}
func NewImageParser() *ImageParser {
return &ImageParser{}
}
func (*ImageParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 5 {
return 0, false
}
if tokens[0].Type != tokenizer.ExclamationMark {
return 0, false
}
if tokens[1].Type != tokenizer.LeftSquareBracket {
return 0, false
}
cursor, altText := 2, ""
for ; cursor < len(tokens)-2; cursor++ {
if tokens[cursor].Type == tokenizer.Newline {
return 0, false
}
if tokens[cursor].Type == tokenizer.RightSquareBracket {
break
}
altText += tokens[cursor].Value
}
if tokens[cursor+1].Type != tokenizer.LeftParenthesis {
return 0, false
}
cursor += 2
contentTokens, matched := []*tokenizer.Token{}, false
for _, token := range tokens[cursor:] {
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space {
return 0, false
}
if token.Type == tokenizer.RightParenthesis {
matched = true
break
}
contentTokens = append(contentTokens, token)
}
if !matched || len(contentTokens) == 0 {
return 0, false
}
return cursor + len(contentTokens) + 1, true
}
func (p *ImageParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
altTextTokens := []*tokenizer.Token{}
for _, token := range tokens[2:] {
if token.Type == tokenizer.RightSquareBracket {
break
}
altTextTokens = append(altTextTokens, token)
}
contentTokens := tokens[2+len(altTextTokens)+2 : size-1]
return &ast.Image{
AltText: tokenizer.Stringify(altTextTokens),
URL: tokenizer.Stringify(contentTokens),
}, nil
}

View File

@@ -1,46 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestImageParser(t *testing.T) {
tests := []struct {
text string
image ast.Node
}{
{
text: "![](https://example.com)",
image: &ast.Image{
AltText: "",
URL: "https://example.com",
},
},
{
text: "! [](https://example.com)",
image: nil,
},
{
text: "![alte]( htt ps :/ /example.com)",
image: nil,
},
{
text: "![al te](https://example.com)",
image: &ast.Image{
AltText: "al te",
URL: "https://example.com",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewImageParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.image}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,59 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type ItalicParser struct {
ContentTokens []*tokenizer.Token
}
func NewItalicParser() *ItalicParser {
return &ItalicParser{}
}
func (*ItalicParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 3 {
return 0, false
}
prefixTokens := tokens[:1]
if prefixTokens[0].Type != tokenizer.Asterisk && prefixTokens[0].Type != tokenizer.Underscore {
return 0, false
}
prefixTokenType := prefixTokens[0].Type
contentTokens := []*tokenizer.Token{}
matched := false
for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline {
return 0, false
}
if token.Type == prefixTokenType {
matched = true
break
}
contentTokens = append(contentTokens, token)
}
if !matched || len(contentTokens) == 0 {
return 0, false
}
return len(contentTokens) + 2, true
}
func (p *ItalicParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
prefixTokenType := tokens[0].Type
contentTokens := tokens[1 : size-1]
return &ast.Italic{
Symbol: prefixTokenType,
Content: tokenizer.Stringify(contentTokens),
}, nil
}

View File

@@ -1,50 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestItalicParser(t *testing.T) {
tests := []struct {
text string
italic ast.Node
}{
{
text: "*Hello world!",
italic: nil,
},
{
text: "*Hello*",
italic: &ast.Italic{
Symbol: "*",
Content: "Hello",
},
},
{
text: "* Hello *",
italic: &ast.Italic{
Symbol: "*",
Content: " Hello ",
},
},
{
text: "*1* Hello * *",
italic: &ast.Italic{
Symbol: "*",
Content: "1",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewItalicParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.italic}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,33 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type LineBreakParser struct{}
func NewLineBreakParser() *LineBreakParser {
return &LineBreakParser{}
}
func (*LineBreakParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) == 0 {
return 0, false
}
if tokens[0].Type != tokenizer.Newline {
return 0, false
}
return 1, true
}
func (p *LineBreakParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
return &ast.LineBreak{}, nil
}

View File

@@ -1,74 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type LinkParser struct{}
func NewLinkParser() *LinkParser {
return &LinkParser{}
}
func (*LinkParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 5 {
return 0, false
}
if tokens[0].Type != tokenizer.LeftSquareBracket {
return 0, false
}
textTokens := []*tokenizer.Token{}
for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline {
return 0, false
}
if token.Type == tokenizer.RightSquareBracket {
break
}
textTokens = append(textTokens, token)
}
if len(textTokens)+4 >= len(tokens) {
return 0, false
}
if tokens[2+len(textTokens)].Type != tokenizer.LeftParenthesis {
return 0, false
}
urlTokens := []*tokenizer.Token{}
for _, token := range tokens[3+len(textTokens):] {
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space {
return 0, false
}
if token.Type == tokenizer.RightParenthesis {
break
}
urlTokens = append(urlTokens, token)
}
if 4+len(urlTokens)+len(textTokens) > len(tokens) {
return 0, false
}
return 4 + len(urlTokens) + len(textTokens), true
}
func (p *LinkParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
textTokens := []*tokenizer.Token{}
for _, token := range tokens[1:] {
if token.Type == tokenizer.RightSquareBracket {
break
}
textTokens = append(textTokens, token)
}
urlTokens := tokens[2+len(textTokens)+1 : size-1]
return &ast.Link{
Text: tokenizer.Stringify(textTokens),
URL: tokenizer.Stringify(urlTokens),
}, nil
}

View File

@@ -1,53 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestLinkParser(t *testing.T) {
tests := []struct {
text string
link ast.Node
}{
{
text: "[](https://example.com)",
link: &ast.Link{
Text: "",
URL: "https://example.com",
},
},
{
text: "! [](https://example.com)",
link: nil,
},
{
text: "[alte]( htt ps :/ /example.com)",
link: nil,
},
{
text: "[your/slash](https://example.com)",
link: &ast.Link{
Text: "your/slash",
URL: "https://example.com",
},
},
{
text: "[hello world](https://example.com)",
link: &ast.Link{
Text: "hello world",
URL: "https://example.com",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewLinkParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.link}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,56 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type MathParser struct{}
func NewMathParser() *MathParser {
return &MathParser{}
}
func (*MathParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 3 {
return 0, false
}
if tokens[0].Type != tokenizer.DollarSign {
return 0, false
}
contentTokens := []*tokenizer.Token{}
for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline {
return 0, false
}
if token.Type == tokenizer.DollarSign {
break
}
contentTokens = append(contentTokens, token)
}
if len(contentTokens) == 0 {
return 0, false
}
if len(contentTokens)+2 > len(tokens) {
return 0, false
}
if tokens[len(contentTokens)+1].Type != tokenizer.DollarSign {
return 0, false
}
return len(contentTokens) + 2, true
}
func (p *MathParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
return &ast.Math{
Content: tokenizer.Stringify(tokens[1 : size-1]),
}, nil
}

View File

@@ -1,56 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type MathBlockParser struct{}
func NewMathBlockParser() *MathBlockParser {
return &MathBlockParser{}
}
func (*MathBlockParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 7 {
return 0, false
}
if tokens[0].Type != tokenizer.DollarSign || tokens[1].Type != tokenizer.DollarSign || tokens[2].Type != tokenizer.Newline {
return 0, false
}
cursor := 3
matched := false
for ; cursor < len(tokens)-2; cursor++ {
if tokens[cursor].Type == tokenizer.Newline && tokens[cursor+1].Type == tokenizer.DollarSign && tokens[cursor+2].Type == tokenizer.DollarSign {
if cursor+2 == len(tokens)-1 {
cursor += 3
matched = true
break
} else if tokens[cursor+3].Type == tokenizer.Newline {
cursor += 3
matched = true
break
}
}
}
if !matched {
return 0, false
}
return cursor, true
}
func (p *MathBlockParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
return &ast.MathBlock{
Content: tokenizer.Stringify(tokens[3 : size-3]),
}, nil
}

View File

@@ -1,36 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestMathBlockParser(t *testing.T) {
tests := []struct {
text string
link ast.Node
}{
{
text: "$$\n(1+x)^2\n$$",
link: &ast.MathBlock{
Content: "(1+x)^2",
},
},
{
text: "$$\na=3\n$$",
link: &ast.MathBlock{
Content: "a=3",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewMathBlockParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.link}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,30 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestMathParser(t *testing.T) {
tests := []struct {
text string
link ast.Node
}{
{
text: "$\\sqrt{3x-1}+(1+x)^2$",
link: &ast.Math{
Content: "\\sqrt{3x-1}+(1+x)^2",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewMathParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.link}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,73 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type OrderedListParser struct{}
func NewOrderedListParser() *OrderedListParser {
return &OrderedListParser{}
}
func (*OrderedListParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 4 {
return 0, false
}
indent := 0
for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
corsor := indent
if tokens[corsor].Type != tokenizer.Number || tokens[corsor+1].Type != tokenizer.Dot || tokens[corsor+2].Type != tokenizer.Space {
return 0, false
}
contentTokens := []*tokenizer.Token{}
for _, token := range tokens[corsor+3:] {
if token.Type == tokenizer.Newline {
break
}
contentTokens = append(contentTokens, token)
}
if len(contentTokens) == 0 {
return 0, false
}
return indent + len(contentTokens) + 3, true
}
func (p *OrderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
indent := 0
for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
contentTokens := tokens[indent+3 : size]
children, err := ParseInline(contentTokens)
if err != nil {
return nil, err
}
return &ast.OrderedList{
Number: tokens[indent].Value,
Indent: indent,
Children: children,
}, nil
}

View File

@@ -1,71 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestOrderedListParser(t *testing.T) {
tests := []struct {
text string
node ast.Node
}{
{
text: "1.asd",
node: nil,
},
{
text: "1. Hello World",
node: &ast.OrderedList{
Number: "1",
Children: []ast.Node{
&ast.Text{
Content: "Hello World",
},
},
},
},
{
text: " 1. Hello World",
node: &ast.OrderedList{
Number: "1",
Indent: 2,
Children: []ast.Node{
&ast.Text{
Content: "Hello World",
},
},
},
},
{
text: "1aa. Hello World",
node: nil,
},
{
text: "22. Hello *World*",
node: &ast.OrderedList{
Number: "22",
Children: []ast.Node{
&ast.Text{
Content: "Hello ",
},
&ast.Italic{
Symbol: "*",
Content: "World",
},
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewOrderedListParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.node}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,45 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type ParagraphParser struct {
ContentTokens []*tokenizer.Token
}
func NewParagraphParser() *ParagraphParser {
return &ParagraphParser{}
}
func (*ParagraphParser) Match(tokens []*tokenizer.Token) (int, bool) {
contentTokens := []*tokenizer.Token{}
for _, token := range tokens {
if token.Type == tokenizer.Newline {
break
}
contentTokens = append(contentTokens, token)
}
if len(contentTokens) == 0 {
return 0, false
}
return len(contentTokens), true
}
func (p *ParagraphParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
children, err := ParseInline(tokens[:size])
if err != nil {
return nil, err
}
return &ast.Paragraph{
Children: children,
}, nil
}

View File

@@ -1,63 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestParagraphParser(t *testing.T) {
tests := []struct {
text string
paragraph ast.Node
}{
{
text: "",
paragraph: nil,
},
{
text: "\n",
paragraph: nil,
},
{
text: "Hello world!",
paragraph: &ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Hello world!",
},
},
},
},
{
text: "Hello world!\n",
paragraph: &ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Hello world!",
},
},
},
},
{
text: "Hello world!\n\nNew paragraph.",
paragraph: &ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Hello world!",
},
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewParagraphParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.paragraph}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,127 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type Context struct {
BlockParsers []BlockParser
InlineParsers []InlineParser
}
type BaseParser interface {
Match(tokens []*tokenizer.Token) (int, bool)
Parse(tokens []*tokenizer.Token) (ast.Node, error)
}
type InlineParser interface {
BaseParser
}
type BlockParser interface {
BaseParser
}
func Parse(tokens []*tokenizer.Token) ([]ast.Node, error) {
return ParseBlock(tokens)
}
var defaultBlockParsers = []BlockParser{
NewCodeBlockParser(),
NewTableParser(),
NewHorizontalRuleParser(),
NewHeadingParser(),
NewBlockquoteParser(),
NewTaskListParser(),
NewUnorderedListParser(),
NewOrderedListParser(),
NewMathBlockParser(),
NewParagraphParser(),
NewLineBreakParser(),
}
func ParseBlock(tokens []*tokenizer.Token) ([]ast.Node, error) {
return ParseBlockWithParsers(tokens, defaultBlockParsers)
}
func ParseBlockWithParsers(tokens []*tokenizer.Token, blockParsers []BlockParser) ([]ast.Node, error) {
nodes := []ast.Node{}
var prevNode ast.Node
for len(tokens) > 0 {
for _, blockParser := range blockParsers {
size, matched := blockParser.Match(tokens)
if matched {
node, err := blockParser.Parse(tokens)
if err != nil {
return nil, errors.New("parse error")
}
tokens = tokens[size:]
if prevNode != nil {
prevNode.SetNextSibling(node)
node.SetPrevSibling(prevNode)
}
prevNode = node
nodes = append(nodes, node)
break
}
}
}
return nodes, nil
}
var defaultInlineParsers = []InlineParser{
NewEscapingCharacterParser(),
NewBoldItalicParser(),
NewImageParser(),
NewLinkParser(),
NewAutoLinkParser(),
NewBoldParser(),
NewItalicParser(),
NewHighlightParser(),
NewCodeParser(),
NewMathParser(),
NewTagParser(),
NewStrikethroughParser(),
NewLineBreakParser(),
NewTextParser(),
}
func ParseInline(tokens []*tokenizer.Token) ([]ast.Node, error) {
return ParseInlineWithParsers(tokens, defaultInlineParsers)
}
func ParseInlineWithParsers(tokens []*tokenizer.Token, inlineParsers []InlineParser) ([]ast.Node, error) {
nodes := []ast.Node{}
var prevNode ast.Node
for len(tokens) > 0 {
for _, inlineParser := range inlineParsers {
size, matched := inlineParser.Match(tokens)
if matched {
node, err := inlineParser.Parse(tokens)
if err != nil {
return nil, errors.New("parse error")
}
tokens = tokens[size:]
if prevNode != nil {
// Merge text nodes if possible.
if prevNode.Type() == ast.TextNode && node.Type() == ast.TextNode {
prevNode.(*ast.Text).Content += node.(*ast.Text).Content
break
}
prevNode.SetNextSibling(node)
node.SetPrevSibling(prevNode)
}
nodes = append(nodes, node)
prevNode = node
break
}
}
}
return nodes, nil
}

View File

@@ -1,228 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestParser(t *testing.T) {
tests := []struct {
text string
nodes []ast.Node
}{
{
text: "Hello world!",
nodes: []ast.Node{
&ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Hello world!",
},
},
},
},
},
{
text: "# Hello world!",
nodes: []ast.Node{
&ast.Heading{
Level: 1,
Children: []ast.Node{
&ast.Text{
Content: "Hello world!",
},
},
},
},
},
{
text: "\\# Hello world!",
nodes: []ast.Node{
&ast.Paragraph{
Children: []ast.Node{
&ast.EscapingCharacter{
Symbol: "#",
},
&ast.Text{
Content: " Hello world!",
},
},
},
},
},
{
text: "**Hello** world!",
nodes: []ast.Node{
&ast.Paragraph{
Children: []ast.Node{
&ast.Bold{
Symbol: "*",
Children: []ast.Node{
&ast.Text{
Content: "Hello",
},
},
},
&ast.Text{
Content: " world!",
},
},
},
},
},
{
text: "Hello **world**!\nHere is a new line.",
nodes: []ast.Node{
&ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Hello ",
},
&ast.Bold{
Symbol: "*",
Children: []ast.Node{
&ast.Text{
Content: "world",
},
},
},
&ast.Text{
Content: "!",
},
},
},
&ast.LineBreak{},
&ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Here is a new line.",
},
},
},
},
},
{
text: "Hello **world**!\n```javascript\nconsole.log(\"Hello world!\");\n```",
nodes: []ast.Node{
&ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Hello ",
},
&ast.Bold{
Symbol: "*",
Children: []ast.Node{
&ast.Text{
Content: "world",
},
},
},
&ast.Text{
Content: "!",
},
},
},
&ast.LineBreak{},
&ast.CodeBlock{
Language: "javascript",
Content: "console.log(\"Hello world!\");",
},
},
},
{
text: "Hello world!\n\nNew paragraph.",
nodes: []ast.Node{
&ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "Hello world!",
},
},
},
&ast.LineBreak{},
&ast.LineBreak{},
&ast.Paragraph{
Children: []ast.Node{
&ast.Text{
Content: "New paragraph.",
},
},
},
},
},
{
text: "1. hello\n- [ ] world",
nodes: []ast.Node{
&ast.OrderedList{
Number: "1",
Children: []ast.Node{
&ast.Text{
Content: "hello",
},
},
},
&ast.LineBreak{},
&ast.TaskList{
Symbol: tokenizer.Hyphen,
Complete: false,
Children: []ast.Node{
&ast.Text{
Content: "world",
},
},
},
},
},
{
text: "- [ ] hello\n- [x] world",
nodes: []ast.Node{
&ast.TaskList{
Symbol: tokenizer.Hyphen,
Complete: false,
Children: []ast.Node{
&ast.Text{
Content: "hello",
},
},
},
&ast.LineBreak{},
&ast.TaskList{
Symbol: tokenizer.Hyphen,
Complete: true,
Children: []ast.Node{
&ast.Text{
Content: "world",
},
},
},
},
},
{
text: "\n\n",
nodes: []ast.Node{
&ast.LineBreak{},
&ast.LineBreak{},
},
},
{
text: "\n$$\na=3\n$$",
nodes: []ast.Node{
&ast.LineBreak{},
&ast.MathBlock{
Content: "a=3",
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
nodes, _ := Parse(tokens)
require.Equal(t, restore.Restore(test.nodes), restore.Restore(nodes))
}
}

View File

@@ -1,51 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type StrikethroughParser struct{}
func NewStrikethroughParser() *StrikethroughParser {
return &StrikethroughParser{}
}
func (*StrikethroughParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 5 {
return 0, false
}
if tokens[0].Type != tokenizer.Tilde || tokens[1].Type != tokenizer.Tilde {
return 0, false
}
cursor, matched := 2, false
for ; cursor < len(tokens)-1; cursor++ {
token, nextToken := tokens[cursor], tokens[cursor+1]
if token.Type == tokenizer.Newline || nextToken.Type == tokenizer.Newline {
return 0, false
}
if token.Type == tokenizer.Tilde && nextToken.Type == tokenizer.Tilde {
matched = true
break
}
}
if !matched {
return 0, false
}
return cursor + 2, true
}
func (p *StrikethroughParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
contentTokens := tokens[2 : size-2]
return &ast.Strikethrough{
Content: tokenizer.Stringify(contentTokens),
}, nil
}

View File

@@ -1,47 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestStrikethroughParser(t *testing.T) {
tests := []struct {
text string
strikethrough ast.Node
}{
{
text: "~~Hello world",
strikethrough: nil,
},
{
text: "~~Hello~~",
strikethrough: &ast.Strikethrough{
Content: "Hello",
},
},
{
text: "~~ Hello ~~",
strikethrough: &ast.Strikethrough{
Content: " Hello ",
},
},
{
text: "~~1~~ Hello ~~~",
strikethrough: &ast.Strikethrough{
Content: "1",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewStrikethroughParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.strikethrough}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,164 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type TableParser struct{}
func NewTableParser() *TableParser {
return &TableParser{}
}
func (*TableParser) Match(tokens []*tokenizer.Token) (int, bool) {
headerTokens := []*tokenizer.Token{}
for _, token := range tokens {
if token.Type == tokenizer.Newline {
break
}
headerTokens = append(headerTokens, token)
}
if len(headerTokens) < 5 || len(tokens) < len(headerTokens)+3 {
return 0, false
}
delimiterTokens := []*tokenizer.Token{}
for _, token := range tokens[len(headerTokens)+1:] {
if token.Type == tokenizer.Newline {
break
}
delimiterTokens = append(delimiterTokens, token)
}
if len(delimiterTokens) < 5 || len(tokens) < len(headerTokens)+len(delimiterTokens)+3 {
return 0, false
}
rowTokens := []*tokenizer.Token{}
for index, token := range tokens[len(headerTokens)+len(delimiterTokens)+2:] {
temp := len(headerTokens) + len(delimiterTokens) + 2 + index
if token.Type == tokenizer.Newline && temp != len(tokens)-1 && tokens[temp+1].Type != tokenizer.Pipe {
break
}
rowTokens = append(rowTokens, token)
}
if len(rowTokens) < 5 {
return 0, false
}
// Check header.
if len(headerTokens) < 5 {
return 0, false
}
headerCells, ok := matchTableCellTokens(headerTokens)
if headerCells == 0 || !ok {
return 0, false
}
// Check delimiter.
if len(delimiterTokens) < 5 {
return 0, false
}
delimiterCells, ok := matchTableCellTokens(delimiterTokens)
if delimiterCells != headerCells || !ok {
return 0, false
}
for _, t := range tokenizer.Split(delimiterTokens, tokenizer.Pipe) {
delimiterTokens := t[1 : len(t)-1]
if len(delimiterTokens) < 3 {
return 0, false
}
if (delimiterTokens[0].Type != tokenizer.Colon && delimiterTokens[0].Type != tokenizer.Hyphen) || (delimiterTokens[len(delimiterTokens)-1].Type != tokenizer.Colon && delimiterTokens[len(delimiterTokens)-1].Type != tokenizer.Hyphen) {
return 0, false
}
for _, token := range delimiterTokens[1 : len(delimiterTokens)-1] {
if token.Type != tokenizer.Hyphen {
return 0, false
}
}
}
// Check rows.
if len(rowTokens) < 5 {
return 0, false
}
rows := tokenizer.Split(rowTokens, tokenizer.Newline)
if len(rows) == 0 {
return 0, false
}
for _, row := range rows {
cells, ok := matchTableCellTokens(row)
if cells != headerCells || !ok {
return 0, false
}
}
return len(headerTokens) + len(delimiterTokens) + len(rowTokens) + 2, true
}
func (p *TableParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
rawRows := tokenizer.Split(tokens[:size-1], tokenizer.Newline)
headerTokens := rawRows[0]
dilimiterTokens := rawRows[1]
rowTokens := rawRows[2:]
header := make([]string, 0)
delimiter := make([]string, 0)
rows := make([][]string, 0)
for _, t := range tokenizer.Split(headerTokens, tokenizer.Pipe) {
header = append(header, tokenizer.Stringify(t[1:len(t)-1]))
}
for _, t := range tokenizer.Split(dilimiterTokens, tokenizer.Pipe) {
delimiter = append(delimiter, tokenizer.Stringify(t[1:len(t)-1]))
}
for _, row := range rowTokens {
cells := make([]string, 0)
for _, t := range tokenizer.Split(row, tokenizer.Pipe) {
cells = append(cells, tokenizer.Stringify(t[1:len(t)-1]))
}
rows = append(rows, cells)
}
return &ast.Table{
Header: header,
Delimiter: delimiter,
Rows: rows,
}, nil
}
func matchTableCellTokens(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) == 0 {
return 0, false
}
pipes := 0
for _, token := range tokens {
if token.Type == tokenizer.Pipe {
pipes++
}
}
cells := tokenizer.Split(tokens, tokenizer.Pipe)
if len(cells) != pipes-1 {
return 0, false
}
for _, cellTokens := range cells {
if len(cellTokens) == 0 {
return 0, false
}
if cellTokens[0].Type != tokenizer.Space {
return 0, false
}
if cellTokens[len(cellTokens)-1].Type != tokenizer.Space {
return 0, false
}
}
return len(cells), true
}

View File

@@ -1,57 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestTableParser(t *testing.T) {
tests := []struct {
text string
table ast.Node
}{
{
text: "| header |\n| --- |\n| cell |\n",
table: &ast.Table{
Header: []string{"header"},
Delimiter: []string{"---"},
Rows: [][]string{
{"cell"},
},
},
},
{
text: "| header1 | header2 |\n| --- | ---- |\n| cell1 | cell2 |\n| cell3 | cell4 |",
table: &ast.Table{
Header: []string{"header1", "header2"},
Delimiter: []string{"---", "----"},
Rows: [][]string{
{"cell1", "cell2"},
{"cell3", "cell4"},
},
},
},
{
text: "| header1 | header2 |\n| :-- | ----: |\n| cell1 | cell2 |\n| cell3 | cell4 |",
table: &ast.Table{
Header: []string{"header1", "header2"},
Delimiter: []string{":--", "----:"},
Rows: [][]string{
{"cell1", "cell2"},
{"cell3", "cell4"},
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewTableParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.table}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,47 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type TagParser struct{}
func NewTagParser() *TagParser {
return &TagParser{}
}
func (*TagParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 2 {
return 0, false
}
if tokens[0].Type != tokenizer.PoundSign {
return 0, false
}
contentTokens := []*tokenizer.Token{}
for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space || token.Type == tokenizer.PoundSign {
break
}
contentTokens = append(contentTokens, token)
}
if len(contentTokens) == 0 {
return 0, false
}
return len(contentTokens) + 1, true
}
func (p *TagParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
contentTokens := tokens[1:size]
return &ast.Tag{
Content: tokenizer.Stringify(contentTokens),
}, nil
}

View File

@@ -1,45 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestTagParser(t *testing.T) {
tests := []struct {
text string
tag ast.Node
}{
{
text: "*Hello world",
tag: nil,
},
{
text: "# Hello World",
tag: nil,
},
{
text: "#tag",
tag: &ast.Tag{
Content: "tag",
},
},
{
text: "#tag/subtag 123",
tag: &ast.Tag{
Content: "tag/subtag",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewTagParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.tag}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,83 +0,0 @@
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type TaskListParser struct{}
func NewTaskListParser() *TaskListParser {
return &TaskListParser{}
}
func (*TaskListParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 7 {
return 0, false
}
indent := 0
for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
symbolToken := tokens[indent]
if symbolToken.Type != tokenizer.Hyphen && symbolToken.Type != tokenizer.Asterisk && symbolToken.Type != tokenizer.PlusSign {
return 0, false
}
if tokens[indent+1].Type != tokenizer.Space {
return 0, false
}
if tokens[indent+2].Type != tokenizer.LeftSquareBracket || (tokens[indent+3].Type != tokenizer.Space && tokens[indent+3].Value != "x") || tokens[indent+4].Type != tokenizer.RightSquareBracket {
return 0, false
}
if tokens[indent+5].Type != tokenizer.Space {
return 0, false
}
contentTokens := []*tokenizer.Token{}
for _, token := range tokens[indent+6:] {
if token.Type == tokenizer.Newline {
break
}
contentTokens = append(contentTokens, token)
}
if len(contentTokens) == 0 {
return 0, false
}
return indent + len(contentTokens) + 6, true
}
func (p *TaskListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
indent := 0
for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
symbolToken := tokens[indent]
contentTokens := tokens[indent+6 : size]
children, err := ParseInline(contentTokens)
if err != nil {
return nil, err
}
return &ast.TaskList{
Symbol: symbolToken.Type,
Indent: indent,
Complete: tokens[indent+3].Value == "x",
Children: children,
}, nil
}

View File

@@ -1,71 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/gomark/restore"
)
func TestTaskListParser(t *testing.T) {
tests := []struct {
text string
node ast.Node
}{
{
text: "*asd",
node: nil,
},
{
text: "+ [ ] Hello World",
node: &ast.TaskList{
Symbol: tokenizer.PlusSign,
Complete: false,
Children: []ast.Node{
&ast.Text{
Content: "Hello World",
},
},
},
},
{
text: " + [ ] Hello World",
node: &ast.TaskList{
Symbol: tokenizer.PlusSign,
Indent: 2,
Complete: false,
Children: []ast.Node{
&ast.Text{
Content: "Hello World",
},
},
},
},
{
text: "* [x] **Hello**",
node: &ast.TaskList{
Symbol: tokenizer.Asterisk,
Complete: true,
Children: []ast.Node{
&ast.Bold{
Symbol: "*",
Children: []ast.Node{
&ast.Text{
Content: "Hello",
},
},
},
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewTaskListParser().Parse(tokens)
require.Equal(t, restore.Restore([]ast.Node{test.node}), restore.Restore([]ast.Node{node}))
}
}

View File

@@ -1,30 +0,0 @@
package parser
import (
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type TextParser struct {
Content string
}
func NewTextParser() *TextParser {
return &TextParser{}
}
func (*TextParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) == 0 {
return 0, false
}
return 1, true
}
func (*TextParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
if len(tokens) == 0 {
return &ast.Text{}, nil
}
return &ast.Text{
Content: tokens[0].String(),
}, nil
}

Some files were not shown because too many files have changed in this diff Show More