2 Commits

Author SHA1 Message Date
Rohit Vighne
88bdefcd90 Merge d29b8f2afa into c348593ef4 2025-03-27 11:21:41 -04:00
Rohit Vighne
d29b8f2afa Listen on AF_UNIX socket if -addr is a path 2025-03-27 11:19:26 -04:00
28 changed files with 680 additions and 849 deletions

View File

@@ -38,4 +38,3 @@ jobs:
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64

1
.gitignore vendored
View File

@@ -6,4 +6,3 @@
*.db-wal *.db-wal
*.syso *.syso
versioninfo.rc versioninfo.rc
.DS_Store

View File

@@ -1,14 +1,4 @@
# upcoming # upcoming
- (new) serve on unix socket (thanks to @rvighne)
- (new) more auto-refresh options: 12h & 24h (thanks to @aswerkljh for suggestion)
- (fix) smooth scrolling on iOS (thanks to gatheraled)
- (fix) displaying youtube shorts in "Read Here" (thanks to @Dean-Corso for the report)
- (etc) theme-color support (thanks to @asimpson)
- (etc) cookie security measures (thanks to Tom Fitzhenry)
- (etc) restrict access to internal IPs for page crawler (thanks to Omar Kurt)
# v2.5 (2025-03-26)
- (new) Fever API support (thanks to @icefed) - (new) Fever API support (thanks to @icefed)
- (new) editable feed link (thanks to @adaszko) - (new) editable feed link (thanks to @adaszko)

0
doc/todo.txt Normal file
View File

View File

@@ -10,7 +10,7 @@ Name=yarr
Exec=$HOME/.local/bin/yarr -open Exec=$HOME/.local/bin/yarr -open
Icon=yarr Icon=yarr
Type=Application Type=Application
Categories=Internet;Network;News;Feed; Categories=Internet;
END END
if [[ ! -d "$HOME/.local/share/icons" ]]; then if [[ ! -d "$HOME/.local/share/icons" ]]; then

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 173 KiB

2
go.mod
View File

@@ -6,7 +6,7 @@ toolchain go1.23.5
require ( require (
github.com/mattn/go-sqlite3 v1.14.24 github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/net v0.38.0 golang.org/x/net v0.37.0
golang.org/x/sys v0.31.0 golang.org/x/sys v0.31.0
) )

10
go.sum
View File

@@ -1,8 +1,14 @@
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=

View File

@@ -1,11 +1,10 @@
VERSION=2.6 VERSION=2.5
GITHASH=$(shell git rev-parse --short=8 HEAD) GITHASH=$(shell git rev-parse --short=8 HEAD)
GO_TAGS = sqlite_foreign_keys sqlite_json GO_TAGS = sqlite_foreign_keys sqlite_json
GO_LDFLAGS = -s -w -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITHASH)' GO_LDFLAGS = -s -w -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITHASH)'
GO_FLAGS = -tags "$(GO_TAGS)" -ldflags="$(GO_LDFLAGS)" GO_FLAGS = -tags "$(GO_TAGS)" -ldflags="$(GO_LDFLAGS)"
GO_FLAGS_DEBUG = -tags "$(GO_TAGS) debug"
GO_FLAGS_GUI = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS)" GO_FLAGS_GUI = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS)"
GO_FLAGS_GUI_WIN = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS) -H windowsgui" GO_FLAGS_GUI_WIN = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS) -H windowsgui"
@@ -76,7 +75,7 @@ windows_arm64_gui: src/platform/versioninfo.rc
GOOS=windows GOARCH=arm64 go build $(GO_FLAGS_GUI_WIN) -o out/$@/yarr.exe ./cmd/yarr GOOS=windows GOARCH=arm64 go build $(GO_FLAGS_GUI_WIN) -o out/$@/yarr.exe ./cmd/yarr
serve: serve:
go run $(GO_FLAGS_DEBUG) ./cmd/yarr -db local.db go run $(GO_FLAGS) ./cmd/yarr -db local.db
test: test:
go test $(GO_FLAGS) ./... go test $(GO_FLAGS) ./...

View File

@@ -1,5 +1,3 @@
//go:build !debug
package assets package assets
import "embed" import "embed"

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>

Before

Width:  |  Height:  |  Size: 269 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>

Before

Width:  |  Height:  |  Size: 268 B

View File

@@ -8,7 +8,6 @@
<link rel="icon" href="./static/graphicarts/favicon.svg" type="image/svg+xml"> <link rel="icon" href="./static/graphicarts/favicon.svg" type="image/svg+xml">
<link rel="alternate icon" href="./static/graphicarts/favicon.png" type="image/png"> <link rel="alternate icon" href="./static/graphicarts/favicon.png" type="image/png">
<link rel="manifest" href="./manifest.json" /> <link rel="manifest" href="./manifest.json" />
<meta name="theme-color" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script> <script>
window.app = window.app || {} window.app = window.app || {}
@@ -24,21 +23,21 @@
<div class="p-2 toolbar d-flex align-items-center"> <div class="p-2 toolbar d-flex align-items-center">
<div class="icon mx-2">{% inline "anchor.svg" %}</div> <div class="icon mx-2">{% inline "anchor.svg" %}</div>
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<button class="toolbar-item ml-1" <button class="toolbar-item"
:class="{active: filterSelected == 'unread'}" :class="{active: filterSelected == 'unread'}"
:aria-pressed="filterSelected == 'unread'" :aria-pressed="filterSelected == 'unread'"
title="Unread" title="Unread"
@click="filterSelected = 'unread'"> @click="filterSelected = 'unread'">
<span class="icon">{% inline "circle-full.svg" %}</span> <span class="icon">{% inline "circle-full.svg" %}</span>
</button> </button>
<button class="toolbar-item mx-1" <button class="toolbar-item"
:class="{active: filterSelected == 'starred'}" :class="{active: filterSelected == 'starred'}"
:aria-pressed="filterSelected == 'starred'" :aria-pressed="filterSelected == 'starred'"
title="Starred" title="Starred"
@click="filterSelected = 'starred'"> @click="filterSelected = 'starred'">
<span class="icon">{% inline "star-full.svg" %}</span> <span class="icon">{% inline "star-full.svg" %}</span>
</button> </button>
<button class="toolbar-item mr-1" <button class="toolbar-item"
:class="{active: filterSelected == ''}" :class="{active: filterSelected == ''}"
:aria-pressed="filterSelected == ''" :aria-pressed="filterSelected == ''"
title="All" title="All"
@@ -79,20 +78,12 @@
<header class="dropdown-header" role="heading" aria-level="2">Auto Refresh</header> <header class="dropdown-header" role="heading" aria-level="2">Auto Refresh</header>
<div class="row text-center m-0"> <div class="row text-center m-0">
<button class="dropdown-item col-4 px-0" <button class="dropdown-item col-4 px-0" :aria-pressed="!refreshRate" :class="{active: !refreshRate}" @click.stop="refreshRate = 0">0</button>
@click.stop="changeRefreshRate(-1)" <button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 10" :class="{active: refreshRate == 10}" @click.stop="refreshRate = 10">10m</button>
:disabled="!refreshRate"> <button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 30" :class="{active: refreshRate == 30}" @click.stop="refreshRate = 30">30m</button>
<span class="icon"> <button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 60" :class="{active: refreshRate == 60}" @click.stop="refreshRate = 60">1h</button>
{% inline "chevron-down.svg" %} <button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 120" :class="{active: refreshRate == 120}" @click.stop="refreshRate = 120">2h</button>
</span> <button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 240" :class="{active: refreshRate == 240}" @click.stop="refreshRate = 240">4h</button>
</button>
<div class="col-4 d-flex align-items-center justify-content-center">{{ refreshRateTitle }}</div>
<button class="dropdown-item col-4 px-0"
@click.stop="changeRefreshRate(1)" :disabled="refreshRate === refreshRateOptions.at(-1).value">
<span class="icon">
{% inline "chevron-up.svg" %}
</span>
</button>
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@@ -131,7 +122,7 @@
</button> </button>
</dropdown> </dropdown>
</div> </div>
<div id="feed-list-scroll" class="p-2 overflow-auto scroll-touch border-top flex-grow-1"> <div id="feed-list-scroll" class="p-2 overflow-auto border-top flex-grow-1">
<label class="selectgroup"> <label class="selectgroup">
<input type="radio" name="feed" value="" v-model="feedSelected"> <input type="radio" name="feed" value="" v-model="feedSelected">
<div class="selectgroup-label d-flex align-items-center w-100"> <div class="selectgroup-label d-flex align-items-center w-100">
@@ -144,8 +135,10 @@
</label> </label>
<div v-for="folder in foldersWithFeeds"> <div v-for="folder in foldersWithFeeds">
<label class="selectgroup mt-1" <label class="selectgroup mt-1"
:class="{'d-none': mustHideFolder(folder)}" :class="{'d-none': filterSelected
v-if="folder.id"> && !(current.folder.id == folder.id || current.feed.folder_id == folder.id)
&& !filteredFolderStats[folder.id]
&& (!itemSelectedDetails || (feedsById[itemSelectedDetails.feed_id] || {}).folder_id != folder.id)}">
<input type="radio" name="feed" :value="'folder:'+folder.id" v-model="feedSelected" v-if="folder.id"> <input type="radio" name="feed" :value="'folder:'+folder.id" v-model="feedSelected" v-if="folder.id">
<div class="selectgroup-label d-flex align-items-center w-100" v-if="folder.id"> <div class="selectgroup-label d-flex align-items-center w-100" v-if="folder.id">
<span class="icon mr-2" <span class="icon mr-2"
@@ -159,7 +152,10 @@
</label> </label>
<div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}"> <div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}">
<label class="selectgroup" <label class="selectgroup"
:class="{'d-none': mustHideFeed(feed)}" :class="{'d-none': filterSelected
&& !(current.feed.id == feed.id)
&& !filteredFeedStats[feed.id]
&& (!itemSelectedDetails || itemSelectedDetails.feed_id != feed.id)}"
v-for="feed in folder.feeds"> v-for="feed in folder.feeds">
<input type="radio" name="feed" :value="'feed:'+feed.id" v-model="feedSelected"> <input type="radio" name="feed" :value="'feed:'+feed.id" v-model="feedSelected">
<div class="selectgroup-label d-flex align-items-center w-100"> <div class="selectgroup-label d-flex align-items-center w-100">
@@ -276,12 +272,12 @@
</button> </button>
</dropdown> </dropdown>
</div> </div>
<div id="item-list-scroll" class="p-2 overflow-auto scroll-touch border-top flex-grow-1" v-scroll="loadMoreItems" ref="itemlist"> <div id="item-list-scroll" class="p-2 overflow-auto border-top flex-grow-1" v-scroll="loadMoreItems" ref="itemlist">
<label v-for="item in items" :key="item.id" <label v-for="item in items" :key="item.id"
class="selectgroup"> class="selectgroup">
<input type="radio" name="item" :value="item.id" v-model="itemSelected"> <input type="radio" name="item" :value="item.id" v-model="itemSelected">
<div class="selectgroup-label d-flex flex-column"> <div class="selectgroup-label d-flex flex-column">
<div style="line-height: 100%; opacity: .7; margin-bottom: .1rem;" class="d-flex align-items-center"> <div style="line-height: 1; opacity: .7; margin-bottom: .1rem;" class="d-flex align-items-center">
<transition name="indicator"> <transition name="indicator">
<span class="icon icon-small mr-1" v-if="item.status=='unread'">{% inline "circle-full.svg" %}</span> <span class="icon icon-small mr-1" v-if="item.status=='unread'">{% inline "circle-full.svg" %}</span>
<span class="icon icon-small mr-1" v-if="item.status=='starred'">{% inline "star-full.svg" %}</span> <span class="icon icon-small mr-1" v-if="item.status=='starred'">{% inline "star-full.svg" %}</span>
@@ -351,7 +347,7 @@
</div> </div>
<div v-if="itemSelectedDetails" <div v-if="itemSelectedDetails"
ref="content" ref="content"
class="content px-4 pt-3 pb-5 border-top overflow-auto scroll-touch" class="content px-4 pt-3 pb-5 border-top overflow-auto"
:class="{'font-serif': theme.font == 'serif', 'font-monospace': theme.font == 'monospace'}" :class="{'font-serif': theme.font == 'serif', 'font-monospace': theme.font == 'monospace'}"
:style="{'font-size': theme.size + 'rem'}"> :style="{'font-size': theme.size + 'rem'}">
<div class="content-wrapper"> <div class="content-wrapper">
@@ -423,7 +419,6 @@
<tr><td colspan=2>&nbsp;</td></tr> <tr><td colspan=2>&nbsp;</td></tr>
<tr><td><kbd>j</kbd> <kbd>k</kbd></td> <td>next / prev article</td></tr> <tr><td><kbd>j</kbd> <kbd>k</kbd></td> <td>next / prev article</td></tr>
<tr><td><kbd>l</kbd> <kbd>h</kbd></td> <td>next / prev feed</td></tr> <tr><td><kbd>l</kbd> <kbd>h</kbd></td> <td>next / prev feed</td></tr>
<tr><td><kbd>q</kbd></td> <td>close article</td></tr>
<tr><td colspan=2>&nbsp;</td></tr> <tr><td colspan=2>&nbsp;</td></tr>
<tr><td><kbd>R</kbd></td> <td>mark all read</td></tr> <tr><td><kbd>R</kbd></td> <td>mark all read</td></tr>

View File

@@ -211,7 +211,6 @@ var vm = new Vue({
api.feeds.list_errors().then(function(errors) { api.feeds.list_errors().then(function(errors) {
vm.feed_errors = errors vm.feed_errors = errors
}) })
this.updateMetaTheme(app.settings.theme_name)
}, },
data: function() { data: function() {
var s = app.settings var s = app.settings
@@ -250,25 +249,9 @@ var vm = new Vue({
'font': s.theme_font, 'font': s.theme_font,
'size': s.theme_size, 'size': s.theme_size,
}, },
'themeColors': {
'night': '#0e0e0e',
'sepia': '#f4f0e5',
'light': '#fff',
},
'refreshRate': s.refresh_rate, 'refreshRate': s.refresh_rate,
'authenticated': app.authenticated, 'authenticated': app.authenticated,
'feed_errors': {}, 'feed_errors': {},
'refreshRateOptions': [
{ title: "0", value: 0 },
{ title: "10m", value: 10 },
{ title: "30m", value: 30 },
{ title: "1h", value: 60 },
{ title: "2h", value: 120 },
{ title: "4h", value: 240 },
{ title: "12h", value: 720 },
{ title: "24h", value: 1440 },
],
} }
}, },
computed: { computed: {
@@ -326,17 +309,12 @@ var vm = new Vue({
contentVideos: function() { contentVideos: function() {
if (!this.itemSelectedDetails) return [] if (!this.itemSelectedDetails) return []
return (this.itemSelectedDetails.media_links || []).filter(l => l.type === 'video') return (this.itemSelectedDetails.media_links || []).filter(l => l.type === 'video')
}, }
refreshRateTitle: function () {
const entry = this.refreshRateOptions.find(o => o.value === this.refreshRate)
return entry ? entry.title : '0'
},
}, },
watch: { watch: {
'theme': { 'theme': {
deep: true, deep: true,
handler: function(theme) { handler: function(theme) {
this.updateMetaTheme(theme.name)
document.body.classList.value = 'theme-' + theme.name document.body.classList.value = 'theme-' + theme.name
api.settings.update({ api.settings.update({
theme_name: theme.name, theme_name: theme.name,
@@ -412,9 +390,6 @@ var vm = new Vue({
}, },
}, },
methods: { methods: {
updateMetaTheme: function(theme) {
document.querySelector("meta[name='theme-color']").content = this.themeColors[theme]
},
refreshStats: function(loopMode) { refreshStats: function(loopMode) {
return api.status().then(function(data) { return api.status().then(function(data) {
if (loopMode && !vm.itemSelected) vm.refreshItems() if (loopMode && !vm.itemSelected) vm.refreshItems()
@@ -778,18 +753,9 @@ var vm = new Vue({
// navigation helper, navigate relative to selected feed // navigation helper, navigate relative to selected feed
navigateToFeed: function(relativePosition) { navigateToFeed: function(relativePosition) {
let vm = this let vm = this
const navigationList = this.foldersWithFeeds var navigationList = Array.from(document.querySelectorAll('#col-feed-list input[name=feed]'))
.filter(folder => !folder.id || !vm.mustHideFolder(folder)) .filter(function(r) { return r.offsetParent !== null && r.value !== 'folder:null' })
.map((folder) => { .map(function(r) { return r.value })
if (this.mustHideFolder(folder)) return []
const folds = folder.id ? [`folder:${folder.id}`] : []
const feeds = (folder.is_expanded || !folder.id)
? (folder.feeds || []).filter(f => !vm.mustHideFeed(f)).map(f => `feed:${f.id}`)
: []
return folds.concat(feeds)
})
.flat()
navigationList.unshift('')
var currentFeedPosition = navigationList.indexOf(vm.feedSelected) var currentFeedPosition = navigationList.indexOf(vm.feedSelected)
@@ -812,24 +778,6 @@ var vm = new Vue({
if (target && scroll) scrollto(target, scroll) if (target && scroll) scrollto(target, scroll)
}) })
}, },
changeRefreshRate: function(offset) {
const curIdx = this.refreshRateOptions.findIndex(o => o.value === this.refreshRate)
if (curIdx <= 0 && offset < 0) return
if (curIdx >= (this.refreshRateOptions.length - 1) && offset > 0) return
this.refreshRate = this.refreshRateOptions[curIdx + offset].value
},
mustHideFolder: function (folder) {
return this.filterSelected
&& !(this.current.folder.id == folder.id || this.current.feed.folder_id == folder.id)
&& !this.filteredFolderStats[folder.id]
&& (!this.itemSelectedDetails || (this.feedsById[this.itemSelectedDetails.feed_id] || {}).folder_id != folder.id)
},
mustHideFeed: function (feed) {
return this.filterSelected
&& !(this.current.feed.id == feed.id)
&& !this.filteredFeedStats[feed.id]
&& (!this.itemSelectedDetails || this.itemSelectedDetails.feed_id != feed.id)
},
} }
}) })

View File

@@ -60,9 +60,6 @@ var shortcutFunctions = {
scrollBackward: function() { scrollBackward: function() {
helperFunctions.scrollContent(-1) helperFunctions.scrollContent(-1)
}, },
closeItem: function () {
vm.itemSelected = null
},
showAll() { showAll() {
vm.filterSelected = '' vm.filterSelected = ''
}, },
@@ -88,7 +85,6 @@ var keybindings = {
"h": shortcutFunctions.previousFeed, "h": shortcutFunctions.previousFeed,
"f": shortcutFunctions.scrollForward, "f": shortcutFunctions.scrollForward,
"b": shortcutFunctions.scrollBackward, "b": shortcutFunctions.scrollBackward,
"q": shortcutFunctions.closeItem,
"1": shortcutFunctions.showUnread, "1": shortcutFunctions.showUnread,
"2": shortcutFunctions.showStarred, "2": shortcutFunctions.showStarred,
"3": shortcutFunctions.showAll, "3": shortcutFunctions.showAll,
@@ -107,7 +103,6 @@ var codebindings = {
"KeyH": shortcutFunctions.previousFeed, "KeyH": shortcutFunctions.previousFeed,
"KeyF": shortcutFunctions.scrollForward, "KeyF": shortcutFunctions.scrollForward,
"KeyB": shortcutFunctions.scrollBackward, "KeyB": shortcutFunctions.scrollBackward,
"KeyQ": shortcutFunctions.closeItem,
"Digit1": shortcutFunctions.showUnread, "Digit1": shortcutFunctions.showUnread,
"Digit2": shortcutFunctions.showStarred, "Digit2": shortcutFunctions.showStarred,
"Digit3": shortcutFunctions.showAll, "Digit3": shortcutFunctions.showAll,

View File

@@ -100,10 +100,6 @@ select.form-control:not([multiple]):not([size]) {
padding-right: 0; padding-right: 0;
} }
.scroll-touch {
-webkit-overflow-scrolling: touch;
}
/* custom elements */ /* custom elements */
.font-serif { .font-serif {

View File

@@ -22,8 +22,6 @@ func VideoIFrame(link string) string {
youtubeID := "" youtubeID := ""
if l.Host == "www.youtube.com" && l.Path == "/watch" { if l.Host == "www.youtube.com" && l.Path == "/watch" {
youtubeID = l.Query().Get("v") youtubeID = l.Query().Get("v")
} else if l.Host == "www.youtube.com" && strings.HasPrefix(l.Path, "/shorts/") {
youtubeID = strings.TrimPrefix(l.Path, "/shorts/")
} else if l.Host == "youtu.be" { } else if l.Host == "youtu.be" {
youtubeID = strings.TrimLeft(l.Path, "/") youtubeID = strings.TrimLeft(l.Path, "/")
} }

View File

@@ -1,4 +1,4 @@
//go:build linux || freebsd || openbsd //go:build linux
package platform package platform

View File

@@ -7,6 +7,7 @@ import (
"encoding/hex" "encoding/hex"
"net/http" "net/http"
"strings" "strings"
"time"
) )
func IsAuthenticated(req *http.Request, username, password string) bool { func IsAuthenticated(req *http.Request, username, password string) bool {
@@ -23,12 +24,10 @@ func IsAuthenticated(req *http.Request, username, password string) bool {
func Authenticate(rw http.ResponseWriter, username, password, basepath string) { func Authenticate(rw http.ResponseWriter, username, password, basepath string) {
http.SetCookie(rw, &http.Cookie{ http.SetCookie(rw, &http.Cookie{
Name: "auth", Name: "auth",
Value: username + ":" + secret(username, password), Value: username + ":" + secret(username, password),
MaxAge: 604800, // 1 week Expires: time.Now().Add(time.Hour * 24 * 7), // 1 week,
Path: basepath, Path: basepath,
Secure: true,
SameSite: http.SameSiteLaxMode,
}) })
} }

View File

@@ -34,7 +34,7 @@ func (s *Server) handler() http.Handler {
BasePath: s.BasePath, BasePath: s.BasePath,
Username: s.Username, Username: s.Username,
Password: s.Password, Password: s.Password,
Public: []string{"/static", "/fever", "/manifest.json"}, Public: []string{"/static", "/fever"},
DB: s.db, DB: s.db,
} }
r.Use(a.Handler) r.Use(a.Handler)
@@ -513,10 +513,6 @@ func (s *Server) handlePageCrawl(c *router.Context) {
}) })
return return
} }
if isInternalFromURL(url) {
log.Printf("attempt to access internal IP %s from %s", url, c.Req.RemoteAddr)
return
}
body, err := worker.GetBody(url) body, err := worker.GetBody(url)
if err != nil { if err != nil {

View File

@@ -1,35 +0,0 @@
package server
import (
"net"
"net/url"
"strings"
)
func isInternalFromURL(urlStr string) bool {
parsedURL, err := url.Parse(urlStr)
if err != nil {
return false
}
host := parsedURL.Host
// Handle "host:port" format
if strings.Contains(host, ":") {
host, _, err = net.SplitHostPort(host)
if err != nil {
return false
}
}
if host == "localhost" {
return true
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast()
}

View File

@@ -1,31 +0,0 @@
package server
import "testing"
func TestIsInternalFromURL(t *testing.T) {
tests := []struct {
url string
expected bool
}{
{"http://192.168.1.1:8080", true},
{"http://10.0.0.5", true},
{"http://172.16.0.1", true},
{"http://172.31.255.255", true},
{"http://172.32.0.1", false}, // outside private range
{"http://127.0.0.1", true},
{"http://127.0.0.1:7000", true},
{"http://127.0.0.1:7000/secret", true},
{"http://169.254.0.5", true},
{"http://localhost", true}, // resolves to 127.0.0.1
{"http://8.8.8.8", false},
{"http://google.com", false}, // resolves to public IPs
{"invalid-url", false}, // invalid format
{"", false}, // empty string
}
for _, test := range tests {
result := isInternalFromURL(test.url)
if result != test.expected {
t.Errorf("isInternalFromURL(%q) = %v; want %v", test.url, result, test.expected)
}
}
}

View File

@@ -54,14 +54,10 @@ type MediaLink struct {
type MediaLinks []MediaLink type MediaLinks []MediaLink
func (m *MediaLinks) Scan(src any) error { func (m *MediaLinks) Scan(src any) error {
switch data := src.(type) { if data, ok := src.([]byte); ok {
case []byte:
return json.Unmarshal(data, m) return json.Unmarshal(data, m)
case string:
return json.Unmarshal([]byte(data), m)
default:
return nil
} }
return nil
} }
func (m MediaLinks) Value() (driver.Value, error) { func (m MediaLinks) Value() (driver.Value, error) {
@@ -423,6 +419,7 @@ func (s *Storage) DeleteOldItems() {
where status != ? where status != ?
group by i.feed_id group by i.feed_id
`, itemsKeepSize, STARRED) `, itemsKeepSize, STARRED)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return return

File diff suppressed because it is too large Load Diff

View File

@@ -924,7 +924,7 @@ func inBodyIM(p *parser) bool {
p.addElement() p.addElement()
p.im = inFramesetIM p.im = inFramesetIM
return true return true
case a.Address, a.Article, a.Aside, a.Blockquote, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Main, a.Menu, a.Nav, a.Ol, a.P, a.Search, a.Section, a.Summary, a.Ul: case a.Address, a.Article, a.Aside, a.Blockquote, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Main, a.Menu, a.Nav, a.Ol, a.P, a.Section, a.Summary, a.Ul:
p.popUntil(buttonScope, a.P) p.popUntil(buttonScope, a.P)
p.addElement() p.addElement()
case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6: case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
@@ -1136,7 +1136,7 @@ func inBodyIM(p *parser) bool {
return false return false
} }
return true return true
case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Main, a.Menu, a.Nav, a.Ol, a.Pre, a.Search, a.Section, a.Summary, a.Ul: case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Main, a.Menu, a.Nav, a.Ol, a.Pre, a.Section, a.Summary, a.Ul:
p.popUntil(defaultScope, p.tok.DataAtom) p.popUntil(defaultScope, p.tok.DataAtom)
case a.Form: case a.Form:
if p.oe.contains(a.Template) { if p.oe.contains(a.Template) {

View File

@@ -839,22 +839,8 @@ func (z *Tokenizer) readStartTag() TokenType {
if raw { if raw {
z.rawTag = strings.ToLower(string(z.buf[z.data.start:z.data.end])) z.rawTag = strings.ToLower(string(z.buf[z.data.start:z.data.end]))
} }
// Look for a self-closing token (e.g. <br/>). // Look for a self-closing token like "<br/>".
// if z.err == nil && z.buf[z.raw.end-2] == '/' {
// Originally, we did this by just checking that the last character of the
// tag (ignoring the closing bracket) was a solidus (/) character, but this
// is not always accurate.
//
// We need to be careful that we don't misinterpret a non-self-closing tag
// as self-closing, as can happen if the tag contains unquoted attribute
// values (i.e. <p a=/>).
//
// To avoid this, we check that the last non-bracket character of the tag
// (z.raw.end-2) isn't the same character as the last non-quote character of
// the last attribute of the tag (z.pendingAttr[1].end-1), if the tag has
// attributes.
nAttrs := len(z.attr)
if z.err == nil && z.buf[z.raw.end-2] == '/' && (nAttrs == 0 || z.raw.end-2 != z.attr[nAttrs-1][1].end-1) {
return SelfClosingTagToken return SelfClosingTagToken
} }
return StartTagToken return StartTagToken

2
vendor/modules.txt vendored
View File

@@ -1,7 +1,7 @@
# github.com/mattn/go-sqlite3 v1.14.24 # github.com/mattn/go-sqlite3 v1.14.24
## explicit; go 1.19 ## explicit; go 1.19
github.com/mattn/go-sqlite3 github.com/mattn/go-sqlite3
# golang.org/x/net v0.38.0 # golang.org/x/net v0.37.0
## explicit; go 1.23.0 ## explicit; go 1.23.0
golang.org/x/net/html golang.org/x/net/html
golang.org/x/net/html/atom golang.org/x/net/html/atom