1329 lines
45 KiB
Markdown
1329 lines
45 KiB
Markdown
# Org Social Relay
|
|
|
|
## Introduction
|
|
|
|
Org Social Relay is a P2P system that acts as an intermediary between all [Org Social](https://git.andros.dev/org-social/org-social) files. It scans the network, creating an index of users, mentions, replies, groups and threads. This allows you to:
|
|
|
|
```mermaid
|
|
graph TD
|
|
Node1["🖥️ Relay"]
|
|
Node2["🖥️ Relay"]
|
|
Node3["🖥️ Relay"]
|
|
|
|
%% Social.org instances with icons
|
|
Social1_1["📄 social.org"]
|
|
Social1_2["📄 social.org"]
|
|
Social2_1["📄 social.org"]
|
|
Social2_2["📄 social.org"]
|
|
Social3_1["📄 social.org"]
|
|
Social3_2["📄 social.org"]
|
|
|
|
%% Node to social.org connections
|
|
Social1_1 -->|"⚓ Connects"| Node1
|
|
Social1_2 -->|"⚓ Connects"| Node1
|
|
Social2_1 -->|"⚓ Connects"| Node2
|
|
Social2_2 -->|"⚓ Connects"| Node2
|
|
Social3_1 -->|"⚓ Connects"| Node3
|
|
Social3_2 -->|"⚓ Connects"| Node3
|
|
|
|
%% Bidirectional connections between nodes
|
|
Node1 <-.->|"👥 Share Feeds"| Node2
|
|
Node2 <-.->|"👥 Share Feeds"| Node3
|
|
Node1 <-.->|"👥 Share Feeds"| Node3
|
|
|
|
%% Modern color scheme with gradients
|
|
classDef socialStyle fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff,font-weight:bold
|
|
classDef nodeStyle fill:#f093fb,stroke:#f5576c,stroke-width:3px,color:#fff,font-weight:bold
|
|
|
|
%% Apply styles
|
|
class Social1_1,Social1_2,Social2_1,Social2_2,Social3_1,Social3_2 socialStyle
|
|
class Node1,Node2,Node3 nodeStyle
|
|
```
|
|
[Source](/diagram.mmd)
|
|
|
|
- Receive mentions and replies.
|
|
- Have a more comprehensive notification system.
|
|
- Read or participate in threads.
|
|
- Perform searches (tags and full text).
|
|
- Participate in groups.
|
|
- See who boosted your posts.
|
|
- Notify external sites linked from posts via [Webmention](https://www.w3.org/TR/webmention/) (sending only; receiving them is not possible since plain text feeds cannot advertise an endpoint).
|
|
- Follow Mastodon/ActivityPub accounts and RSS/Atom feeds from any Org Social client through [bridges](#bridges-follow-activitypub-accounts-and-rss-feeds) (virtual social.org feeds).
|
|
|
|
## Concepts
|
|
|
|
- **Relay**: A server running Org Social Relay (this software). Each relay maintains a local `relay-list.txt` file with URLs of other relays. Relays scan the network and share feed information with each other, creating a decentralized P2P network.
|
|
- **Client**: An application that connects to a Relay to get information. It can be Org Social or any other application that implements the Org Social Relay API.
|
|
|
|
## Installation
|
|
|
|
You need to have Docker and Docker Compose installed.
|
|
|
|
### 1. Create a `.env` file based on `envExample`
|
|
|
|
```bash
|
|
cp envExample .env
|
|
```
|
|
|
|
### 2. Edit variables as needed
|
|
|
|
```bash
|
|
nano .env
|
|
```
|
|
|
|
#### Important Environment Variables
|
|
|
|
- **`GROUPS`**: Comma-separated list of group names available in the relay (optional)
|
|
- Example: `GROUPS=Emacs,Org Social,Elisp`
|
|
- Group names can have spaces and capital letters
|
|
- Slugs (for URLs) are generated automatically (lowercase, spaces become hyphens)
|
|
- Leave empty if no groups are needed
|
|
- Groups allow users to participate in topic-based discussions
|
|
|
|
### 3. Run with Docker Compose
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
## Managing Relay Nodes
|
|
|
|
### Local Relay List
|
|
|
|
This relay uses a local file (`relay-list.txt` in the project root) to manage the list of other relay nodes it connects to.
|
|
|
|
To add or remove relay nodes from your local instance:
|
|
|
|
```bash
|
|
# Edit the relay list file
|
|
nano relay-list.txt
|
|
|
|
# Add one relay URL per line, for example:
|
|
# https://relay.org-social.org
|
|
# https://other-relay.example.com
|
|
```
|
|
|
|
The relay will automatically discover feeds from these nodes every 3 hours.
|
|
|
|
### Make your Org Social Relay public
|
|
|
|
If you want your Relay to be discoverable by other relay nodes, you can make a Pull Request to add your relay URL to the `relay-list.txt` file in this repository:
|
|
|
|
**Steps to add your relay:**
|
|
|
|
1. Fork this repository
|
|
2. Edit `relay-list.txt` and add your relay URL on a new line:
|
|
```
|
|
https://your-relay.example.com
|
|
```
|
|
3. Create a Pull Request with your changes
|
|
4. Once merged, all relay nodes that update their code (via `git pull`) will automatically discover your relay
|
|
|
|
This way, the network of relays grows organically as new nodes are added to the shared list.
|
|
|
|
## Updating
|
|
|
|
To update your Org Social Relay to the latest version:
|
|
|
|
```bash
|
|
# Navigate to your installation directory
|
|
cd /path/to/org-social-relay
|
|
|
|
# Check if Docker containers are running
|
|
docker compose up -d --build
|
|
|
|
# Pull the latest changes
|
|
git pull
|
|
|
|
# Apply database migrations (if any)
|
|
docker compose exec django python manage.py migrate
|
|
|
|
# Restart the services
|
|
docker compose restart
|
|
```
|
|
|
|
## Endpoints for clients
|
|
|
|
### Important Note: URL Encoding
|
|
|
|
When passing URLs as query parameters (like `feed` or `post`), they **must be URL-encoded** to avoid conflicts with special characters like `#`, `?`, `&`, etc.
|
|
|
|
**Examples:**
|
|
- `https://example.com/social.org` → `https%3A%2F%2Fexample.com%2Fsocial.org`
|
|
- `https://foo.org/social.org#2025-02-03T23:05:00+0100` → `https%3A%2F%2Ffoo.org%2Fsocial.org%232025-02-03T23%3A05%3A00%2B0100`
|
|
|
|
You can use:
|
|
- Manual encoding: `curl "http://localhost:8080/endpoint/?param=encoded_url"`
|
|
- curl's automatic encoding: `curl -G "http://localhost:8080/endpoint/" --data-urlencode "param=unencoded_url"`
|
|
|
|
### HTTP Caching
|
|
|
|
All Relay endpoints that return data include HTTP caching headers:
|
|
|
|
- **`ETag`**: A unique identifier for the current state of the relay (e.g., `"a1b2c3d4"`). This value changes when the relay scans feeds for updates.
|
|
- **`Last-Modified`**: The timestamp when the relay last scanned feeds (e.g., `Wed, 01 Nov 2025 10:15:00 GMT`).
|
|
|
|
All endpoints return the same `ETag` and `Last-Modified` values, which represent the global state of the relay. These headers are updated by the periodic feed scanning task.
|
|
|
|
**Example:**
|
|
```sh
|
|
curl -i http://localhost:8080/mentions/?feed=https://example.com/social.org
|
|
|
|
# Response headers include:
|
|
# ETag: "abc123"
|
|
# Last-Modified: Wed, 01 Nov 2025 10:15:00 GMT
|
|
```
|
|
|
|
### CORS (Cross-Origin Resource Sharing)
|
|
|
|
All Relay endpoints have CORS enabled with `Access-Control-Allow-Origin: *`, allowing direct access from any frontend/web application. This means you can call the API directly from JavaScript in the browser without CORS restrictions.
|
|
|
|
**Example:**
|
|
```javascript
|
|
// Fetch notifications directly from the browser
|
|
fetch('http://localhost:8080/notifications/?feed=https://example.com/social.org')
|
|
.then(response => response.json())
|
|
.then(data => console.log(data));
|
|
```
|
|
|
|
### Root
|
|
|
|
`/` - Basic information about the relay.
|
|
|
|
```sh
|
|
curl http://localhost:8080/
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": {
|
|
"name": "Org Social Relay",
|
|
"description": "P2P system for Org Social files"
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/", "method": "GET"},
|
|
"feeds": {"href": "/feeds/", "method": "GET"},
|
|
"add-feed": {"href": "/feeds/", "method": "POST"},
|
|
"feed-content": {"href": "/feed-content/?feed={feed_url}", "method": "GET", "templated": true},
|
|
"notifications": {"href": "/notifications/?feed={feed_url}", "method": "GET", "templated": true},
|
|
"sse-notifications": {"href": "/sse/notifications/?feed={feed_url}", "method": "GET", "templated": true},
|
|
"sse-notifications-all": {"href": "/sse/notifications/", "method": "GET"},
|
|
"mentions": {"href": "/mentions/?feed={feed_url}", "method": "GET", "templated": true},
|
|
"reactions": {"href": "/reactions/?feed={feed_url}", "method": "GET", "templated": true},
|
|
"replies-to": {"href": "/replies-to/?feed={feed_url}", "method": "GET", "templated": true},
|
|
"boosts": {"href": "/boosts/?post={post_url}", "method": "GET", "templated": true},
|
|
"interactions": {"href": "/interactions/?post={post_url}", "method": "GET", "templated": true},
|
|
"replies": {"href": "/replies/?post={post_url}", "method": "GET", "templated": true},
|
|
"search": {"href": "/search/?q={query}", "method": "GET", "templated": true},
|
|
"groups": {"href": "/groups/", "method": "GET"},
|
|
"group-messages": {"href": "/groups/{group_slug}/", "method": "GET", "templated": true},
|
|
"polls": {"href": "/polls/", "method": "GET"},
|
|
"poll-votes": {"href": "/polls/votes/?post={post_url}", "method": "GET", "templated": true},
|
|
"profile": {"href": "/profile/?feed={feed_url}", "method": "GET", "templated": true},
|
|
"rss": {"href": "/rss.xml", "method": "GET", "description": "RSS feed of latest posts (supports ?tag={tag} and ?feed={feed_url} filters)"},
|
|
"bridge": {"href": "/bridge/", "method": "GET"},
|
|
"bridge-activitypub": {"href": "/bridge/activitypub/@{user}@{instance}/", "method": "GET", "templated": true},
|
|
"bridge-rss": {"href": "/bridge/rss/?url={feed_url}", "method": "GET", "templated": true}
|
|
}
|
|
}
|
|
```
|
|
|
|
### List feeds
|
|
|
|
`/feeds/` - List all registered feeds.
|
|
|
|
```sh
|
|
curl http://localhost:8080/feeds/
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
"https://example.com/social.org",
|
|
"https://another-example.com/social.org"
|
|
],
|
|
"_links": {
|
|
"self": {"href": "/feeds/", "method": "GET"},
|
|
"add": {"href": "/feeds/", "method": "POST"}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Add feed
|
|
|
|
`/feeds/` - Add a new feed to be scanned.
|
|
|
|
```sh
|
|
curl -X POST http://localhost:8080/feeds/ -d '{"feed": "https://example.com/path/to/your/file.org"}' -H "Content-Type: application/json"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": {
|
|
"feed": "https://example.com/path/to/your/file.org"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get notifications
|
|
|
|
`/notifications/?feed={url feed}` - Get all notifications (mentions, reactions, replies, and boosts) received by a given feed. Results are ordered from most recent to oldest.
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/notifications/?feed=https%3A%2F%2Fexample.com%2Fsocial.org"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/notifications/" --data-urlencode "feed=https://example.com/social.org"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
{
|
|
"type": "boost",
|
|
"post": "https://alice.org/social.org#2025-02-05T14:00:00+0100",
|
|
"boosted": "https://example.com/social.org#2025-02-05T10:00:00+0100"
|
|
},
|
|
{
|
|
"type": "reaction",
|
|
"post": "https://alice.org/social.org#2025-02-05T13:15:00+0100",
|
|
"emoji": "❤",
|
|
"parent": "https://example.com/social.org#2025-02-05T12:00:00+0100"
|
|
},
|
|
{
|
|
"type": "reply",
|
|
"post": "https://bob.org/social.org#2025-02-05T12:30:00+0100",
|
|
"parent": "https://example.com/social.org#2025-02-05T10:00:00+0100"
|
|
},
|
|
{
|
|
"type": "mention",
|
|
"post": "https://charlie.org/social.org#2025-02-05T11:20:00+0100"
|
|
},
|
|
{
|
|
"type": "reaction",
|
|
"post": "https://diana.org/social.org#2025-02-04T15:45:00+0100",
|
|
"emoji": "🚀",
|
|
"parent": "https://example.com/social.org#2025-02-04T10:00:00+0100"
|
|
}
|
|
],
|
|
"meta": {
|
|
"feed": "https://example.com/social.org",
|
|
"total": 5,
|
|
"by_type": {
|
|
"mentions": 1,
|
|
"reactions": 2,
|
|
"replies": 1,
|
|
"boosts": 1
|
|
}
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/notifications/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"},
|
|
"mentions": {"href": "/mentions/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"},
|
|
"reactions": {"href": "/reactions/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"},
|
|
"replies-to": {"href": "/replies-to/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
Each notification includes:
|
|
- `type`: The notification type (`"mention"`, `"reaction"`, `"reply"`, or `"boost"`)
|
|
- `post`: The notification post URL (format: `{author_feed}#{timestamp}`)
|
|
- `emoji`: (Only for reactions) The reaction emoji
|
|
- `parent`: (Only for reactions and replies) The post URL that received the notification
|
|
- `boosted`: (Only for boosts) The original post URL that was boosted
|
|
|
|
**Mentions** only have `type` and `post` because you are the one being mentioned in someone else's post.
|
|
|
|
**Reactions and replies** have `parent` to indicate which of your posts received the reaction/reply.
|
|
|
|
**Boosts** have `boosted` to indicate which of your posts was shared.
|
|
|
|
To extract the author's feed from the `post` field, simply take the part before the `#` character. For example, from `https://alice.org/social.org#2025-02-05T13:15:00+0100`, the author is `https://alice.org/social.org`.
|
|
|
|
The `by_type` breakdown in `meta` allows you to show notification counts per type in your UI.
|
|
|
|
**Optional parameters:**
|
|
- `type`: Filter by notification type (`mention`, `reaction`, `reply`, `boost`)
|
|
- Example: `/notifications/?feed={feed}&type=reaction`
|
|
- Example: `/notifications/?feed={feed}&type=boost`
|
|
|
|
### Real-time notifications (SSE)
|
|
|
|
`/sse/notifications/` - Subscribe to real-time notifications via Server-Sent Events. The `?feed=` parameter is optional:
|
|
|
|
- **With `?feed=`**: streams only notifications for that feed.
|
|
- **Without `?feed=`**: streams all notifications from all feeds (intended for backends such as push notification services).
|
|
|
|
```sh
|
|
# Per-feed stream
|
|
curl -N "http://localhost:8080/sse/notifications/?feed=https://example.com/social.org"
|
|
|
|
# Global stream (all feeds)
|
|
curl -N "http://localhost:8080/sse/notifications/"
|
|
```
|
|
|
|
**Events:**
|
|
|
|
- `connected` - Connection established
|
|
```
|
|
event: connected
|
|
data: {"feed": "https://example.com/social.org", "status": "connected"}
|
|
```
|
|
In the global stream the `feed` field is omitted:
|
|
```
|
|
event: connected
|
|
data: {"status": "connected"}
|
|
```
|
|
|
|
- `heartbeat` - Connection keepalive (every 30s)
|
|
```
|
|
event: heartbeat
|
|
data: {"status": "alive", "timestamp": 1733392800}
|
|
```
|
|
|
|
- `notification` - New notification received (per-feed, same structure as `/notifications/`)
|
|
```
|
|
event: notification
|
|
data: {"type": "mention", "post": "https://alice.org/social.org#2025-02-05T11:20:00+0100"}
|
|
```
|
|
```
|
|
event: notification
|
|
data: {"type": "reply", "post": "https://bob.org/social.org#...", "parent": "https://example.com/social.org#..."}
|
|
```
|
|
```
|
|
event: notification
|
|
data: {"type": "reaction", "post": "https://carol.org/social.org#...", "emoji": "❤", "parent": "https://example.com/social.org#..."}
|
|
```
|
|
```
|
|
event: notification
|
|
data: {"type": "boost", "post": "https://dave.org/social.org#...", "boosted": "https://example.com/social.org#..."}
|
|
```
|
|
|
|
In the global stream, each notification includes a `target_feed` field identifying which feed received it:
|
|
```
|
|
event: notification
|
|
data: {"target_feed": "https://example.com/social.org", "type": "mention", "post": "https://alice.org/social.org#2025-02-05T11:20:00+0100"}
|
|
```
|
|
|
|
### Get mentions
|
|
|
|
`/mentions/?feed={url feed}` - Get mentions for a given feed. Results are ordered from most recent to oldest.
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/mentions/?feed=https%3A%2F%2Fexample.com%2Fsocial.org"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/mentions/" --data-urlencode "feed=https://example.com/social.org"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
"https://foo.org/social.org#2025-02-03T23:05:00+0100",
|
|
"https://bar.org/social.org#2025-02-04T10:15:00+0100",
|
|
"https://baz.org/social.org#2025-02-05T08:30:00+0100"
|
|
],
|
|
"meta": {
|
|
"feed": "https://example.com/social.org",
|
|
"total": 3
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/mentions/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get reactions
|
|
|
|
`/reactions/?feed={url feed}` - Get all reactions received by posts from a given feed. A reaction is a special post with a `:MOOD:` property and `:REPLY_TO:` pointing to the reacted post. Results are ordered from most recent to oldest.
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/reactions/?feed=https%3A%2F%2Fexample.com%2Fsocial.org"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/reactions/" --data-urlencode "feed=https://example.com/social.org"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
{
|
|
"post": "https://alice.org/social.org#2025-02-05T13:15:00+0100",
|
|
"emoji": "❤",
|
|
"parent": "https://example.com/social.org#2025-02-05T12:00:00+0100"
|
|
},
|
|
{
|
|
"post": "https://bob.org/social.org#2025-02-05T14:30:00+0100",
|
|
"emoji": "🚀",
|
|
"parent": "https://example.com/social.org#2025-02-05T12:00:00+0100"
|
|
},
|
|
{
|
|
"post": "https://charlie.org/social.org#2025-02-04T11:20:00+0100",
|
|
"emoji": "👍",
|
|
"parent": "https://example.com/social.org#2025-02-04T10:00:00+0100"
|
|
}
|
|
],
|
|
"meta": {
|
|
"feed": "https://example.com/social.org",
|
|
"total": 3
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/reactions/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
The response includes:
|
|
- `post`: The reaction post URL (format: `{author_feed}#{timestamp}`)
|
|
- `emoji`: The reaction emoji (from `:MOOD:` property)
|
|
- `parent`: The post URL that received the reaction
|
|
|
|
To extract the author's feed from the `post` field, simply take the part before the `#` character. For example, from `https://alice.org/social.org#2025-02-05T13:15:00+0100`, the author is `https://alice.org/social.org`.
|
|
|
|
**Note:** According to Org Social specification, reactions are posts with:
|
|
- `:REPLY_TO:` property pointing to the reacted post
|
|
- `:MOOD:` property containing the emoji
|
|
- Empty or minimal content
|
|
|
|
### Get replies to feed
|
|
|
|
`/replies-to/?feed={url feed}` - Get all replies received by posts from a given feed. A reply is a post with a `:REPLY_TO:` property pointing to one of your posts (but without a `:MOOD:` or `:POLL_OPTION:` property, which would make it a reaction or poll vote instead). Results are ordered from most recent to oldest.
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/replies-to/?feed=https%3A%2F%2Fexample.com%2Fsocial.org"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/replies-to/" --data-urlencode "feed=https://example.com/social.org"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
{
|
|
"post": "https://alice.org/social.org#2025-02-05T13:15:00+0100",
|
|
"parent": "https://example.com/social.org#2025-02-05T12:00:00+0100"
|
|
},
|
|
{
|
|
"post": "https://bob.org/social.org#2025-02-05T14:30:00+0100",
|
|
"parent": "https://example.com/social.org#2025-02-05T12:00:00+0100"
|
|
},
|
|
{
|
|
"post": "https://charlie.org/social.org#2025-02-04T11:20:00+0100",
|
|
"parent": "https://example.com/social.org#2025-02-04T10:00:00+0100"
|
|
}
|
|
],
|
|
"meta": {
|
|
"feed": "https://example.com/social.org",
|
|
"total": 3
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/replies-to/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
The response includes:
|
|
- `post`: The reply post URL (format: `{author_feed}#{timestamp}`)
|
|
- `parent`: The post URL that received the reply
|
|
|
|
To extract the author's feed from the `post` field, simply take the part before the `#` character. For example, from `https://alice.org/social.org#2025-02-05T13:15:00+0100`, the author is `https://alice.org/social.org`.
|
|
|
|
**Note:** This endpoint shows direct replies to your posts. To see the full conversation thread of a specific post, use the `/replies/?post={post_url}` endpoint instead.
|
|
|
|
### Get boosts
|
|
|
|
`/boosts/?post={url post}` - Get all boosts (reposts/shares) for a specific post. A boost is when someone shares your post on their timeline using the `:INCLUDE:` property. Results are ordered from most recent to oldest.
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/boosts/?post=https%3A%2F%2Fexample.com%2Fsocial.org%232025-02-05T10%3A00%3A00%2B0100"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/boosts/" --data-urlencode "post=https://example.com/social.org#2025-02-05T10:00:00+0100"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
"https://alice.org/social.org#2025-02-05T14:00:00+0100",
|
|
"https://bob.org/social.org#2025-02-05T15:30:00+0100",
|
|
"https://charlie.org/social.org#2025-02-05T16:45:00+0100"
|
|
],
|
|
"meta": {
|
|
"post": "https://example.com/social.org#2025-02-05T10:00:00+0100",
|
|
"total": 3
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/boosts/?post=https%3A%2F%2Fexample.com%2Fsocial.org%232025-02-05T10%3A00%3A00%2B0100", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
The response includes:
|
|
- A list of boost post URLs (format: `{booster_feed}#{timestamp}`)
|
|
|
|
To extract the booster's feed from each post URL, simply take the part before the `#` character. For example, from `https://alice.org/social.org#2025-02-05T14:00:00+0100`, the booster is `https://alice.org/social.org`.
|
|
|
|
**Note:** According to Org Social specification, boosts are posts with the `:INCLUDE:` property pointing to the boosted post.
|
|
|
|
### Get interactions (all-in-one)
|
|
|
|
`/interactions/?post={url post}` - Get all interactions for a specific post in a single request. This endpoint consolidates reactions, replies, and boosts for optimal performance. Results are ordered from most recent to oldest.
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/interactions/?post=https%3A%2F%2Fexample.com%2Fsocial.org%232025-02-05T10%3A00%3A00%2B0100"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/interactions/" --data-urlencode "post=https://example.com/social.org#2025-02-05T10:00:00+0100"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": {
|
|
"reactions": [
|
|
{
|
|
"post": "https://alice.org/social.org#2025-02-05T13:15:00+0100",
|
|
"emoji": "❤"
|
|
},
|
|
{
|
|
"post": "https://bob.org/social.org#2025-02-05T14:30:00+0100",
|
|
"emoji": "🚀"
|
|
}
|
|
],
|
|
"replies": [
|
|
"https://charlie.org/social.org#2025-02-05T12:30:00+0100",
|
|
"https://diana.org/social.org#2025-02-05T15:00:00+0100"
|
|
],
|
|
"boosts": [
|
|
"https://alice.org/social.org#2025-02-05T14:00:00+0100",
|
|
"https://bob.org/social.org#2025-02-05T15:30:00+0100"
|
|
]
|
|
},
|
|
"meta": {
|
|
"post": "https://example.com/social.org#2025-02-05T10:00:00+0100",
|
|
"total_reactions": 2,
|
|
"total_replies": 2,
|
|
"total_boosts": 2,
|
|
"parentChain": [
|
|
"https://original.org/social.org#2025-02-04T09:00:00+0100",
|
|
"https://parent.org/social.org#2025-02-05T08:00:00+0100"
|
|
]
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/interactions/?post=https%3A%2F%2Fexample.com%2Fsocial.org%232025-02-05T10%3A00%3A00%2B0100", "method": "GET"},
|
|
"reactions": {"href": "/reactions/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"},
|
|
"replies": {"href": "/replies/?post=https%3A%2F%2Fexample.com%2Fsocial.org%232025-02-05T10%3A00%3A00%2B0100", "method": "GET"},
|
|
"boosts": {"href": "/boosts/?post=https%3A%2F%2Fexample.com%2Fsocial.org%232025-02-05T10%3A00%3A00%2B0100", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
The response includes:
|
|
- `reactions`: Array of reaction objects with `post` (URL) and `emoji`
|
|
- `replies`: Array of reply post URLs (excludes reactions - posts without mood)
|
|
- `boosts`: Array of boost post URLs
|
|
- `parentChain`: Array of parent post URLs from oldest to most recent (empty if post is root)
|
|
|
|
**Note:** This endpoint is optimized for displaying post details in a single request. It excludes nested replies (use `/replies/?post={post_url}` for the full thread tree) and only includes direct replies to maintain simplicity and performance.
|
|
|
|
The `parentChain` allows you to reconstruct the conversation context by showing all parent posts from the original root post up to the immediate parent. If the post is a reply to another post, you'll get the full chain; if it's a root post, the array will be empty.
|
|
|
|
**Use cases:**
|
|
- Display a post with all its interactions in one request
|
|
- Show conversation context with parent chain
|
|
- Optimize mobile/web apps by reducing HTTP requests
|
|
- Get post engagement metrics (reactions, replies, boosts count)
|
|
|
|
### Get raw feed content
|
|
|
|
`/feed-content/?feed={url feed}` - Get the raw content of an Org Social feed file. This endpoint fetches and returns the original `.org` file content from the specified feed URL.
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/feed-content/?feed=https%3A%2F%2Fexample.com%2Fsocial.org"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/feed-content/" --data-urlencode "feed=https://example.com/social.org"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": {
|
|
"content": "#+TITLE: My Social Feed\n#+AUTHOR: John Doe\n\n* 2025-02-05T10:00:00+0100\n:PROPERTIES:\n:ID: 2025-02-05T10:00:00+0100\n:END:\n\nHello, world! This is my first post.\n\n* 2025-02-05T12:30:00+0100\n:PROPERTIES:\n:ID: 2025-02-05T12:30:00+0100\n:REPLY_TO: https://alice.org/social.org#2025-02-05T10:15:00+0100\n:END:\n\nThis is a reply to Alice's post.\n"
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/feed-content/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
The response includes:
|
|
- `content`: The raw text content of the feed file (Org Mode format)
|
|
|
|
**Use cases:**
|
|
- Debug feed format issues
|
|
- Parse feeds locally in client applications
|
|
- Backup or archive feed content
|
|
- Analyze feed structure and properties
|
|
- Validate Org Social format compliance
|
|
|
|
**Error handling:**
|
|
- Returns 400 if the `feed` parameter is missing or invalid
|
|
- Returns 404 if the feed is not registered in the relay
|
|
- Returns 502 if the feed URL cannot be fetched (network error, server down, etc.)
|
|
|
|
**Note:**
|
|
- This endpoint fetches the feed content directly from the source URL in real-time and does not use cached data.
|
|
- The content is returned exactly as stored in the original `.org` file, preserving all formatting, whitespace, and Org Mode properties.
|
|
|
|
### Get replies/threads
|
|
|
|
`/replies/?post={url post}` - Get replies for a given post. This will return a tree structure with all the replies to posts in the given feed. If you want to see the entire tree, you must use the meta `parent` as a `post`.
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/replies/?post=https%3A%2F%2Ffoo.org%2Fsocial.org%232025-02-03T23%3A05%3A00%2B0100"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/replies/" --data-urlencode "post=https://foo.org/social.org#2025-02-03T23:05:00+0100"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
{
|
|
"post": "https://bar.org/social.org#2025-02-02T14:30:00+0100",
|
|
"children": [
|
|
{
|
|
"post": "https://baz.org/social.org#2025-02-03T09:45:00+0100",
|
|
"children": [],
|
|
"moods": [
|
|
{
|
|
"emoji": "❤",
|
|
"posts": [
|
|
"https://alice.org/social.org#2025-02-03T10:00:00+0100"
|
|
]
|
|
},
|
|
{
|
|
"emoji": "👍",
|
|
"posts": [
|
|
"https://bob.org/social.org#2025-02-03T10:15:00+0100",
|
|
"https://charlie.org/social.org#2025-02-03T10:30:00+0100"
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"post": "https://qux.org/social.org#2025-02-04T16:20:00+0100",
|
|
"children": [
|
|
{
|
|
"post": "https://quux.org/social.org#2025-02-05T11:10:00+0100",
|
|
"children": [],
|
|
"moods": []
|
|
}
|
|
],
|
|
"moods": [
|
|
{
|
|
"emoji": "🚀",
|
|
"posts": [
|
|
"https://diana.org/social.org#2025-02-04T17:00:00+0100"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
],
|
|
"moods": [
|
|
{
|
|
"emoji": "⭐",
|
|
"posts": [
|
|
"https://eve.org/social.org#2025-02-02T15:00:00+0100"
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"post": "https://corge.org/social.org#2025-02-03T18:00:00+0100",
|
|
"children": [],
|
|
"moods": []
|
|
}
|
|
],
|
|
"meta": {
|
|
"parent": "https://foo.org/social.org#2025-02-03T23:05:00+0100",
|
|
"parentChain": [
|
|
"https://root.org/social.org#2025-02-01T10:00:00+0100",
|
|
"https://foo.org/social.org#2025-02-03T23:05:00+0100"
|
|
]
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/replies/?post=https%3A%2F%2Ffoo.org%2Fsocial.org%232025-02-03T23%3A05%3A00%2B0100", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
Each node in the tree includes:
|
|
- `post`: The post URL
|
|
- `children`: Array of direct reply nodes (recursive structure)
|
|
- `moods`: Array of emoji reactions with their posts
|
|
|
|
### Search
|
|
|
|
`/search/?q={query}` - Search posts by free text.
|
|
`/search/?tag={tag}` - Search posts by tag.
|
|
|
|
```sh
|
|
curl http://localhost:8080/search/?q=emacs
|
|
```
|
|
|
|
Optional parameters:
|
|
|
|
- `page`: Page number (default: 1)
|
|
- `perPage`: Results per page (default: 10, max: 50)
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
"https://foo.org/social.org#2025-02-03T23:05:00+0100",
|
|
"https://bar.org/social.org#2025-02-04T10:15:00+0100",
|
|
"..."
|
|
],
|
|
"meta": {
|
|
"query": "example",
|
|
"total": 150,
|
|
"page": 1,
|
|
"perPage": 10,
|
|
"hasNext": true,
|
|
"hasPrevious": false
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/search/?q=example&page=1", "method": "GET"},
|
|
"next": {"href": "/search/?q=example&page=2", "method": "GET"},
|
|
"previous": null
|
|
}
|
|
}
|
|
```
|
|
|
|
### List groups
|
|
|
|
`/groups/` - List all groups from the relay.
|
|
|
|
```sh
|
|
curl http://localhost:8080/groups/
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
"Emacs",
|
|
"Org Mode",
|
|
"Programming"
|
|
],
|
|
"_links": {
|
|
"self": {
|
|
"href": "/groups/",
|
|
"method": "GET"
|
|
},
|
|
"groups": [
|
|
{
|
|
"name": "Emacs",
|
|
"href": "/groups/emacs/",
|
|
"method": "GET"
|
|
},
|
|
{
|
|
"name": "Org Mode",
|
|
"href": "/groups/org-mode/",
|
|
"method": "GET"
|
|
},
|
|
{
|
|
"name": "Programming",
|
|
"href": "/groups/programming/",
|
|
"method": "GET"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Example with no groups configured:**
|
|
```json
|
|
{
|
|
"type": "Error",
|
|
"errors": ["No groups configured in this relay"],
|
|
"data": []
|
|
}
|
|
```
|
|
|
|
### Get group messages
|
|
|
|
`/groups/{group_slug}/` - Get messages from a group. The URL uses the group slug (lowercase, spaces replaced with hyphens).
|
|
|
|
```sh
|
|
curl http://localhost:8080/groups/emacs/
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
{
|
|
"post": "https://foo.org/social.org#2025-02-03T23:05:00+0100",
|
|
"children": []
|
|
},
|
|
{
|
|
"post": "https://bar.org/social.org#2025-02-04T10:15:00+0100",
|
|
"children": [
|
|
{
|
|
"post": "https://baz.org/social.org#2025-02-05T08:30:00+0100",
|
|
"children": []
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"meta": {
|
|
"group": "Emacs",
|
|
"members": [
|
|
"https://alice.org/social.org",
|
|
"https://bob.org/social.org",
|
|
"https://charlie.org/social.org"
|
|
]
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/groups/emacs/", "method": "GET"},
|
|
"group-list": {"href": "/groups/", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
### List polls
|
|
|
|
`/polls/` - List all polls from the relay. Results are ordered from most recent to oldest.
|
|
|
|
```sh
|
|
curl http://localhost:8080/polls/
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
"https://foo.org/social.org#2025-02-03T23:05:00+0100",
|
|
"https://bar.org/social.org#2025-02-04T10:15:00+0100",
|
|
"https://baz.org/social.org#2025-02-05T08:30:00+0100"
|
|
],
|
|
"meta": {
|
|
"total": 3
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/polls/", "method": "GET"},
|
|
"votes": {"href": "/polls/votes/?post={post_url}", "method": "GET", "templated": true}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get profile
|
|
|
|
`/profile/?feed={url feed}` - Get profile information for a given feed, including the list of feeds that follow it (followers).
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/profile/?feed=https%3A%2F%2Fexample.com%2Fsocial.org"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/profile/" --data-urlencode "feed=https://example.com/social.org"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": {
|
|
"feed": "https://example.com/social.org",
|
|
"followers": [
|
|
"https://alice.org/social.org",
|
|
"https://bob.org/social.org",
|
|
"https://charlie.org/social.org"
|
|
]
|
|
},
|
|
"meta": {
|
|
"feed": "https://example.com/social.org",
|
|
"total_followers": 3
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/profile/?feed=https%3A%2F%2Fexample.com%2Fsocial.org", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
The response includes:
|
|
- `feed`: The queried feed URL
|
|
- `followers`: List of feed URLs that follow this feed (i.e. feeds that have included this feed in their following list)
|
|
|
|
**Error handling:**
|
|
- Returns 400 if the `feed` parameter is missing or invalid
|
|
- Returns 404 if the feed is not registered in the relay
|
|
|
|
### Get poll votes
|
|
|
|
`/polls/votes/?post={url post}` - Get votes for a specific poll.
|
|
|
|
```sh
|
|
# URL must be encoded when passed as query parameter
|
|
curl "http://localhost:8080/polls/votes/?post=https%3A%2F%2Ffoo.org%2Fsocial.org%232025-02-03T23%3A05%3A00%2B0100"
|
|
|
|
# Or use curl's --data-urlencode for automatic encoding:
|
|
curl -G "http://localhost:8080/polls/votes/" --data-urlencode "post=https://foo.org/social.org#2025-02-03T23:05:00+0100"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": [
|
|
{
|
|
"option": "Cat",
|
|
"votes": [
|
|
"https://alice.org/social.org#2025-02-04T10:15:00+0100",
|
|
"https://bob.org/social.org#2025-02-04T11:30:00+0100"
|
|
]
|
|
},
|
|
{
|
|
"option": "Dog",
|
|
"votes": [
|
|
"https://charlie.org/social.org#2025-02-04T12:45:00+0100"
|
|
]
|
|
},
|
|
{
|
|
"option": "Fish",
|
|
"votes": []
|
|
},
|
|
{
|
|
"option": "Bird",
|
|
"votes": [
|
|
"https://diana.org/social.org#2025-02-04T14:20:00+0100"
|
|
]
|
|
}
|
|
],
|
|
"meta": {
|
|
"poll": "https://foo.org/social.org#2025-02-03T23:05:00+0100",
|
|
"total_votes": 4
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/polls/votes/?post=https%3A%2F%2Ffoo.org%2Fsocial.org%232025-02-03T23%3A05%3A00%2B0100", "method": "GET"},
|
|
"polls": {"href": "/polls/", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get statistics
|
|
|
|
`/stats/` - Get aggregated statistics about the activity registered by the Relay. Data is grouped by year and month, plus a block of global counters. The month of each post is determined by its ID (RFC 3339 timestamp), not by when the Relay discovered it.
|
|
|
|
```sh
|
|
curl "http://localhost:8080/stats/"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": {
|
|
"years": {
|
|
"2025": {
|
|
"11": {
|
|
"active_accounts": 42,
|
|
"total_posts": 512,
|
|
"posts": 300,
|
|
"replies": 120,
|
|
"boosts": 40,
|
|
"reactions": 35,
|
|
"group_messages": 15,
|
|
"polls": 7
|
|
},
|
|
"12": {
|
|
"active_accounts": 51,
|
|
"total_posts": 640,
|
|
"posts": 380,
|
|
"replies": 150,
|
|
"boosts": 52,
|
|
"reactions": 41,
|
|
"group_messages": 22,
|
|
"polls": 4
|
|
}
|
|
},
|
|
"2026": {
|
|
"01": {
|
|
"active_accounts": 58,
|
|
"total_posts": 701,
|
|
"posts": 410,
|
|
"replies": 170,
|
|
"boosts": 60,
|
|
"reactions": 48,
|
|
"group_messages": 25,
|
|
"polls": 9
|
|
}
|
|
}
|
|
},
|
|
"global": {
|
|
"registered_feeds": 128,
|
|
"total_accounts": 115,
|
|
"total_posts": 4210,
|
|
"total_follows": 340,
|
|
"active_groups": 6
|
|
}
|
|
},
|
|
"meta": {
|
|
"generated_at": "2026-01-15T10:30:00+00:00"
|
|
},
|
|
"_links": {
|
|
"self": {"href": "/stats/", "method": "GET"},
|
|
"feeds": {"href": "/feeds/", "method": "GET"}
|
|
}
|
|
}
|
|
```
|
|
|
|
Counter definitions per month:
|
|
|
|
- `active_accounts`: Number of distinct accounts that published at least one post (of any kind) during that month.
|
|
- `total_posts`: Every post registered that month, regardless of its type.
|
|
- `posts`: Original posts, meaning posts that are neither a reply (`:REPLY_TO:`) nor a boost (`:INCLUDE:`).
|
|
- `replies`: Posts with the `:REPLY_TO:` property that contain text content. Reactions are excluded.
|
|
- `boosts`: Posts with the `:INCLUDE:` property.
|
|
- `reactions`: Replies with a `:MOOD:` property and no text content.
|
|
- `group_messages`: Posts published in any group (`:GROUP:` property).
|
|
- `polls`: Polls created that month (posts with `:POLL_END:`). Poll closings are not counted.
|
|
|
|
Global counters:
|
|
|
|
- `registered_feeds`: Total number of feeds currently registered in the Relay.
|
|
- `total_accounts`: Total number of accounts (profiles) known by the Relay.
|
|
- `total_posts`: Total number of posts registered, all time.
|
|
- `total_follows`: Total number of follow relationships between accounts.
|
|
- `active_groups`: Number of groups with at least one message.
|
|
|
|
**Note:** Counters are not mutually exclusive: a reply inside a group counts in both `replies` and `group_messages`, and a poll also counts in `posts`. Only `posts`, `replies`, `reactions` and `boosts` are disjoint between them, and together they add up to `total_posts`. Months with no activity are omitted. Like the rest of the endpoints, the response is cached and refreshed after each feed scan.
|
|
|
|
### Bridges (follow ActivityPub accounts and RSS feeds)
|
|
|
|
Bridges expose external accounts as **virtual social.org feeds** served by the relay. Any Org Social client can follow them with a plain `#+FOLLOW:` line, no client changes needed.
|
|
|
|
Registration is implicit: the first request of an unknown account fetches it from the origin and stores it. Active bridges are refreshed every 15 minutes; bridges nobody has requested for 90 days are removed automatically (and re-created on the next request).
|
|
|
|
#### ActivityPub bridge
|
|
|
|
`/bridge/activitypub/@{user}@{instance}/` - A Mastodon (or any ActivityPub) account as a virtual social.org feed (plain text).
|
|
|
|
```sh
|
|
curl http://localhost:8080/bridge/activitypub/@Mastodon@mastodon.social/
|
|
```
|
|
|
|
```org
|
|
#+TITLE: Mastodon
|
|
#+NICK: Mastodon
|
|
#+DESCRIPTION: Our mission is to connect the world...
|
|
#+AVATAR: https://files.mastodon.social/accounts/avatars/.../avatar.png
|
|
#+LINK: https://mastodon.social/@Mastodon
|
|
|
|
* Posts
|
|
** 2026-03-27T16:27:02+00:00
|
|
:PROPERTIES:
|
|
:LANG: en
|
|
:TAGS: fossback mastodon opensourcedesign ux
|
|
:END:
|
|
|
|
Following on from yesterday's blog post...
|
|
```
|
|
|
|
To follow the account, users add to their `social.org`:
|
|
|
|
```org
|
|
#+FOLLOW: https://relay.org-social.org/bridge/activitypub/@Mastodon@mastodon.social/
|
|
```
|
|
|
|
Notes:
|
|
|
|
- Handles are case-insensitive; non-canonical spellings redirect (301) to the lowercase canonical URL.
|
|
- Only public top-level posts are bridged (no replies, no boosts), like Mastodon's public RSS.
|
|
- Content warnings become a `CW:` first line; media attachments become Org links; hashtags become `:TAGS:`.
|
|
- Instances running in *authorized fetch* mode (signed requests required) cannot be bridged.
|
|
|
|
`/bridge/activitypub/` (without handle) lists the accounts already bridged by this relay.
|
|
|
|
#### RSS/Atom bridge
|
|
|
|
`/bridge/rss/?url={feed_url}` - An RSS or Atom feed as a virtual social.org feed (plain text). The `url` parameter must be URL-encoded.
|
|
|
|
```sh
|
|
curl -G http://localhost:8080/bridge/rss/ --data-urlencode "url=https://xkcd.com/rss.xml"
|
|
```
|
|
|
|
```org
|
|
#+TITLE: xkcd.com
|
|
#+NICK: xkcd_com
|
|
#+DESCRIPTION: xkcd.com: A webcomic of romance and math humor.
|
|
#+LINK: https://xkcd.com/
|
|
|
|
* Posts
|
|
** 2026-07-08T04:00:00+00:00
|
|
:PROPERTIES:
|
|
:END:
|
|
|
|
*** Airport Meeting
|
|
|
|
[[https://imgs.xkcd.com/comics/airport_meeting.png][Although it was a setback for physics...]]
|
|
|
|
[[https://xkcd.com/3269/]]
|
|
```
|
|
|
|
To follow the feed:
|
|
|
|
```org
|
|
#+FOLLOW: https://relay.org-social.org/bridge/rss/?url=https%3A%2F%2Fxkcd.com%2Frss.xml
|
|
```
|
|
|
|
Notes:
|
|
|
|
- Entry titles become `***` sub-headings inside the post, followed by the converted content and a link to the original article.
|
|
- Entries without a publication date are skipped; entries sharing the same date get IDs shifted forward one second so none is lost.
|
|
|
|
`/bridge/rss/` (without `url`) lists the feeds already bridged by this relay.
|
|
|
|
#### Bridged feeds and the rest of the API
|
|
|
|
Bridged accounts are stored as regular profiles, so the existing endpoints (`/profile/`, `/search/`, `/rss.xml?feed=...`, ...) work with the bridge URL as the `feed` parameter. Bridged posts never trigger Webmentions nor notifications: the relay only mirrors external content.
|
|
|
|
## Groups Configuration
|
|
|
|
Org Social Relay supports organizing posts into topic-based groups. Users can join groups to participate in focused discussions.
|
|
|
|
### Configuring Groups
|
|
|
|
To configure groups in your relay, set the `GROUPS` environment variable with a comma-separated list of group names:
|
|
|
|
```bash
|
|
# In your .env file
|
|
GROUPS=Emacs,Org Social,Elisp,Programming,Tech
|
|
```
|
|
|
|
### Groups Configuration Examples
|
|
|
|
**No groups (default):**
|
|
```bash
|
|
GROUPS=
|
|
# or simply omit the GROUPS variable
|
|
```
|
|
|
|
**Single group:**
|
|
```bash
|
|
GROUPS=Emacs
|
|
```
|
|
|
|
**Multiple groups:**
|
|
```bash
|
|
GROUPS=Emacs,Org Social,Elisp
|
|
```
|
|
|
|
### Using Groups
|
|
|
|
Once configured, users can:
|
|
1. View group-specific message threads
|
|
2. Discover other group members
|
|
3. Post messages to specific groups
|
|
|
|
The groups endpoints will only be available when groups are configured via the `GROUPS` environment variable.
|
|
|
|
## RSS Feed
|
|
|
|
Org Social Relay provides an RSS feed of different types of content.
|
|
|
|
- The latest posts scanned from all registered feeds.
|
|
|
|
```sh
|
|
http://localhost:8080/rss.xml
|
|
```
|
|
|
|
- By tag.
|
|
|
|
```sh
|
|
curl http://localhost:8080/rss.xml?tag=emacs
|
|
```
|
|
|
|
- By author feed.
|
|
|
|
```sh
|
|
curl http://localhost:8080/rss.xml?feed=https%3A%2F%2Forg-social.org%2Fsocial.org
|
|
```
|
|
|
|
This RSS feed can be used in RSS readers to stay updated with new posts from the relay, but is limited to the latest 200 posts.
|
|
|
|
## Technical information
|
|
|
|
The relay list is stored locally in `relay-list.txt`.
|
|
|
|
### Crons
|
|
|
|
#### Scan feeds
|
|
|
|
**Every minute**, Relay will scan all registered feeds for new posts, mentions, replies, polls, and profile updates. After each scan, the cache is automatically cleared to ensure all endpoints return fresh data.
|
|
|
|
During the scan, users continue to see cached data (complete and consistent, even if slightly outdated). Once the scan completes and cache is cleared, the next request will fetch fresh data from the database.
|
|
|
|
#### Scan other nodes
|
|
|
|
Every 3 hours, Relay will search for new users on other nodes.
|
|
|
|
#### Discover new feeds
|
|
|
|
Every day at midnight, Relay analyzes the feeds of all registered users to discover new feeds they follow.
|
|
|
|
#### Cleanup stale feeds
|
|
|
|
Every 3 days at 2 AM, Relay automatically removes feeds that haven't been successfully fetched (HTTP 200) in the last 3 days. This keeps the relay efficient by removing inactive or dead feeds.
|
|
|
|
#### Refresh bridges
|
|
|
|
Every 15 minutes, Relay refetches the bridged ActivityPub accounts and RSS feeds that have been requested in the last 7 days. Dormant bridges are refreshed inline on their next request instead.
|
|
|
|
#### Cleanup stale bridges
|
|
|
|
Every 3 days at 3 AM, Relay removes bridged accounts that nobody has requested in the last 90 days, together with their stored profile and posts. They are re-created automatically if someone requests them again.
|
|
|
|
## Contributing
|
|
|
|
Contributions are welcome! Please see the [contribution guidelines](https://git.andros.dev/andros/contribute) for instructions on how to submit issues or pull requests.
|
|
|
|
## License
|
|
|
|
Org Social is free software, released under the [GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.html).
|