Skip to content
Snippets Groups Projects
Verified Commit e2a5a411 authored by Spotlight Deveaux's avatar Spotlight Deveaux :fox:
Browse files

Fully convert to encoding

parent a12b4627
Branches main
No related tags found
No related merge requests found
......@@ -7,69 +7,72 @@
//
public protocol SwordRPCDelegate: AnyObject {
func swordRPCDidConnect(
/// Called back when our RPC connects to Discord.
/// - Parameter rpc: The current RPC to work with.
func rpcDidConnect(
_ rpc: SwordRPC
)
func swordRPCDidDisconnect(
/// Called when Discord disconnects our RPC.
/// - Parameters:
/// - rpc: The current RPC to work with.
/// - code: The disconnection code, if given by Discord.
/// - msg: The disconnection reason, if given by Discord.
func rpcDidDisconnect(
_ rpc: SwordRPC,
code: Int?,
message msg: String?
)
func swordRPCDidReceiveError(
/// Called when the RPC receives an error from Discord.
/// The connection will be terminated immediately.
/// - Parameters:
/// - rpc: The current RPC to work with.
/// - code: The error code as provided by Discord.
/// - msg: The error message as provided by Discord.
func rpcDidReceiveError(
_ rpc: SwordRPC,
code: Int,
message msg: String
)
func swordRPCDidJoinGame(
/// Called when Discord notifies us a user joined a game.
/// - Parameters:
/// - rpc: The current RPC to work with.
/// - secret: The join secret for the invite.
func rpcDidJoinGame(
_ rpc: SwordRPC,
secret: String
)
func swordRPCDidSpectateGame(
/// Called when Discord notifies us a client is spectating a game.
/// - Parameters:
/// - rpc: The current RPC to work with.
/// - secret: The spectate secret for the invite.
func rpcDidSpectateGame(
_ rpc: SwordRPC,
secret: String
)
func swordRPCDidReceiveJoinRequest(
/// Called when Discord notifies us the client received a join request.
/// - Parameters:
/// - rpc: The current RPC to work with.
/// - user: The user requesting an invite.
/// - secret: The spectate secret for the request.
func rpcDidReceiveJoinRequest(
_ rpc: SwordRPC,
request: JoinRequest,
user: PartialUser,
secret: String
)
}
/// A dummy extension providing empty, default functions for our protocol.
/// We do this to avoid using optional, as it forces our functions to be @objc.
public extension SwordRPCDelegate {
func swordRPCDidConnect(
_: SwordRPC
) {}
func swordRPCDidDisconnect(
_: SwordRPC,
code _: Int?,
message _: String?
) {}
func swordRPCDidReceiveError(
_: SwordRPC,
code _: Int,
message _: String
) {}
func swordRPCDidJoinGame(
_: SwordRPC,
secret _: String
) {}
func swordRPCDidSpectateGame(
_: SwordRPC,
secret _: String
) {}
func swordRPCDidReceiveJoinRequest(
_: SwordRPC,
request _: JoinRequest,
secret _: String
) {}
func rpcDidConnect(_: SwordRPC) {}
func rpcDidDisconnect(_: SwordRPC, code _: Int?, message _: String?) {}
func rpcDidReceiveError(_: SwordRPC, code _: Int, message _: String) {}
func rpcDidJoinGame(_: SwordRPC, secret _: String) {}
func rpcDidSpectateGame(_: SwordRPC, secret _: String) {}
func rpcDidReceiveJoinRequest(_: SwordRPC, user _: PartialUser, secret _: String) {}
}
......@@ -11,21 +11,25 @@ import Foundation
extension SwordRPC {
/// Sends a handshake to begin RPC interaction.
func handshake() throws {
let response = Handshake(version: 1, clientId: appId)
let response = AuthorizationRequest(version: 1, clientId: appId)
try send(response, opcode: .handshake)
}
func subscribe(_ event: Event) {
let response = GenericResponse(cmd: .subscribe, evt: event)
try? send(response)
/// Emits a subscribe request for the given command type.
/// https://discord.com/developers/docs/topics/rpc#subscribe
/// - Parameter type: The event type to subscribe for.
func subscribe(_ type: EventType) {
let command = Command(cmd: .subscribe, evt: type)
try? send(command)
}
/// Handles incoming events from Discord.
/// - Parameter payload: JSON given over IPC.
func handleEvent(_ payload: String) {
var data = decode(payload)
guard let evt = data["evt"] as? String,
let event = Event(rawValue: evt)
let event = EventType(rawValue: evt)
else {
return
}
......@@ -36,37 +40,37 @@ extension SwordRPC {
case .error:
let code = data["code"] as! Int
let message = data["message"] as! String
errorHandler?(self, code, message)
delegate?.swordRPCDidReceiveError(self, code: code, message: message)
delegate?.rpcDidReceiveError(self, code: code, message: message)
case .join:
let secret = data["secret"] as! String
joinGameHandler?(self, secret)
delegate?.swordRPCDidJoinGame(self, secret: secret)
delegate?.rpcDidJoinGame(self, secret: secret)
case .joinRequest:
let requestData = data["user"] as! [String: Any]
// let joinRequest = try! decoder.decode(
// JoinRequest.self, from: encode(requestData)
// )
// TODO: Resolve
let joinRequest = JoinRequest(avatar: "0", discriminator: "0000", userId: "0", username: "0")
let user = data["user"] as! [String: String]
// TODO: can we properly decode this without doing this manually?
let joinRequest = PartialUser(
avatar: user["avatar"]!,
discriminator: user["discriminator"]!,
userId: user["id"]!,
username: user["username"]!
)
let secret = data["secret"] as! String
joinRequestHandler?(self, joinRequest, secret)
delegate?.swordRPCDidReceiveJoinRequest(self, request: joinRequest, secret: secret)
delegate?.rpcDidReceiveJoinRequest(self, user: joinRequest, secret: secret)
case .ready:
connectHandler?(self)
delegate?.swordRPCDidConnect(self)
delegate?.rpcDidConnect(self)
updatePresence()
case .spectate:
let secret = data["secret"] as! String
spectateGameHandler?(self, secret)
delegate?.swordRPCDidSpectateGame(self, secret: secret)
delegate?.rpcDidSpectateGame(self, secret: secret)
}
}
/// Updates the presence.
func updatePresence() {
worker.asyncAfter(deadline: .now() + .seconds(15)) { [unowned self] in
self.updatePresence()
......@@ -77,18 +81,12 @@ extension SwordRPC {
self.presence = nil
let json = """
{
"cmd": "SET_ACTIVITY",
"args": {
"pid": \(self.pid),
"activity": \(String(data: try! self.encoder.encode(presence), encoding: .utf8)!)
},
"nonce": "\(UUID().uuidString)"
}
"""
let command = Command(cmd: .setActivity, args: [
"pid": .int(Int(self.pid)),
"activity": .activity(presence),
])
try? self.send(json: json)
try? self.send(command)
}
}
}
......@@ -75,22 +75,31 @@ public class SwordRPC {
print("[SwordRPC] Discord not detected")
}
/// Sets the presence for this RPC connection.
/// - Parameter presence: The presence to display.
public func setPresence(_ presence: RichPresence) {
self.presence = presence
}
public func reply(to request: JoinRequest, with reply: JoinReply) {
let json = """
{
"cmd": "\(
reply == .yes ? "SEND_ACTIVITY_JOIN_INVITE" : "CLOSE_ACTIVITY_JOIN_REQUEST"
)",
"args": {
"user_id": "\(request.userId)"
}
/// Replies to an activity join request.
/// - Parameters:
/// - user: The user making the request
/// - reply: Whether to accept or decline the request.
public func reply(to user: PartialUser, with reply: JoinReply) {
var type: CommandType
switch reply {
case .yes:
type = .sendActivityJoinInvite
case .ignore, .no:
type = .closeActivityJoinRequest
}
"""
try? send(json: json)
// We must give Discord the requesting user's ID to handle.
let command = Command(cmd: type, args: [
"user_id": .string(user.userId),
])
try? send(command)
}
}
......@@ -6,27 +6,29 @@
// Copyright © 2017 Alejandro Alonso. All rights reserved.
//
enum Event: String, Codable {
case error = "ERROR"
case join = "ACTIVITY_JOIN"
case joinRequest = "ACTIVITY_JOIN_REQUEST"
case ready = "READY"
case spectate = "ACTIVITY_SPECTATE"
}
enum Command: String, Codable {
/// Command types to send over RPC.
/// https://discord.com/developers/docs/topics/rpc#commands-and-events-rpc-commands
enum CommandType: String, Codable {
case dispatch = "DISPATCH"
case authorize = "AUTHORIZE"
case subscribe = "SUBSCRIBE"
case setActivity = "SET_ACTIVITY"
case sendActivityJoinInvite = "SEND_ACTIVITY_JOIN_INVITE"
case closeActivityJoinRequest = "CLOSE_ACTIVITY_JOIN_REQUEST"
}
struct Response: Decodable {
let cmd: Command
let evt: Event
let data: [String: String]?
/// Possible event types.
/// https://discord.com/developers/docs/topics/rpc#commands-and-events-rpc-events
enum EventType: String, Codable {
case error = "ERROR"
case join = "ACTIVITY_JOIN"
case joinRequest = "ACTIVITY_JOIN_REQUEST"
case ready = "READY"
case spectate = "ACTIVITY_SPECTATE"
}
public enum JoinReply: Int {
/// An enum for a reply to a join request, defining yes or ignore/no types.
public enum JoinReply {
case no
case yes
case ignore
......
......@@ -6,7 +6,9 @@
// Copyright © 2017 Alejandro Alonso. All rights reserved.
//
public struct JoinRequest: Decodable {
/// Used to represent a partial user given by Discord.
/// For example: https://discord.com/developers/docs/topics/rpc#activityjoinrequest-example-activity-join-request-dispatch-payload
public struct PartialUser: Decodable {
let avatar: String
let discriminator: String
let userId: String
......
//
// Requests.swift
//
//
// Created by Spotlight Deveaux on 2022-01-17.
//
import Foundation
/// Describes the format needed for an authorization request.
/// https://discord.com/developers/docs/topics/rpc#authenticating-rpc-authorize-example
struct AuthorizationRequest: Encodable {
let version: Int
let clientId: String
enum CodingKeys: String, CodingKey {
case version = "v"
case clientId = "client_id"
}
}
/// RequestArg permits a union-like type for arguments to encode.
enum RequestArg: Encodable {
/// An integer value.
case int(Int)
/// A string value.
case string(String)
/// An activity value.
case activity(RichPresence)
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .int(int):
try container.encode(int)
case let .string(string):
try container.encode(string)
case let .activity(presence):
try container.encode(presence)
}
}
}
/// A generic format for a payload with a command, possibly used for an event.
struct Command: Encodable {
/// The type of command to issue to Discord. For normal events, this should be .dispatch.
let cmd: CommandType
/// The nonce for this command. It should typically be an automatically generated UUID.
let nonce: String = UUID().uuidString
/// Arguments sent alongside the command.
var args: [String: RequestArg]?
/// The event type this command pertains to, if needed.
var evt: EventType?
}
/// A generic format for sending an event.
struct Event: Encodable {
/// The event type to handle.
let eventType: EventType
/// Arguments sent alongside the event.
var args: [String: RequestArg]?
/// Convenience initializer to create an event with the given type.
init(_ event: EventType) {
eventType = event
}
/// Convenience initializer to create an event with the given type and arguments.
init(_ event: EventType, args: [String: RequestArg]?) {
eventType = event
self.args = args
}
func encode(to encoder: Encoder) throws {
// All events are dispatched.
var command = Command(cmd: .dispatch, args: args)
command.evt = eventType
try command.encode(to: encoder)
}
}
//
// Responses.swift
//
//
// Created by Spotlight Deveaux on 2022-01-17.
//
import Foundation
struct Handshake: Encodable {
let version: Int
let clientId: String
enum CodingKeys: String, CodingKey {
case version = "v"
case clientId = "client_id"
}
}
struct GenericResponse: Encodable {
let cmd: Command
let evt: Event
let nonce: String = UUID().uuidString
}
......@@ -33,11 +33,6 @@ extension SwordRPC {
let data = try response.toJSON()
try client?.send(data: data, opcode: opcode)
}
/// Sends the given JSON string.
func send(json: String) throws {
try client?.send(data: json, opcode: .frame)
}
}
extension Encodable {
......
......@@ -5,7 +5,6 @@
// Created by Spotlight Deveaux on 2022-01-17.
//
import CryptoKit
import Foundation
import NIOCore
import NIOPosix
......
......@@ -17,6 +17,7 @@ enum IPCOpcode: UInt32 {
case pong = 4
}
/// A structure for the IPC payload.
struct IPCPayload {
let opcode: IPCOpcode
let payload: String
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment