Ver Fonte

feat(integration): add integration with archive.org

Tested locally:

```console
$ Tue 26 Aug 17:34:05 CEST 2025
$ go build && ./miniflux.app -c ./config.ini  -debug
level=DEBUG msg="Starting daemon..."
level=DEBUG msg="Starting background scheduler..."
level=DEBUG msg="Worker started" worker_id=15
level=DEBUG msg="Worker started" worker_id=0

[…]

level=DEBUG msg="Incoming request" client_ip=127.0.0.1 request.method=POST request.uri=/entry/save/29773 request.protocol=HTTP/1.1 request.execution_time=5.57385ms
level=DEBUG msg="Sending entry to archive.org" user_id=1 entry_id=29773 entry_url=https://sumnerevans.com/portfolio/
level=DEBUG msg="Sending entry to archive.org" title=Portfolio url=https://sumnerevans.com/portfolio/
^C
$ curl -I -H "User-Agent: Mozilla"  https://web.archive.org/web/20250826153413/https://sumnerevans.com/portfolio/ | grep orig-date
x-archive-orig-date: Tue, 26 Aug 2025 15:34:13 GMT
$
```
jvoisin há 9 meses atrás
pai
commit
79b0d0b9cc

+ 7 - 0
internal/database/migrations.go

@@ -1341,4 +1341,11 @@ var migrations = [...]func(tx *sql.Tx) error{
 
 		return nil
 	},
+	func(tx *sql.Tx) (err error) {
+		sql := `
+			ALTER TABLE integrations ADD COLUMN archiveorg_enabled bool default 'f'
+		`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 43 - 0
internal/integration/archiveorg/archiveorg.go

@@ -0,0 +1,43 @@
+// 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=15d"
+
+type Client struct{}
+
+func NewClient() *Client {
+	return &Client{}
+}
+
+func (c *Client) SendURL(entryURL, title string) {
+	// We're using a goroutine here as submissions to archive.org might take a long time
+	// and trigger a timeout on miniflux' side.
+	go func(entryURL string) {
+		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),
+				slog.String("title", title),
+				slog.String("url", entryURL),
+			)
+			return
+		}
+		if res.StatusCode > 299 {
+			slog.Error("archiveorg: failed with status code",
+				slog.String("title", title),
+				slog.String("url", entryURL),
+				slog.Int("code", res.StatusCode),
+			)
+		}
+		res.Body.Close()
+	}(entryURL)
+}

+ 11 - 1
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"
@@ -398,6 +399,16 @@ 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),
+		)
+
+		archiveorg.NewClient().SendURL(entry.URL, entry.Title)
+	}
+
 	if userIntegrations.WebhookEnabled {
 		var webhookURL string
 		if entry.Feed != nil && entry.Feed.WebhookURL != "" {
@@ -506,7 +517,6 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 			)
 		}
 	}
-
 	if userIntegrations.WebhookEnabled {
 		var webhookURL string
 		if feed.WebhookURL != "" {

+ 1 - 0
internal/locale/translations/de_DE.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Webhook-URL überschreiben",
     "form.import.label.file": "OPML-Datei",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Artikel zu archive.org pushen",
     "form.integration.apprise_activate": "Artikel zu Apprise pushen",
     "form.integration.apprise_services_url": "Kommaseparierte Liste von Apprise-Dienst-URLs",
     "form.integration.apprise_url": "Apprise-API-URL",

+ 1 - 0
internal/locale/translations/el_EL.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Παράκαμψη διεύθυνσης URL webhook",
     "form.import.label.file": "Αρχείο OPML",
     "form.import.label.url": "Διεύθυνση URL",
+    "form.integration.archiveorg_activate": "Προώθηση καταχωρήσεων στο archive.org",
     "form.integration.apprise_activate": "Προώθηση καταχωρήσεων στο Apprise",
     "form.integration.apprise_services_url": "Λίστα διευθύνσεων URL υπηρεσιών Apprise διαχωρισμένων με κόμμα",
     "form.integration.apprise_url": "Διεύθυνση URL API Apprise",

+ 1 - 0
internal/locale/translations/en_US.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Override webhook url",
     "form.import.label.file": "OPML file",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Push entries to archive.org",
     "form.integration.apprise_activate": "Push entries to Apprise",
     "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/es_ES.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Invalidar la URL del webhook",
     "form.import.label.file": "Archivo OPML",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Enviar entradas a archive.org",
     "form.integration.apprise_activate": "Enviar artículos a Apprise",
     "form.integration.apprise_services_url": "Lista separada por comas de las URL del servicio Apprise",
     "form.integration.apprise_url": "URL de la API de Apprise",

+ 1 - 0
internal/locale/translations/fi_FI.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Override webhook url",
     "form.import.label.file": "OPML-tiedosto",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Työnnä merkinnät osoitteeseen archive.org",
     "form.integration.apprise_activate": "Push entries to Apprise",
     "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/fr_FR.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Remplacer l'URL du webhook",
     "form.import.label.file": "Fichier OPML",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Envoyer les articles vers archive.org",
     "form.integration.apprise_activate": "Envoyer les articles vers Apprise",
     "form.integration.apprise_services_url": "Liste des services Apprise séparés par des virgules",
     "form.integration.apprise_url": "URL de l'API Apprise",

+ 1 - 0
internal/locale/translations/hi_IN.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Override webhook url",
     "form.import.label.file": "ओपीएमएल फ़ाइल",
     "form.import.label.url": "यूआरएल",
+    "form.integration.archiveorg_activate": "प्रविष्टियों को archive.org पर भेजें",
     "form.integration.apprise_activate": "Push entries to Apprise",
     "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/id_ID.json

@@ -209,6 +209,7 @@
     "form.feed.label.webhook_url": "Timpa URL Webhook",
     "form.import.label.file": "Berkas OPML",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Push entries to archive.org",
     "form.integration.apprise_activate": "Kirim artikel ke Apprise",
     "form.integration.apprise_services_url": "Daftar yang dipisahkan koma untuk URL layanan Apprise",
     "form.integration.apprise_url": "URL API Apprise",

+ 1 - 0
internal/locale/translations/it_IT.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Override webhook url",
     "form.import.label.file": "File OPML",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Invia le voci ad archive.org",
     "form.integration.apprise_activate": "Push entries to Apprise",
     "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/ja_JP.json

@@ -209,6 +209,7 @@
     "form.feed.label.webhook_url": "Override webhook url",
     "form.import.label.file": "OPML ファイル",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "エントリーをarchive.orgにプッシュする",
     "form.integration.apprise_activate": "Push entries to Apprise",
     "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/nan_Latn_pehoeji.json

@@ -209,6 +209,7 @@
     "form.feed.label.webhook_url": "Ngī kái webhook bāng-chí",
     "form.import.label.file": "OPML tóng-àn",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Push entries to archive.org",
     "form.integration.apprise_activate": "Thui sàng siau-sit khì Apprise",
     "form.integration.apprise_services_url": "Iōng tō͘-tiám keh khui ê Apprise ho̍k-bū bāng-chí lia̍t-pió",
     "form.integration.apprise_url": "Apprise API bāng-chí",

+ 1 - 0
internal/locale/translations/nl_NL.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Overschrijf webhook URL",
     "form.import.label.file": "OPML-bestand",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Push entries to archive.org",
     "form.integration.apprise_activate": "Artikelen opslaan in Apprise",
     "form.integration.apprise_services_url": "Door komma's gescheiden lijst van Apprise service URL's",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/pl_PL.json

@@ -215,6 +215,7 @@
     "form.feed.label.webhook_url": "Zastąp adres URL webhooka",
     "form.import.label.file": "Plik OPML",
     "form.import.label.url": "Adres URL",
+    "form.integration.archiveorg_activate": "Prześlij wpisy do archive.org",
     "form.integration.apprise_activate": "Przesyłaj wpisy do Apprise",
     "form.integration.apprise_services_url": "Oddzielona przecinkami lista adresów URL usługi Apprise",
     "form.integration.apprise_url": "Adres URL API Apprise",

+ 1 - 0
internal/locale/translations/pt_BR.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Sobrescrever URL do webhook",
     "form.import.label.file": "Arquivo OPML",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Enviar itens para o archive.org",
     "form.integration.apprise_activate": "Enviar itens para o Apprise",
     "form.integration.apprise_services_url": "Lista de URLs de serviços Apprise separadas por vírgula",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/ro_RO.json

@@ -215,6 +215,7 @@
     "form.feed.label.webhook_url": "URL Webhook (pentru a primi notificări despre evenimentele de intrare)",
     "form.import.label.file": "Fișier OPML",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Trimite înregistrările pe archive.org",
     "form.integration.apprise_activate": "Trimite înregistrările pe Apprise",
     "form.integration.apprise_services_url": "URL-uri separate de virgulă cu servicii Apprise",
     "form.integration.apprise_url": "URL API Apprise",

+ 1 - 0
internal/locale/translations/ru_RU.json

@@ -215,6 +215,7 @@
     "form.feed.label.webhook_url": "Переопределить URL вебхука",
     "form.import.label.file": "OPML файл",
     "form.import.label.url": "Ссылка",
+    "form.integration.archiveorg_activate": "TОтправить статьи в archive.org",
     "form.integration.apprise_activate": "Отправить статьи в Apprise",
     "form.integration.apprise_services_url": "Список ссылок сервисов Apprise, разделенный запятой",
     "form.integration.apprise_url": "Ссылка на Apprise API",

+ 1 - 0
internal/locale/translations/tr_TR.json

@@ -212,6 +212,7 @@
     "form.feed.label.webhook_url": "Webhook URL'sini geçersiz kıl",
     "form.import.label.file": "OPML dosyası",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "Makaleleri archive.org'a gönder",
     "form.integration.apprise_activate": "Makaleleri Apprise'a gönder",
     "form.integration.apprise_services_url": "Apprise hizmet URL'lerinin virgülle ayrılmış listesi",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/uk_UA.json

@@ -215,6 +215,7 @@
     "form.feed.label.webhook_url": "Перевизначити URL вебхука",
     "form.import.label.file": "Файл OPML",
     "form.import.label.url": "URL-адреса",
+    "form.integration.archiveorg_activate": "Надсилати записи у archive.org",
     "form.integration.apprise_activate": "Надсилати записи у Apprise",
     "form.integration.apprise_services_url": "Список URL сервісів Apprise, розділених комами",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/zh_CN.json

@@ -209,6 +209,7 @@
     "form.feed.label.webhook_url": "覆盖 Webhook URL",
     "form.import.label.file": "OPML 文件",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "将新条目推送到 archive.org",
     "form.integration.apprise_activate": "将新条目推送到 Apprise",
     "form.integration.apprise_services_url": "使用逗号分隔的 Apprise 服务 URL 列表",
     "form.integration.apprise_url": "Apprise API URL",

+ 1 - 0
internal/locale/translations/zh_TW.json

@@ -209,6 +209,7 @@
     "form.feed.label.webhook_url": "覆蓋webhook URL",
     "form.import.label.file": "OPML 檔案",
     "form.import.label.url": "URL",
+    "form.integration.archiveorg_activate": "推送文章到 archive.org",
     "form.integration.apprise_activate": "推送文章到 Apprise",
     "form.integration.apprise_services_url": "使用逗號分隔的 Apprise 服務網址列表",
     "form.integration.apprise_url": "Apprise API 網址",

+ 1 - 0
internal/model/integration.go

@@ -123,4 +123,5 @@ type Integration struct {
 	PushoverToken                    string
 	PushoverDevice                   string
 	PushoverPrefix                   string
+	ArchiveorgEnabled                bool
 }

+ 9 - 4
internal/storage/integration.go

@@ -226,7 +226,8 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			linktaco_api_token,
 			linktaco_org_slug,
 			linktaco_tags,
-			linktaco_visibility
+			linktaco_visibility,
+			archiveorg_enabled
 		FROM
 			integrations
 		WHERE
@@ -352,6 +353,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.LinktacoOrgSlug,
 		&integration.LinktacoTags,
 		&integration.LinktacoVisibility,
+		&integration.ArchiveorgEnabled,
 	)
 	switch {
 	case err == sql.ErrNoRows:
@@ -485,9 +487,10 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			linktaco_api_token=$114,
 			linktaco_org_slug=$115,
 			linktaco_tags=$116,
-			linktaco_visibility=$117
+			linktaco_visibility=$117,
+			archiveorg_enabled=$118
 		WHERE
-			user_id=$118
+			user_id=$119
 	`
 	_, err := s.db.Exec(
 		query,
@@ -608,6 +611,7 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.LinktacoOrgSlug,
 		integration.LinktacoTags,
 		integration.LinktacoVisibility,
+		integration.ArchiveorgEnabled,
 		integration.UserID,
 	)
 
@@ -651,7 +655,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 {

+ 12 - 0
internal/template/templates/views/integrations.html

@@ -15,6 +15,18 @@
         <div role="alert" class="alert alert-error">{{ .errorMessage }}</div>
     {{ end }}
 
+    <details {{ if .form.ArchiveorgEnabled }}open{{ end }}>
+        <summary>Archive.org</summary>
+        <div class="form-section">
+            <label>
+                <input type="checkbox" name="archiveorg_enabled" value="1" {{ if .form.ArchiveorgEnabled }}checked{{ end }}> {{ t "form.integration.archiveorg_activate" }}
+            </label>
+            <div class="buttons">
+                <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
+            </div>
+        </div>
+    </details>
+
     <details {{ if .form.AppriseEnabled }}open{{ end }}>
         <summary>Apprise</summary>
         <div class="form-section">

+ 3 - 0
internal/ui/form/integration.go

@@ -129,6 +129,7 @@ type IntegrationForm struct {
 	PushoverToken                    string
 	PushoverDevice                   string
 	PushoverPrefix                   string
+	ArchiveorgEnabled                bool
 }
 
 // Merge copy form values to the model.
@@ -247,6 +248,7 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.PushoverToken = i.PushoverToken
 	integration.PushoverDevice = i.PushoverDevice
 	integration.PushoverPrefix = i.PushoverPrefix
+	integration.ArchiveorgEnabled = i.ArchiveorgEnabled
 }
 
 // NewIntegrationForm returns a new IntegrationForm.
@@ -368,6 +370,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",
 	}
 }
 

+ 1 - 0
internal/ui/integration_show.go

@@ -142,6 +142,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))