Metropolis is an asynchronous client library for Matrix Messaging Protocol written in pure Swift. It provides a low-level wrapper for client-server API integrated with the Swift type system.
Matrix is an open standard for interoperable, decentralised, real-time communication over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication - or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history.
- Account Management
- Session Management
- Device Management
- Room Directory
- Room Participation
- Presence
- Media
- User Data
- User Directory
- Full Text Search
- Push Notifications
- End-To-End Encryption
Metropolis supports installation via Swift Package Manager.
To add the package as your project's dependency, put the repository url into the dependencies section in Package.swift like this:
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "PackageName",
dependencies: [
.package(url: "https://github.com/picoHz/Metropolis.git", .branch("main"))
]
)You can also add dependencies from Xcode. https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app
Note that this library is basically just a thin wrapper of Matrix client-server API and does not provide high-level functionalities, so you need to read the Matrix Specification to understand how these endpoints work.
Public endpoints are accessible without login.
let client = try MPClient(url: URL(string: "https://matrix.org")!)
try await client.checkUsernameAvailability(username: "metro")let client = try MPClient(url: URL(string: "https://matrix.org")!)
let method = MPPasswordAuth(
identifier: .user(user: "metro"),
password: "pass")
let authClient = try await client.login(method: method)client.login() method accepts any objects that conform to MPAuthMethod protocol.
public protocol MPAuthMethod: Encodable {
var type: String { get }
}public struct CustomAuth: MPAuthMethod {
let type: String = "com.example.custom"
var customName: String
var customList: [String]
}
let authClient = try await client.login(method: CustomAuth(customName: "aaa", customList: ["bbb"]))The property names will be converted to snakecase.
For example, CustomAuth will be encoded into the following JSON.
{
"type": "com.example.custom",
"custom_name": "aaa",
"custom_list": ["bbb"]
}var interactiveAuth = try await authClient.changePassword(newPassword: "new_password")
guard case .failure(var state) = try await interactiveAuth.authenticate(method: method) {
return
}
while true {
if !state.flows[0].stages.contains("m.login.password") {
// Password authentication is not supported.
break
}
// Ask user to authenticate.
let method = MPPasswordAuth(
identifier: .user(user: "metro"),
password: "old_password")
switch try await interactiveAuth.authenticate(method: method, state: state) {
case .success(_):
break
case .failure(let newState):
// Authentication failed or needs additional authentication.
state = newState
continue
}
}Paginated endpoints return a stream object that implements AsyncSequence.
let stream = try await client.getPublicRooms(limit: 100)
for try await chunk in stream {
// Fetch results.
}
// Iterate manually.
var iter = stream.makeAsyncIterator()
let chunk = try await iter.next()
// Get an estimated room count from the iterator.
iter.totalRoomCountEstimate
// Resume the iteration using a pagination token.
let newStream = try await client.getPublicRooms(limit: 100, since: iter.nextBatch)Public Room Directory supports bidirectional pagination.
You can make a backward iterator using prevBatch token.
let stream = try await client.getPublicRooms(limit: 100)
var iter = stream.makeAsyncIterator()
while try await iter.next() != nil {}
let backwardStream = try await client.getPublicRooms(limit: 100, since: iter.prevBatch)
for try await chunk in backwardStream {
// Fetch backward results.
}Metropolis does not directly provide methods to download/upload media. Instead, it provides the following methods returning URLs for downloading or uploading so you can use your favorite HTTP client library to handle them.
public protocol MPMediaAPI {
func getMediaDownloadUrl(serverName: String, mediaId: String) -> URL
func getMediaThumbnailUrl(serverName: String, mediaId: String) -> URL
func getMediaUploadUrl() -> URL
}Some endpoints use unstructured (a.k.a schema-free) JSON objects. Metropolis handles them with SwiftyJSON.
let profile: JSON = try await client.getProfile(userId: "@metro@example.com")
// Access as a JSON object
if let displayName = profile.dictionaryValue["displayname"].string {
print(displayName)
}
// Decode as a struct
struct Profile: Decodable { var displayname: String }
let data = try JSONDecoder().decode(Profile.self, from: profile.rawData())
print(data.displayname)To run tests, you need running a matrix instance on localhost:8080.
The following commands run up-to-date demo server instances. Please check Synapse Documantation for more information about Matrix servers.
git clone https://github.com/matrix-org/synapse.git
cd synapse
python3 -m venv ./env
source ./env/bin/activate
pip install -e ".[all,dev]"
./demo/start.shIf there is already a running matrix instance on localhost:8080,
you can simply run swift test.
Metropolis is released under the MIT license.
Headline Photo by Leonhard Niederwimmer on Unsplash