# OrgSocialKit A pure Swift library for iOS to interact with [Org Social](https://github.com/tanrax/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`: ```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`: ```swift 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 ```swift // 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. ```swift 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. ```swift // 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 ```swift 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`: ```swift 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. ```swift 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:** ```swift 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`:** ```swift 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 ```swift 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: ```org #+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](https://github.com/tanrax/org-social). --- ## License GPL-3.0