mirror of
https://github.com/makew0rld/amfora.git
synced 2024-11-23 22:23:48 +03:00
Add subscription manager, fix corrupt read/write & short URI panic
More info on corrupt reads and writes: https://github.com/makeworld-the-better-one/amfora/issues/61#issuecomment-735042805 The panic was occuring due to lines like: if u[:6] == "about:" These would cause panics for URIs that were shorter than 6, like "docs/". strings.HasPrefix was used instead to fix this.
This commit is contained in:
parent
62102d4f98
commit
1a2fba92c2
2
NOTES.md
2
NOTES.md
@ -20,4 +20,4 @@
|
||||
- Bookmark keys aren't deleted, just set to `""`
|
||||
- Waiting on [this viper PR](https://github.com/spf13/viper/pull/519) to be merged
|
||||
- Help table cells aren't dynamically wrapped
|
||||
- Filed [issue 29](https://gitlab.com/tslocum/cview/-/issues/29)
|
||||
- Filed [issue 29](https://gitlab.com/tslocum/cview/-/issues/29)
|
||||
|
@ -165,8 +165,9 @@ Amfora ❤️ open source!
|
||||
- It uses [tcell](https://github.com/gdamore/tcell) for low level terminal operations
|
||||
- [Viper](https://github.com/spf13/viper) for configuration and TOFU storing
|
||||
- [go-gemini](https://github.com/makeworld-the-better-one/go-gemini), my forked and updated Gemini client/server library
|
||||
- My [progressbar fork](https://github.com/makeworld-the-better-one/progressbar)
|
||||
- My [progressbar fork](https://github.com/makeworld-the-better-one/progressbar) - pull request [here](https://github.com/schollz/progressbar/pull/69)
|
||||
- [go-humanize](https://github.com/dustin/go-humanize)
|
||||
- My [gofeed fork](https://github.com/makeworld-the-better-one/gofeed) - pull request [here](https://github.com/mmcdole/gofeed/pull/164)
|
||||
|
||||
## License
|
||||
This project is licensed under the GPL v3.0. See the [LICENSE](./LICENSE) file for details.
|
||||
|
@ -576,8 +576,11 @@ func Reload() {
|
||||
// URL loads and handles the provided URL for the current tab.
|
||||
// It should be an absolute URL.
|
||||
func URL(u string) {
|
||||
if u[:6] == "about:" {
|
||||
handleAbout(tabs[curTab], u)
|
||||
t := tabs[curTab]
|
||||
if strings.HasPrefix(u, "about:") {
|
||||
if ok := handleAbout(t, u); ok {
|
||||
t.addToHistory(u)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -585,7 +588,7 @@ func URL(u string) {
|
||||
// Assume it's a Gemini URL
|
||||
u = "gemini://" + u
|
||||
}
|
||||
go goURL(tabs[curTab], u)
|
||||
go goURL(t, u)
|
||||
}
|
||||
|
||||
func NumTabs() int {
|
||||
|
@ -167,27 +167,34 @@ func handleFavicon(t *tab, host, old string) {
|
||||
// 'about:'. It will display errors if the URL is not recognized,
|
||||
// but not display anything if an 'about:' URL is not passed.
|
||||
//
|
||||
// It does not add the displayed page to history.
|
||||
//
|
||||
// It returns a bool indicating if the provided URL could be handled.
|
||||
func handleAbout(t *tab, u string) bool {
|
||||
if u[:6] != "about:" {
|
||||
if !strings.HasPrefix(u, "about:") {
|
||||
return false
|
||||
}
|
||||
|
||||
switch u {
|
||||
case "about:bookmarks":
|
||||
Bookmarks(t)
|
||||
t.addToHistory("about:bookmarks")
|
||||
return true
|
||||
case "about:subscriptions":
|
||||
Subscriptions(t)
|
||||
t.addToHistory("about:subscriptions")
|
||||
return true
|
||||
case "about:newtab":
|
||||
temp := newTabPage // Copy
|
||||
setPage(t, &temp)
|
||||
t.applyBottomBar()
|
||||
return true
|
||||
}
|
||||
|
||||
if u == "about:manage-subscriptions" || (len(u) > 27 && u[:27] == "about:manage-subscriptions?") {
|
||||
ManageSubscriptions(t, u)
|
||||
// Don't count remove command in history
|
||||
return u == "about:manage-subscriptions"
|
||||
}
|
||||
|
||||
Error("Error", "Not a valid 'about:' URL.")
|
||||
return false
|
||||
}
|
||||
@ -244,7 +251,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||
|
||||
App.SetFocus(t.view)
|
||||
|
||||
if u[:6] == "about:" {
|
||||
if strings.HasPrefix(u, "about:") {
|
||||
return ret(u, handleAbout(t, u))
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ You can customize this page by creating a gemtext file called newtab.gmi, in Amf
|
||||
Happy browsing!
|
||||
|
||||
=> about:bookmarks Bookmarks
|
||||
=> about:subscriptions Feed and Page Tracking
|
||||
=> about:subscriptions Subscriptions
|
||||
|
||||
=> //gemini.circumlunar.space Project Gemini
|
||||
=> https://github.com/makeworld-the-better-one/amfora Amfora homepage [HTTPS]
|
||||
|
@ -19,8 +19,10 @@ import (
|
||||
// Not when a URL is opened on a new tab for the first time.
|
||||
// It will handle setting the bottomBar.
|
||||
func followLink(t *tab, prev, next string) {
|
||||
if next[:6] == "about:" {
|
||||
handleAbout(t, next)
|
||||
if strings.HasPrefix(next, "about:") {
|
||||
if ok := handleAbout(t, next); ok {
|
||||
t.addToHistory(next)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/makeworld-the-better-one/amfora/renderer"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/makeworld-the-better-one/amfora/subscriptions"
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"github.com/mmcdole/gofeed"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@ -28,7 +29,7 @@ func toLocalDay(t time.Time) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
|
||||
// Feeds displays the feeds page on the current tab.
|
||||
// Subscriptions displays the subscriptions page on the current tab.
|
||||
func Subscriptions(t *tab) {
|
||||
logger.Log.Println("display.Subscriptions called")
|
||||
|
||||
@ -43,9 +44,10 @@ func Subscriptions(t *tab) {
|
||||
|
||||
logger.Log.Println("started rendering subscriptions page")
|
||||
|
||||
subscriptionPageRaw := "# Subscriptions\n\n" +
|
||||
rawPage := "# Subscriptions\n\n" +
|
||||
"See the help (by pressing ?) for details on how to use this page.\n\n" +
|
||||
"If you just opened Amfora then updates will appear incrementally. Reload the page to see them.\n"
|
||||
"If you just opened Amfora then updates will appear incrementally. Reload the page to see them.\n\n" +
|
||||
"=> about:manage-subscriptions Manage subscriptions\n"
|
||||
|
||||
// curDay represents what day of posts the loop is on.
|
||||
// It only goes backwards in time.
|
||||
@ -67,21 +69,21 @@ func Subscriptions(t *tab) {
|
||||
if pub.Before(curDay) {
|
||||
// This post is on a new day, add a day header
|
||||
curDay = pub
|
||||
subscriptionPageRaw += fmt.Sprintf("\n## %s\n\n", curDay.Format("Jan 02, 2006"))
|
||||
rawPage += fmt.Sprintf("\n## %s\n\n", curDay.Format("Jan 02, 2006"))
|
||||
}
|
||||
if entry.Title == "" || entry.Title == "/" {
|
||||
// Just put author/title
|
||||
// Mainly used for when you're tracking the root domain of a site
|
||||
subscriptionPageRaw += fmt.Sprintf("=>%s %s\n", entry.URL, entry.Prefix)
|
||||
rawPage += fmt.Sprintf("=>%s %s\n", entry.URL, entry.Prefix)
|
||||
} else {
|
||||
// Include title and dash
|
||||
subscriptionPageRaw += fmt.Sprintf("=>%s %s - %s\n", entry.URL, entry.Prefix, entry.Title)
|
||||
rawPage += fmt.Sprintf("=>%s %s - %s\n", entry.URL, entry.Prefix, entry.Title)
|
||||
}
|
||||
}
|
||||
|
||||
content, links := renderer.RenderGemini(subscriptionPageRaw, textWidth(), leftMargin(), false)
|
||||
content, links := renderer.RenderGemini(rawPage, textWidth(), leftMargin(), false)
|
||||
page := structs.Page{
|
||||
Raw: subscriptionPageRaw,
|
||||
Raw: rawPage,
|
||||
Content: content,
|
||||
Links: links,
|
||||
URL: "about:subscriptions",
|
||||
@ -97,6 +99,57 @@ func Subscriptions(t *tab) {
|
||||
logger.Log.Println("done rendering subscriptions page")
|
||||
}
|
||||
|
||||
// ManageSubscriptions displays the subscription managing page in
|
||||
// the current tab. `u` is the URL entered by the user.
|
||||
func ManageSubscriptions(t *tab, u string) {
|
||||
if len(u) > 27 && u[:27] == "about:manage-subscriptions?" {
|
||||
// There's a query string, aka a URL to unsubscribe from
|
||||
manageSubscriptionQuery(t, u)
|
||||
return
|
||||
}
|
||||
|
||||
rawPage := "# Manage Subscriptions\n\n" +
|
||||
"Below is list of URLs, both feeds and pages. Navigate to the link to unsubscribe from that feed or page.\n\n"
|
||||
|
||||
for _, u2 := range subscriptions.AllURLS() {
|
||||
rawPage += fmt.Sprintf(
|
||||
"=>%s %s\n",
|
||||
"about:manage-subscriptions?"+gemini.QueryEscape(u2),
|
||||
u2,
|
||||
)
|
||||
}
|
||||
|
||||
content, links := renderer.RenderGemini(rawPage, textWidth(), leftMargin(), false)
|
||||
page := structs.Page{
|
||||
Raw: rawPage,
|
||||
Content: content,
|
||||
Links: links,
|
||||
URL: "about:manage-subscriptions",
|
||||
Width: termW,
|
||||
Mediatype: structs.TextGemini,
|
||||
}
|
||||
go cache.AddPage(&page)
|
||||
setPage(t, &page)
|
||||
t.applyBottomBar()
|
||||
}
|
||||
|
||||
func manageSubscriptionQuery(t *tab, u string) {
|
||||
sub, err := gemini.QueryUnescape(u[27:])
|
||||
if err != nil {
|
||||
Error("URL Error", "Invalid query string: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = subscriptions.Remove(sub)
|
||||
if err != nil {
|
||||
ManageSubscriptions(t, "about:manage-subscriptions") // Reload
|
||||
Error("Save Error", "Error saving the unsubscription to disk: "+err.Error())
|
||||
return
|
||||
}
|
||||
ManageSubscriptions(t, "about:manage-subscriptions") // Reload
|
||||
Info("Unsubscribed from " + sub)
|
||||
}
|
||||
|
||||
// openSubscriptionModal displays the "Add subscription" modal
|
||||
// It returns whether the user wanted to subscribe to feed/page.
|
||||
// The subscribed arg specifies whether this feed/page is already
|
||||
|
@ -146,9 +146,8 @@ func (t *tab) pageDown() {
|
||||
t.view.ScrollTo(row+(termH/4)*3, col)
|
||||
}
|
||||
|
||||
// hasContent returns true when the tab has a page that could be displayed.
|
||||
// The most likely situation where false would be returned is when the default
|
||||
// new tab content is being displayed.
|
||||
// hasContent returns false when the tab's page is malformed,
|
||||
// has no content or URL, or if it's an 'about:' page.
|
||||
func (t *tab) hasContent() bool {
|
||||
if t.page == nil || t.view == nil {
|
||||
return false
|
||||
|
@ -73,8 +73,7 @@ type pageJSON struct {
|
||||
var data = jsonData{
|
||||
feedMu: &sync.RWMutex{},
|
||||
pageMu: &sync.RWMutex{},
|
||||
Feeds: make(map[string]*gofeed.Feed),
|
||||
Pages: make(map[string]*pageJSON),
|
||||
// Maps are created in Init()
|
||||
}
|
||||
|
||||
// PageEntry is a single item on a subscriptions page.
|
||||
|
@ -6,10 +6,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -42,20 +44,29 @@ func Init() error {
|
||||
f, err := os.Open(config.SubscriptionPath)
|
||||
if err == nil {
|
||||
// File exists and could be opened
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err == nil && fi.Size() > 0 {
|
||||
// File is not empty
|
||||
dec := json.NewDecoder(f)
|
||||
err = dec.Decode(&data)
|
||||
if err != nil && err != io.EOF {
|
||||
|
||||
jsonBytes, err := ioutil.ReadAll(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read subscriptions.json error: %w", err)
|
||||
}
|
||||
err = json.Unmarshal(jsonBytes, &data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("subscriptions.json is corrupted: %w", err) //nolint:goerr113
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
} else if !os.IsNotExist(err) {
|
||||
// There's an error opening the file, but it's not bc is doesn't exist
|
||||
return fmt.Errorf("open subscriptions.json error: %w", err) //nolint:goerr113
|
||||
} else {
|
||||
// File does not exist, initialize maps
|
||||
data.Feeds = make(map[string]*gofeed.Feed)
|
||||
data.Pages = make(map[string]*pageJSON)
|
||||
}
|
||||
|
||||
LastUpdated = time.Now()
|
||||
@ -130,26 +141,21 @@ func writeJSON() error {
|
||||
writeMu.Lock()
|
||||
defer writeMu.Unlock()
|
||||
|
||||
f, err := os.OpenFile(config.SubscriptionPath, os.O_WRONLY|os.O_CREATE, 0666)
|
||||
data.Lock()
|
||||
logger.Log.Println("subscriptions.writeJSON acquired data lock")
|
||||
jsonBytes, err := json.MarshalIndent(&data, "", " ")
|
||||
data.Unlock()
|
||||
if err != nil {
|
||||
logger.Log.Println("subscriptions.writeJSON error", err)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
data.Lock()
|
||||
logger.Log.Println("subscriptions.writeJSON acquired data lock")
|
||||
err = enc.Encode(&data)
|
||||
data.Unlock()
|
||||
|
||||
err = ioutil.WriteFile(config.SubscriptionPath, jsonBytes, 0666)
|
||||
if err != nil {
|
||||
logger.Log.Println("subscriptions.writeJSON error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddFeed stores a feed.
|
||||
@ -363,3 +369,37 @@ func updateAll() {
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// AllURLs returns all the subscribed-to URLS, sorted alphabetically.
|
||||
func AllURLS() []string {
|
||||
data.RLock()
|
||||
defer data.RUnlock()
|
||||
|
||||
urls := make([]string, len(data.Feeds)+len(data.Pages))
|
||||
i := 0
|
||||
for k := range data.Feeds {
|
||||
urls[i] = k
|
||||
i++
|
||||
}
|
||||
for k := range data.Pages {
|
||||
urls[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
sort.Strings(urls)
|
||||
return urls
|
||||
}
|
||||
|
||||
// Remove removes a subscription from memory and from the disk.
|
||||
// The URL must be provided. It will do nothing if the URL is
|
||||
// not an actual subscription.
|
||||
//
|
||||
// It returns any errors that occured when saving to disk.
|
||||
func Remove(u string) error {
|
||||
data.Lock()
|
||||
// Just delete from both instead of using a loop to find it
|
||||
delete(data.Feeds, u)
|
||||
delete(data.Pages, u)
|
||||
data.Unlock()
|
||||
return writeJSON()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user