aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAKP <tom@tdpain.net>2022-04-15 21:19:08 +0100
committerAKP <tom@tdpain.net>2022-04-15 21:19:08 +0100
commit0cdd005135a4cda07ae81bf2fc03495d02c24122 (patch)
treea3572cd737796505427d507f4d1e1d49399c95ff
parent9318987a49b698819edc9641c4d19637beb2131e (diff)
Add OPML import functionality
-rw-r--r--CHANGELOG.md2
-rw-r--r--walrss/internal/core/feeds.go33
-rw-r--r--walrss/internal/core/opml/opml.go58
-rw-r--r--walrss/internal/http/exportImport.go37
-rw-r--r--walrss/internal/http/http.go1
-rw-r--r--walrss/internal/urls/urls.go3
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"