ainame/swift-mixi2
(Unofficial) mixi2 SDK for Swift
Requirements
- Swift 6.2+
- macOS 15+, iOS 18+, Linux
Installation
Add the package to your Package.swift:
.package(url: "https://github.com/ainame/swift-mixi2", from: "0.0.2"),Then add the Mixi2 product to your target:
.product(name: "Mixi2", package: "swift-mixi2"),Optional: Hummingbird webhook adapter
To use the built-in HummingbirdAdapter for receiving webhooks, enable the HummingbirdWebhookAdapter trait (requires swift-tools-version 6.2+):
.package(url: "https://github.com/ainame/swift-mixi2", from: "0.0.2", traits: ["HummingbirdWebhookAdapter"]),Usage
### Configuration
Build a `Mixi2.Configuration` with your credentials:
```swift
let authenticator = ClientCredentialsAuthenticator(
clientID: "your-client-id",
clientSecret: "your-client-secret",
tokenURL: URL(string: "https://<token-host>/oauth/token")!
)
let config = Mixi2.Configuration(
apiHost: "<api-host>",
streamHost: "<stream-host>",
authenticator: authenticator,
authKey: "your-auth-key", // optional
webhookPublicKey: yourPublicKeyData // optional, required for webhook mode
)
```
### Building a bot
`Bot` + `EventRouter` is the primary way to handle events. `Bot` manages connections and drives the event loop; `EventRouter` routes each event to a typed handler registered with `on(_:handler:)`.
Choose an event reception mode based on your use case:
| Mode | Recommended for |
|------|----------------|
| gRPC stream (default) | Local development, prototyping |
| HTTP Webhook | Production, serverless |
`Bot` conforms to `ServiceLifecycle.Service` — wrap it in a `ServiceGroup` to get graceful SIGTERM/SIGINT shutdown:
```swift
import Logging
import Mixi2
import ServiceLifecycle
let router = EventRouter()
router.on(PostCreatedEvent.self) { context, event in
print("[post] \(event.issuer.userID): \(event.post.text)")
}
router.on(ChatMessageReceivedEvent.self) { context, event in
print("[chat] \(event.issuer.userID): \(event.message.text)")
}
let bot = try Bot(configuration: config, router: router)
let serviceGroup = ServiceGroup(services: [bot], logger: Logger(label: "MyBot"))
try await serviceGroup.run()
```
`on(_:handler:)` is generic over any type conforming to `Mixi2EventMessage`, so adding a handler for a new event type requires no changes to `EventRouter` — just pass the type. Multiple handlers for the same type are called in registration order.
Each handler receives a `Bot.Context` as its first argument. Use `context.apiClient` to make API calls from within a handler:
```swift
router.on(ChatMessageReceivedEvent.self) { context, event in
var reply = SendChatMessageRequest()
reply.roomID = event.message.roomID
reply.text = "echo: \(event.message.text)"
_ = try await context.apiClient.sendChatMessage(reply)
}
```
### Building a webhook bot
`Bot` also supports receiving events via HTTP webhooks. Enable the `HummingbirdWebhookAdapter` trait (see [Installation](#installation)) and set `mode: .webhook(...)` at init time:
```swift
import Logging
import Mixi2
import ServiceLifecycle
let config = Mixi2.Configuration(
apiHost: "<api-host>",
streamHost: "<stream-host>",
authenticator: authenticator,
webhookPublicKey: Data(base64Encoded: publicKeyBase64)!
)
let router = EventRouter()
router.on(ChatMessageReceivedEvent.self) { context, event in
var reply = SendChatMessageRequest()
reply.roomID = event.message.roomID
reply.text = "echo: \(event.message.text)"
_ = try await context.apiClient.sendChatMessage(reply)
}
let bot = try Bot(configuration: config, router: router,
mode: .webhook(HummingbirdAdapter(port: 8080)))
let serviceGroup = ServiceGroup(services: [bot], logger: Logger(label: "MyBot"))
try await serviceGroup.run()
```
`HummingbirdAdapter` exposes `POST /events` (webhook receiver) and `GET /healthz` (liveness probe).
For custom HTTP frameworks, implement the `WebhookServerAdapter` protocol and pass an instance as `mode: .webhook(yourAdapter)`.
### Making API calls
For unary RPCs without event streaming, use `Mixi2.with(configuration:)`. It starts the connection, runs your closure, then shuts down cleanly — even if the closure throws:
```swift
try await Mixi2.with(configuration: config) { mixi2 in
let response = try await mixi2.apiClient.getUsers(.with {
$0.userIDList = ["user-123"]
})
print(response.users)
}
```
`mixi2.apiClient` exposes all unary RPCs from the ApplicationService:
| Method | Description |
|--------|-------------|
| `getUsers(_:)` | Fetch users by ID |
| `getPosts(_:)` | Fetch posts by ID |
| `createPost(_:)` | Create a post |
| `deletePost(_:)` | Delete a post |
| `initiatePostMediaUpload(_:)` | Start a media upload and get an upload URL |
| `getPostMediaStatus(_:)` | Check media upload/processing status |
| `sendChatMessage(_:)` | Send a chat message to a room |
| `getStamps(_:)` | List available stamps |
| `addStampToPost(_:)` | Add a stamp to a post |
### Low-level event streaming
`EventStream` can be used directly when you don't need `Bot`:
```swift
let stream = EventStream(client: mixi2.streamClient)
try await stream.run { event in
switch event.eventType {
case .postCreated:
print("New post: \(event.postCreatedEvent.post.text)")
case .chatMessageReceived:
print("New message: \(event.chatMessageReceivedEvent.message.text)")
default:
break
}
}
```
PING events are filtered automatically. The stream reconnects on failure with exponential backoff (1 s / 2 s / 4 s, up to 3 retries).
### Webhook handling
For most use cases, use `Bot` with `HummingbirdAdapter` as shown above. If you need to integrate with a different HTTP framework, use `WebhookHandler` directly — it validates the Ed25519 signature, checks the timestamp is within ±5 minutes, and deserializes the payload.
```swift
import Mixi2
let handler = try WebhookHandler(publicKeyBytes: yourEd25519PublicKeyBytes)
// In your HTTP request handler:
let events = try handler.handle(
body: requestBody,
signature: request.headers["x-mixi2-application-event-signature"]!,
timestamp: request.headers["x-mixi2-application-event-timestamp"]!
)
for event in events {
// process event (PING events are already filtered out)
}
```
`handle(body:signature:timestamp:)` throws `WebhookError` on any verification failure:
| Error | Cause |
|-------|-------|
| `.invalidSignatureEncoding` | Signature header is not valid base64 |
| `.invalidTimestamp` | Timestamp header is not a valid integer |
| `.timestampTooOld` | Request is more than 5 minutes old |
| `.timestampInFuture` | Request timestamp is more than 5 minutes in the future |
| `.signatureInvalid` | Ed25519 signature does not match |Links
License
MIT
Package Metadata
Repository: ainame/swift-mixi2
Stars: 6
Forks: 0
Open issues: 0
Default branch: main
Primary language: swift
License: MIT
README: README.md