diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6035c7cb9d2e9..5f8d7ea601259 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,11 +1,11 @@ name: Bug Report -description: Create a report to help us improve +description: If something isn't working as expected labels: [bug] body: - type: markdown attributes: value: | - If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead. + Before submitting a bug report, please check if the issue is already present in the issues. If it is, please add a reaction to the issue. If it isn't, please fill out the form below. - type: textarea attributes: label: Describe the bug @@ -24,8 +24,15 @@ body: 3. See error validations: required: true + - type: input + attributes: + label: The version of Memos you're using + description: | + Provide the version of Memos you're using. + validations: + required: true - type: textarea attributes: label: Screenshots or additional context description: | - Add screenshots or any other context about the problem. + If applicable, add screenshots to help explain your problem. And add any other context about the problem here. Such as the device you're using, etc. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index e9298d19a669a..6a312f56de644 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,28 +1,36 @@ name: Feature Request -description: Suggest an idea for this project +description: If you have a suggestion for a new feature labels: [enhancement] body: - type: markdown attributes: value: | - Thanks for taking the time to suggest an idea for memos! + Before submitting a feature request, please check if the issue is already present in the issues. If it is, please add a reaction to the issue. If it isn't, please fill out the form below. - type: textarea attributes: - label: Is your feature request related to a problem? + label: Describe the solution you'd like description: | - A clear and concise description of what the problem is. + A clear and concise description of what you want to happen. placeholder: | - I'm always frustrated when [...] + It would be great if [...] validations: required: true - - type: textarea + - type: dropdown attributes: - label: Describe the solution you'd like - description: | - A clear and concise description of what you want to happen. + label: Type of feature + description: What type of feature is this? + options: + - User Interface (UI) + - User Experience (UX) + - API + - Documentation + - Integrations + - Other + default: 0 validations: required: true - type: textarea attributes: label: Additional context - description: Add any other context or screenshots about the feature request. + description: | + What are you trying to do? Why is this important to you? diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index 01a50d6f4feda..0573d9c3a9e3b 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -17,14 +17,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@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 @@ -37,9 +37,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: - go-version: 1.21 + go-version: 1.22 check-latest: true cache: true - name: Run all tests diff --git a/.github/workflows/build-and-push-release-image.yml b/.github/workflows/build-and-push-release-image.yml index 4fba97afd5bb1..ddb04ca2acaa5 100644 --- a/.github/workflows/build-and-push-release-image.yml +++ b/.github/workflows/build-and-push-release-image.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Extract build args # Extract version from branch name @@ -25,13 +25,13 @@ jobs: echo "VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: neosmemo password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -39,26 +39,25 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: install: true version: v0.9.1 - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | 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 }} - name: Build and Push id: docker_build - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: ./ file: ./Dockerfile diff --git a/.github/workflows/build-and-push-stable-image.yml b/.github/workflows/build-and-push-stable-image.yml new file mode 100644 index 0000000000000..3c399563790e6 --- /dev/null +++ b/.github/workflows/build-and-push-stable-image.yml @@ -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 }} diff --git a/.github/workflows/build-and-push-test-image.yml b/.github/workflows/build-and-push-test-image.yml new file mode 100644 index 0000000000000..419dec832fa04 --- /dev/null +++ b/.github/workflows/build-and-push-test-image.yml @@ -0,0 +1,60 @@ +name: build-and-push-test-image + +on: + push: + branches: [main] + +jobs: + build-and-push-test-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 + flavor: | + latest=false + tags: | + type=raw,value=test + + - name: Build and Push + id: docker_build + uses: docker/build-push-action@v5 + with: + context: ./ + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index f5d57eaed2ddd..0000000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,74 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [main] - pull_request: - # The branches below must be a subset of the branches above - branches: [main] - paths: - - "go.mod" - - "go.sum" - - "**.go" - - "proto/**" - - "web/**" - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ["go", "javascript"] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml new file mode 100644 index 0000000000000..af4f5dcb8c3ed --- /dev/null +++ b/.github/workflows/frontend-tests.yml @@ -0,0 +1,48 @@ +name: Frontend Test + +on: + push: + branches: [main] + pull_request: + branches: + - main + - "release/*.*.*" + paths: + - "web/**" + +jobs: + eslint-checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v2.4.0 + with: + version: 8 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: pnpm + cache-dependency-path: "web/pnpm-lock.yaml" + - run: pnpm install + working-directory: web + - name: Run eslint check + run: pnpm lint + working-directory: web + + frontend-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v2.4.0 + with: + version: 8 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: pnpm + cache-dependency-path: "web/pnpm-lock.yaml" + - run: pnpm install + working-directory: web + - name: Run frontend build + run: pnpm build + working-directory: web diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000000..f787269dbf50d --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,17 @@ +name: Close Stale Issues + +on: + schedule: + - cron: "0 */8 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - uses: actions/stale@v9.0.0 + with: + days-before-issue-stale: 14 + days-before-issue-close: 7 diff --git a/.gitignore b/.gitignore index 240621df1419b..17f64ee21ad8a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ tmp # Frontend asset web/dist -server/dist +server/frontend/dist # build folder build @@ -16,4 +16,9 @@ build # Jetbrains .idea +# Docker Compose Environment File +.env + bin/air + +dev-dist diff --git a/.golangci.yaml b/.golangci.yaml index 0c1ba43265965..31aac66c2df25 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -67,6 +67,14 @@ linters-settings: disabled: true - name: early-return disabled: true + - name: use-any + disabled: true + - name: exported + disabled: true + - name: unhandled-error + disabled: true + - name: if-return + disabled: true gocritic: disabled-checks: - ifElseChain diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 12a59cb012e3e..0000000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["golang.go"] -} diff --git a/.vscode/project.code-workspace b/.vscode/project.code-workspace deleted file mode 100644 index b27ee896d466d..0000000000000 --- a/.vscode/project.code-workspace +++ /dev/null @@ -1,12 +0,0 @@ -{ - "folders": [ - { - "name": "server", - "path": "../" - }, - { - "name": "web", - "path": "../web" - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index bf4a44ad73aff..0000000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "json.schemaDownload.enable":true, - "go.lintOnSave": "workspace", - "go.lintTool": "golangci-lint", -} diff --git a/Dockerfile b/Dockerfile index 0a5e3d34cc624..effaa8a6e068a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,18 +6,17 @@ 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 . . -COPY --from=frontend /frontend-build/web/dist ./server/dist -RUN CGO_ENABLED=0 go build -o memos ./main.go +RUN CGO_ENABLED=0 go build -o memos ./bin/memos/main.go # Make workspace with above generated files. FROM whatwewant/alpine:v3.17-1 AS monolithic @@ -26,6 +25,7 @@ WORKDIR /usr/local/memos RUN apk add --no-cache tzdata ENV TZ="UTC" +COPY --from=frontend /frontend-build/web/dist /usr/local/memos/dist COPY --from=backend /backend-build/memos /usr/local/memos/ EXPOSE 5230 diff --git a/README.md b/README.md index 20f3524607d59..3136668381498 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -# memos - - + A privacy-first, lightweight note-taking service. Easily capture and share your great thoughts. @@ -10,13 +8,11 @@ A privacy-first, lightweight note-taking service. Easily capture and share your Live Demo
-![demo](https://www.usememos.com/demo.webp) +![demo](https://www.usememos.com/demo.png) ## Key points @@ -29,7 +25,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. @@ -41,23 +37,22 @@ Learn more about [other installation methods](https://www.usememos.com/docs/inst Contributions are what make the open-source community such an amazing place to learn, inspire, and create. We greatly appreciate any contributions you make. Thank you for being a part of our community! 🥰 - + ---- +## Internationalization + +Memos supports multiple languages. You can help us translate Memos into your language. We use Weblate to manage translations. -- [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 -- [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. diff --git a/SECURITY.md b/SECURITY.md index af97c79abca7d..48ab17ab2f976 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,4 +4,4 @@ Report security bugs via GitHub [issues](https://github.com/usememos/memos/issues). -For more information, please contact [stevenlgtm@gmail.com](stevenlgtm@gmail.com). +For more information, please contact [usememos@gmail.com](usememos@gmail.com). diff --git a/api/v1/rss.go b/api/v1/rss.go deleted file mode 100644 index 8d8da824d9641..0000000000000 --- a/api/v1/rss.go +++ /dev/null @@ -1,211 +0,0 @@ -package v1 - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "github.com/gorilla/feeds" - "github.com/labstack/echo/v4" - "github.com/pkg/errors" - "github.com/yuin/goldmark" - - "github.com/usememos/memos/internal/util" - "github.com/usememos/memos/store" -) - -const maxRSSItemCount = 100 -const maxRSSItemTitleLength = 100 - -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++ { - memo := memoList[i] - feed.Items[i] = &feeds.Item{ - Title: getRSSItemTitle(memo.Content), - Link: &feeds.Link{Href: baseURL + "/m/" + fmt.Sprintf("%d", memo.ID)}, - Description: getRSSItemDescription(memo.Content), - Created: time.Unix(memo.CreatedTs, 0), - Enclosure: &feeds.Enclosure{Url: baseURL + "/m/" + fmt.Sprintf("%d", memo.ID) + "/image"}, - } - if len(memo.ResourceIDList) > 0 { - resourceID := memo.ResourceIDList[0] - resource, err := s.Store.GetResource(ctx, &store.FindResource{ - ID: &resourceID, - }) - if err != nil { - return "", err - } - if resource == nil { - return "", errors.Errorf("Resource not found: %d", resourceID) - } - 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", - LogoURL: "", - Description: "", - Locale: "en", - Appearance: "system", - ExternalURL: "", - } - if systemSetting != nil { - if err := json.Unmarshal([]byte(systemSetting.Value), customizedProfile); err != nil { - return nil, err - } - } - return customizedProfile, nil -} - -func getRSSItemTitle(content string) string { - var title string - if isTitleDefined(content) { - title = strings.Split(content, "\n")[0][2:] - } else { - 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 { - var description string - if isTitleDefined(content) { - var firstLineEnd = strings.Index(content, "\n") - description = strings.Trim(content[firstLineEnd+1:], " ") - } else { - description = content - } - - // TODO: use our `./plugin/gomark` parser to handle markdown-like content. - var buf bytes.Buffer - if err := goldmark.Convert([]byte(description), &buf); err != nil { - panic(err) - } - return buf.String() -} - -func isTitleDefined(content string) bool { - return strings.HasPrefix(content, "# ") -} diff --git a/api/v1/user_setting.go b/api/v1/user_setting.go deleted file mode 100644 index 52ae45803cbb6..0000000000000 --- a/api/v1/user_setting.go +++ /dev/null @@ -1,174 +0,0 @@ -package v1 - -import ( - "encoding/json" - "net/http" - - "github.com/labstack/echo/v4" - "github.com/pkg/errors" - "golang.org/x/exp/slices" - - "github.com/usememos/memos/store" -) - -type UserSettingKey string - -const ( - // UserSettingLocaleKey is the key type for user locale. - UserSettingLocaleKey UserSettingKey = "locale" - // UserSettingAppearanceKey is the key type for user appearance. - UserSettingAppearanceKey UserSettingKey = "appearance" - // UserSettingMemoVisibilityKey is the key type for user preference memo default visibility. - UserSettingMemoVisibilityKey UserSettingKey = "memo-visibility" - // UserSettingTelegramUserIDKey is the key type for telegram UserID of memos user. - UserSettingTelegramUserIDKey UserSettingKey = "telegram-user-id" -) - -// String returns the string format of UserSettingKey type. -func (key UserSettingKey) String() string { - switch key { - case UserSettingLocaleKey: - return "locale" - case UserSettingAppearanceKey: - return "appearance" - case UserSettingMemoVisibilityKey: - return "memo-visibility" - case UserSettingTelegramUserIDKey: - return "telegram-user-id" - } - return "" -} - -var ( - UserSettingLocaleValue = []string{ - "ar", - "de", - "en", - "es", - "fr", - "hi", - "hr", - "it", - "ja", - "ko", - "nl", - "pl", - "pt-BR", - "ru", - "sl", - "sv", - "tr", - "uk", - "vi", - "zh-Hans", - "zh-Hant", - } - UserSettingAppearanceValue = []string{"system", "light", "dark"} - UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public} -) - -type UserSetting struct { - UserID int32 `json:"userId"` - Key UserSettingKey `json:"key"` - Value string `json:"value"` -} - -type UpsertUserSettingRequest struct { - UserID int32 `json:"-"` - Key UserSettingKey `json:"key"` - Value string `json:"value"` -} - -func (s *APIV1Service) registerUserSettingRoutes(g *echo.Group) { - g.POST("/user/setting", s.UpsertUserSetting) -} - -// UpsertUserSetting godoc -// -// @Summary Upsert user setting -// @Tags user-setting -// @Accept json -// @Produce json -// @Param body body UpsertUserSettingRequest true "Request object." -// @Success 200 {object} store.UserSetting "Created user setting" -// @Failure 400 {object} nil "Malformatted post user setting upsert request | Invalid user setting format" -// @Failure 401 {object} nil "Missing auth session" -// @Failure 500 {object} nil "Failed to upsert user setting" -// @Router /api/v1/user/setting [POST] -func (s *APIV1Service) UpsertUserSetting(c echo.Context) error { - ctx := c.Request().Context() - userID, ok := c.Get(userIDContextKey).(int32) - if !ok { - return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session") - } - - userSettingUpsert := &UpsertUserSettingRequest{} - if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err) - } - if err := userSettingUpsert.Validate(); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid user setting format").SetInternal(err) - } - - userSettingUpsert.UserID = userID - userSetting, err := s.Store.UpsertUserSetting(ctx, &store.UserSetting{ - UserID: userID, - Key: userSettingUpsert.Key.String(), - Value: userSettingUpsert.Value, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err) - } - - userSettingMessage := convertUserSettingFromStore(userSetting) - return c.JSON(http.StatusOK, userSettingMessage) -} - -func (upsert UpsertUserSettingRequest) Validate() error { - if upsert.Key == UserSettingLocaleKey { - localeValue := "en" - err := json.Unmarshal([]byte(upsert.Value), &localeValue) - if err != nil { - return errors.New("failed to unmarshal user setting locale value") - } - if !slices.Contains(UserSettingLocaleValue, localeValue) { - return errors.New("invalid user setting locale value") - } - } else if upsert.Key == UserSettingAppearanceKey { - appearanceValue := "system" - err := json.Unmarshal([]byte(upsert.Value), &appearanceValue) - if err != nil { - return errors.New("failed to unmarshal user setting appearance value") - } - if !slices.Contains(UserSettingAppearanceValue, appearanceValue) { - return errors.New("invalid user setting appearance value") - } - } else if upsert.Key == UserSettingMemoVisibilityKey { - memoVisibilityValue := Private - err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue) - if err != nil { - return errors.New("failed to unmarshal user setting memo visibility value") - } - if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) { - return errors.New("invalid user setting memo visibility value") - } - } else if upsert.Key == UserSettingTelegramUserIDKey { - var key string - err := json.Unmarshal([]byte(upsert.Value), &key) - if err != nil { - return errors.New("invalid user setting telegram user id value") - } - } else { - return errors.New("invalid user setting key") - } - - return nil -} - -func convertUserSettingFromStore(userSetting *store.UserSetting) *UserSetting { - return &UserSetting{ - UserID: userSetting.UserID, - Key: UserSettingKey(userSetting.Key), - Value: userSetting.Value, - } -} diff --git a/api/v2/acl_config.go b/api/v2/acl_config.go deleted file mode 100644 index 489bc50a0267e..0000000000000 --- a/api/v2/acl_config.go +++ /dev/null @@ -1,26 +0,0 @@ -package v2 - -import "strings" - -var authenticationAllowlistMethods = map[string]bool{ - "/memos.api.v2.SystemService/GetSystemInfo": true, - "/memos.api.v2.UserService/GetUser": true, - "/memos.api.v2.MemoService/ListMemos": true, -} - -// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication. -func isUnauthorizeAllowedMethod(fullMethodName string) bool { - if strings.HasPrefix(fullMethodName, "/grpc.reflection") { - return true - } - return authenticationAllowlistMethods[fullMethodName] -} - -var allowedMethodsOnlyForAdmin = map[string]bool{ - "/memos.api.v2.UserService/CreateUser": true, -} - -// isOnlyForAdminAllowedMethod returns true if the method is allowed to be called only by admin. -func isOnlyForAdminAllowedMethod(methodName string) bool { - return allowedMethodsOnlyForAdmin[methodName] -} diff --git a/api/v2/memo_service.go b/api/v2/memo_service.go deleted file mode 100644 index 2b2bac5aa8f71..0000000000000 --- a/api/v2/memo_service.go +++ /dev/null @@ -1,266 +0,0 @@ -package v2 - -import ( - "context" - - "github.com/google/cel-go/cel" - "github.com/pkg/errors" - v1alpha1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" - "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) CreateMemo(ctx context.Context, request *apiv2pb.CreateMemoRequest) (*apiv2pb.CreateMemoResponse, error) { - 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") - } - - create := &store.Memo{ - CreatorID: user.ID, - Content: request.Content, - Visibility: store.Visibility(request.Visibility.String()), - } - memo, err := s.Store.CreateMemo(ctx, create) - if err != nil { - return nil, err - } - - response := &apiv2pb.CreateMemoResponse{ - Memo: convertMemoFromStore(memo), - } - return response, nil -} - -func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemosRequest) (*apiv2pb.ListMemosResponse, error) { - memoFind := &store.FindMemo{} - if request.Filter != "" { - filter, err := parseListMemosFilter(request.Filter) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) - } - if filter.Visibility != nil { - memoFind.VisibilityList = []store.Visibility{*filter.Visibility} - } - if filter.CreatedTsBefore != nil { - memoFind.CreatedTsBefore = filter.CreatedTsBefore - } - if filter.CreatedTsAfter != nil { - memoFind.CreatedTsAfter = filter.CreatedTsAfter - } - } - 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 request.CreatorId != nil { - memoFind.CreatorID = request.CreatorId - } - - // Remove the private memos from the list if the user is not the creator. - if user != nil && request.CreatorId != nil && *request.CreatorId != user.ID { - var filteredVisibility []store.Visibility - for _, v := range memoFind.VisibilityList { - if v != store.Private { - filteredVisibility = append(filteredVisibility, v) - } - } - memoFind.VisibilityList = filteredVisibility - } - - if request.PageSize != 0 { - offset := int(request.Page * request.PageSize) - limit := int(request.PageSize) - memoFind.Offset = &offset - memoFind.Limit = &limit - } - memos, err := s.Store.ListMemos(ctx, memoFind) - if err != nil { - return nil, err - } - - memoMessages := make([]*apiv2pb.Memo, len(memos)) - for i, memo := range memos { - memoMessages[i] = convertMemoFromStore(memo) - } - - response := &apiv2pb.ListMemosResponse{ - Memos: memoMessages, - } - return response, nil -} - -func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv2pb.GetMemoRequest) (*apiv2pb.GetMemoResponse, error) { - memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ - ID: &request.Id, - }) - 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") - } - } - - response := &apiv2pb.GetMemoResponse{ - Memo: convertMemoFromStore(memo), - } - return response, nil -} - -func (s *APIV2Service) CreateMemoComment(ctx context.Context, request *apiv2pb.CreateMemoCommentRequest) (*apiv2pb.CreateMemoCommentResponse, error) { - // Create the comment memo first. - createMemoResponse, err := s.CreateMemo(ctx, request.Create) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to create memo") - } - - // Build the relation between the comment memo and the original memo. - memo := createMemoResponse.Memo - _, err = s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{ - MemoID: memo.Id, - RelatedMemoID: request.Id, - Type: store.MemoRelationComment, - }) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to create memo relation") - } - - response := &apiv2pb.CreateMemoCommentResponse{ - Memo: memo, - } - return response, nil -} - -func (s *APIV2Service) ListMemoComments(ctx context.Context, request *apiv2pb.ListMemoCommentsRequest) (*apiv2pb.ListMemoCommentsResponse, error) { - memoRelationComment := store.MemoRelationComment - memoRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ - RelatedMemoID: &request.Id, - Type: &memoRelationComment, - }) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to list memo relations") - } - - var memos []*apiv2pb.Memo - for _, memoRelation := range memoRelations { - memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ - ID: &memoRelation.MemoID, - }) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get memo") - } - if memo != nil { - memos = append(memos, convertMemoFromStore(memo)) - } - } - - response := &apiv2pb.ListMemoCommentsResponse{ - Memos: memos, - } - return response, nil -} - -// ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter. -var ListMemosFilterCELAttributes = []cel.EnvOption{ - cel.Variable("visibility", cel.StringType), - cel.Variable("created_ts_before", cel.IntType), - cel.Variable("created_ts_after", cel.IntType), -} - -type ListMemosFilter struct { - Visibility *store.Visibility - CreatedTsBefore *int64 - CreatedTsAfter *int64 -} - -func parseListMemosFilter(expression string) (*ListMemosFilter, error) { - e, err := cel.NewEnv(ListMemosFilterCELAttributes...) - if err != nil { - return nil, err - } - ast, issues := e.Compile(expression) - if issues != nil { - return nil, errors.Errorf("found issue %v", issues) - } - filter := &ListMemosFilter{} - expr, err := cel.AstToParsedExpr(ast) - if err != nil { - return nil, err - } - callExpr := expr.GetExpr().GetCallExpr() - findField(callExpr, filter) - return filter, nil -} - -func findField(callExpr *v1alpha1.Expr_Call, filter *ListMemosFilter) { - if len(callExpr.Args) == 2 { - idExpr := callExpr.Args[0].GetIdentExpr() - if idExpr != nil { - if idExpr.Name == "visibility" { - visibility := store.Visibility(callExpr.Args[1].GetConstExpr().GetStringValue()) - filter.Visibility = &visibility - } - if idExpr.Name == "created_ts_before" { - createdTsBefore := callExpr.Args[1].GetConstExpr().GetInt64Value() - filter.CreatedTsBefore = &createdTsBefore - } - if idExpr.Name == "created_ts_after" { - createdTsAfter := callExpr.Args[1].GetConstExpr().GetInt64Value() - filter.CreatedTsAfter = &createdTsAfter - } - return - } - } - for _, arg := range callExpr.Args { - callExpr := arg.GetCallExpr() - if callExpr != nil { - findField(callExpr, filter) - } - } -} - -func convertMemoFromStore(memo *store.Memo) *apiv2pb.Memo { - return &apiv2pb.Memo{ - Id: int32(memo.ID), - RowStatus: convertRowStatusFromStore(memo.RowStatus), - CreatedTs: memo.CreatedTs, - UpdatedTs: memo.UpdatedTs, - CreatorId: int32(memo.CreatorID), - Content: memo.Content, - Visibility: convertVisibilityFromStore(memo.Visibility), - Pinned: memo.Pinned, - } -} - -func convertVisibilityFromStore(visibility store.Visibility) apiv2pb.Visibility { - switch visibility { - case store.Private: - return apiv2pb.Visibility_PRIVATE - case store.Protected: - return apiv2pb.Visibility_PROTECTED - case store.Public: - return apiv2pb.Visibility_PUBLIC - default: - return apiv2pb.Visibility_VISIBILITY_UNSPECIFIED - } -} diff --git a/api/v2/system_service.go b/api/v2/system_service.go deleted file mode 100644 index af37a22c734c8..0000000000000 --- a/api/v2/system_service.go +++ /dev/null @@ -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 _, path := range request.UpdateMask.Paths { - if path == "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 path == "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 path == "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 path == "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 -} diff --git a/api/v2/tag_service.go b/api/v2/tag_service.go deleted file mode 100644 index f1cd8561a6e1b..0000000000000 --- a/api/v2/tag_service.go +++ /dev/null @@ -1,64 +0,0 @@ -package v2 - -import ( - "context" - - "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) UpsertTag(ctx context.Context, request *apiv2pb.UpsertTagRequest) (*apiv2pb.UpsertTagResponse, error) { - user, err := getCurrentUser(ctx, s.Store) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get user") - } - - tag, err := s.Store.UpsertTag(ctx, &store.Tag{ - Name: request.Name, - CreatorID: user.ID, - }) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err) - } - - return &apiv2pb.UpsertTagResponse{ - Tag: convertTagFromStore(tag), - }, nil -} - -func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRequest) (*apiv2pb.ListTagsResponse, error) { - tags, err := s.Store.ListTags(ctx, &store.FindTag{ - CreatorID: request.CreatorId, - }) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err) - } - - response := &apiv2pb.ListTagsResponse{} - for _, tag := range tags { - response.Tags = append(response.Tags, convertTagFromStore(tag)) - } - return response, nil -} - -func (s *APIV2Service) DeleteTag(ctx context.Context, request *apiv2pb.DeleteTagRequest) (*apiv2pb.DeleteTagResponse, error) { - err := s.Store.DeleteTag(ctx, &store.DeleteTag{ - Name: request.Tag.Name, - CreatorID: request.Tag.CreatorId, - }) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err) - } - - return &apiv2pb.DeleteTagResponse{}, nil -} - -func convertTagFromStore(tag *store.Tag) *apiv2pb.Tag { - return &apiv2pb.Tag{ - Name: tag.Name, - CreatorId: int32(tag.CreatorID), - } -} diff --git a/cmd/memos.go b/bin/memos/main.go similarity index 71% rename from cmd/memos.go rename to bin/memos/main.go index c391d35e26c3d..9669d8188f661 100644 --- a/cmd/memos.go +++ b/bin/memos/main.go @@ -1,8 +1,9 @@ -package cmd +package main import ( "context" "fmt" + "log/slog" "net/http" "os" "os/signal" @@ -10,12 +11,10 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "go.uber.org/zap" - "github.com/usememos/memos/internal/log" + "github.com/usememos/memos/internal/jobs" "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 +31,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", @@ -49,29 +48,27 @@ var ( dbDriver, err := db.NewDBDriver(profile) if err != nil { cancel() - log.Error("failed to create db driver", zap.Error(err)) + slog.Error("failed to create db driver", err) return } if err := dbDriver.Migrate(ctx); err != nil { cancel() - log.Error("failed to migrate db", zap.Error(err)) + slog.Error("failed to migrate database", err) return } - store := store.New(dbDriver, profile) - s, err := server.NewServer(ctx, profile, store) - if err != nil { + storeInstance := store.New(dbDriver, profile) + if err := storeInstance.MigrateManually(ctx); err != nil { cancel() - log.Error("failed to create server", zap.Error(err)) + slog.Error("failed to migrate manually", err) return } - if profile.Metric { - println("metric collection is enabled") - // nolint - metric.NewMetricClient(s.ID, *profile) - } else { - println("metric collection is disabled") + s, err := server.NewServer(ctx, profile, storeInstance) + if err != nil { + cancel() + slog.Error("failed to create server", err) + return } c := make(chan os.Signal, 1) @@ -80,17 +77,19 @@ var ( // which is taken as the graceful shutdown signal for many systems, eg., Kubernetes, Gunicorn. signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { - sig := <-c - log.Info(fmt.Sprintf("%s received.\n", sig.String())) + <-c s.Shutdown(ctx) cancel() }() 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)) + slog.Error("failed to start server", err) cancel() } } @@ -102,7 +101,6 @@ var ( ) func Execute() error { - defer log.Sync() return rootCmd.Execute() } @@ -115,7 +113,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 { @@ -141,7 +139,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) } @@ -150,7 +148,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") } @@ -163,17 +161,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() { @@ -183,9 +182,17 @@ 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() { + err := Execute() + if err != nil { + panic(err) + } } diff --git a/cmd/copydb.go b/cmd/copydb.go deleted file mode 100644 index fed5c1963ed7a..0000000000000 --- a/cmd/copydb.go +++ /dev/null @@ -1,383 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "strings" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - _profile "github.com/usememos/memos/server/profile" - "github.com/usememos/memos/store" - "github.com/usememos/memos/store/db" -) - -var ( - copydbCmdFlagFrom = "from" - copydbCmd = &cobra.Command{ - Use: "copydb", // `copydb` is a shortened for 'copy database' - Short: "Copy data between db drivers", - Run: func(cmd *cobra.Command, _ []string) { - s, err := cmd.Flags().GetString(copydbCmdFlagFrom) - if err != nil { - println("fail to get from driver DSN") - println(err) - return - } - ss := strings.Split(s, "://") - if len(ss) != 2 { - println("fail to parse from driver DSN, should be like 'sqlite://memos_prod.db' or 'mysql://user:pass@tcp(host)/memos'") - return - } - - fromProfile := &_profile.Profile{Driver: ss[0], DSN: ss[1]} - - err = copydb(fromProfile, profile) - if err != nil { - fmt.Printf("fail to copydb: %s\n", err) - return - } - - println("done") - }, - } -) - -func init() { - copydbCmd.Flags().String(copydbCmdFlagFrom, "sqlite://memos_prod.db", "From driver DSN") - - rootCmd.AddCommand(copydbCmd) -} - -func copydb(fromProfile, toProfile *_profile.Profile) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - toDriver, err := db.NewDBDriver(toProfile) - if err != nil { - return errors.Wrap(err, "fail to create `to` driver") - } - - if err := toDriver.Migrate(ctx); err != nil { - return errors.Wrap(err, "fail to migrate db") - } - - fromDriver, err := db.NewDBDriver(fromProfile) - if err != nil { - return errors.Wrap(err, "fail to create `from` driver") - } - - // Register here if any table is added - copyMap := map[string]func(context.Context, store.Driver, store.Driver) error{ - "activity": copyActivity, - "idp": copyIdp, - "memo": copyMemo, - "memo_organizer": copyMemoOrganizer, - "memo_relation": copyMemoRelation, - "resource": copyResource, - "storage": copyStorage, - "system_setting": copySystemSettings, - "tag": copyTag, - "user": copyUser, - "user_setting": copyUserSettings, - } - - toDb := toDriver.GetDB() - for table := range copyMap { - println("Checking " + table + "...") - var cnt int - err := toDb.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+table).Scan(&cnt) - if err != nil { - return errors.Wrapf(err, "fail to check '%s'", table) - } - if cnt > 0 { - return errors.Errorf("table '%s' is not empty", table) - } - } - - for _, f := range copyMap { - err = f(ctx, fromDriver, toDriver) - if err != nil { - return errors.Wrap(err, "fail to copy data") - } - } - - return nil -} - -func copyActivity(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying Activity...") - list, err := fromDriver.ListActivities(ctx, &store.FindActivity{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.CreateActivity(ctx, &store.Activity{ - ID: item.ID, - CreatorID: item.CreatorID, - CreatedTs: item.CreatedTs, - Level: item.Level, - Type: item.Type, - Payload: item.Payload, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} - -func copyIdp(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying IdentityProvider...") - list, err := fromDriver.ListIdentityProviders(ctx, &store.FindIdentityProvider{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.CreateIdentityProvider(ctx, &store.IdentityProvider{ - ID: item.ID, - Name: item.Name, - Type: item.Type, - IdentifierFilter: item.IdentifierFilter, - Config: item.Config, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} - -func copyMemo(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying Memo...") - list, err := fromDriver.ListMemos(ctx, &store.FindMemo{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.CreateMemo(ctx, &store.Memo{ - ID: item.ID, - CreatorID: item.CreatorID, - CreatedTs: item.CreatedTs, - UpdatedTs: item.UpdatedTs, - RowStatus: item.RowStatus, - Content: item.Content, - Visibility: item.Visibility, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} - -func copyMemoOrganizer(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying MemoOrganizer...") - list, err := fromDriver.ListMemoOrganizer(ctx, &store.FindMemoOrganizer{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.UpsertMemoOrganizer(ctx, &store.MemoOrganizer{ - MemoID: item.MemoID, - UserID: item.UserID, - Pinned: item.Pinned, - }) - if err != nil { - return err - } - } - println("\tDONE") - return nil -} - -func copyMemoRelation(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying MemoRelation...") - list, err := fromDriver.ListMemoRelations(ctx, &store.FindMemoRelation{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.UpsertMemoRelation(ctx, &store.MemoRelation{ - MemoID: item.MemoID, - RelatedMemoID: item.RelatedMemoID, - Type: item.Type, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} - -func copyResource(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying Resource...") - list, err := fromDriver.ListResources(ctx, &store.FindResource{GetBlob: true}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.CreateResource(ctx, &store.Resource{ - ID: item.ID, - CreatorID: item.CreatorID, - CreatedTs: item.CreatedTs, - UpdatedTs: item.UpdatedTs, - Filename: item.Filename, - Blob: item.Blob, - ExternalLink: item.ExternalLink, - Type: item.Type, - Size: item.Size, - InternalPath: item.InternalPath, - MemoID: item.MemoID, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} - -func copyStorage(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying Storage...") - list, err := fromDriver.ListStorages(ctx, &store.FindStorage{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.CreateStorage(ctx, &store.Storage{ - ID: item.ID, - Name: item.Name, - Type: item.Type, - Config: item.Config, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} - -func copySystemSettings(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying SystemSettings...") - list, err := fromDriver.ListSystemSettings(ctx, &store.FindSystemSetting{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.UpsertSystemSetting(ctx, &store.SystemSetting{ - Name: item.Name, - Value: item.Value, - Description: item.Description, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} - -func copyTag(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying Tag...") - list, err := fromDriver.ListTags(ctx, &store.FindTag{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.UpsertTag(ctx, &store.Tag{ - Name: item.Name, - CreatorID: item.CreatorID, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} - -func copyUser(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying User...") - list, err := fromDriver.ListUsers(ctx, &store.FindUser{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.CreateUser(ctx, &store.User{ - ID: item.ID, - CreatedTs: item.CreatedTs, - UpdatedTs: item.UpdatedTs, - RowStatus: item.RowStatus, - Username: item.Username, - Role: item.Role, - Email: item.Email, - Nickname: item.Nickname, - PasswordHash: item.PasswordHash, - AvatarURL: item.AvatarURL, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} - -func copyUserSettings(ctx context.Context, fromDriver, toDriver store.Driver) error { - println("Copying UserSettings...") - list, err := fromDriver.ListUserSettings(ctx, &store.FindUserSetting{}) - if err != nil { - return err - } - - fmt.Printf("\tTotal %d records\n", len(list)) - for _, item := range list { - _, err := toDriver.UpsertUserSetting(ctx, &store.UserSetting{ - Key: item.Key, - Value: item.Value, - UserID: item.UserID, - }) - if err != nil { - return err - } - } - - println("\tDONE") - return nil -} diff --git a/cmd/mvrss.go b/cmd/mvrss.go deleted file mode 100644 index 402b42cb0e4c9..0000000000000 --- a/cmd/mvrss.go +++ /dev/null @@ -1,99 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/spf13/cobra" - - "github.com/usememos/memos/store" - "github.com/usememos/memos/store/db/sqlite" -) - -var ( - mvrssCmdFlagFrom = "from" - mvrssCmdFlagTo = "to" - mvrssCmd = &cobra.Command{ - Use: "mvrss", // `mvrss` is a shortened for 'means move resource' - Short: "Move resource between storage", - Run: func(cmd *cobra.Command, _ []string) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - from, err := cmd.Flags().GetString(mvrssCmdFlagFrom) - if err != nil { - fmt.Printf("failed to get from storage, error: %+v\n", err) - return - } - - to, err := cmd.Flags().GetString(mvrssCmdFlagTo) - if err != nil { - fmt.Printf("failed to get to storage, error: %+v\n", err) - return - } - - if from != "local" || to != "db" { - fmt.Printf("only local=>db be supported currently\n") - return - } - - driver, err := sqlite.NewDB(profile) - if err != nil { - fmt.Printf("failed to create db driver, error: %+v\n", err) - return - } - if err := driver.Migrate(ctx); err != nil { - fmt.Printf("failed to migrate db, error: %+v\n", err) - return - } - - s := store.New(driver, profile) - resources, err := s.ListResources(ctx, &store.FindResource{}) - if err != nil { - fmt.Printf("failed to list resources, error: %+v\n", err) - return - } - - var emptyString string - for _, res := range resources { - if res.InternalPath == "" { - continue - } - - buf, err := os.ReadFile(res.InternalPath) - if err != nil { - fmt.Printf("Resource %5d failed to read file: %s\n", res.ID, err) - continue - } - - if len(buf) != int(res.Size) { - fmt.Printf("Resource %5d size of file %d != %d\n", res.ID, len(buf), res.Size) - continue - } - - update := store.UpdateResource{ - ID: res.ID, - Blob: buf, - InternalPath: &emptyString, - } - _, err = s.UpdateResource(ctx, &update) - if err != nil { - fmt.Printf("Resource %5d failed to update: %s\n", res.ID, err) - continue - } - - fmt.Printf("Resource %5d copy %12d bytes from %s\n", res.ID, len(buf), res.InternalPath) - } - println("done") - }, - } -) - -func init() { - mvrssCmd.Flags().String(mvrssCmdFlagFrom, "local", "From storage") - mvrssCmd.Flags().String(mvrssCmdFlagTo, "db", "To Storage") - - rootCmd.AddCommand(mvrssCmd) -} diff --git a/cmd/setup.go b/cmd/setup.go deleted file mode 100644 index 3683b46c911c1..0000000000000 --- a/cmd/setup.go +++ /dev/null @@ -1,142 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "time" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/crypto/bcrypt" - - "github.com/usememos/memos/internal/util" - "github.com/usememos/memos/store" - "github.com/usememos/memos/store/db/sqlite" -) - -var ( - setupCmdFlagHostUsername = "host-username" - setupCmdFlagHostPassword = "host-password" - setupCmd = &cobra.Command{ - Use: "setup", - Short: "Make initial setup for memos", - Run: func(cmd *cobra.Command, _ []string) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - hostUsername, err := cmd.Flags().GetString(setupCmdFlagHostUsername) - if err != nil { - fmt.Printf("failed to get owner username, error: %+v\n", err) - return - } - - hostPassword, err := cmd.Flags().GetString(setupCmdFlagHostPassword) - if err != nil { - fmt.Printf("failed to get owner password, error: %+v\n", err) - return - } - - driver, err := sqlite.NewDB(profile) - if err != nil { - fmt.Printf("failed to create db driver, error: %+v\n", err) - return - } - if err := driver.Migrate(ctx); err != nil { - fmt.Printf("failed to migrate db, error: %+v\n", err) - return - } - - store := store.New(driver, profile) - if err := ExecuteSetup(ctx, store, hostUsername, hostPassword); err != nil { - fmt.Printf("failed to setup, error: %+v\n", err) - return - } - }, - } -) - -func init() { - setupCmd.Flags().String(setupCmdFlagHostUsername, "", "Owner username") - setupCmd.Flags().String(setupCmdFlagHostPassword, "", "Owner password") - - rootCmd.AddCommand(setupCmd) -} - -func ExecuteSetup(ctx context.Context, store *store.Store, hostUsername, hostPassword string) error { - s := setupService{store: store} - return s.Setup(ctx, hostUsername, hostPassword) -} - -type setupService struct { - store *store.Store -} - -func (s setupService) Setup(ctx context.Context, hostUsername, hostPassword string) error { - if err := s.makeSureHostUserNotExists(ctx); err != nil { - return err - } - - if err := s.createUser(ctx, hostUsername, hostPassword); err != nil { - return errors.Wrap(err, "create user") - } - return nil -} - -func (s setupService) makeSureHostUserNotExists(ctx context.Context) error { - hostUserType := store.RoleHost - existedHostUsers, err := s.store.ListUsers(ctx, &store.FindUser{Role: &hostUserType}) - if err != nil { - return errors.Wrap(err, "find user list") - } - - if len(existedHostUsers) != 0 { - return errors.New("host user already exists") - } - - return nil -} - -func (s setupService) createUser(ctx context.Context, hostUsername, hostPassword string) error { - userCreate := &store.User{ - Username: hostUsername, - // The new signup user should be normal user by default. - Role: store.RoleHost, - Nickname: hostUsername, - } - - if len(userCreate.Username) < 3 { - return errors.New("username is too short, minimum length is 3") - } - if len(userCreate.Username) > 32 { - return errors.New("username is too long, maximum length is 32") - } - if len(hostPassword) < 3 { - return errors.New("password is too short, minimum length is 3") - } - if len(hostPassword) > 512 { - return errors.New("password is too long, maximum length is 512") - } - if len(userCreate.Nickname) > 64 { - return errors.New("nickname is too long, maximum length is 64") - } - if userCreate.Email != "" { - if len(userCreate.Email) > 256 { - return errors.New("email is too long, maximum length is 256") - } - if !util.ValidateEmail(userCreate.Email) { - return errors.New("invalid email format") - } - } - - passwordHash, err := bcrypt.GenerateFromPassword([]byte(hostPassword), bcrypt.DefaultCost) - if err != nil { - return errors.Wrap(err, "failed to hash password") - } - - userCreate.PasswordHash = string(passwordHash) - if _, err := s.store.CreateUser(ctx, userCreate); err != nil { - return errors.Wrap(err, "failed to create user") - } - - return nil -} diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml deleted file mode 100644 index 2c1e07d978965..0000000000000 --- a/docker-compose.dev.yaml +++ /dev/null @@ -1,70 +0,0 @@ -services: - db: - image: mysql - volumes: - - ./.air/mysql:/var/lib/mysql - api: - image: cosmtrek/air - working_dir: /work - command: ["-c", "./scripts/.air.toml"] - environment: - - "MEMOS_DSN=root@tcp(db)/memos" - - "MEMOS_DRIVER=mysql" - volumes: - - .:/work/ - - .air/go-build:/root/.cache/go-build - - $HOME/go/pkg/:/go/pkg/ # Cache for go mod shared with the host - web: - image: node:18-alpine - working_dir: /work - depends_on: ["api"] - ports: ["3001:3001"] - environment: ["DEV_PROXY_SERVER=http://api:8081/"] - entrypoint: ["/bin/sh", "-c"] - command: ["corepack enable && pnpm install && pnpm dev"] - volumes: - - ./web:/work - - ./.air/node_modules/:/work/node_modules/ # Cache for Node Modules - - # Services below are used for developers to run once - # - # You can just run `docker compose run --rm SERVICE_NAME` to use - # For example: - # To regenerate typescript code of gRPC proto - # Just run `docker compose run --rm buf` - # - # All of theses services belongs to profile 'tools' - # This will prevent to launch by normally `docker compose up` unexpectly - - # Generate typescript code of gRPC proto - buf: - profiles: ["tools"] - image: bufbuild/buf - working_dir: /work/proto - command: generate - volumes: - - ./proto:/work/proto - - ./web/src/types/:/work/web/src/types/ - - # Do golang static code check before create PR - golangci-lint: - profiles: ["tools"] - image: golangci/golangci-lint:v1.54.2 - working_dir: /work/ - entrypoint: golangci-lint - command: run -v - volumes: - - $HOME/go/pkg/:/go/pkg/ # Cache for go mod shared with the host - - .air/go-build:/root/.cache/go-build - - .:/work/ - - # run npm - npm: - profiles: ["tools"] - image: node:18-alpine - working_dir: /work - environment: ["NPM_CONFIG_UPDATE_NOTIFIER=false"] - entrypoint: "npm" - volumes: - - ./web:/work - - ./.air/node_modules/:/work/node_modules/ diff --git a/docs/api/v1.md b/docs/api/v1.md deleted file mode 100644 index 0514b97548d65..0000000000000 --- a/docs/api/v1.md +++ /dev/null @@ -1,1607 +0,0 @@ -# memos API - -A privacy-first, lightweight note-taking service. - -## Version: 1.0 - -**Contact information:** -API Support -- {t("common.about")} {customizedProfile.name} -
- -{t("about.memos-description")}
-{customizedProfile.description || t("about.no-server-description")}
-{t("memo.archived-memos")}
- -{t("memo.fetching-data")}
-{t("memo.no-archived-memos")}
-- {t("setting.account-section.change-password")} ({propsUser.username}) + {t("setting.account-section.change-password")} ({user.nickname})
- +{t("auth.new-password")}
-{t("auth.repeat-new-password")}
- -{t("message.change-memo-created-time")}
- +{t("setting.account-section.change-password")}
- +{t("auth.new-password")}
-{t("auth.repeat-new-password")}
- -Create access token
- +{t(isCreating ? "setting.sso-section.create-sso" : "setting.sso-section.update-sso")}
- +{"Add references"}
- +{getDateTimeString(option.displayTime)}
++ {searchText ? getHighlightedContent(option.content) : option.content} +
+{getDateTimeString(memo.displayTime)}
+ {memo.content}{t("resource.create-dialog.title")}
-{t("tag-list.create-tag")}
-{t("tag.create-tag")}
+{t("tag-list.all-tags")}
+{t("tag.all-tags")}
{isCreating ? "Create webhook" : "Edit webhook"}
+{title}
-{content}
@@ -86,6 +86,6 @@ export const showCommonDialog = (props: CommonDialogProps) => { dialogName: `common-dialog ${props?.className ?? ""}`, }, CommonDialog, - props + props, ); }; diff --git a/web/src/components/DisablePasswordLoginDialog.tsx b/web/src/components/DisablePasswordLoginDialog.tsx index 061586df36dbd..18499324c6c32 100644 --- a/web/src/components/DisablePasswordLoginDialog.tsx +++ b/web/src/components/DisablePasswordLoginDialog.tsx @@ -1,4 +1,4 @@ -import { Button } from "@mui/joy"; +import { Button, IconButton, Input } from "@mui/joy"; import { useState } from "react"; import { toast } from "react-hot-toast"; import * as api from "@/helpers/api"; @@ -45,7 +45,7 @@ const DisablePasswordLoginDialog: React.FC{t("setting.system-section.disable-password-login")}
-{t("setting.system-section.disable-password-login-final-warning")}
- +{t("setting.system-section.disable-password-login-final-warning")}
+ > ) : ( -{t("setting.system-section.disable-password-login-warning")}
+{t("setting.system-section.disable-password-login-warning")}
)}{t("embed-memo.title")}
-{t("embed-memo.text")}
-
- {memoEmbeddedCode()}
-
- - {t("embed-memo.only-public-supported")} - - {t("embed-memo.copy")} - -
-+ {t("inbox.version-update", { + version: activity?.payload?.versionUpdate?.version, + })} +
++ {children.map((child, index) => ( ++ ); +}; + +export default Blockquote; diff --git a/web/src/components/MemoContent/Bold.tsx b/web/src/components/MemoContent/Bold.tsx new file mode 100644 index 0000000000000..b7651d11e11d2 --- /dev/null +++ b/web/src/components/MemoContent/Bold.tsx @@ -0,0 +1,19 @@ +import { Node } from "@/types/node"; +import Renderer from "./Renderer"; + +interface Props { + symbol: string; + children: Node[]; +} + +const Bold: React.FC+ ))} +
{content}
;
+};
+
+export default Code;
diff --git a/web/src/components/MemoContent/CodeBlock.tsx b/web/src/components/MemoContent/CodeBlock.tsx
new file mode 100644
index 0000000000000..3165b01000172
--- /dev/null
+++ b/web/src/components/MemoContent/CodeBlock.tsx
@@ -0,0 +1,64 @@
+import classNames from "classnames";
+import copy from "copy-to-clipboard";
+import hljs from "highlight.js";
+import toast from "react-hot-toast";
+import Icon from "../Icon";
+import MermaidBlock from "./MermaidBlock";
+import { BaseProps } from "./types";
+
+// Special languages that are rendered differently.
+enum SpecialLanguage {
+ HTML = "__html",
+ MERMAID = "mermaid",
+}
+
+interface Props extends BaseProps {
+ language: string;
+ content: string;
+}
+
+const CodeBlock: React.FC
+
+
+ {message}
; +}; + +export default Error; diff --git a/web/src/components/MemoContent/EmbeddedContent/index.tsx b/web/src/components/MemoContent/EmbeddedContent/index.tsx new file mode 100644 index 0000000000000..b44db2d714ee4 --- /dev/null +++ b/web/src/components/MemoContent/EmbeddedContent/index.tsx @@ -0,0 +1,25 @@ +import EmbeddedMemo from "./EmbeddedMemo"; +import EmbeddedResource from "./EmbeddedResource"; +import Error from "./Error"; + +interface Props { + resourceName: string; + params: string; +} + +const extractResourceTypeAndId = (resourceName: string) => { + const [resourceType, resourceId] = resourceName.split("/"); + return { resourceType, resourceId }; +}; + +const EmbeddedContent = ({ resourceName, params }: Props) => { + const { resourceType, resourceId } = extractResourceTypeAndId(resourceName); + if (resourceType === "memos") { + return+ {content} ++ ); +}; + +export default MermaidBlock; diff --git a/web/src/components/MemoContent/OrderedList.tsx b/web/src/components/MemoContent/OrderedList.tsx new file mode 100644 index 0000000000000..75d5f99b6d7dd --- /dev/null +++ b/web/src/components/MemoContent/OrderedList.tsx @@ -0,0 +1,36 @@ +import { repeat } from "lodash-es"; +import { Node } from "@/types/node"; +import Renderer from "./Renderer"; +import { BaseProps } from "./types"; + +interface Props extends BaseProps { + number: string; + indent: number; + children: Node[]; +} + +const OrderedList: React.FC
+ {children.map((child, index) => (
+
{message}
; +}; + +export default Error; diff --git a/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx b/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx new file mode 100644 index 0000000000000..5112ef5936a1a --- /dev/null +++ b/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx @@ -0,0 +1,47 @@ +import { useEffect } from "react"; +import useLoading from "@/hooks/useLoading"; +import useNavigateTo from "@/hooks/useNavigateTo"; +import { useMemoStore } from "@/store/v1"; +import Error from "./Error"; + +interface Props { + resourceId: string; + params: string; +} + +const ReferencedMemo = ({ resourceId, params: paramsStr }: Props) => { + const navigateTo = useNavigateTo(); + const loadingState = useLoading(); + const memoStore = useMemoStore(); + const memo = memoStore.getMemoByName(resourceId); + const params = new URLSearchParams(paramsStr); + + useEffect(() => { + memoStore.getOrFetchMemoByName(resourceId).finally(() => loadingState.setFinish()); + }, [resourceId]); + + if (loadingState.isLoading) { + return null; + } + if (!memo) { + return
;
+ case NodeType.IMAGE:
+ return + {h} + | + ))} +
---|
+ {r} + | + ))} +
e.stopPropagation()}> - No tags found -
- )} -