Files
andros d3a6a0ed3e Initial commit: OrgSocialKit library
Pure Swift library for iOS (iOS 17+) to interact with Org Social,
a decentralized social network based on plain-text social.org files.

Features:
- FeedFetcher: async download of social.org content
- OrgSocialParser: full parser for profile headers and posts
- TimelineFetcher: concurrent multi-feed timeline with relay support
- PostWriter: create and upload posts to vhost
- 43 tests across 8 suites, all passing
2026-04-18 20:03:46 +02:00

8.7 KiB

OrgSocialKit

A pure Swift library for iOS to interact with Org Social, a decentralized social network based on plain-text social.org files served over HTTP.

No external dependencies. Requires iOS 17+ or macOS 14+.


Installation

Swift Package Manager

Add the package to your Package.swift:

dependencies: [
    .package(url: "https://github.com/tanrax/org-social-ios-lib", from: "0.1.0"),
],
targets: [
    .target(
        name: "YourApp",
        dependencies: ["OrgSocialKit"]
    ),
]

Or in Xcode: File > Add Package Dependencies and paste the repository URL.


Features

Feature Status
Fetch feed content from a URL
Parse profile and posts
Timeline assembly
Post creation and upload
Relay API Planned

Usage

Fetch and parse a feed

The typical flow is to fetch the raw content with FeedFetcher and then parse it with OrgSocialParser:

import OrgSocialKit

let url = URL(string: "https://andros.dev/social.org")!
let fetcher = FeedFetcher()
let parser = OrgSocialParser()

do {
    let content = try await fetcher.fetch(from: url)
    var profile = parser.parse(content)
    profile.feedURL = url   // set the source URL (the parser doesn't know it)

    print(profile.nick ?? "")        // "andros"
    print(profile.posts.count)       // number of posts
} catch {
    print(error.localizedDescription)
}

Work with the profile

// Profile metadata
profile.title        // "Andros Fenollosa"
profile.nick         // "andros"
profile.description  // "Software developer."
profile.avatar       // URL?
profile.languages    // ["en", "es"]
profile.follows      // [OrgSocialFollow]
profile.groups       // [OrgSocialGroup]
profile.contacts     // ["mailto:hi@andros.dev"]
profile.pinned       // "2025-03-10T09:00:00+01:00"

// Iterate follows
for follow in profile.follows {
    print(follow.name ?? "unknown", follow.url)
}

Fetch the timeline

TimelineFetcher downloads all relevant feeds concurrently and returns a merged, filtered, and date-sorted list of posts.

import OrgSocialKit

// Parse your own profile first
let myURL = URL(string: "https://andros.dev/social.org")!
var profile = OrgSocialParser().parse(try await FeedFetcher().fetch(from: myURL))
profile.feedURL = myURL

// Fetch the timeline (relay enabled by default)
let posts = await TimelineFetcher().fetch(following: profile)

Relay behaviour:

  • useRelay: true (default): downloads the full feed list from the relay network. If the relay is unreachable it falls back to the local #+FOLLOW: list automatically.
  • useRelay: false: uses only the profile's #+FOLLOW: list, no network call to the relay.
// Relay off — local follows only
let options = TimelineOptions(useRelay: false)
let posts = await TimelineFetcher().fetch(following: profile, options: options)

// Custom options
let options = TimelineOptions(
    relayURL: URL(string: "https://relay.org-social.org")!,
    useRelay: true,
    maxConcurrentDownloads: 10,
    maxPostAgeDays: 7,          // nil = no date limit
    languageFilter: ["en", "es"] // [] = all languages
)

What the timeline filters out automatically:

  • Pure emoji reactions (posts with MOOD + REPLY_TO and no body text)
  • Group posts (use a dedicated group feed for those)
  • Posts older than maxPostAgeDays
  • Posts in languages not in languageFilter (when the filter is non-empty)
  • VISIBILITY: mention posts where the user is neither the author nor a mentioned party

Work with posts

for post in profile.posts {
    print(post.timestamp)    // "2025-03-10T09:00:00+01:00"
    print(post.date)         // Date
    print(post.text)         // body text (Org Mode syntax preserved)
    print(post.lang ?? "")   // "en"
    print(post.tags)         // ["emacs", "swift"]

    // Post type hints
    if post.replyTo != nil   { /* it's a reply */ }
    if post.include != nil   { /* it's a boost */ }
    if post.pollEnd != nil   { /* it's a poll */ }
    if post.mood != nil      { /* it's a reaction */ }
    if post.visibility == "mention" { /* mention-only */ }
}

Handle fetch errors

fetch(from:) throws FeedFetcherError:

do {
    let content = try await fetcher.fetch(from: url)
} catch FeedFetcherError.invalidURL {
    // The URL scheme is not http or https
} catch FeedFetcherError.httpError(let statusCode) {
    print("Server returned \(statusCode)")
} catch FeedFetcherError.decodingError {
    // Response body is not valid UTF-8
} catch FeedFetcherError.networkError(let message) {
    print("Network failure: \(message)")
}

Create and publish a post

PostWriter appends a new post to your feed content and uploads the result to the vhost.

import OrgSocialKit

let feedURL = URL(string: "https://andros.dev/social.org")!
let writer = PostWriter()

// Fetch the current feed content
let content = try await FeedFetcher().fetch(from: feedURL)

// Build and append the new post
let options = NewPostOptions(
    text: "Hello from OrgSocialKit!",
    lang: "en",
    tags: "swift ios"
)
let (updatedContent, postURL) = try writer.appendPost(
    to: content,
    feedURL: feedURL,
    options: options
)

print(postURL) // "https://andros.dev/social.org#2025-04-18T12:00:00+02:00"

// Upload the updated feed to the vhost
try await writer.upload(content: updatedContent, to: feedURL)

Post options:

let options = NewPostOptions(
    text: "Body text. Org Mode syntax supported.",
    lang: "en",                    // ISO 639-1 code (optional)
    tags: "emacs swift",           // space-separated (optional)
    replyTo: "https://feed.url/social.org#TIMESTAMP",  // reply (optional)
    include: "https://feed.url/social.org#TIMESTAMP",  // boost (optional)
    mood: "🚀",                    // emoji reaction (optional)
    group: "Swift Dev https://relay.example.com",       // group (optional)
    visibility: .mention,          // .public (default) or .mention
    client: "my-app"               // defaults to "org-social-ios"
)

Errors from PostWriter:

do {
    try await writer.upload(content: updatedContent, to: feedURL)
} catch PostWriterError.missingPostsSection {
    // Feed has no "* Posts" heading
} catch PostWriterError.uploadFailed(let statusCode) {
    print("vhost returned HTTP \(statusCode)")
} catch PostWriterError.networkError(let message) {
    print("Network failure: \(message)")
}

Custom URLSession

let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 10
let fetcher = FeedFetcher(session: URLSession(configuration: config))
let writer = PostWriter(session: URLSession(configuration: config))

Data model

OrgSocialProfile

Property Type Notes
title String? Required by spec
nick String? Required by spec, no spaces
description String?
avatar URL?
links [URL] Multiple allowed
location String?
birthday String? YYYY-MM-DD
languages [String] ISO 639-1 codes
feedURL URL? Set by caller after parsing
pinned String? RFC 3339 timestamp
follows [OrgSocialFollow]
groups [OrgSocialGroup]
contacts [String] Email, XMPP, etc.
posts [OrgSocialPost] In file order

OrgSocialPost

Property Type Notes
timestamp String RFC 3339, unique ID
date Date Parsed from timestamp
text String Body (Org Mode syntax preserved)
lang String? ISO 639-1
tags [String]
client String? App that created the post
replyTo String? url#timestamp
include String? url#timestamp (boost)
pollEnd Date? Poll close date
pollOption String? Vote selection
group String? "Name relay-url"
mood String? Emoji reaction
migration String? "old-url new-url"
visibility String? "public" or "mention"

What is Org Social?

Org Social is a decentralized social network. Each user hosts a plain-text file called social.org on their own web server. The file contains both their profile and all their posts, readable by humans and machines alike. There is no central server or registration required.

A minimal social.org looks like this:

#+TITLE: My Feed
#+NICK: andros
#+FOLLOW: shom https://shom.dev/social.org

* Posts

** 2025-03-10T09:00:00+01:00
:PROPERTIES:
:LANG: en
:END:

Hello, Org Social!

Learn more at the official protocol repository.


License

GPL-3.0