Mixi2

0.0.2

(Unofficial) mixi2 SDK for Swift
ainame/swift-mixi2

What's New

0.0.2

2026-03-29T19:08:46Z

Added

  • Added support for the DeletePost unary RPC via mixi2.apiClient.deletePost(_:).

Changed

  • Updated vendored mixi2-api to v1.1.0.
  • Regenerated gRPC/protobuf Swift sources for the mixi2-api v1.1.0 schema.
  • Improved Linux support documentation and Docker build workflow for package consumers.

swift-mixi2

An unofficial Swift client library for the mixi2 Application API (gRPC). Provides auth, gRPC streaming, and webhook support on top of generated protobuf stubs.

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:

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:

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:

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) and set mode: .webhook(...) at init time:

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:

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:

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.

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

Description

  • Swift Tools 6.2.0
View More Packages from this Author

Dependencies

Last updated: Sun May 17 2026 11:47:01 GMT-0900 (Hawaii-Aleutian Daylight Time)