diff --git a/server/rss.go b/server/rss.go index 0540b9aa..1d04436a 100644 --- a/server/rss.go +++ b/server/rss.go @@ -1,8 +1,11 @@ package server import ( + "context" + "encoding/json" "net/http" "strconv" + "strings" "time" "github.com/gorilla/feeds" @@ -10,10 +13,94 @@ import ( "github.com/usememos/memos/api" ) +func generateRSSFromMemoList(memoList []*api.Memo, baseURL string, profile *api.CustomizedProfile) (string, error) { + if len(memoList) == 0 { + return "", nil + } + + feed := &feeds.Feed{ + Title: profile.Name, + Link: &feeds.Link{Href: baseURL}, + Description: profile.Description, + Created: time.Now(), + } + + feed.Items = make([]*feeds.Item, len(memoList)) + for i, memo := range memoList { + var useTitle = strings.HasPrefix(memo.Content, "# ") + + var title string + if useTitle { + title = strings.Split(memo.Content, "\n")[0][2:] + } else { + title = memo.Creator.Username + "-memos-" + strconv.Itoa(memo.ID) + } + + var description string + if useTitle { + var firstLineEnd = strings.Index(memo.Content, "\n") + description = memo.Content[firstLineEnd+1:] + } else { + description = memo.Content + } + + feed.Items[i] = &feeds.Item{ + Title: title, + Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)}, + Description: description, + Created: time.Unix(memo.CreatedTs, 0), + } + } + + rss, err := feed.ToRss() + if err != nil { + return "", err + } + + rssPrefix := `` + + return rss[len(rssPrefix):], nil +} + func (s *Server) registerRSSRoutes(g *echo.Group) { + g.GET("/explore/rss.xml", func(c echo.Context) error { + ctx := c.Request().Context() + + systemCustomizedProfile, err := getSystemCustomizedProfile(ctx, s) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err) + } + + normalStatus := api.Normal + memoFind := api.MemoFind{ + RowStatus: &normalStatus, + VisibilityList: []api.Visibility{ + api.Public, + }, + } + memoList, err := s.Store.FindMemoList(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 := generateRSSFromMemoList(memoList, baseURL, &systemCustomizedProfile) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err) + } + + return c.XMLBlob(http.StatusOK, []byte(rss)) + }) + g.GET("/u/:id/rss.xml", func(c echo.Context) error { ctx := c.Request().Context() + systemCustomizedProfile, err := getSystemCustomizedProfile(ctx, s) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err) + } + id, err := strconv.Atoi(c.Param("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "User id is not a number").SetInternal(err) @@ -32,41 +119,66 @@ func (s *Server) registerRSSRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err) } - userFind := api.UserFind{ - ID: &id, - } - user, err := s.Store.FindUser(ctx, &userFind) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err) - } - baseURL := c.Scheme() + "://" + c.Request().Host - feed := &feeds.Feed{ - Title: "Memos", - Link: &feeds.Link{Href: baseURL}, - Description: "Memos", - Author: &feeds.Author{Name: user.Username}, - Created: time.Now(), - } - - feed.Items = make([]*feeds.Item, len(memoList)) - for i, memo := range memoList { - feed.Items[i] = &feeds.Item{ - Title: user.Username + "-memos-" + strconv.Itoa(memo.ID), - Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)}, - Description: memo.Content, - Created: time.Unix(memo.CreatedTs, 0), - } - } - - rss, err := feed.ToRss() + rss, err := generateRSSFromMemoList(memoList, baseURL, &systemCustomizedProfile) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err) } - rssPrefix := `` - - return c.XMLBlob(http.StatusOK, []byte(rss[len(rssPrefix):])) + return c.XMLBlob(http.StatusOK, []byte(rss)) }) } + +func getSystemCustomizedProfile(ctx context.Context, s *Server) (api.CustomizedProfile, error) { + systemStatus := api.SystemStatus{ + CustomizedProfile: api.CustomizedProfile{ + Name: "memos", + LogoURL: "", + Description: "", + Locale: "en", + Appearance: "system", + ExternalURL: "", + }, + } + + systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{}) + if err != nil { + return api.CustomizedProfile{}, err + } + for _, systemSetting := range systemSettingList { + if systemSetting.Name == api.SystemSettingServerID || systemSetting.Name == api.SystemSettingSecretSessionName { + continue + } + + var value interface{} + err := json.Unmarshal([]byte(systemSetting.Value), &value) + if err != nil { + return api.CustomizedProfile{}, err + } + + if systemSetting.Name == api.SystemSettingCustomizedProfileName { + valueMap := value.(map[string]interface{}) + systemStatus.CustomizedProfile = api.CustomizedProfile{} + if v := valueMap["name"]; v != nil { + systemStatus.CustomizedProfile.Name = v.(string) + } + if v := valueMap["logoUrl"]; v != nil { + systemStatus.CustomizedProfile.LogoURL = v.(string) + } + if v := valueMap["description"]; v != nil { + systemStatus.CustomizedProfile.Description = v.(string) + } + if v := valueMap["locale"]; v != nil { + systemStatus.CustomizedProfile.Locale = v.(string) + } + if v := valueMap["appearance"]; v != nil { + systemStatus.CustomizedProfile.Appearance = v.(string) + } + if v := valueMap["externalUrl"]; v != nil { + systemStatus.CustomizedProfile.ExternalURL = v.(string) + } + } + } + return systemStatus.CustomizedProfile, nil +} diff --git a/web/src/components/MemosHeader.tsx b/web/src/components/MemosHeader.tsx index 795825da..1cbe3bf7 100644 --- a/web/src/components/MemosHeader.tsx +++ b/web/src/components/MemosHeader.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { useLocationStore, useMemoStore, useShortcutStore } from "../store/module"; +import { useLocationStore, useMemoStore, useShortcutStore, useUserStore } from "../store/module"; import Icon from "./Icon"; import SearchBar from "./SearchBar"; import { toggleSidebar } from "./Sidebar"; @@ -11,6 +11,8 @@ const MemosHeader = () => { const locationStore = useLocationStore(); const memoStore = useMemoStore(); const shortcutStore = useShortcutStore(); + const userStore = useUserStore(); + const user = userStore.state.user; const query = locationStore.state.query; const shortcuts = shortcutStore.state.shortcuts; const [titleText, setTitleText] = useState("MEMOS"); @@ -46,6 +48,11 @@ const MemosHeader = () => { {titleText} + {user && ( + + + + )} diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 2036bac6..b016a84a 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -9,6 +9,7 @@ import toastHelper from "../components/Toast"; import MemoContent from "../components/MemoContent"; import MemoResources from "../components/MemoResources"; import MemoFilter from "../components/MemoFilter"; +import Icon from "../components/Icon"; import { TAG_REG } from "../labs/marked/parser"; import "../less/explore.less"; @@ -115,6 +116,9 @@ const Explore = () => {
{customizedProfile.name} + + +
{!loadingState.isLoading && user ? ( diff --git a/web/vite.config.ts b/web/vite.config.ts index 2d063c21..f3d51e14 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -21,6 +21,10 @@ export default defineConfig({ target: "http://localhost:8081/", changeOrigin: true, }, + "/explore/rss.xml": { + target: "http://localhost:8081/", + changeOrigin: true, + }, }, }, resolve: {