Skip to content

Commit

Permalink
Initial feed creation and feed_item population
Browse files Browse the repository at this point in the history
  • Loading branch information
brendanv committed Aug 27, 2024
1 parent 0b63215 commit 7b2bbe6
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 1 deletion.
6 changes: 6 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (

require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
Expand Down Expand Up @@ -51,11 +52,16 @@ require (
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mmcdole/gofeed v1.3.0 // indirect
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
Expand Down
17 changes: 17 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1r
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
Expand Down Expand Up @@ -141,6 +144,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg=
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
Expand All @@ -162,6 +166,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand All @@ -183,6 +189,15 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk=
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down Expand Up @@ -210,6 +225,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down Expand Up @@ -264,6 +280,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505214959-0714010a04ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
Expand Down
141 changes: 141 additions & 0 deletions backend/lynx/feeds/feeds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package feeds

import (
"net/http"
"time"

"github.com/labstack/echo/v5"
"github.com/mmcdole/gofeed"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
)

// FeedResult contains the parsed feed and the ETag and Last-Modified
// headers received from the remote server
type FeedResult struct {
Feed *gofeed.Feed
ETag string
LastModified string
}

// LoadFeedFromURL fetches and parses a feed from the given URL.
// It optionally uses etag and ifModifiedSince for conditional requests.
func LoadFeedFromURL(url string, etag string, ifModifiedSince time.Time) (*FeedResult, error) {
fp := gofeed.NewParser()

client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}

if etag != "" {
req.Header.Set("If-None-Match", etag)
}
if !ifModifiedSince.IsZero() {
req.Header.Set("If-Modified-Since", ifModifiedSince.Format(http.TimeFormat))
}

resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotModified {
return &FeedResult{}, nil
}

feed, err := fp.Parse(resp.Body)
if err != nil {
return nil, err
}

return &FeedResult{
Feed: feed,
ETag: resp.Header.Get("ETag"),
LastModified: resp.Header.Get("Last-Modified"),
}, nil
}

func SaveNewFeedItems(app core.App, feed *gofeed.Feed, user string, feedId string, lastArticlePubDate time.Time) error {
collection, err := app.Dao().FindCollectionByNameOrId("feed_items")
if err != nil {
return err
}
for _, item := range feed.Items {
if item.PublishedParsed != nil && !item.PublishedParsed.After(lastArticlePubDate) {
continue
}
existingItem, _ := app.Dao().FindFirstRecordByFilter(
"feed_items",
"feed = {:feed} && guid = {:guid}",
map[string]interface{}{"feed": feedId, "guid": item.GUID},
)
if existingItem == nil {
newItem := models.NewRecord(collection)
newItem.Set("user", user)
newItem.Set("feed", feedId)
newItem.Set("title", item.Title)
newItem.Set("pub_date", item.PublishedParsed)
newItem.Set("guid", item.GUID)
newItem.Set("description", item.Description)
newItem.Set("url", item.Link)
if err := app.Dao().SaveRecord(newItem); err != nil {
return err
}
}
}
return nil
}

// SaveNewFeed extracts the URL from the request, loads the
// feed, and saves it to the database along with the first
// set of feed items.
func SaveNewFeed(app core.App, c echo.Context) error {
authRecord, _ := c.Get(apis.ContextAuthRecordKey).(*models.Record)
if authRecord == nil {
return apis.NewForbiddenError("Not authenticated", nil)
}

url := c.FormValue("url")
if url == "" {
return apis.NewBadRequestError("URL is required", nil)
}

feedResult, err := LoadFeedFromURL(url, "", time.Time{})
if err != nil {
return apis.NewBadRequestError("Error parsing feed", err)
}

collection, err := app.Dao().FindCollectionByNameOrId("feeds")
if err != nil {
return apis.NewBadRequestError("Failed to find feeds collection", err)
}

record := models.NewRecord(collection)
record.Set("user", authRecord.Id)
record.Set("feed_url", url)
record.Set("name", feedResult.Feed.Title)
record.Set("description", feedResult.Feed.Description)
if feedResult.Feed.Image != nil {
record.Set("image_url", feedResult.Feed.Image.URL)
}
record.Set("etag", feedResult.ETag)
record.Set("modified", feedResult.LastModified)
record.Set("last_fetched_at", time.Now().UTC().Format(time.RFC3339))
record.Set("auto_add_feed_items_to_library", false)

if err := app.Dao().SaveRecord(record); err != nil {
return apis.NewBadRequestError("Failed to save feed", err)
}

if err := SaveNewFeedItems(app, feedResult.Feed, authRecord.Id, record.Id, time.Time{}); err != nil {
return apis.NewBadRequestError("Failed to save feed items", err)
}

return c.JSON(200, map[string]interface{}{
"id": record.Id,
})
}
79 changes: 79 additions & 0 deletions backend/lynx/feeds/feeds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package feeds

import (
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestLoadFeedFromURL(t *testing.T) {
// Set up a mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("If-None-Match") == "some-etag" {
w.WriteHeader(http.StatusNotModified)
return
}
w.Header().Set("ETag", "new-etag")
w.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Sample Feed</title>
<description>A sample feed</description>
<item>
<title>Sample Item</title>
<link>http://example.com/item</link>
<pubDate>Wed, 21 Oct 2015 07:28:00 GMT</pubDate>
</item>
</channel>
</rss>`))
}))
defer server.Close()

testCases := []struct {
name string
etag string
ifModifiedSince time.Time
expectedResult func(*testing.T, *FeedResult)
}{
{
name: "LoadFeedFromURL works with no etag",
etag: "",
ifModifiedSince: time.Time{},
expectedResult: func(t *testing.T, result *FeedResult) {
if result.Feed.Title != "Sample Feed" {
t.Errorf("Expected feed title to be 'Sample Feed', got '%s'", result.Feed.Title)
}
if result.Feed.Description != "A sample feed" {
t.Errorf("Expected feed description to be 'A sample feed', got '%s'", result.Feed.Description)
}
if result.Feed.Items[0].Title != "Sample Item" {
t.Errorf("Expected feed item title to be 'Sample Item', got '%s'", result.Feed.Items[0].Title)
}
if result.ETag != "new-etag" {
t.Errorf("Expected etag to be 'new-etag', got '%s'", result.ETag)
}
},
},
{
name: "LoadFeedFromURL returns empty response on 304",
etag: "some-etag",
ifModifiedSince: time.Time{},
expectedResult: func(t *testing.T, result *FeedResult) {
if result.Feed != nil {
t.Errorf("Expected feed to be nil, got '%+v'", result.Feed)
}
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := LoadFeedFromURL(server.URL, tc.etag, tc.ifModifiedSince)
if err != nil {
t.Fatal(err)
}
tc.expectedResult(t, result)
})
}
}
20 changes: 19 additions & 1 deletion backend/lynx/lynx.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,23 @@ import (
"github.com/pocketbase/pocketbase/tools/routine"
"github.com/pocketbase/pocketbase/tools/security"

"main/lynx/summarizer"
"main/lynx/feeds"
"main/lynx/singlefile"
"main/lynx/summarizer"
)

var parseUrlHandlerFunc = handleParseURL
var parseFeedHandlerFunc = feeds.SaveNewFeed

// Interfaces for dependency injection for summarization tests
type Summarizer interface {
MaybeSummarizeLink(app core.App, linkID string)
}

var CurrentSummarizer Summarizer = &DefaultSummarizer{}

type DefaultSummarizer struct{}

func (s *DefaultSummarizer) MaybeSummarizeLink(app core.App, linkID string) {
summarizer.MaybeSummarizeLink(app, linkID)
}
Expand Down Expand Up @@ -65,6 +70,19 @@ func InitializePocketbase(app core.App) {
},
})

e.Router.AddRoute(echo.Route{
Method: http.MethodPost,
Path: "/lynx/parse_feed",
Handler: func(c echo.Context) error {
return parseFeedHandlerFunc(app, c)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
apiKeyAuth,
apis.RequireAdminOrRecordAuth(),
},
})

return nil
})

Expand Down
Loading

0 comments on commit 7b2bbe6

Please sign in to comment.