Files
org-social.el/org-social-lib.md
2026-04-18 16:02:30 +02:00

1711 lines
44 KiB
Markdown

# org-social-lib.el — API Reference
A hypothetical standalone Emacs Lisp library extracted from `org-social.el` that exposes the core logic for working with Org Social feeds. Other packages or configurations can `(require 'org-social-lib)` to parse feeds, create posts, validate files, interact with a relay, or sync with a host — without depending on the full org-social UI.
---
## Installation
```elisp
(use-package org-social-lib
:after org)
```
**Dependencies:** `emacs >= 30.1`, `org >= 9.0`, `async-http-queue >= 0.1`
---
## Table of Contents
- [Configuration Variables](#configuration-variables)
- [Parsing](#parsing)
- [Profile](#profile)
- [Post Creation](#post-creation)
- [Validation](#validation)
- [Feed Fetching](#feed-fetching)
- [Timeline](#timeline)
- [Notifications](#notifications)
- [Relay](#relay)
- [Host (vfile)](#host-vfile)
- [Utilities](#utilities)
- [Hooks](#hooks)
- [Data Structures Reference](#data-structures-reference)
---
## Configuration Variables
These variables configure the library's behavior. All have sensible defaults.
---
### `org-social-lib-file`
Path to the user's `social.org` file.
```elisp
(setq org-social-lib-file "~/social.org")
```
**Type:** `string` (file path or `http(s)://` URL for hosted files)
**Default:** `"~/social.org"`
---
### `org-social-lib-relay`
URL of the relay server. Set to `nil` to disable relay features.
```elisp
(setq org-social-lib-relay "https://relay.org-social.org")
```
**Type:** `string | nil`
**Default:** `"https://relay.org-social.org"`
---
### `org-social-lib-my-public-url`
The public HTTP URL where your `social.org` is accessible. Required for relay registration and notifications.
```elisp
(setq org-social-lib-my-public-url "https://example.com/social.org")
```
**Type:** `string | nil`
**Default:** `nil`
---
### `org-social-lib-max-post-age-days`
Only fetch posts newer than this many days. Set to `nil` to fetch all posts.
```elisp
(setq org-social-lib-max-post-age-days 14)
```
**Type:** `integer | nil`
**Default:** `14`
---
### `org-social-lib-max-concurrent-downloads`
Maximum number of feeds downloaded in parallel.
```elisp
(setq org-social-lib-max-concurrent-downloads 20)
```
**Type:** `integer`
**Default:** `20`
---
### `org-social-lib-language-filter`
List of BCP 47 language codes (2-5 lowercase letters, optionally followed by a subtag, e.g. `"en"`, `"pt-br"`, `"zh-hans"`). When set, only posts with a matching `:LANG:` property are included in the timeline. `nil` shows all posts.
```elisp
(setq org-social-lib-language-filter '("en" "es"))
```
**Type:** `(list string) | nil`
**Default:** `nil`
---
## Parsing
Functions for extracting structured data from the raw text of a `social.org` file. All parsing functions are pure: they take a string and return an alist.
---
### `org-social-lib-parse-feed`
```
(org-social-lib-parse-feed CONTENT) → profile-alist
```
Parses the full content of a `social.org` file. Returns a profile alist with all header metadata and a `posts` key containing the list of parsed posts.
**Arguments:**
- `CONTENT``string`. Raw text of a `social.org` file.
**Returns:** A [profile alist](#profile-alist).
**Example:**
```elisp
(let* ((content (with-temp-buffer
(insert-file-contents "~/social.org")
(buffer-string)))
(profile (org-social-lib-parse-feed content)))
(message "Nick: %s" (alist-get 'nick profile))
(message "Posts: %d" (length (alist-get 'posts profile))))
```
---
### `org-social-lib-parse-posts`
```
(org-social-lib-parse-posts CONTENT) → (list post-alist)
```
Extracts only the posts from a feed string, skipping profile headers. Useful when you only need to process posts.
**Arguments:**
- `CONTENT``string`. Raw text of a `social.org` file.
**Returns:** List of [post alists](#post-alist), ordered as they appear in the file (newest last unless manually sorted).
**Example:**
```elisp
(let ((posts (org-social-lib-parse-posts content)))
(dolist (post posts)
(message "[%s] %s"
(alist-get 'timestamp post)
(alist-get 'text post))))
```
---
### `org-social-lib-get-header`
```
(org-social-lib-get-header CONTENT KEYWORD) → string | (list string) | nil
```
Extracts a `#+KEYWORD:` value from a feed string. Returns `nil` if the keyword is not present. For keywords that can appear multiple times (`FOLLOW`, `GROUP`), returns a list of strings instead of a single string.
**Arguments:**
- `CONTENT``string`. Raw feed text.
- `KEYWORD``string`. Keyword name, e.g. `"NICK"`, `"TITLE"`, `"AVATAR"`.
**Returns:** `string | (list string) | nil`
**Example:**
```elisp
(org-social-lib-get-header content "NICK") ; => "andros"
(org-social-lib-get-header content "AVATAR") ; => "https://example.com/avatar.jpg"
(org-social-lib-get-header content "NONEXISTENT") ; => nil
```
---
### `org-social-lib-parse-follow-list`
```
(org-social-lib-parse-follow-list CONTENT) → (list follow-alist)
```
Extracts all `#+FOLLOW:` entries from a feed string.
**Returns:** List of alists with `'name` (optional) and `'url` keys.
**Example:**
```elisp
(dolist (follow (org-social-lib-parse-follow-list content))
(message "Following %s at %s"
(or (alist-get 'name follow) "unknown")
(alist-get 'url follow)))
```
---
### `org-social-lib-parse-timestamp`
```
(org-social-lib-parse-timestamp TIMESTAMP) → float-time | nil
```
Converts an RFC 3339 timestamp string to a float (Unix time). Returns `nil` if the string cannot be parsed.
**Arguments:**
- `TIMESTAMP``string`. RFC 3339 format, e.g. `"2025-03-10T09:00:00+01:00"`.
**Returns:** `float | nil`
**Example:**
```elisp
(org-social-lib-parse-timestamp "2025-03-10T09:00:00+01:00") ; => 1741597200.0
```
---
### `org-social-lib-extract-mentioned-urls`
```
(org-social-lib-extract-mentioned-urls TEXT) → (list string)
```
Finds all `[[org-social:URL][...]]` mention links in a post body and returns the list of feed URLs.
**Arguments:**
- `TEXT``string`. Post body text.
**Returns:** List of feed URL strings.
**Example:**
```elisp
(org-social-lib-extract-mentioned-urls
"Hey [[org-social:https://shom.dev/social.org][shom]], what do you think?")
; => ("https://shom.dev/social.org")
```
---
## Profile
High-level functions for reading and modifying your own `social.org` profile. All write operations persist changes to `org-social-lib-file` immediately and upload to the host if it is a vfile.
---
### `org-social-lib-get-my-profile`
```
(org-social-lib-get-my-profile) → profile-alist
```
Reads and parses your own `social.org` file. Returns the full [profile alist](#profile-alist) including the posts list.
This is a convenience wrapper around reading `org-social-lib-file` and calling `org-social-lib-parse-feed`. It always reads from disk, so the result reflects the current saved state.
**Returns:** [profile alist](#profile-alist)
**Example:**
```elisp
(let ((me (org-social-lib-get-my-profile)))
(message "You are @%s with %d posts"
(alist-get 'nick me)
(length (alist-get 'posts me))))
; => "You are @andros with 42 posts"
```
---
### `org-social-lib-update-profile`
```
(org-social-lib-update-profile &key nick title description avatar location
birthday language link) → t
```
Updates one or more profile header keywords in `org-social-lib-file`. Only the keywords you pass are modified; the rest remain unchanged. Saves the file after editing.
**Keyword Arguments:** All optional. Any keyword not provided is left as-is.
- `:nick``string`. New username (no spaces).
- `:title``string`. Feed title.
- `:description``string`. Bio.
- `:avatar``string`. URL to profile image (JPG or PNG).
- `:location``string`. Geographic location.
- `:birthday``string`. Birth date.
- `:language``string`. Default language code.
- `:link``string`. Personal website URI.
**Returns:** `t`
**Example:**
```elisp
(org-social-lib-update-profile
:description "Software developer and Emacs enthusiast."
:avatar "https://example.com/new-avatar.jpg")
; => t
```
---
### `org-social-lib-follow`
```
(org-social-lib-follow URL &optional nick) → t | error
```
Adds a `#+FOLLOW:` entry to `org-social-lib-file` and saves. Signals an error if `URL` is already in the follow list.
**Arguments:**
- `URL``string`. Feed URL to follow (`http(s)://...`).
- `NICK``string | nil`. Optional display name for the entry.
**Returns:** `t`
**Example:**
```elisp
(org-social-lib-follow "https://shom.dev/social.org" "shom")
; Adds: #+FOLLOW: shom https://shom.dev/social.org
; => t
(org-social-lib-follow "https://shom.dev/social.org")
; Adds: #+FOLLOW: https://shom.dev/social.org
; => t
(org-social-lib-follow "https://shom.dev/social.org")
; => error: "Already following https://shom.dev/social.org"
```
---
### `org-social-lib-unfollow`
```
(org-social-lib-unfollow URL) → t | error
```
Removes the `#+FOLLOW:` entry matching `URL` from `org-social-lib-file` and saves. Signals an error if the URL is not in the follow list.
**Arguments:**
- `URL``string`. Feed URL to remove.
**Returns:** `t`
**Example:**
```elisp
(org-social-lib-unfollow "https://shom.dev/social.org")
; Removes the matching #+FOLLOW: line
; => t
(org-social-lib-unfollow "https://unknown.example.com/social.org")
; => error: "Not following https://unknown.example.com/social.org"
```
---
### `org-social-lib-pin-post`
```
(org-social-lib-pin-post TIMESTAMP) → t
```
Sets (or replaces) the `#+PINNED:` header in `org-social-lib-file` to the given post timestamp. Saves the file.
**Arguments:**
- `TIMESTAMP``string`. RFC 3339 timestamp of the post to pin.
**Returns:** `t`
**Example:**
```elisp
(org-social-lib-pin-post "2025-03-10T09:00:00+01:00")
; Sets: #+PINNED: 2025-03-10T09:00:00+01:00
; => t
```
---
## Post Creation
Two levels of API are available:
- **High-level** (`org-social-lib-new-post`, `org-social-lib-new-reaction`, etc.): write directly to the configured `social.org` file and save. Suitable for most use cases.
- **Low-level** (`org-social-lib-new-post-template`, etc.): return the Org text as a string without touching any file. Useful when you manage the file yourself or need to preview before writing.
---
### `org-social-lib-new-post`
```
(org-social-lib-new-post &key reply-url reply-id group visibility lang content) → string
```
Appends a new post to `org-social-lib-file`, saves the file, and returns the generated post URL (`feed-url#timestamp`).
If `org-social-lib-file` is a vfile, the file is uploaded to the host after saving.
**Keyword Arguments:**
- `:reply-url``string | nil`. Feed URL of the post being replied to.
- `:reply-id``string | nil`. Timestamp ID of the post being replied to.
- `:group``string | nil`. Group entry in the form `"Group Name https://relay.url"`.
- `:visibility``"mention" | nil`. Sets `:VISIBILITY: mention`.
- `:lang``string | nil`. Language code. Defaults to `org-social-lib-default-lang`.
- `:content``string | nil`. Post body text. If `nil`, opens the file and positions the cursor inside the new post for interactive editing.
**Returns:** `string` — the new post URL (`feed-url#timestamp`).
**Example:**
```elisp
;; Non-interactive: post with content
(org-social-lib-new-post :content "Hello from org-social-lib!"
:lang "en")
; => "https://andros.dev/social.org#2025-03-10T09:00:00+01:00"
;; Interactive: opens buffer and positions cursor for typing
(org-social-lib-new-post)
;; Reply
(org-social-lib-new-post :reply-url "https://shom.dev/social.org"
:reply-id "2025-03-09T18:00:00+01:00"
:content "Totally agree!")
;; Mention-only post
(org-social-lib-new-post :visibility "mention"
:content (format "Hey %s, check this out."
(org-social-lib-mention-link
"https://shom.dev/social.org"
"shom")))
```
---
### `org-social-lib-new-reaction`
```
(org-social-lib-new-reaction REPLY-URL REPLY-ID MOOD) → string
```
Appends a reaction post (emoji, no body) to `org-social-lib-file`, saves, and returns the new post URL.
**Arguments:**
- `REPLY-URL``string`. Feed URL of the post being reacted to.
- `REPLY-ID``string`. Timestamp ID of the post being reacted to.
- `MOOD``string`. Emoji or short text (< 200 chars).
**Returns:** `string` — the new post URL.
**Example:**
```elisp
(org-social-lib-new-reaction "https://tanrax.com/social.org"
"2025-03-10T08:00:00+01:00"
"❤️")
```
---
### `org-social-lib-new-boost`
```
(org-social-lib-new-boost POST-URL &optional comment) → string
```
Appends a boost (reshare) post to `org-social-lib-file` and saves.
**Arguments:**
- `POST-URL``string`. Full post URL (`feed-url#timestamp`).
- `COMMENT``string | nil`. Optional quote comment.
**Returns:** `string` — the new post URL.
**Example:**
```elisp
(org-social-lib-new-boost
"https://tanrax.com/social.org#2025-03-10T08:00:00+01:00"
"This is worth reading.")
```
---
### `org-social-lib-new-poll`
```
(org-social-lib-new-poll QUESTION OPTIONS DAYS &key lang) → string
```
Appends a poll post to `org-social-lib-file` and saves.
**Arguments:**
- `QUESTION``string`. The poll question.
- `OPTIONS``(list string)`. List of option labels.
- `DAYS``integer`. Days until the poll closes.
- `:lang``string | nil`.
**Returns:** `string` — the new post URL.
**Example:**
```elisp
(org-social-lib-new-poll "Favorite Emacs feature?"
'("Org Mode" "Magit" "LSP" "The extensibility")
7
:lang "en")
```
---
### `org-social-lib-vote-poll`
```
(org-social-lib-vote-poll POST-URL OPTION) → string | error
```
Submits a vote on a poll by appending a poll-vote post to `org-social-lib-file` and saving. Validates that `OPTION` exists in the original poll before writing.
**Arguments:**
- `POST-URL``string`. Full URL of the poll post (`feed-url#timestamp`).
- `OPTION``string`. The option text exactly as it appears in the poll.
**Returns:** `string` — the new vote post URL. Signals an error if the option does not exist in the poll or the poll has already closed.
**Example:**
```elisp
(org-social-lib-vote-poll
"https://andros.dev/social.org#2025-03-10T12:00:00+01:00"
"Org Mode")
; => "https://me.example.com/social.org#2025-03-10T12:05:00+01:00"
(org-social-lib-vote-poll
"https://andros.dev/social.org#2025-03-10T12:00:00+01:00"
"Nonexistent option")
; => error: "Option not found in poll"
```
---
### `org-social-lib-delete-post`
```
(org-social-lib-delete-post TIMESTAMP) → t | error
```
Removes the post with the given timestamp from `org-social-lib-file` and saves. Signals an error if no post with that timestamp is found.
**Arguments:**
- `TIMESTAMP``string`. RFC 3339 timestamp identifying the post.
**Returns:** `t`
**Example:**
```elisp
(org-social-lib-delete-post "2025-03-10T09:00:00+01:00")
; => t
(org-social-lib-delete-post "1999-01-01T00:00:00+00:00")
; => error: "Post not found: 1999-01-01T00:00:00+00:00"
```
---
### `org-social-lib-new-migration`
```
(org-social-lib-new-migration OLD-URL NEW-URL) → string
```
Creates a migration post in `org-social-lib-file` and saves. This is the high-level wrapper: it writes the post, updates any self-referential `#+FOLLOW:` entries that point to `OLD-URL`, and returns the new post URL.
**Arguments:**
- `OLD-URL``string`. Your previous `social.org` public URL.
- `NEW-URL``string`. Your new `social.org` public URL.
**Returns:** `string` — the new migration post URL.
**Example:**
```elisp
(org-social-lib-new-migration
"https://old-server.com/social.org"
"https://new-server.com/social.org")
; => "https://new-server.com/social.org#2025-03-10T15:00:00+01:00"
```
---
### Low-level template functions
The following functions return the Org text as a string **without writing to any file**. Use them when you need to preview, transform, or manage file writes yourself.
---
### `org-social-lib-generate-timestamp`
```
(org-social-lib-generate-timestamp) → string
```
Generates a new RFC 3339 timestamp for use as a post ID.
**Returns:** `string` — e.g. `"2025-03-10T09:00:00+01:00"`
**Example:**
```elisp
(org-social-lib-generate-timestamp) ; => "2025-03-10T09:00:00+01:00"
```
---
### `org-social-lib-new-post-template`
```
(org-social-lib-new-post-template &key reply-url reply-id group visibility lang) → string
```
Returns the Org text for a new post, ready to be inserted at the end of the `* Posts` section.
**Keyword Arguments:**
- `:reply-url``string | nil`. Feed URL of the post being replied to.
- `:reply-id``string | nil`. Timestamp ID of the post being replied to.
- `:group``string | nil`. Group entry in the form `"Group Name https://relay.url"`.
- `:visibility``"mention" | nil`. Sets `:VISIBILITY: mention`. Defaults to no visibility property.
- `:lang``string | nil`. Language code. Defaults to `org-social-lib-default-lang`.
**Returns:** `string` — Org Mode text block.
**Example:**
```elisp
;; Simple post
(insert (org-social-lib-new-post-template))
;; Reply
(insert (org-social-lib-new-post-template
:reply-url "https://shom.dev/social.org"
:reply-id "2025-03-09T18:00:00+01:00"))
;; Mention-only post
(insert (org-social-lib-new-post-template :visibility "mention" :lang "es"))
```
Generated output example:
```org
** 2025-03-10T09:00:00+01:00
:PROPERTIES:
:LANG: en
:TAGS:
:CLIENT: my-package
:REPLY_TO: https://shom.dev/social.org#2025-03-09T18:00:00+01:00
:END:
```
---
### `org-social-lib-new-reaction-template`
```
(org-social-lib-new-reaction-template REPLY-URL REPLY-ID MOOD) → string
```
Returns the Org text for a reaction post (emoji response with no body).
**Arguments:**
- `REPLY-URL``string`. Feed URL of the post being reacted to.
- `REPLY-ID``string`. Timestamp ID of the post being reacted to.
- `MOOD``string`. Emoji or short text (< 200 chars).
**Returns:** `string`
**Example:**
```elisp
(insert (org-social-lib-new-reaction-template
"https://tanrax.com/social.org"
"2025-03-10T08:00:00+01:00"
"👍"))
```
---
### `org-social-lib-new-boost-template`
```
(org-social-lib-new-boost-template POST-URL &optional comment) → string
```
Returns the Org text for a boost (reshare), optionally with a quote comment.
**Arguments:**
- `POST-URL``string`. Full post URL (`feed-url#timestamp`).
- `COMMENT``string | nil`. Optional text accompanying the boost.
**Returns:** `string`
**Example:**
```elisp
;; Simple boost
(insert (org-social-lib-new-boost-template
"https://tanrax.com/social.org#2025-03-10T08:00:00+01:00"))
;; Quote boost
(insert (org-social-lib-new-boost-template
"https://tanrax.com/social.org#2025-03-10T08:00:00+01:00"
"This is worth reading."))
```
---
### `org-social-lib-new-poll-template`
```
(org-social-lib-new-poll-template QUESTION OPTIONS DAYS &key lang) → string
```
Returns the Org text for a poll post.
**Arguments:**
- `QUESTION``string`. The poll question (used as the post body header).
- `OPTIONS``(list string)`. List of option labels.
- `DAYS``integer`. How many days until the poll closes.
- `:lang``string | nil`. Language code.
**Returns:** `string`
**Example:**
```elisp
(insert (org-social-lib-new-poll-template
"Favorite Emacs feature?"
'("Org Mode" "Magit" "LSP" "The extensibility")
7
:lang "en"))
```
Generated output:
```org
** 2025-03-10T12:00:00+01:00
:PROPERTIES:
:LANG: en
:POLL_END: 2025-03-17T12:00:00+01:00
:END:
Favorite Emacs feature?
- [ ] Org Mode
- [ ] Magit
- [ ] LSP
- [ ] The extensibility
```
---
### `org-social-lib-new-migration-template`
```
(org-social-lib-new-migration-template OLD-URL NEW-URL) → string
```
Returns the Org text for an account migration post.
**Arguments:**
- `OLD-URL``string`. The old feed URL.
- `NEW-URL``string`. The new feed URL.
**Returns:** `string`
**Example:**
```elisp
(insert (org-social-lib-new-migration-template
"https://old-server.com/social.org"
"https://new-server.com/social.org"))
```
---
## Validation
Functions for validating a `social.org` buffer or string. Validators return structured error lists rather than signaling errors, so callers can decide how to surface them.
---
### `org-social-lib-validate-buffer`
```
(org-social-lib-validate-buffer) → (list error-plist)
```
Validates the current buffer as a `social.org` file. Must be called with the target buffer current.
**Returns:** List of error plists, each with `:line`, `:column`, `:message`, `:suggestion`, and `:context` keys. Returns `nil` if valid.
**Example:**
```elisp
(with-current-buffer (find-file-noselect "~/social.org")
(let ((errors (org-social-lib-validate-buffer)))
(if errors
(dolist (err errors)
(message "Line %d: %s"
(plist-get err :line)
(plist-get err :message)))
(message "File is valid."))))
```
---
### `org-social-lib-validate-string`
```
(org-social-lib-validate-string CONTENT) → (list error-plist)
```
Validates a `social.org` string without requiring a buffer visit.
**Arguments:**
- `CONTENT``string`. Raw feed content.
**Returns:** List of error plists (same format as `org-social-lib-validate-buffer`) or `nil`.
**Example:**
```elisp
(let ((errors (org-social-lib-validate-string my-feed-string)))
(unless errors
(message "Feed is valid")))
```
---
### `org-social-lib-validate-post`
```
(org-social-lib-validate-post POST) → (list error-plist) | nil
```
Validates a single post alist against the specification rules.
**Arguments:**
- `POST` — [post alist](#post-alist).
**Returns:** List of error plists (same format as `org-social-lib-validate-buffer`), or `nil` if valid.
**Example:**
```elisp
(let* ((posts (org-social-lib-parse-posts content))
(first-post (car posts))
(errors (org-social-lib-validate-post first-post)))
(when errors
(message "Invalid post: %s" (car errors))))
```
---
## Feed Fetching
Asynchronous functions for downloading and parsing remote feeds. All network calls are non-blocking.
---
### `org-social-lib-fetch-feed`
```
(org-social-lib-fetch-feed URL CALLBACK &key timeout filter-date)
```
Fetches a single feed asynchronously and calls `CALLBACK` with the parsed content.
**Arguments:**
- `URL``string`. The feed URL.
- `CALLBACK``function`. Called with `(content)` on success, `nil` on failure.
- `:timeout``integer`. Seconds before giving up. Default: `5`.
- `:filter-date``string | nil`. RFC 3339 date: only include posts on or after this date.
**Example:**
```elisp
(org-social-lib-fetch-feed
"https://shom.dev/social.org"
(lambda (content)
(if content
(let ((profile (org-social-lib-parse-feed content)))
(message "Fetched %d posts from %s"
(length (alist-get 'posts profile))
(alist-get 'nick profile)))
(message "Failed to fetch feed"))))
```
---
### `org-social-lib-fetch-feeds`
```
(org-social-lib-fetch-feeds URLS CALLBACK &key max-concurrent timeout filter-date)
```
Fetches multiple feeds in parallel. Calls `CALLBACK` once when all downloads complete.
**Arguments:**
- `URLS``(list string)`. List of feed URLs.
- `CALLBACK``function`. Called with a list of `(url . content)` pairs. `content` is `nil` for failed downloads.
- `:max-concurrent``integer`. Parallel download limit. Default: `org-social-lib-max-concurrent-downloads`.
- `:timeout``integer`. Per-feed timeout in seconds. Default: `5`.
- `:filter-date``string | nil`. Only include posts on or after this date.
**Example:**
```elisp
(org-social-lib-fetch-feeds
'("https://shom.dev/social.org"
"https://tanrax.com/social.org"
"https://notxor.nueva-actitud.org/social.org")
(lambda (results)
(dolist (pair results)
(let ((url (car pair))
(content (cdr pair)))
(if content
(message "OK: %s" url)
(message "FAILED: %s" url))))))
```
---
### `org-social-lib-fetch-feeds-from-profile`
```
(org-social-lib-fetch-feeds-from-profile PROFILE CALLBACK &key max-concurrent timeout)
```
Convenience wrapper that reads the follow list from a [profile alist](#profile-alist) and fetches all followed feeds.
**Arguments:**
- `PROFILE` — profile alist. Must include a `'follow` key.
- `CALLBACK``function`. Called with a list of `(url . content)` pairs.
**Example:**
```elisp
(let ((my-profile (org-social-lib-parse-feed
(with-temp-buffer
(insert-file-contents org-social-lib-file)
(buffer-string)))))
(org-social-lib-fetch-feeds-from-profile
my-profile
(lambda (results)
(message "Downloaded %d feeds" (length results)))))
```
---
## Timeline
Functions for assembling and filtering a unified timeline from multiple feeds.
---
### `org-social-lib-get-timeline`
```
(org-social-lib-get-timeline CALLBACK &key max-concurrent filter-date languages
exclude-reactions)
```
High-level entry point. Reads your follow list from `org-social-lib-file`, downloads all followed feeds in parallel, assembles and filters the timeline, and calls `CALLBACK` with the result. Equivalent to calling `get-my-profile``fetch-feeds-from-profile``build-timeline``filter-timeline` in sequence.
**Keyword Arguments:**
- `:max-concurrent``integer`. Parallel download limit. Default: `org-social-lib-max-concurrent-downloads`.
- `:filter-date``string | nil`. RFC 3339 cutoff date. Default: computed from `org-social-lib-max-post-age-days`.
- `:languages``(list string) | nil`. Language filter. Default: `org-social-lib-language-filter`.
- `:exclude-reactions``boolean`. Strip pure emoji reactions. Default: `t`.
**CALLBACK** receives `(timeline)` — a sorted list of [post alists](#post-alist).
**Example:**
```elisp
(org-social-lib-get-timeline
(lambda (timeline)
(message "Timeline ready: %d posts" (length timeline))
(let ((first (car timeline)))
(message "Latest post by @%s: %s"
(alist-get 'author-nick first)
(alist-get 'text first)))))
; => "Timeline ready: 87 posts"
; => "Latest post by @shom: Just discovered org-roam v2..."
```
---
### `org-social-lib-build-timeline`
```
(org-social-lib-build-timeline FEEDS) → (list post-alist)
```
Merges posts from multiple feed results into a single sorted timeline. Applies visibility rules.
**Arguments:**
- `FEEDS` — list of `(url . content)` pairs, as returned by `org-social-lib-fetch-feeds`.
**Returns:** List of [post alists](#post-alist), sorted by date descending, with author metadata attached (`'author-nick`, `'author-url`, `'author-avatar`).
**Example:**
```elisp
(org-social-lib-fetch-feeds
my-follow-urls
(lambda (results)
(let ((timeline (org-social-lib-build-timeline results)))
(message "Timeline has %d posts" (length timeline)))))
```
---
### `org-social-lib-filter-timeline`
```
(org-social-lib-filter-timeline TIMELINE &key exclude-reactions exclude-groups
languages my-url)
```
Applies filters to a timeline list.
**Keyword Arguments:**
- `:exclude-reactions``boolean`. Remove posts that are pure emoji reactions. Default: `t`.
- `:exclude-groups``boolean`. Remove group posts. Default: `nil`.
- `:languages``(list string) | nil`. Keep only posts with a matching `:LANG:` property.
- `:my-url``string | nil`. Your feed URL. Required to evaluate `VISIBILITY: mention` posts.
**Returns:** Filtered list of post alists.
**Example:**
```elisp
(let ((visible-posts
(org-social-lib-filter-timeline
timeline
:exclude-reactions t
:languages '("en" "es")
:my-url "https://andros.dev/social.org")))
(message "%d posts after filtering" (length visible-posts)))
```
---
### `org-social-lib-find-mentions`
```
(org-social-lib-find-mentions TIMELINE MY-NICK MY-URL) → (list post-alist)
```
Scans a timeline and returns posts that mention the given user.
**Arguments:**
- `TIMELINE` — list of post alists.
- `MY-NICK``string`. Your username.
- `MY-URL``string`. Your feed URL.
**Returns:** List of post alists where the body contains `[[org-social:MY-URL][...]]`.
**Example:**
```elisp
(let ((mentions (org-social-lib-find-mentions timeline "andros" my-url)))
(message "You have %d mentions" (length mentions)))
```
---
### `org-social-lib-find-replies`
```
(org-social-lib-find-replies TIMELINE MY-URL) → (list post-alist)
```
Returns posts from the timeline that are replies to any of your posts.
**Arguments:**
- `TIMELINE` — list of post alists.
- `MY-URL``string`. Your feed URL.
**Returns:** List of post alists with `(alist-get 'reply_to post)` matching `MY-URL#...`.
---
## Notifications
High-level functions that aggregate notification data from multiple sources (local timeline scan + relay) into a single result.
---
### `org-social-lib-get-notifications`
```
(org-social-lib-get-notifications CALLBACK &key sources)
```
Collects all notifications for the current user and calls `CALLBACK` with the combined, date-sorted list. By default it queries both the local timeline and the relay.
**Keyword Arguments:**
- `:sources``(list symbol) | nil`. Which sources to query. Accepted values: `'local` (scan downloaded timeline), `'relay` (query relay mentions/replies endpoint). Default: `'(local relay)`.
**CALLBACK** receives `(notifications)` — a list of notification alists sorted by date descending.
Each notification alist has:
- `'type``'mention`, `'reply`, `'reaction`, `'active-poll`, or `'poll-result`
- `'author``string`. Nick of the person who triggered the notification.
- `'author-url``string`. Feed URL of that person.
- `'timestamp``string`. RFC 3339 timestamp of the source post.
- `'post-url``string`. Full URL of the source post.
- `'text``string | nil`. Preview of the post text.
**Example:**
```elisp
(org-social-lib-get-notifications
(lambda (notifications)
(message "You have %d notifications" (length notifications))
(dolist (n notifications)
(pcase (alist-get 'type n)
('mention (message "@%s mentioned you" (alist-get 'author n)))
('reply (message "@%s replied to your post" (alist-get 'author n)))
('reaction (message "@%s reacted to your post" (alist-get 'author n))))))
:sources '(local relay))
; => "You have 5 notifications"
; => "@shom mentioned you"
; => "@tanrax replied to your post"
```
---
### `org-social-lib-get-thread`
```
(org-social-lib-get-thread POST-URL CALLBACK)
```
Fetches the full thread for a post: retrieves all replies from the relay, then fetches the source feed for each reply to get the full post data. Calls `CALLBACK` with the assembled thread.
**Arguments:**
- `POST-URL``string`. Full post URL (`feed-url#timestamp`).
- `CALLBACK``function`. Called with `(thread)` — a list of [post alists](#post-alist) sorted by date ascending (chronological order), not including the original post itself.
**Example:**
```elisp
(org-social-lib-get-thread
"https://andros.dev/social.org#2025-03-10T09:00:00+01:00"
(lambda (thread)
(message "Thread has %d replies" (length thread))
(dolist (reply thread)
(message " @%s: %s"
(alist-get 'author-nick reply)
(alist-get 'text reply)))))
; => "Thread has 3 replies"
; => " @shom: Great point!"
; => " @tanrax: Totally agree."
; => " @user3: Thanks for sharing."
```
---
## Relay
All relay functions are asynchronous and use a callback pattern. They discover relay endpoints dynamically, so no hardcoded paths are needed.
---
### `org-social-lib-relay-register`
```
(org-social-lib-relay-register RELAY-URL FEED-URL &optional callback)
```
Registers a feed with the relay server.
**Arguments:**
- `RELAY-URL``string`. Relay base URL.
- `FEED-URL``string`. Your `social.org` public URL.
- `CALLBACK``function | nil`. Called with `t` on success, `nil` on failure.
**Example:**
```elisp
(org-social-lib-relay-register
"https://relay.org-social.org"
"https://andros.dev/social.org"
(lambda (ok)
(message (if ok "Registered!" "Registration failed."))))
```
---
### `org-social-lib-relay-fetch-feeds`
```
(org-social-lib-relay-fetch-feeds RELAY-URL CALLBACK)
```
Fetches the list of all feeds known to the relay.
**Arguments:**
- `RELAY-URL``string`.
- `CALLBACK``function`. Called with `(list string)` of feed URLs, or `nil` on failure.
**Example:**
```elisp
(org-social-lib-relay-fetch-feeds
"https://relay.org-social.org"
(lambda (feeds)
(message "Relay knows %d feeds" (length feeds))))
```
---
### `org-social-lib-relay-fetch-mentions`
```
(org-social-lib-relay-fetch-mentions RELAY-URL FEED-URL CALLBACK)
```
Fetches post URLs that mention the given feed.
**Arguments:**
- `RELAY-URL``string`.
- `FEED-URL``string`. Your public feed URL.
- `CALLBACK``function`. Called with `(list string)` of post URLs (`feed-url#timestamp`), or `nil`.
**Example:**
```elisp
(org-social-lib-relay-fetch-mentions
"https://relay.org-social.org"
"https://andros.dev/social.org"
(lambda (post-urls)
(dolist (url post-urls)
(message "Mentioned in: %s" url))))
```
---
### `org-social-lib-relay-fetch-replies`
```
(org-social-lib-relay-fetch-replies RELAY-URL POST-URL CALLBACK)
```
Fetches all replies to a specific post.
**Arguments:**
- `RELAY-URL``string`.
- `POST-URL``string`. Full post URL (`feed-url#timestamp`).
- `CALLBACK``function`. Called with `(list post-alist)` or `nil`.
**Example:**
```elisp
(org-social-lib-relay-fetch-replies
"https://relay.org-social.org"
"https://andros.dev/social.org#2025-03-10T09:00:00+01:00"
(lambda (replies)
(message "This post has %d replies" (length replies))))
```
---
### `org-social-lib-relay-fetch-interactions`
```
(org-social-lib-relay-fetch-interactions RELAY-URL POST-URL CALLBACK)
```
Fetches all interactions (reactions, replies, boosts) for a post.
**Arguments:**
- `RELAY-URL``string`.
- `POST-URL``string`.
- `CALLBACK``function`. Called with an alist with `'replies`, `'reactions`, and `'boosts` keys, or `nil`.
**Example:**
```elisp
(org-social-lib-relay-fetch-interactions
"https://relay.org-social.org"
my-post-url
(lambda (data)
(when data
(message "Reactions: %d, Replies: %d, Boosts: %d"
(length (alist-get 'reactions data))
(length (alist-get 'replies data))
(length (alist-get 'boosts data))))))
```
---
### `org-social-lib-relay-search`
```
(org-social-lib-relay-search RELAY-URL QUERY CALLBACK &key type page per-page)
```
Searches posts indexed by the relay.
**Arguments:**
- `RELAY-URL``string`.
- `QUERY``string`. Search term.
- `CALLBACK``function`. Called with `(results meta)`. `results` is a list of post alists; `meta` is an alist with `'page`, `'per-page`, `'total`.
- `:type``'tag | nil`. Set to `'tag` to search by hashtag instead of full text.
- `:page``integer`. Page number. Default: `1`.
- `:per-page``integer`. Results per page. Default: `10`.
**Example:**
```elisp
;; Full-text search
(org-social-lib-relay-search
"https://relay.org-social.org"
"emacs org-mode"
(lambda (results meta)
(message "Found %d results (page %d of %d)"
(length results)
(alist-get 'page meta)
(/ (alist-get 'total meta) (alist-get 'per-page meta)))))
;; Hashtag search
(org-social-lib-relay-search
"https://relay.org-social.org"
"emacs"
#'my-display-results
:type 'tag
:per-page 20)
```
---
### `org-social-lib-relay-fetch-groups`
```
(org-social-lib-relay-fetch-groups RELAY-URL CALLBACK)
```
Fetches the list of groups available on the relay.
**Arguments:**
- `RELAY-URL``string`.
- `CALLBACK``function`. Called with a list of group alists. Each has `'name`, `'href`, `'method`, and `'relay-url` keys.
**Example:**
```elisp
(org-social-lib-relay-fetch-groups
"https://relay.org-social.org"
(lambda (groups)
(dolist (g groups)
(message "Group: %s" (alist-get 'name g)))))
```
---
### `org-social-lib-relay-fetch-group-posts`
```
(org-social-lib-relay-fetch-group-posts GROUP CALLBACK)
```
Fetches posts and member list for a specific group.
**Arguments:**
- `GROUP` — group alist as returned by `org-social-lib-relay-fetch-groups`.
- `CALLBACK``function`. Called with an alist containing `'posts` and `'members` keys.
**Example:**
```elisp
(org-social-lib-relay-fetch-groups
"https://relay.org-social.org"
(lambda (groups)
(when groups
(org-social-lib-relay-fetch-group-posts
(car groups)
(lambda (data)
(message "%d posts, %d members"
(length (alist-get 'posts data))
(length (alist-get 'members data))))))))
```
---
### `org-social-lib-relay-fetch-poll-votes`
```
(org-social-lib-relay-fetch-poll-votes RELAY-URL POST-URL CALLBACK)
```
Fetches the vote counts for a poll post.
**Arguments:**
- `RELAY-URL``string`.
- `POST-URL``string`. Full URL of the poll post.
- `CALLBACK``function`. Called with a list of alists, each with `'option` and `'votes` keys. `'votes` is a list of voter feed URLs.
**Example:**
```elisp
(org-social-lib-relay-fetch-poll-votes
"https://relay.org-social.org"
"https://andros.dev/social.org#2025-03-10T12:00:00+01:00"
(lambda (votes)
(dolist (option votes)
(message "%s: %d votes"
(alist-get 'option option)
(length (alist-get 'votes option))))))
```
---
## Host (vfile)
Functions for working with remotely hosted `social.org` files. A **vfile** is any `social.org` whose path starts with `http(s)://`.
---
### `org-social-lib-vfile-p`
```
(org-social-lib-vfile-p PATH) → boolean
```
Returns `t` if `PATH` is a remote (vfile) path.
**Example:**
```elisp
(org-social-lib-vfile-p "~/social.org") ; => nil
(org-social-lib-vfile-p "https://host.example.com/social.org") ; => t
```
---
### `org-social-lib-download-vfile`
```
(org-social-lib-download-vfile VFILE-URL CALLBACK)
```
Downloads a hosted `social.org` to the local cache asynchronously.
**Arguments:**
- `VFILE-URL``string`. The hosted file URL.
- `CALLBACK``function`. Called with the local cache path on success, `nil` on failure.
**Example:**
```elisp
(org-social-lib-download-vfile
"https://host.example.com/social.org"
(lambda (local-path)
(if local-path
(message "Cached at %s" local-path)
(message "Download failed"))))
```
---
### `org-social-lib-upload-vfile`
```
(org-social-lib-upload-vfile VFILE-URL LOCAL-FILE &optional callback)
```
Uploads the local file to the host server. Used after saving changes.
**Arguments:**
- `VFILE-URL``string`. The hosted file URL (used to determine the host).
- `LOCAL-FILE``string`. Path to the local file to upload.
- `CALLBACK``function | nil`. Called with `t` on success, `nil` on failure.
**Example:**
```elisp
(org-social-lib-upload-vfile
"https://host.example.com/social.org"
(org-social-lib-vfile-local-path "https://host.example.com/social.org")
(lambda (ok)
(message (if ok "Uploaded." "Upload failed."))))
```
---
### `org-social-lib-vfile-local-path`
```
(org-social-lib-vfile-local-path VFILE-URL) → string
```
Returns the local cache path for the active vfile. The path is derived from the active account name (from `org-social-accounts--current`), not from `VFILE-URL` — the URL argument is accepted for API consistency but is not used to compute the path.
**Arguments:**
- `VFILE-URL``string`. Accepted but not used to derive the path.
**Returns:** `string` — local file path inside `user-emacs-directory`.
**Example:**
```elisp
;; With no active account:
(org-social-lib-vfile-local-path "https://host.example.com/social.org")
; => "~/.emacs.d/v-social.org"
;; With account "work" active (org-social-accounts--current = "work"):
(org-social-lib-vfile-local-path "https://host.example.com/social.org")
; => "~/.emacs.d/v-social-work.org"
```
---
## Utilities
---
### `org-social-lib-post-url`
```
(org-social-lib-post-url FEED-URL TIMESTAMP) → string
```
Constructs a full post URL from a feed URL and a post timestamp.
**Example:**
```elisp
(org-social-lib-post-url "https://andros.dev/social.org"
"2025-03-10T09:00:00+01:00")
; => "https://andros.dev/social.org#2025-03-10T09:00:00+01:00"
```
---
### `org-social-lib-post-type`
```
(org-social-lib-post-type POST) → symbol
```
Infers the type of a post from its properties.
**Returns:** One of: `'post`, `'reply`, `'reaction`, `'boost`, `'poll`, `'poll-vote`, `'group`, `'migration`.
**Example:**
```elisp
(org-social-lib-post-type post) ; => 'reply
```
---
### `org-social-lib-mention-link`
```
(org-social-lib-mention-link FEED-URL NICK) → string
```
Returns an Org link for mentioning a user in post content.
**Example:**
```elisp
(org-social-lib-mention-link "https://shom.dev/social.org" "shom")
; => "[[org-social:https://shom.dev/social.org][shom]]"
```
---
## Hooks
---
### `org-social-lib-after-fetch-hook`
Abnormal hook run after all feeds in a batch have been downloaded. Called with a single argument: the list of `(url . content)` pairs.
```elisp
(add-hook 'org-social-lib-after-fetch-hook
(lambda (results)
(message "Fetched %d feeds" (length results))))
```
---
### `org-social-lib-after-save-hook`
Hook run after the local `social.org` file is saved. Useful for triggering an upload to a vfile host.
```elisp
(add-hook 'org-social-lib-after-save-hook
(lambda ()
(org-social-lib-upload-vfile
org-social-lib-file
(org-social-lib-vfile-local-path org-social-lib-file))))
```
---
## Data Structures Reference
### Profile Alist
```elisp
`((nick . "andros")
(title . "Andros Fenollosa")
(description . "Software developer.")
(avatar . "https://example.com/avatar.jpg")
(location . "Spain")
(birthday . "1990-01-15")
(language . "en")
(url . "https://andros.dev/social.org")
(pinned . "2025-03-10T09:00:00+01:00")
(follow . (((name . "shom") (url . "https://shom.dev/social.org"))
((name . nil) (url . "https://tanrax.com/social.org"))))
(group . (((name . "Emacs") (relay-url . "https://relay.org-social.org"))))
(posts . ( ... )))
```
### Post Alist
```elisp
`((timestamp . "2025-03-10T09:00:00+01:00")
(date . 1741597200.0) ; float-time, for sorting
(text . "Post content text.")
(lang . "en") ; nil if absent
(tags . "emacs org") ; nil if absent
(client . "org-social.el") ; nil if absent
(reply_to . nil) ; "url#timestamp" or nil
(mood . nil) ; emoji string or nil
(include . nil) ; "url#timestamp" or nil
(poll_end . nil) ; RFC 3339 string or nil
(poll_option . nil) ; string or nil
(group . nil) ; "Name relay-url" or nil
(visibility . nil) ; "public", "mention", or nil
(migration . nil) ; not extracted by the parser — requires custom handling
;; Added during timeline assembly:
(author-nick . "andros")
(author-url . "https://andros.dev/social.org")
(author-avatar . "https://example.com/avatar.jpg"))
```
### Follow Alist
```elisp
`((name . "shom") ; nil if not specified
(url . "https://shom.dev/social.org"))
```
### Group Alist
```elisp
`((name . "Emacs")
(relay-url . "https://relay.org-social.org"))
```
### Error Plist (from validation)
```elisp
(list :line 42
:column 5
:message "REPLY_TO must be in format URL#timestamp"
:suggestion "Use the format https://feed.url#2025-03-10T09:00:00+01:00"
:context "The line text where the error was found")
```
Access with `plist-get`: `(plist-get err :line)`, `(plist-get err :message)`, etc.