diff options
| author | AKP <tom@tdpain.net> | 2022-04-15 21:19:08 +0100 |
|---|---|---|
| committer | AKP <tom@tdpain.net> | 2022-04-15 21:19:08 +0100 |
| commit | 0cdd005135a4cda07ae81bf2fc03495d02c24122 (patch) | |
| tree | a3572cd737796505427d507f4d1e1d49399c95ff | |
| parent | 9318987a49b698819edc9641c4d19637beb2131e (diff) | |
Add OPML import functionality
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | walrss/internal/core/feeds.go | 33 | ||||
| -rw-r--r-- | walrss/internal/core/opml/opml.go | 58 | ||||
| -rw-r--r-- | walrss/internal/http/exportImport.go | 37 | ||||
| -rw-r--r-- | walrss/internal/http/http.go | 1 | ||||
| -rw-r--r-- | walrss/internal/urls/urls.go | 3 |
6 files changed, 120 insertions, 14 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index f8af81a..2a34834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added -* Support for OPML exports +* Support for OPML imports and exports ## [0.1.0] - 2022-04-14 Initial release diff --git a/walrss/internal/core/feeds.go b/walrss/internal/core/feeds.go index 3aefd4c..9ba6545 100644 --- a/walrss/internal/core/feeds.go +++ b/walrss/internal/core/feeds.go @@ -87,3 +87,36 @@ func ExportFeedsForUser(st *state.State, userID string) ([]byte, error) { } return opml.FromFeeds(feeds, user.Email).ToBytes() } + +func ImportFeedsForUser(st *state.State, userID string, opmlXML []byte) error { + o, err := opml.FromBytes(opmlXML) + if err != nil { + return AsUserError(400, err) + } + + // This will be used to filter out feeds included in OPML that would cause + // duplicates + existingURLs := make(map[string]struct{}) + { + feeds, err := GetFeedsForUser(st, userID) + if err != nil { + return err + } + for _, feed := range feeds { + if _, found := existingURLs[feed.URL]; !found { + existingURLs[feed.URL] = struct{}{} + } + } + } + + for _, feed := range o.ToFeeds() { + if _, found := existingURLs[feed.URL]; found { + continue + } + if _, err := NewFeed(st, userID, feed.Name, feed.URL); err != nil { + return err + } + } + + return nil +} diff --git a/walrss/internal/core/opml/opml.go b/walrss/internal/core/opml/opml.go index 809a6b8..7029d72 100644 --- a/walrss/internal/core/opml/opml.go +++ b/walrss/internal/core/opml/opml.go @@ -3,6 +3,8 @@ package opml import ( "encoding/xml" "github.com/codemicro/walrss/walrss/internal/db" + "github.com/lithammer/shortuuid/v4" + "strings" "time" ) @@ -19,15 +21,6 @@ type OPML struct { } `xml:"body"` } -// Outline holds all information about an outline. -type Outline struct { - Outlines []*Outline `xml:"outline"` - Text string `xml:"text,attr"` - Title string `xml:"title,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - XMLURL string `xml:"xmlUrl,attr,omitempty"` -} - func FromBytes(x []byte) (*OPML, error) { o := new(OPML) if err := xml.Unmarshal(x, o); err != nil { @@ -36,10 +29,6 @@ func FromBytes(x []byte) (*OPML, error) { return o, nil } -func (o *OPML) ToBytes() ([]byte, error) { - return xml.Marshal(o) -} - func FromFeeds(feeds []*db.Feed, userEmailAddress string) *OPML { o := new(OPML) o.Version = "2.0" @@ -58,3 +47,46 @@ func FromFeeds(feeds []*db.Feed, userEmailAddress string) *OPML { return o } + +func (o *OPML) ToBytes() ([]byte, error) { + return xml.Marshal(o) +} + +func (o *OPML) ToFeeds() []*db.Feed { + var out []*db.Feed + for _, item := range o.Body.Outlines { + out = append(out, item.ToFeeds()...) + } + return out +} + +type Outline struct { + Outlines []*Outline `xml:"outline"` + Text string `xml:"text,attr"` + Title string `xml:"title,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + XMLURL string `xml:"xmlUrl,attr,omitempty"` +} + +func (o *Outline) ToFeeds() []*db.Feed { + var out []*db.Feed + + if strings.EqualFold(o.Type, "rss") { + name := o.Text + if o.Title != "" { + name = o.Title + } + + out = append(out, &db.Feed{ + ID: shortuuid.New(), + URL: o.XMLURL, + Name: name, + }) + } + + for _, item := range o.Outlines { + out = append(out, item.ToFeeds()...) + } + + return out +} diff --git a/walrss/internal/http/exportImport.go b/walrss/internal/http/exportImport.go index 89b2ecf..c28ac02 100644 --- a/walrss/internal/http/exportImport.go +++ b/walrss/internal/http/exportImport.go @@ -1,8 +1,12 @@ package http import ( + "errors" "github.com/codemicro/walrss/walrss/internal/core" + "github.com/codemicro/walrss/walrss/internal/urls" "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" + "io" ) func (s *Server) exportAsOPML(ctx *fiber.Ctx) error { @@ -19,3 +23,36 @@ func (s *Server) exportAsOPML(ctx *fiber.Ctx) error { ctx.Set(fiber.HeaderContentType, "application/xml") return ctx.Send(exported) } + +func (s *Server) importFromOPML(ctx *fiber.Ctx) error { + currentUserID := getCurrentUserID(ctx) + if currentUserID == "" { + return requestFragmentSignIn(ctx, urls.Index) + } + + file, err := ctx.FormFile("file") + if err != nil { + if errors.Is(err, fasthttp.ErrMissingFile) { + return core.NewUserError("missing file") + } + return err + } + + fileHandle, err := file.Open() + if err != nil { + return err + } + + fileContents, err := io.ReadAll(fileHandle) + if err != nil { + return err + } + + if err := core.ImportFeedsForUser(s.state, currentUserID, fileContents); err != nil { + return err + } + + ctx.Set("HX-Refresh", "true") + ctx.Status(fiber.StatusNoContent) + return nil +} diff --git a/walrss/internal/http/http.go b/walrss/internal/http/http.go index 295a91f..3909772 100644 --- a/walrss/internal/http/http.go +++ b/walrss/internal/http/http.go @@ -92,6 +92,7 @@ func (s *Server) registerHandlers() { s.app.Post(urls.SendTestEmail, s.sendTestEmail) s.app.Get(urls.ExportAsOPML, s.exportAsOPML) + s.app.Post(urls.ImportFromOPML, s.importFromOPML) s.app.Use(urls.Statics, static.NewHandler()) } diff --git a/walrss/internal/urls/urls.go b/walrss/internal/urls/urls.go index 835bbc7..6452aab 100644 --- a/walrss/internal/urls/urls.go +++ b/walrss/internal/urls/urls.go @@ -21,6 +21,9 @@ const ( Export = "/export" ExportAsOPML = Export + "/opml" + Import = "/import" + ImportFromOPML = Import + "/opml" + New = "/new" NewFeedItem = New + "/feed" |
