Skip to content

Commit

Permalink
Merge branch 'miniflux:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
Sevichecc authored Dec 16, 2024
2 parents 9497fb5 + a06657b commit cbdd837
Show file tree
Hide file tree
Showing 46 changed files with 753 additions and 411 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ require (
github.com/lib/pq v1.10.9
github.com/prometheus/client_golang v1.20.5
github.com/tdewolff/minify/v2 v2.21.2
github.com/yuin/goldmark v1.7.8
golang.org/x/crypto v0.30.0
golang.org/x/crypto v0.31.0
golang.org/x/image v0.23.0
golang.org/x/net v0.32.0
golang.org/x/oauth2 v0.24.0
golang.org/x/term v0.27.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
Expand Down
1 change: 1 addition & 0 deletions internal/api/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
}

feedModificationRequest.Patch(originalFeed)
originalFeed.ResetErrorCounter()
if err := h.store.UpdateFeed(originalFeed); err != nil {
json.ServerError(w, r, err)
return
Expand Down
5 changes: 0 additions & 5 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (

"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/database"
"miniflux.app/v2/internal/locale"
"miniflux.app/v2/internal/storage"
"miniflux.app/v2/internal/ui/static"
"miniflux.app/v2/internal/version"
Expand Down Expand Up @@ -153,10 +152,6 @@ func Parse() {
slog.Info("The default value for DATABASE_URL is used")
}

if err := locale.LoadCatalogMessages(); err != nil {
printErrorAndExit(fmt.Errorf("unable to load translations: %v", err))
}

if err := static.CalculateBinaryFileChecksums(); err != nil {
printErrorAndExit(fmt.Errorf("unable to calculate binary file checksums: %v", err))
}
Expand Down
18 changes: 18 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2116,6 +2116,24 @@ func TestFetchYouTubeWatchTime(t *testing.T) {
}
}

func TestYouTubeApiKey(t *testing.T) {
os.Clearenv()
os.Setenv("YOUTUBE_API_KEY", "AAAAAAAAAAAAAaaaaaaaaaaaaa0000000000000")

parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}

expected := "AAAAAAAAAAAAAaaaaaaaaaaaaa0000000000000"
result := opts.YouTubeApiKey()

if result != expected {
t.Fatalf(`Unexpected YOUTUBE_API_KEY value, got %v instead of %v`, result, expected)
}
}

func TestYouTubeEmbedUrlOverride(t *testing.T) {
os.Clearenv()
os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/")
Expand Down
9 changes: 9 additions & 0 deletions internal/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const (
defaultFetchNebulaWatchTime = false
defaultFetchOdyseeWatchTime = false
defaultFetchYouTubeWatchTime = false
defaultYouTubeApiKey = ""
defaultYouTubeEmbedUrlOverride = "https://www.youtube-nocookie.com/embed/"
defaultCreateAdmin = false
defaultAdminUsername = ""
Expand Down Expand Up @@ -149,6 +150,7 @@ type Options struct {
fetchOdyseeWatchTime bool
fetchYouTubeWatchTime bool
filterEntryMaxAgeDays int
youTubeApiKey string
youTubeEmbedUrlOverride string
oauth2UserCreationAllowed bool
oauth2ClientID string
Expand Down Expand Up @@ -228,6 +230,7 @@ func NewOptions() *Options {
fetchNebulaWatchTime: defaultFetchNebulaWatchTime,
fetchOdyseeWatchTime: defaultFetchOdyseeWatchTime,
fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime,
youTubeApiKey: defaultYouTubeApiKey,
youTubeEmbedUrlOverride: defaultYouTubeEmbedUrlOverride,
oauth2UserCreationAllowed: defaultOAuth2UserCreation,
oauth2ClientID: defaultOAuth2ClientID,
Expand Down Expand Up @@ -503,6 +506,11 @@ func (o *Options) FetchYouTubeWatchTime() bool {
return o.fetchYouTubeWatchTime
}

// YouTubeApiKey returns the YouTube API key if defined.
func (o *Options) YouTubeApiKey() string {
return o.youTubeApiKey
}

// YouTubeEmbedUrlOverride returns YouTube URL which will be used for embeds
func (o *Options) YouTubeEmbedUrlOverride() string {
return o.youTubeEmbedUrlOverride
Expand Down Expand Up @@ -733,6 +741,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"SERVER_TIMING_HEADER": o.serverTimingHeader,
"WATCHDOG": o.watchdog,
"WORKER_POOL_SIZE": o.workerPoolSize,
"YOUTUBE_API_KEY": redactSecretValue(o.youTubeApiKey, redactSecret),
"YOUTUBE_EMBED_URL_OVERRIDE": o.youTubeEmbedUrlOverride,
"WEBAUTHN": o.webAuthn,
}
Expand Down
2 changes: 2 additions & 0 deletions internal/config/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.fetchOdyseeWatchTime = parseBool(value, defaultFetchOdyseeWatchTime)
case "FETCH_YOUTUBE_WATCH_TIME":
p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime)
case "YOUTUBE_API_KEY":
p.opts.youTubeApiKey = parseString(value, defaultYouTubeApiKey)
case "YOUTUBE_EMBED_URL_OVERRIDE":
p.opts.youTubeEmbedUrlOverride = parseString(value, defaultYouTubeEmbedUrlOverride)
case "WATCHDOG":
Expand Down
66 changes: 39 additions & 27 deletions internal/integration/apprise/apprise.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"time"

Expand All @@ -26,42 +27,53 @@ func NewClient(serviceURL, baseURL string) *Client {
return &Client{serviceURL, baseURL}
}

func (c *Client) SendNotification(entry *model.Entry) error {
func (c *Client) SendNotification(feed *model.Feed, entries model.Entries) error {
if c.baseURL == "" || c.servicesURL == "" {
return fmt.Errorf("apprise: missing base URL or services URL")
}

message := "[" + entry.Title + "]" + "(" + entry.URL + ")" + "\n\n"
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/notify")
if err != nil {
return fmt.Errorf(`apprise: invalid API endpoint: %v`, err)
}
for _, entry := range entries {
message := "[" + entry.Title + "]" + "(" + entry.URL + ")" + "\n\n"
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/notify")
if err != nil {
return fmt.Errorf(`apprise: invalid API endpoint: %v`, err)
}

requestBody, err := json.Marshal(map[string]any{
"urls": c.servicesURL,
"body": message,
})
if err != nil {
return fmt.Errorf("apprise: unable to encode request body: %v", err)
}
requestBody, err := json.Marshal(map[string]any{
"urls": c.servicesURL,
"body": message,
"title": feed.Title,
})
if err != nil {
return fmt.Errorf("apprise: unable to encode request body: %v", err)
}

request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("apprise: unable to create request: %v", err)
}
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("apprise: unable to create request: %v", err)
}

request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)

httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("apprise: unable to send request: %v", err)
}
defer response.Body.Close()
slog.Debug("Sending Apprise notification",
slog.String("apprise_url", c.baseURL),
slog.String("services_url", c.servicesURL),
slog.String("title", feed.Title),
slog.String("body", message),
slog.String("entry_url", entry.URL),
)

httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("apprise: unable to send request: %v", err)
}
defer response.Body.Close()

if response.StatusCode >= 400 {
return fmt.Errorf("apprise: unable to send a notification: url=%s status=%d", apiEndpoint, response.StatusCode)
if response.StatusCode >= 400 {
return fmt.Errorf("apprise: unable to send a notification: url=%s status=%d", apiEndpoint, response.StatusCode)
}
}

return nil
Expand Down
53 changes: 23 additions & 30 deletions internal/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,30 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
}
}

if userIntegrations.AppriseEnabled {
slog.Debug("Sending new entries to Apprise",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int("nb_entries", len(entries)),
slog.Int64("feed_id", feed.ID),
)

appriseServiceURLs := userIntegrations.AppriseServicesURL
if feed.AppriseServiceURLs != "" {
appriseServiceURLs = feed.AppriseServiceURLs
}

client := apprise.NewClient(
appriseServiceURLs,
userIntegrations.AppriseURL,
)

if err := client.SendNotification(feed, entries); err != nil {
slog.Warn("Unable to send new entries to Apprise", slog.Any("error", err))
}
}

// Integrations that only support sending individual entries
if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
if userIntegrations.TelegramBotEnabled {
for _, entry := range entries {
if userIntegrations.TelegramBotEnabled {
slog.Debug("Sending a new entry to Telegram",
Expand All @@ -541,35 +563,6 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
)
}
}

if userIntegrations.AppriseEnabled {
slog.Debug("Sending a new entry to Apprise",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
slog.String("apprise_url", userIntegrations.AppriseURL),
)

appriseServiceURLs := userIntegrations.AppriseServicesURL
if feed.AppriseServiceURLs != "" {
appriseServiceURLs = feed.AppriseServiceURLs
}

client := apprise.NewClient(
appriseServiceURLs,
userIntegrations.AppriseURL,
)

if err := client.SendNotification(entry); err != nil {
slog.Error("Unable to send entry to Apprise",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
slog.String("apprise_url", userIntegrations.AppriseURL),
slog.Any("error", err),
)
}
}
}
}
}
15 changes: 12 additions & 3 deletions internal/locale/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,26 @@ import (
type translationDict map[string]interface{}
type catalog map[string]translationDict

var defaultCatalog catalog
var defaultCatalog = make(catalog, len(AvailableLanguages))

//go:embed translations/*.json
var translationFiles embed.FS

func GetTranslationDict(language string) (translationDict, error) {
if _, ok := defaultCatalog[language]; !ok {
var err error
if defaultCatalog[language], err = loadTranslationFile(language); err != nil {
return nil, err
}
}
return defaultCatalog[language], nil
}

// LoadCatalogMessages loads and parses all translations encoded in JSON.
func LoadCatalogMessages() error {
var err error
defaultCatalog = make(catalog, len(AvailableLanguages()))

for language := range AvailableLanguages() {
for language := range AvailableLanguages {
defaultCatalog[language], err = loadTranslationFile(language)
if err != nil {
return err
Expand Down
26 changes: 23 additions & 3 deletions internal/locale/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestLoadCatalog(t *testing.T) {
}

func TestAllKeysHaveValue(t *testing.T) {
for language := range AvailableLanguages() {
for language := range AvailableLanguages {
messages, err := loadTranslationFile(language)
if err != nil {
t.Fatalf(`Unable to load translation messages for language %q`, language)
Expand Down Expand Up @@ -71,7 +71,7 @@ func TestMissingTranslations(t *testing.T) {
t.Fatal(`Unable to parse reference language`)
}

for language := range AvailableLanguages() {
for language := range AvailableLanguages {
if language == refLang {
continue
}
Expand All @@ -90,7 +90,27 @@ func TestMissingTranslations(t *testing.T) {
}

func TestTranslationFilePluralForms(t *testing.T) {
for language := range AvailableLanguages() {
var numberOfPluralFormsPerLanguage = map[string]int{
"en_US": 2,
"es_ES": 2,
"fr_FR": 2,
"de_DE": 2,
"pl_PL": 3,
"pt_BR": 2,
"zh_CN": 1,
"zh_TW": 1,
"nl_NL": 2,
"ru_RU": 3,
"it_IT": 2,
"ja_JP": 1,
"tr_TR": 2,
"el_EL": 2,
"fi_FI": 2,
"hi_IN": 2,
"uk_UA": 3,
"id_ID": 1,
}
for language := range AvailableLanguages {
messages, err := loadTranslationFile(language)
if err != nil {
t.Fatalf(`Unable to load translation messages for language %q`, language)
Expand Down
Loading

0 comments on commit cbdd837

Please sign in to comment.