8000 feat(sources): RSS and Atom news feeds as a source by adeelahmad · Pull Request #453 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ The backend (`fastapi_backend`) and the frontend (`nextjs-frontend`) are version
each run shows what its actions did. Try one on an activity without keeping or sending anything, or run it now;
Settings → Automations switches them all off and sets how far they may go
([Running automations](docs/automations.md#running-automations)).
- **News feeds.** Subscribe to a blog's or a news site's RSS or Atom feed: add it on the Sources page (its address,
or just the site's) and watch it, and each new post comes into a namespace as text, with its link, author and date,
so answers cite it. Feeds are asked for again only when they've changed, and a private feed's address is kept like a
password ([News feeds](docs/processing.md#news-feeds-rss-and-atom)).
- **Each answer says what produced it.** The *i* in an answer's header shows the model and the host it ran on, the
tools it was offered and used, the scope, its limits and what it used, tokens, cost (an estimate from your prices),
how long it took, how it ended and which tool results read like instructions to an AI, with the calls made for it.
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,14 @@ listed under [In progress](#in-progress), and what comes next under [Planned](#p
[adding an MCP server](docs/assistant.md#adding-an-mcp-server), [the MCP runner](docs/assistant.md#the-mcp-runner);
[checks](docs/requirements.md#extend-the-assistant))
- **Bring in everything.** Watched folders on S3, Dropbox, Google Drive, OneDrive, SFTP, SMB, WebDAV or local disks (a
local folder in four clicks); IMAP mail, Gmail and Google Workspace, iCal calendars and Google Calendar; a built-in
MQTT broker, syslog and webhook streams; import webhooks for scanners and phone shortcuts. Google accounts connect
with Connect with Google once Lens has a Google client, and Dropbox, OneDrive and Google Drive can sign in through
rclone on the Lens server, with no app of your own to register. Upload audio, video, PDFs, Office files, photos, JSON
and zip or tar archives, and, with an Apache Tika server (an opt-in extra), documents nothing else here can read;
capture web pages one at a time, from a link list or a bookmarks file, by API or by asking the assistant, from private
networks you allow in Settings too; and import videos and podcasts by their address (off by default).
local folder in four clicks); IMAP mail, Gmail and Google Workspace, iCal calendars, Google Calendar and RSS and Atom
news feeds; a built-in MQTT broker, syslog and webhook streams; import webhooks for scanners and phone shortcuts.
Google accounts connect with Connect with Google once Lens has a Google client, and Dropbox, OneDrive and Google
Drive can sign in through rclone on the Lens server, with no app of your own to register. Upload audio, video, PDFs,
Office files, photos, JSON and zip or tar archives, and, with an Apache Tika server (an opt-in extra), documents
nothing else here can read; capture web pages one at a time, from a link list or a bookmarks file, by API or by asking
the assistant, from private networks you allow in Settings too; and import videos and podcasts by their address (off
by default).
([Sensors](docs/sensors.md), [processing](docs/processing.md), [Apache Tika](docs/configuration.md#apache-tika),
[import webhooks](docs/api.md#import-webhooks); [checks](docs/requirements.md#bring-in-everything))
- **Clip from the browser.** The Lens web clipper for Chrome, Edge, Firefox and Safari turns a page, a selection or a
Expand Down Expand Up @@ -207,7 +208,6 @@ These are open pull requests, not yet part of Lens:
- **The assistant** answering questions about Lens itself, from its docs and this server
([#400](https://github.com/adeelahmad/lense/pull/400)).
- **Chat:** a conversation exported as Markdown, PDF or JSON ([#474](https://github.com/adeelahmad/lense/pull/474)).
- **Sources:** RSS and Atom news feeds ([#453](https://github.com/adeelahmad/lense/pull/453)).
- **Pictures:** low-quality images enhanced before they're read ([#479](https://github.com/adeelahmad/lense/pull/479)).
- **Web clipper:** signing in, or using your API token, not only an import webhook's
([#406](https://github.com/adeelahmad/lense/pull/406)).
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ text (Word, text, Markdown) are imported as text. Settings → Documents:
| `documents.convert_seconds` | 300 | how long making one PDF may take before its job fails, 10–3600 seconds |
| `documents.attachment_resources` | true | whether an email's attachments that Lens can read (documents, images, audio, video, emails) also become resources of their own; they're kept as its files either way |
| `documents.video_urls` | false | whether videos and podcasts can be imported by their address ([API](api.md#videos-and-podcasts)): their media is downloaded with yt-dlp, which Lens fetches on first use |
| `documents.web_networks` | `[]` | private networks (CIDR, such as `192.168.1.0/24`) that web pages, calendar feeds and videos may be fetched from besides the public internet: for an intranet, or a calendar server at home. The web app can't list this machine (loopback) or link-local addresses, where clouds answer with the server's own credentials; archive.yaml or `LENS_WEB_NETWORKS` in `.env` (comma-separated, added to archive.yaml's) can. Those stay whatever Settings saves: the networks listed in Settings are added to them |
| `documents.web_networks` | `[]` | private networks (CIDR, such as `192.168.1.0/24`) that web pages, calendar and news feeds and videos may be fetched from besides the public internet: for an intranet, or a calendar server at home. The web app can't list this machine (loopback) or link-local addresses, where clouds answer with the server's own credentials; archive.yaml or `LENS_WEB_NETWORKS` in `.env` (comma-separated, added to archive.yaml's) can. Those stay whatever Settings saves: the networks listed in Settings are added to them |
| `documents.converter` | `auto` | who makes the PDFs: `auto` uses [anytopdf](#anytopdf) only for what LibreOffice and Chromium can't do here, `anytopdf` uses it for every document and image (and fetches it), `lens` never uses it |
| `documents.anytopdf_url` | none | a conversion node: another machine running `anytopdf queue serve` and `anytopdf queue work`, such as `https://convert.home:8640`. Set, it converts instead of the program here |
| `documents.anytopdf_token` | none | the node's bearer token (its `ANYTOPDF_QUEUE_TOKEN`), kept encrypted |
Expand Down
39 changes: 36 additions & 3 deletions docs/processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,8 @@ A folder scan and a routine run are each claimed first, so several processes doi
## Storage sources

Admins add sources in the app (`/api/v1/sources`): S3 or S3-compatible, Dropbox, Google Drive, OneDrive, SFTP, SMB,
WebDAV, or a folder on this machine; or an email account (IMAP, or Gmail and Google Workspace), a calendar feed (iCal) or Google Calendar, below. A watched folder maps a path on a source to a namespace, with include/exclude
WebDAV, or a folder on this machine; or an email account (IMAP, or Gmail and Google Workspace), a calendar feed (iCal),
Google Calendar or a [news feed](#news-feeds-rss-and-atom) (RSS or Atom), below. A watched folder maps a path on a source to a namespace, with include/exclude
patterns, audio and/or transcripts, a polling interval, how long a file must be unchanged before it is picked up, and
whether files already there are imported (backfill). New audio is queued for the full pipeline; new transcripts are
imported and analysed. Audio stays where it is: it is copied to a cache for processing and streamed from the source for
Expand Down Expand Up @@ -479,8 +480,8 @@ files, then Import (`POST /api/v1/import/source`). The listing marks files that

### Email (IMAP), calendar feeds (iCal) and Google Calendar

Three kinds of source aren't storage and don't use rclone; their messages and events are shown as files, so browsing,
importing chosen ones and watching work as above.
These kinds of source aren't storage and don't use rclone; their messages and events are shown as files, so browsing,
importing chosen ones and watching work as above. [News feeds](#news-feeds-rss-and-atom) work the same way.

- **Email (IMAP)**: host, port, security (SSL/TLS, STARTTLS or none), user and password (an app password where the
provider has them). Mailboxes are the folders, and each message is a file `<mailbox>/<uidvalidity>-<uid>.eml`,
Expand Down Expand Up @@ -548,6 +549,38 @@ importing chosen ones and watching work as above.
a Google calendar's secret address in iCal format (Settings › Integrate calendar) works as a calendar feed.
- `.eml` and `.ics` files in any source, and uploaded through Import, can be read as text this way too.

### News feeds (RSS and Atom)

A news feed (type `rss`) is a source too: Settings › Sources › Add › News feed, then watch it. Watching a feed
subscribes to it: each new post becomes a text resource of the namespace, titl 10000 ed and dated by the post, so answers cite
it and its link.

- **Address**: the feed's own (`https://`, or `feed://`), or a page of the site: when the page names its feed
(`<link rel="alternate" type="application/rss+xml">`, as blogs and news sites do), the first feed it names is read.
A user and password if the feed asks for one. The address is kept encrypted like a password and never shown again,
since a private feed's address often carries its key. It is fetched as calendar feeds are: public addresses only
(and the networks in `documents.web_networks`), on ports 80 and 443, and the password is never sent to another
server, including one that a page names as its feed's.
- **Posts**: the feed is one folder of posts, each a file `<id>.atom` named by its date and title. RSS (0.9x, 1.0,
2.0) and Atom (0.3, 1.0) are read by [feedparser](https://github.com/kurtmckee/feedparser) (BSD), which drops
scripts, styles and event handlers from the posts' HTML and makes their links absolute. Whatever the feed, each post
is kept as an Atom entry document of its own (RFC 4287): its title, the post's page, enclosures (a podcast's audio),
when it was published and updated, its authors, categories, summary and content, and the feed's title and site
(never its address). It comes in as text: the title, its link, who wrote it, when, the feed, its categories and
enclosures, then its text. Its content type is Feed post, so a namespace can give feed posts a pipeline of their own
(Settings › Content types).
- **New posts only, unless you backfill**: as for any watch, a new watch on a feed takes the posts published after it
starts; turn on backfill to bring in those the feed lists already. The newest 500 posts of a feed are listed.
- **Light on the network**: a feed read is reused for a minute, then asked for again with its `ETag` and
`Last-Modified`, so a feed that hasn't changed costs one small request. A post is known by its id (an RSS item's
`guid`), so a feed that reorders or retitles its posts doesn't bring them in twice; a post whose text changes is read
again into the resource it already is. A post the feed drops later keeps its resource and its file.
- `.atom` files uploaded through Import (a single post, or a whole Atom feed) are read as text the same way.

Refine later: JSON Feed (feedparser reads RSS and Atom only); an option to capture each post's whole page, for feeds
that carry only a summary; podcast feeds bringing in each episode's audio to transcribe; OPML import of a feed list;
subscribing in one step (address and namespace) from Settings.

### Gmail and Google Workspace

A Gmail or Google Workspace mailbox (type `gmail`) is read through Google's Gmail API, read only (`gmail.readonly`),
Expand Down
1 change: 1 addition & 0 deletions docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ request, to another check that proves the same thing; loosening a requirement or
| A watched folder on each kind of storage connection, run against rclone in CI | No check yet ([#486](https://github.com/adeelahmad/lense/issues/486)) |
| IMAP mail, Gmail and Google Workspace | [test_mail_calendar_sources.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_mail_calendar_sources.py) `test_an_imap_account_as_a_source`, `test_an_imap_message_is_an_email_document_with_its_attachments`; [test_gmail_source.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_gmail_source.py) `test_a_workspace_mailbox_through_the_gmail_api`, `test_a_connected_google_account` |
| iCal calendars and Google Calendar | [test_mail_calendar_sources.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_mail_calendar_sources.py) `test_a_calendar_feed_as_a_source`; [test_google_calendar_source.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_google_calendar_source.py) `test_a_shared_calendar_read_as_a_service_account`, `test_a_connected_google_account` |
| RSS and Atom news feeds: a feed or a site's page as a source, each post imported as text with its link, author and date; private networks only when allowed | [test_news_feed_source.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_news_feed_source.py) `test_a_news_feed_as_a_source`, `test_feed_addresses`, `test_what_a_feed_holds_stays_data`, `test_a_feed_file_imported_as_text` |
| A built-in MQTT broker, syslog and webhook streams | [test_sensors.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_sensors.py) `test_hub_takes_devices_in_and_organises_them`, `test_syslog_senders_become_sensors`, `test_webhooks_and_one_list_of_sensors`; [journeys.mjs](https://github.com/adeelahmad/lense/blob/main/ci/clicks/journeys.mjs) `sensor-webhook`; [baseline.json](https://github.com/adeelahmad/lense/blob/main/ci/clicks/baseline.json) `sensor-webhook` ≤ 3 clicks |
| Import webhooks for scanners and phone shortcuts | [test_import_hooks.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_import_hooks.py) `test_pushing_files_urls_and_text`, `test_owners_manage_hooks`; [import-hooks.test.tsx](https://github.com/adeelahmad/lense/blob/main/nextjs-frontend/__tests__/import-hooks.test.tsx) |
| Connect with Google; Dropbox, OneDrive and Google Drive sign in through rclone on the server | [test_gmail_source.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_gmail_source.py) `test_connecting_the_account_in_the_browser`; [test_drive_google.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_drive_google.py) `test_drive_connects_with_google_for_rclone`; [test_rclone_signin.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_rclone_signin.py) `test_drive_signs_in_through_rclone_with_no_google_app`, `test_a_browser_on_the_server_reaches_rclone_itself`, `test_onedrive_finds_the_accounts_own_drive` |
Expand Down
2 changes: 1 addition & 1 deletion docs/sensors.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A **sensor** is anything that feeds Lens. There are two families, in one list (`

| Family | Kinds | Channels | What it reports becomes |
| --- | --- | --- | --- |
| Files | S3, Dropbox, Google Drive, OneDrive, SFTP, SMB, WebDAV, a local folder (through rclone); email (IMAP, Gmail and Google Workspace); calendar feeds (iCal); Google Calendar | the folders it watches | resources, run through their pipelines (as sources always have) |
| Files | S3, Dropbox, Google Drive, OneDrive, SFTP, SMB, WebDAV, a local folder (through rclone); email (IMAP, Gmail and Google Workspace); calendar feeds (iCal); Google Calendar; news feeds (RSS and Atom) | the folders it watches | resources, run through their pipelines (as sources always have) |
| Streams | MQTT devices, syslog senders, webhooks | streams: an MQTT topic, a syslog program, a webhook's stream name | readings, kept with retention, and hourly summaries of anything numeric |

File sensors are the storage sources: they are added and changed as sources (`/api/v1/sources`), and nothing about
Expand Down
2 changes: 1 addition & 1 deletion fastapi_backend/app/api/v1/routes/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

router = APIRouter(prefix="/import", tags=["imports"])

IMPORT_EXT = {".txt", ".md", ".markdown", ".mdx", ".docx", ".doc", ".pdf", ".json", ".jsonl", ".srt", ".vtt", ".eml", ".ics"}
IMPORT_EXT = {".txt", ".md", ".markdown", ".mdx", ".docx", ".doc", ".pdf", ".json", ".jsonl", ".srt", ".vtt", ".eml", ".ics", ".atom"}


def _upload_name(filename: str | None) -> str:
Expand Down
1 change: 1 addition & 0 deletions fastapi_backend/app/domain/content_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
("archive", "text", "Archive", "A zip or tar file, and the files in it", {"extensions": [".tar", ".tgz", ".zip"]}),
("email", "text", "Email", "A message, with its attachments", {"extensions": [".eml", ".msg"]}),
("calendar_event", "text", "Calendar event", "An invitation or a calendar entry", {"extensions": [".ics"]}),
("feed_post", "text", "Feed post", "A post from a news feed (RSS or Atom)", {"extensions": [".atom"]}),
] # fmt: skip


Expand Down
Loading
Loading
0