A Discord RPC library for Swift on macOS.
All commands and payloads are wrapped in Swift methods and types.
Note: Works only with IPC for now because the Discord Websocket RPC API seems to be disabled. See this issue.
- macOS >= 11 (Big Sur)
- Swift 5
- No App Sandbox as this library needs to access to Discord UDS file
DiscordRPC library is available as a Swift Package Manager package.
You can use it as a dependency of your own package by adding this in its Package.swift
:
dependencies: [
.Package(url: "https://github.com/aeddi/DiscordRPC.git", from: "1.0.0")
]
Or you can add it to your XCode project by following this tutorial.
You will first need to create an application on the Discord Developer Dashboard and then to retrieve the Client ID (and optionally the Client Secret depending on your use case) in the OAuth2 section.
import DiscordRPC
import Dispatch
// Instantiate DiscordRPC using client ID and Secret
let discordRPC = DiscordRPC(clientID: "XXXXXX", clientSecret: "YYYYYY")
// This handler is called as soon as the Discord RPC handshake succeeded
discordRPC.onConnect { rpc, eventReady in
print("Connection to Discord RPC API succeeded")
do {
// Ask user authorization by displaying a popup on the Discord client
let authorization = try rpc.authorize(oAuth2Scopes: [.rpc])
// Request an access token on Discord servers using the authorization code
let accessToken = try rpc.fetchAccessToken(code: authorization.data.code)
// Authenticate to Discord RPC API using the access token
let authentication = try rpc.authenticate(accessToken: accessToken.accessToken)
print("Hello \(authentication.data.user.username)! o/")
// Receive an event when the local user select a voice channel
_ = try rpc.subscribe(event: .voiceChannelSelect)
} catch {
rpc.disconnect()
print("Error occured: \(error)")
exit(42)
}
}
// This handler is called when a disconnection with the Discord RPC socket occurs
discordRPC.onDisconnect { rpc, eventClose in
print("Disconnection occurred: [\(eventClose.code)] \(eventClose.message)")
}
// This handler is called when a response to an async command is received
discordRPC.onResponse { rpc, nonce, commandType, response in
print("Response received for command \(commandType) with nonce: \(nonce)")
}
// This handler is called when an error for an async command occurs
discordRPC.onError { rpc, nonce, eventError in
print("Error occured on command \(eventError.cmd): [\(eventError.data.code)] \(eventError.data.message)")
}
// This handler is called when an update to a subscribed event is received
discordRPC.onEvent { rpc, eventType, event in
print("Event received of type \(eventType))")
// Cast raw event (Data) to SelectVoiceChannelEvent
if eventType == .voiceChannelSelect {
if let eventSVC = try? EventVoiceChannelSelect.from(data: event) {
if let channelID = eventSVC.data.channelID {
print("Voice channel \(channelID) selected!")
} else {
print("Voice channel deselected!")
}
}
}
}
// Init connection
do {
try discordRPC.connect()
dispatchMain()
} catch {
print("An error occured during connection init: \(error)")
}
You can fetch a user avatar on Discord servers using this helper:
let authentication = try rpc.authenticate(accessToken: accessToken.accessToken)
let user = authentication.data.user
let avatar: NSImage = try user.fetchAvatarImage()
Or this one:
let authentication = try rpc.authenticate(accessToken: accessToken.accessToken)
let user = authentication.data.user
let avatar: NSImage = try fetchUserAvatarImage(id: user.id, avatar: user.avatar)
To avoid asking authorization to user on each run, you can persist the access
token (e.g. by using UserDefaults) and pass it directly to authenticate(accessToken:)
.
func persistentAuthentication(rpc: DiscordRPC) throws -> ResponseAuthenticate {
let defaults = UserDefaults.standard
let now = Date()
// If a previous token exists in UserDefaults and is still valid, use it
if let tokenValidity = defaults.object(forKey: "tokenValidity") as? Date,
tokenValidity > now {
if let accessTokenData = defaults.data(forKey: "token") {
do {
let accessToken = try AccessToken.from(data: accessTokenData)
return try rpc.authenticate(accessToken: accessToken.accessToken)
} catch { /* ignore */ }
}
}
// Else request a new token
let authorization = try rpc.authorize(oAuth2Scopes: [.rpc])
let accessToken = try rpc.fetchAccessToken(code: authorization.data.code)
let authentication = try rpc.authenticate(accessToken: accessToken.accessToken)
// Then save it in UserDefaults for the next call
defaults.set(authentication.data.expires, forKey: "tokenValidity")
defaults.set(try newJSONEncoder().encode(accessToken), forKey: "token")
return authentication
}
Generated using Jazzy and hosted on GitHub Pages.
See Changelog.md.
Thanks to @Azoy for his SwordRPC repo from which I basically copied the IPC communication part.