diff --git a/internal/integration/archiveorg/archiveorg.go b/internal/integration/archiveorg/archiveorg.go new file mode 100644 index 00000000000..7ec3a63a40c --- /dev/null +++ b/internal/integration/archiveorg/archiveorg.go @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package archiveorg + +import ( + "log/slog" + "net/http" + "net/url" +) + +// See https://docs.google.com/document/d/1Nsv52MvSjbLb2PCpHlat0gkzw0EvtSgpKHu4mk0MnrA/edit?tab=t.0 +const options = "delay_wb_availability=1&if_not_archived_within=5d" + +type Client struct{} + +func NewClient() *Client { + return &Client{} +} + +func (c *Client) SendURL(entryURL, title string) error { + // We're using a goroutine here as submissions to archive.org might take a long time, + // and thus trigger a timeout on miniflux' side. + go func(entryURL string) { + slog.Debug("Sending entry to archive.org", + slog.String("title", title)) + + res, err := http.Get("https://web.archive.org/save/" + url.QueryEscape(entryURL) + "?" + options) + if err != nil { + slog.Error("archiveorg: unable to send request: %v", slog.Any("err", err)) + } + defer res.Body.Close() + if res.StatusCode > 299 { + slog.Error("archiveorg: failed with status code", slog.Int("code", res.StatusCode)) + } + }(entryURL) + + return nil +} diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 77a2e00a967..d3b6e95a1b5 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -7,6 +7,7 @@ import ( "log/slog" "miniflux.app/v2/internal/integration/apprise" + "miniflux.app/v2/internal/integration/archiveorg" "miniflux.app/v2/internal/integration/betula" "miniflux.app/v2/internal/integration/cubox" "miniflux.app/v2/internal/integration/discord" @@ -37,6 +38,24 @@ import ( // SendEntry sends the entry to third-party providers when the user click on "Save". func SendEntry(entry *model.Entry, userIntegrations *model.Integration) { + if userIntegrations.ArchiveorgEnabled { + slog.Debug("Sending entry to archive.org", + slog.Int64("user_id", userIntegrations.UserID), + slog.Int64("entry_id", entry.ID), + slog.String("entry_url", entry.URL), + ) + + err := archiveorg.NewClient().SendURL(entry.URL, entry.Title) + if err != nil { + slog.Error("Unable to send entry to archive.org", + slog.Int64("user_id", userIntegrations.UserID), + slog.Int64("entry_id", entry.ID), + slog.String("entry_url", entry.URL), + slog.Any("error", err), + ) + } + } + if userIntegrations.BetulaEnabled { slog.Debug("Sending entry to Betula", slog.Int64("user_id", userIntegrations.UserID), diff --git a/internal/model/integration.go b/internal/model/integration.go index 73d64c3ec4e..6dc726c8b59 100644 --- a/internal/model/integration.go +++ b/internal/model/integration.go @@ -117,4 +117,5 @@ type Integration struct { PushoverToken string PushoverDevice string PushoverPrefix string + ArchiveorgEnabled bool } diff --git a/internal/storage/integration.go b/internal/storage/integration.go index 7bba4037bb9..361b8dfdf3d 100644 --- a/internal/storage/integration.go +++ b/internal/storage/integration.go @@ -221,6 +221,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) { karakeep_enabled, karakeep_api_key, karakeep_url + archiveorg_enabled, FROM integrations WHERE @@ -340,6 +341,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) { &integration.KarakeepEnabled, &integration.KarakeepAPIKey, &integration.KarakeepURL, + &integration.ArchiveorgEnabled, ) switch { case err == sql.ErrNoRows: @@ -467,9 +469,10 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error { rssbridge_token=$108, karakeep_enabled=$109, karakeep_api_key=$110, - karakeep_url=$111 + karakeep_url=$111, + archiveorg_enabled=$112 WHERE - user_id=$112 + user_id=$113 ` _, err := s.db.Exec( query, @@ -584,6 +587,7 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error { integration.KarakeepEnabled, integration.KarakeepAPIKey, integration.KarakeepURL, + integration.ArchiveorgEnabled, integration.UserID, ) @@ -626,7 +630,8 @@ func (s *Storage) HasSaveEntry(userID int64) (result bool) { betula_enabled='t' OR cubox_enabled='t' OR discord_enabled='t' OR - slack_enabled='t' + slack_enabled='t' OR + archiveorg_enabled='t' ) ` if err := s.db.QueryRow(query, userID).Scan(&result); err != nil { diff --git a/internal/template/templates/views/integrations.html b/internal/template/templates/views/integrations.html index 10b5c5c9bd3..0243533dc6c 100644 --- a/internal/template/templates/views/integrations.html +++ b/internal/template/templates/views/integrations.html @@ -15,6 +15,18 @@

{{ t "page.integrations.title" }}

{{ end }} +
+ Archive.org +
+ +
+ +
+
+
+
Apprise
diff --git a/internal/ui/form/integration.go b/internal/ui/form/integration.go index 170043e4769..9b58468258c 100644 --- a/internal/ui/form/integration.go +++ b/internal/ui/form/integration.go @@ -123,6 +123,7 @@ type IntegrationForm struct { PushoverToken string PushoverDevice string PushoverPrefix string + ArchiveorgEnabled bool } // Merge copy form values to the model. @@ -350,6 +351,7 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm { PushoverToken: r.FormValue("pushover_token"), PushoverDevice: r.FormValue("pushover_device"), PushoverPrefix: r.FormValue("pushover_prefix"), + ArchiveorgEnabled: r.FormValue("archiveorg_enabled") == "1", } } diff --git a/internal/ui/integration_show.go b/internal/ui/integration_show.go index afd2571e208..6a8a3dce9ce 100644 --- a/internal/ui/integration_show.go +++ b/internal/ui/integration_show.go @@ -136,6 +136,7 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) { PushoverToken: integration.PushoverToken, PushoverDevice: integration.PushoverDevice, PushoverPrefix: integration.PushoverPrefix, + ArchiveorgEnabled: integration.ArchiveorgEnabled, } sess := session.New(h.store, request.SessionID(r))