├── 0xWDG.xcworkspace ├── contents.xcworkspacedata ├── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ │ └── Package.resolved └── xcuserdata │ └── wesley.xcuserdatad │ └── UserInterfaceState.xcuserstate ├── Discord-Bridging-Header.h ├── Discord.swift ├── DiscordExtension.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── wesley.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── DiscordExtension.xcscheme └── xcuserdata │ └── wesley.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── DiscordIPC.swift ├── README.md └── SwordRPC ├── Delegate.swift ├── Presence.swift ├── RPC.swift ├── SwordRPC.swift ├── Types ├── Enums.swift ├── JoinRequest.swift ├── Requests.swift └── RichPresence.swift ├── Utils.swift └── WebSockets ├── ConnectionClient.swift ├── ConnectionHandler.swift ├── IPCHandler.swift └── IPCPayload.swift /0xWDG.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /0xWDG.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /0xWDG.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-atomics", 6 | "repositoryURL": "https://github.com/apple/swift-atomics.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", 10 | "version": "1.0.2" 11 | } 12 | }, 13 | { 14 | "package": "swift-collections", 15 | "repositoryURL": "https://github.com/apple/swift-collections.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "f504716c27d2e5d4144fa4794b12129301d17729", 19 | "version": "1.0.3" 20 | } 21 | }, 22 | { 23 | "package": "swift-nio", 24 | "repositoryURL": "https://github.com/apple/swift-nio.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "bc4c55b9f9584f09eb971d67d956e28d08caa9d0", 28 | "version": "2.43.1" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /0xWDG.xcworkspace/xcuserdata/wesley.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuroraEditor/Extension-Discord/b33ede7359eace462999c9b6bec2f93a3ad5e937/0xWDG.xcworkspace/xcuserdata/wesley.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Discord-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /Discord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Discord.swift 3 | // DiscordExtension 4 | // 5 | // Created by Wesley de Groot on 07/10/2022. 6 | // 7 | 8 | import Foundation 9 | import AEExtensionKit 10 | 11 | public class DiscordExtension: ExtensionInterface { 12 | let rpc = SwordRPC(appId: "1023938668349640734") 13 | var api: ExtensionAPI 14 | var AuroraAPI: AuroraAPI = { _, _ in } 15 | 16 | init(api: ExtensionAPI) { 17 | rpc.connect() 18 | self.api = api 19 | print("Hello from Discord EXT: \(api)!") 20 | } 21 | 22 | public func register() -> ExtensionManifest { 23 | return .init( 24 | name: "Discord", 25 | displayName: "Discord", 26 | version: "1.0", 27 | minAEVersion: "1.0" 28 | ) 29 | } 30 | 31 | public func respond(action: String, parameters: [String: Any]) -> Bool { 32 | print("respond(action: String, parameters: [String: Any])", action, parameters) 33 | 34 | if action == "didOpen" { 35 | if let workspace = parameters["workspace"] as? String, 36 | let file = parameters["file"] as? String { 37 | print("Setting discord status") 38 | setDiscordStatusTo(project: workspace, custom: file) 39 | } 40 | } 41 | 42 | if action == "registerCallback" { 43 | if let api = parameters["callback"] as? AuroraAPI { 44 | AuroraAPI = api 45 | } 46 | 47 | print("Idling...") 48 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 49 | print("Idling... SET") 50 | self.setDiscordStatusTo( 51 | project: "file://Aurora Editor", 52 | custom: "file://idling" 53 | ) 54 | } 55 | } 56 | 57 | return true 58 | } 59 | 60 | func setDiscordStatusTo(project: String, custom: String) { 61 | rpc.clearPresence() 62 | 63 | let pURL = NSURL(string: project)?.lastPathComponent 64 | let cURL = NSURL(string: custom)?.lastPathComponent 65 | 66 | let fileIcon = NSURL(string: custom)?.pathExtension ?? "Unknown" 67 | 68 | var presence = RichPresence() 69 | // Large (File) icon 70 | presence.assets.largeImage = fileIcon.lowercased() 71 | presence.assets.largeText = "\(fileIcon.uppercased()) File" 72 | 73 | // Small (AE) Icon 74 | presence.assets.smallImage = "auroraeditor" 75 | presence.assets.smallText = "AuroraEditor" 76 | 77 | // Project name 78 | presence.details = pURL ?? project 79 | 80 | // File name 81 | presence.state = cURL ?? custom 82 | 83 | rpc.setPresence(presence) 84 | } 85 | } 86 | 87 | @objc(DiscordExtensionBuilder) 88 | public class DiscordExtensionBuilder: ExtensionBuilder { 89 | public override func build(withAPI api: ExtensionAPI) -> ExtensionInterface { 90 | return DiscordExtension(api: api) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DiscordExtension.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0463E50B27F749CD00806D5C /* Discord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0463E50A27F749CD00806D5C /* Discord.swift */; }; 11 | 2B20A83D28EF4C5B00D14316 /* AEExtensionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2B20A83C28EF4C5B00D14316 /* AEExtensionKit */; }; 12 | 2B2ACB7128FE9EDD00B5B3C6 /* AEExtensionKit in Resources */ = {isa = PBXBuildFile; fileRef = 2B5C5D7D28F0672E00C9334D /* AEExtensionKit */; }; 13 | 2B421AE628FE7F55004E761B /* AEExtensionKit in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2B20A83C28EF4C5B00D14316 /* AEExtensionKit */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 14 | 2B5C5D6628F05EC200C9334D /* DiscordIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D5528F05EC200C9334D /* DiscordIPC.swift */; }; 15 | 2B5C5D6728F05EC200C9334D /* RichPresence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D5828F05EC200C9334D /* RichPresence.swift */; }; 16 | 2B5C5D6828F05EC200C9334D /* Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D5928F05EC200C9334D /* Enums.swift */; }; 17 | 2B5C5D6928F05EC200C9334D /* Requests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D5A28F05EC200C9334D /* Requests.swift */; }; 18 | 2B5C5D6A28F05EC200C9334D /* JoinRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D5B28F05EC200C9334D /* JoinRequest.swift */; }; 19 | 2B5C5D6B28F05EC200C9334D /* Presence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D5C28F05EC200C9334D /* Presence.swift */; }; 20 | 2B5C5D6C28F05EC200C9334D /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D5D28F05EC200C9334D /* Delegate.swift */; }; 21 | 2B5C5D6D28F05EC200C9334D /* ConnectionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D5F28F05EC200C9334D /* ConnectionClient.swift */; }; 22 | 2B5C5D6E28F05EC200C9334D /* IPCHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D6028F05EC200C9334D /* IPCHandler.swift */; }; 23 | 2B5C5D6F28F05EC200C9334D /* ConnectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D6128F05EC200C9334D /* ConnectionHandler.swift */; }; 24 | 2B5C5D7028F05EC200C9334D /* IPCPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D6228F05EC200C9334D /* IPCPayload.swift */; }; 25 | 2B5C5D7128F05EC200C9334D /* SwordRPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D6328F05EC200C9334D /* SwordRPC.swift */; }; 26 | 2B5C5D7228F05EC200C9334D /* RPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D6428F05EC200C9334D /* RPC.swift */; }; 27 | 2B5C5D7328F05EC200C9334D /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C5D6528F05EC200C9334D /* Utils.swift */; }; 28 | 2B5C5D7628F05F0E00C9334D /* Atomics in Frameworks */ = {isa = PBXBuildFile; productRef = 2B5C5D7528F05F0E00C9334D /* Atomics */; }; 29 | 2B5C5D7928F05F3C00C9334D /* NIO in Frameworks */ = {isa = PBXBuildFile; productRef = 2B5C5D7828F05F3C00C9334D /* NIO */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXCopyFilesBuildPhase section */ 33 | 04C325562801B3E900C8DA2D /* Embed Frameworks */ = { 34 | isa = PBXCopyFilesBuildPhase; 35 | buildActionMask = 2147483647; 36 | dstPath = ""; 37 | dstSubfolderSpec = 10; 38 | files = ( 39 | 2B421AE628FE7F55004E761B /* AEExtensionKit in Embed Frameworks */, 40 | ); 41 | name = "Embed Frameworks"; 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXCopyFilesBuildPhase section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 0463E4FF27F7492100806D5C /* DiscordExtension.AEext */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DiscordExtension.AEext; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 0463E50927F749CC00806D5C /* Discord-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Discord-Bridging-Header.h"; sourceTree = ""; }; 49 | 0463E50A27F749CD00806D5C /* Discord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Discord.swift; sourceTree = ""; }; 50 | 2B5C5D5528F05EC200C9334D /* DiscordIPC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscordIPC.swift; sourceTree = ""; }; 51 | 2B5C5D5828F05EC200C9334D /* RichPresence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichPresence.swift; sourceTree = ""; }; 52 | 2B5C5D5928F05EC200C9334D /* Enums.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Enums.swift; sourceTree = ""; }; 53 | 2B5C5D5A28F05EC200C9334D /* Requests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Requests.swift; sourceTree = ""; }; 54 | 2B5C5D5B28F05EC200C9334D /* JoinRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinRequest.swift; sourceTree = ""; }; 55 | 2B5C5D5C28F05EC200C9334D /* Presence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Presence.swift; sourceTree = ""; }; 56 | 2B5C5D5D28F05EC200C9334D /* Delegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; 57 | 2B5C5D5F28F05EC200C9334D /* ConnectionClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionClient.swift; sourceTree = ""; }; 58 | 2B5C5D6028F05EC200C9334D /* IPCHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPCHandler.swift; sourceTree = ""; }; 59 | 2B5C5D6128F05EC200C9334D /* ConnectionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionHandler.swift; sourceTree = ""; }; 60 | 2B5C5D6228F05EC200C9334D /* IPCPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPCPayload.swift; sourceTree = ""; }; 61 | 2B5C5D6328F05EC200C9334D /* SwordRPC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwordRPC.swift; sourceTree = ""; }; 62 | 2B5C5D6428F05EC200C9334D /* RPC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RPC.swift; sourceTree = ""; }; 63 | 2B5C5D6528F05EC200C9334D /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 64 | 2B5C5D7D28F0672E00C9334D /* AEExtensionKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AEExtensionKit; path = ../AEExtensionKit; sourceTree = ""; }; 65 | /* End PBXFileReference section */ 66 | 67 | /* Begin PBXFrameworksBuildPhase section */ 68 | 0463E4FC27F7492100806D5C /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | 2B5C5D7928F05F3C00C9334D /* NIO in Frameworks */, 73 | 2B20A83D28EF4C5B00D14316 /* AEExtensionKit in Frameworks */, 74 | 2B5C5D7628F05F0E00C9334D /* Atomics in Frameworks */, 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | /* End PBXFrameworksBuildPhase section */ 79 | 80 | /* Begin PBXGroup section */ 81 | 0463E4F627F7492100806D5C = { 82 | isa = PBXGroup; 83 | children = ( 84 | 2B5C5D7D28F0672E00C9334D /* AEExtensionKit */, 85 | 2B5C5D5528F05EC200C9334D /* DiscordIPC.swift */, 86 | 2B5C5D5628F05EC200C9334D /* SwordRPC */, 87 | 0463E50A27F749CD00806D5C /* Discord.swift */, 88 | 0463E50027F7492100806D5C /* Products */, 89 | 0463E50927F749CC00806D5C /* Discord-Bridging-Header.h */, 90 | 04C325522801B3E800C8DA2D /* Frameworks */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | 0463E50027F7492100806D5C /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 0463E4FF27F7492100806D5C /* DiscordExtension.AEext */, 98 | ); 99 | name = Products; 100 | sourceTree = ""; 101 | }; 102 | 04C325522801B3E800C8DA2D /* Frameworks */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | ); 106 | name = Frameworks; 107 | sourceTree = ""; 108 | }; 109 | 2B5C5D5628F05EC200C9334D /* SwordRPC */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 2B5C5D5728F05EC200C9334D /* Types */, 113 | 2B5C5D5C28F05EC200C9334D /* Presence.swift */, 114 | 2B5C5D5D28F05EC200C9334D /* Delegate.swift */, 115 | 2B5C5D5E28F05EC200C9334D /* WebSockets */, 116 | 2B5C5D6328F05EC200C9334D /* SwordRPC.swift */, 117 | 2B5C5D6428F05EC200C9334D /* RPC.swift */, 118 | 2B5C5D6528F05EC200C9334D /* Utils.swift */, 119 | ); 120 | path = SwordRPC; 121 | sourceTree = ""; 122 | }; 123 | 2B5C5D5728F05EC200C9334D /* Types */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 2B5C5D5828F05EC200C9334D /* RichPresence.swift */, 127 | 2B5C5D5928F05EC200C9334D /* Enums.swift */, 128 | 2B5C5D5A28F05EC200C9334D /* Requests.swift */, 129 | 2B5C5D5B28F05EC200C9334D /* JoinRequest.swift */, 130 | ); 131 | path = Types; 132 | sourceTree = ""; 133 | }; 134 | 2B5C5D5E28F05EC200C9334D /* WebSockets */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 2B5C5D5F28F05EC200C9334D /* ConnectionClient.swift */, 138 | 2B5C5D6028F05EC200C9334D /* IPCHandler.swift */, 139 | 2B5C5D6128F05EC200C9334D /* ConnectionHandler.swift */, 140 | 2B5C5D6228F05EC200C9334D /* IPCPayload.swift */, 141 | ); 142 | path = WebSockets; 143 | sourceTree = ""; 144 | }; 145 | /* End PBXGroup section */ 146 | 147 | /* Begin PBXNativeTarget section */ 148 | 0463E4FE27F7492100806D5C /* DiscordExtension */ = { 149 | isa = PBXNativeTarget; 150 | buildConfigurationList = 0463E50327F7492100806D5C /* Build configuration list for PBXNativeTarget "DiscordExtension" */; 151 | buildPhases = ( 152 | 0463E4FB27F7492100806D5C /* Sources */, 153 | 0463E4FC27F7492100806D5C /* Frameworks */, 154 | 0463E4FD27F7492100806D5C /* Resources */, 155 | 04C325562801B3E900C8DA2D /* Embed Frameworks */, 156 | 2B5C5D5328F0597500C9334D /* Install to AuroraEditor */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | ); 162 | name = DiscordExtension; 163 | packageProductDependencies = ( 164 | 2B20A83C28EF4C5B00D14316 /* AEExtensionKit */, 165 | 2B5C5D7528F05F0E00C9334D /* Atomics */, 166 | 2B5C5D7828F05F3C00C9334D /* NIO */, 167 | ); 168 | productName = HelloWorldExtension; 169 | productReference = 0463E4FF27F7492100806D5C /* DiscordExtension.AEext */; 170 | productType = "com.apple.product-type.bundle"; 171 | }; 172 | /* End PBXNativeTarget section */ 173 | 174 | /* Begin PBXProject section */ 175 | 0463E4F727F7492100806D5C /* Project object */ = { 176 | isa = PBXProject; 177 | attributes = { 178 | BuildIndependentTargetsInParallel = 1; 179 | LastUpgradeCheck = 1400; 180 | TargetAttributes = { 181 | 0463E4FE27F7492100806D5C = { 182 | CreatedOnToolsVersion = 13.2.1; 183 | LastSwiftMigration = 1320; 184 | }; 185 | }; 186 | }; 187 | buildConfigurationList = 0463E4FA27F7492100806D5C /* Build configuration list for PBXProject "DiscordExtension" */; 188 | compatibilityVersion = "Xcode 13.0"; 189 | developmentRegion = en; 190 | hasScannedForEncodings = 0; 191 | knownRegions = ( 192 | en, 193 | Base, 194 | ); 195 | mainGroup = 0463E4F627F7492100806D5C; 196 | packageReferences = ( 197 | 2B20A83B28EF4C5B00D14316 /* XCRemoteSwiftPackageReference "AEExtensionKit" */, 198 | 2B5C5D7428F05F0E00C9334D /* XCRemoteSwiftPackageReference "swift-atomics" */, 199 | 2B5C5D7728F05F3C00C9334D /* XCRemoteSwiftPackageReference "swift-nio" */, 200 | ); 201 | productRefGroup = 0463E50027F7492100806D5C /* Products */; 202 | projectDirPath = ""; 203 | projectRoot = ""; 204 | targets = ( 205 | 0463E4FE27F7492100806D5C /* DiscordExtension */, 206 | ); 207 | }; 208 | /* End PBXProject section */ 209 | 210 | /* Begin PBXResourcesBuildPhase section */ 211 | 0463E4FD27F7492100806D5C /* Resources */ = { 212 | isa = PBXResourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 2B2ACB7128FE9EDD00B5B3C6 /* AEExtensionKit in Resources */, 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | }; 219 | /* End PBXResourcesBuildPhase section */ 220 | 221 | /* Begin PBXShellScriptBuildPhase section */ 222 | 2B5C5D5328F0597500C9334D /* Install to AuroraEditor */ = { 223 | isa = PBXShellScriptBuildPhase; 224 | alwaysOutOfDate = 1; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | ); 228 | inputFileListPaths = ( 229 | ); 230 | inputPaths = ( 231 | ); 232 | name = "Install to AuroraEditor"; 233 | outputFileListPaths = ( 234 | ); 235 | outputPaths = ( 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | shellPath = /bin/sh; 239 | shellScript = "killall AuroraEditor\ncp -f -R ${BUILT_PRODUCTS_DIR}/*.AEext ~/Library/Application\\ Support/com.auroraeditor/Extensions/\n"; 240 | }; 241 | /* End PBXShellScriptBuildPhase section */ 242 | 243 | /* Begin PBXSourcesBuildPhase section */ 244 | 0463E4FB27F7492100806D5C /* Sources */ = { 245 | isa = PBXSourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | 0463E50B27F749CD00806D5C /* Discord.swift in Sources */, 249 | 2B5C5D7228F05EC200C9334D /* RPC.swift in Sources */, 250 | 2B5C5D7128F05EC200C9334D /* SwordRPC.swift in Sources */, 251 | 2B5C5D7028F05EC200C9334D /* IPCPayload.swift in Sources */, 252 | 2B5C5D6A28F05EC200C9334D /* JoinRequest.swift in Sources */, 253 | 2B5C5D7328F05EC200C9334D /* Utils.swift in Sources */, 254 | 2B5C5D6628F05EC200C9334D /* DiscordIPC.swift in Sources */, 255 | 2B5C5D6E28F05EC200C9334D /* IPCHandler.swift in Sources */, 256 | 2B5C5D6828F05EC200C9334D /* Enums.swift in Sources */, 257 | 2B5C5D6C28F05EC200C9334D /* Delegate.swift in Sources */, 258 | 2B5C5D6928F05EC200C9334D /* Requests.swift in Sources */, 259 | 2B5C5D6728F05EC200C9334D /* RichPresence.swift in Sources */, 260 | 2B5C5D6D28F05EC200C9334D /* ConnectionClient.swift in Sources */, 261 | 2B5C5D6F28F05EC200C9334D /* ConnectionHandler.swift in Sources */, 262 | 2B5C5D6B28F05EC200C9334D /* Presence.swift in Sources */, 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | /* End PBXSourcesBuildPhase section */ 267 | 268 | /* Begin XCBuildConfiguration section */ 269 | 0463E50127F7492100806D5C /* Debug */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ALWAYS_SEARCH_USER_PATHS = NO; 273 | CLANG_ANALYZER_NONNULL = YES; 274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 276 | CLANG_CXX_LIBRARY = "libc++"; 277 | CLANG_ENABLE_MODULES = YES; 278 | CLANG_ENABLE_OBJC_ARC = YES; 279 | CLANG_ENABLE_OBJC_WEAK = YES; 280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_COMMA = YES; 283 | CLANG_WARN_CONSTANT_CONVERSION = YES; 284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 287 | CLANG_WARN_EMPTY_BODY = YES; 288 | CLANG_WARN_ENUM_CONVERSION = YES; 289 | CLANG_WARN_INFINITE_RECURSION = YES; 290 | CLANG_WARN_INT_CONVERSION = YES; 291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 296 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 297 | CLANG_WARN_STRICT_PROTOTYPES = YES; 298 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 299 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 300 | CLANG_WARN_UNREACHABLE_CODE = YES; 301 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 302 | COPY_PHASE_STRIP = NO; 303 | DEAD_CODE_STRIPPING = YES; 304 | DEBUG_INFORMATION_FORMAT = dwarf; 305 | ENABLE_STRICT_OBJC_MSGSEND = YES; 306 | ENABLE_TESTABILITY = YES; 307 | GCC_C_LANGUAGE_STANDARD = gnu11; 308 | GCC_DYNAMIC_NO_PIC = NO; 309 | GCC_NO_COMMON_BLOCKS = YES; 310 | GCC_OPTIMIZATION_LEVEL = 0; 311 | GCC_PREPROCESSOR_DEFINITIONS = ( 312 | "DEBUG=1", 313 | "$(inherited)", 314 | ); 315 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 316 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 317 | GCC_WARN_UNDECLARED_SELECTOR = YES; 318 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 319 | GCC_WARN_UNUSED_FUNCTION = YES; 320 | GCC_WARN_UNUSED_VARIABLE = YES; 321 | MACOSX_DEPLOYMENT_TARGET = 12.1; 322 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 323 | MTL_FAST_MATH = YES; 324 | ONLY_ACTIVE_ARCH = YES; 325 | SDKROOT = macosx; 326 | }; 327 | name = Debug; 328 | }; 329 | 0463E50227F7492100806D5C /* Release */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ALWAYS_SEARCH_USER_PATHS = NO; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_ENABLE_OBJC_WEAK = YES; 340 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 341 | CLANG_WARN_BOOL_CONVERSION = YES; 342 | CLANG_WARN_COMMA = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 345 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 346 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 356 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 357 | CLANG_WARN_STRICT_PROTOTYPES = YES; 358 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 359 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 360 | CLANG_WARN_UNREACHABLE_CODE = YES; 361 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 362 | COPY_PHASE_STRIP = NO; 363 | DEAD_CODE_STRIPPING = YES; 364 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 365 | ENABLE_NS_ASSERTIONS = NO; 366 | ENABLE_STRICT_OBJC_MSGSEND = YES; 367 | GCC_C_LANGUAGE_STANDARD = gnu11; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | MACOSX_DEPLOYMENT_TARGET = 12.1; 376 | MTL_ENABLE_DEBUG_INFO = NO; 377 | MTL_FAST_MATH = YES; 378 | ONLY_ACTIVE_ARCH = NO; 379 | SDKROOT = macosx; 380 | SWIFT_COMPILATION_MODE = wholemodule; 381 | }; 382 | name = Release; 383 | }; 384 | 0463E50427F7492100806D5C /* Debug */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | CLANG_ENABLE_MODULES = YES; 388 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 389 | CODE_SIGN_STYLE = Automatic; 390 | COMBINE_HIDPI_IMAGES = YES; 391 | CURRENT_PROJECT_VERSION = 1; 392 | DEAD_CODE_STRIPPING = YES; 393 | DEVELOPMENT_TEAM = ""; 394 | GENERATE_INFOPLIST_FILE = YES; 395 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 396 | INFOPLIST_KEY_NSPrincipalClass = DiscordExtensionBuilder; 397 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; 398 | LD_RUNPATH_SEARCH_PATHS = ( 399 | "$(inherited)", 400 | "@executable_path/../Frameworks", 401 | "@loader_path/../Frameworks", 402 | ); 403 | MARKETING_VERSION = 1.0; 404 | PRODUCT_BUNDLE_IDENTIFIER = com.auroraeditor.Discord; 405 | PRODUCT_NAME = "$(TARGET_NAME)"; 406 | SKIP_INSTALL = YES; 407 | SWIFT_EMIT_LOC_STRINGS = YES; 408 | SWIFT_OBJC_BRIDGING_HEADER = "Discord-Bridging-Header.h"; 409 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 410 | SWIFT_VERSION = 5.0; 411 | WRAPPER_EXTENSION = AEext; 412 | }; 413 | name = Debug; 414 | }; 415 | 0463E50527F7492100806D5C /* Release */ = { 416 | isa = XCBuildConfiguration; 417 | buildSettings = { 418 | CLANG_ENABLE_MODULES = YES; 419 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 420 | CODE_SIGN_STYLE = Automatic; 421 | COMBINE_HIDPI_IMAGES = YES; 422 | CURRENT_PROJECT_VERSION = 1; 423 | DEAD_CODE_STRIPPING = YES; 424 | DEVELOPMENT_TEAM = ""; 425 | GENERATE_INFOPLIST_FILE = YES; 426 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 427 | INFOPLIST_KEY_NSPrincipalClass = DiscordExtensionBuilder; 428 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; 429 | LD_RUNPATH_SEARCH_PATHS = ( 430 | "$(inherited)", 431 | "@executable_path/../Frameworks", 432 | "@loader_path/../Frameworks", 433 | ); 434 | MARKETING_VERSION = 1.0; 435 | PRODUCT_BUNDLE_IDENTIFIER = com.auroraeditor.Discord; 436 | PRODUCT_NAME = "$(TARGET_NAME)"; 437 | SKIP_INSTALL = YES; 438 | SWIFT_EMIT_LOC_STRINGS = YES; 439 | SWIFT_OBJC_BRIDGING_HEADER = "Discord-Bridging-Header.h"; 440 | SWIFT_VERSION = 5.0; 441 | WRAPPER_EXTENSION = AEext; 442 | }; 443 | name = Release; 444 | }; 445 | /* End XCBuildConfiguration section */ 446 | 447 | /* Begin XCConfigurationList section */ 448 | 0463E4FA27F7492100806D5C /* Build configuration list for PBXProject "DiscordExtension" */ = { 449 | isa = XCConfigurationList; 450 | buildConfigurations = ( 451 | 0463E50127F7492100806D5C /* Debug */, 452 | 0463E50227F7492100806D5C /* Release */, 453 | ); 454 | defaultConfigurationIsVisible = 0; 455 | defaultConfigurationName = Release; 456 | }; 457 | 0463E50327F7492100806D5C /* Build configuration list for PBXNativeTarget "DiscordExtension" */ = { 458 | isa = XCConfigurationList; 459 | buildConfigurations = ( 460 | 0463E50427F7492100806D5C /* Debug */, 461 | 0463E50527F7492100806D5C /* Release */, 462 | ); 463 | defaultConfigurationIsVisible = 0; 464 | defaultConfigurationName = Release; 465 | }; 466 | /* End XCConfigurationList section */ 467 | 468 | /* Begin XCRemoteSwiftPackageReference section */ 469 | 2B20A83B28EF4C5B00D14316 /* XCRemoteSwiftPackageReference "AEExtensionKit" */ = { 470 | isa = XCRemoteSwiftPackageReference; 471 | repositoryURL = "https://github.com/AuroraEditor/AEExtensionKit.git"; 472 | requirement = { 473 | branch = main; 474 | kind = branch; 475 | }; 476 | }; 477 | 2B5C5D7428F05F0E00C9334D /* XCRemoteSwiftPackageReference "swift-atomics" */ = { 478 | isa = XCRemoteSwiftPackageReference; 479 | repositoryURL = "https://github.com/apple/swift-atomics.git"; 480 | requirement = { 481 | kind = upToNextMajorVersion; 482 | minimumVersion = 1.0.0; 483 | }; 484 | }; 485 | 2B5C5D7728F05F3C00C9334D /* XCRemoteSwiftPackageReference "swift-nio" */ = { 486 | isa = XCRemoteSwiftPackageReference; 487 | repositoryURL = "https://github.com/apple/swift-nio.git"; 488 | requirement = { 489 | kind = upToNextMajorVersion; 490 | minimumVersion = 2.0.0; 491 | }; 492 | }; 493 | /* End XCRemoteSwiftPackageReference section */ 494 | 495 | /* Begin XCSwiftPackageProductDependency section */ 496 | 2B20A83C28EF4C5B00D14316 /* AEExtensionKit */ = { 497 | isa = XCSwiftPackageProductDependency; 498 | package = 2B20A83B28EF4C5B00D14316 /* XCRemoteSwiftPackageReference "AEExtensionKit" */; 499 | productName = AEExtensionKit; 500 | }; 501 | 2B5C5D7528F05F0E00C9334D /* Atomics */ = { 502 | isa = XCSwiftPackageProductDependency; 503 | package = 2B5C5D7428F05F0E00C9334D /* XCRemoteSwiftPackageReference "swift-atomics" */; 504 | productName = Atomics; 505 | }; 506 | 2B5C5D7828F05F3C00C9334D /* NIO */ = { 507 | isa = XCSwiftPackageProductDependency; 508 | package = 2B5C5D7728F05F3C00C9334D /* XCRemoteSwiftPackageReference "swift-nio" */; 509 | productName = NIO; 510 | }; 511 | /* End XCSwiftPackageProductDependency section */ 512 | }; 513 | rootObject = 0463E4F727F7492100806D5C /* Project object */; 514 | } 515 | -------------------------------------------------------------------------------- /DiscordExtension.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DiscordExtension.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DiscordExtension.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-atomics", 6 | "repositoryURL": "https://github.com/apple/swift-atomics.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "ff3d2212b6b093db7f177d0855adbc4ef9c5f036", 10 | "version": "1.0.3" 11 | } 12 | }, 13 | { 14 | "package": "swift-collections", 15 | "repositoryURL": "https://github.com/apple/swift-collections.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", 19 | "version": "1.0.4" 20 | } 21 | }, 22 | { 23 | "package": "swift-nio", 24 | "repositoryURL": "https://github.com/apple/swift-nio.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "7e3b50b38e4e66f31db6cf4a784c6af148bac846", 28 | "version": "2.46.0" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /DiscordExtension.xcodeproj/project.xcworkspace/xcuserdata/wesley.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuroraEditor/Extension-Discord/b33ede7359eace462999c9b6bec2f93a3ad5e937/DiscordExtension.xcodeproj/project.xcworkspace/xcuserdata/wesley.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /DiscordExtension.xcodeproj/xcshareddata/xcschemes/DiscordExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /DiscordExtension.xcodeproj/xcuserdata/wesley.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | DiscordExtension.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 0463E4FE27F7492100806D5C 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /DiscordIPC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Discord.swift 3 | // DiscordExtension 4 | // 5 | // Created by Wesley de Groot on 27/09/2022. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | import Network 11 | 12 | class Discord { 13 | enum opcode: UInt32 { 14 | case handshake = 0 15 | case frame = 1 16 | case close = 2 17 | case ping = 3 18 | case pong = 4 19 | } 20 | 21 | var appID: String 22 | private var connection: NWConnection? 23 | private let endpoint: String = NSTemporaryDirectory() + "discord-ipc-0" 24 | 25 | init(appID: String) { 26 | self.appID = appID 27 | } 28 | 29 | func connect() { 30 | print("Connecting to \(endpoint)") 31 | 32 | connection = NWConnection( 33 | to: NWEndpoint.unix(path: endpoint), 34 | using: .tcp 35 | ) 36 | 37 | connection?.stateUpdateHandler = { state in 38 | switch state { 39 | case .setup: 40 | print("Setting up...") 41 | case .preparing: 42 | print("Prepairing...") 43 | case .waiting(let error): 44 | print("Waiting: \(error)") 45 | case .ready: 46 | print("Ready...") 47 | case .failed(let error): 48 | print("Failed: \(error)") 49 | case .cancelled: 50 | print("Cancelled :'(") 51 | default: 52 | break 53 | } 54 | } 55 | 56 | connection?.receiveMessage { completeContent, contentContext, isComplete, error in 57 | print( 58 | String(data: completeContent ?? Data(), encoding: .utf8), 59 | error 60 | ) 61 | } 62 | 63 | connection?.start(queue: .global()) 64 | } 65 | 66 | func uint32encode(opcode: opcode, message string: String) -> Data { 67 | let payload = string.data(using: .utf8)! 68 | 69 | var buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 8 + payload.count, alignment: 0) 70 | 71 | defer { buffer.deallocate() } 72 | 73 | buffer.copyBytes(from: payload) 74 | buffer[8...] = buffer[.. Data { 85 | let jsondata = string.data(using: .utf8)! 86 | 87 | var data = Data() 88 | data.append(UInt8(opcode.rawValue)) 89 | data.append(UInt8(jsondata.count)) 90 | data.append(contentsOf: [UInt8](jsondata)) 91 | 92 | /* 93 | uint32 opcode (0 or 1) 94 | uint32 length (length) 95 | byte[length] jsonData (??) 96 | */ 97 | return data 98 | } 99 | 100 | func handshake() { 101 | connect() 102 | 103 | // We should say "hello", with opcode handshake 104 | let hello = encode(opcode: .handshake, message: "{\"v\":1,\"client_id\":\"\(appID)\"}") 105 | 106 | print("Sending \(String.init(data: hello, encoding: .utf8))") 107 | connection?.send( 108 | content: hello, 109 | completion: .contentProcessed({ error in 110 | print("Error:", error?.localizedDescription) 111 | }) 112 | ) 113 | } 114 | 115 | func handshakev2() { 116 | connect() 117 | 118 | // We should say "hello", with opcode handshake 119 | let hello = uint32encode(opcode: .handshake, message: "{\"v\":1,\"client_id\":\"\(appID)\"}") 120 | 121 | print("Sending (V2) \(String.init(data: hello, encoding: .utf8))") 122 | connection?.send( 123 | content: hello, 124 | completion: .contentProcessed({ error in 125 | print("Error (V2):", error?.localizedDescription) 126 | }) 127 | ) 128 | } 129 | 130 | func setPresence(details: String, state: String, image: String) { 131 | 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Official Discord extension for AuroraEditor 2 | 3 | 4 | Discord 5 | 6 | 7 | > ⚠️ WORK IN PROGRESS 8 | > 9 | > THIS MAY NOT WORK AS EXPECTED. 10 | 11 | THE `0xWDG` workspace is used `AEExtensionKit`, it requires you to have a top level `AEExtensionKit` folder. 12 | 13 | There is no need to use that workspace, but as long as i'm busy with `AEExtensionKit` the workspace fill stay inside this folder. 14 | 15 | --- 16 | 17 | Thanks for using. 18 | 19 | Please report issues on https://github.com/AuroraEditor/AuroraEditor 20 | 21 | For help about extensions go to [#extensions](https://discord.gg/cCcwRFfY8f) on our [Discord](https://discord.gg/QYTtDYMMYj) server. 22 | 23 | ### How to build/use 24 | 25 | 1) Clone this project 26 | 2) Press `⌘B` 27 | 3) Open Aurora Editor 28 | -------------------------------------------------------------------------------- /SwordRPC/Delegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Delegate.swift 3 | // SwordRPC 4 | // 5 | // Created by Alejandro Alonso 6 | // Copyright © 2017 Alejandro Alonso. All rights reserved. 7 | // 8 | 9 | public protocol SwordRPCDelegate: AnyObject { 10 | /// Called back when our RPC connects to Discord. 11 | /// - Parameter rpc: The current RPC to work with. 12 | func rpcDidConnect( 13 | _ rpc: SwordRPC 14 | ) 15 | 16 | /// Called when Discord disconnects our RPC. 17 | /// - Parameters: 18 | /// - rpc: The current RPC to work with. 19 | /// - code: The disconnection code, if given by Discord. 20 | /// - msg: The disconnection reason, if given by Discord. 21 | func rpcDidDisconnect( 22 | _ rpc: SwordRPC, 23 | code: Int?, 24 | message msg: String? 25 | ) 26 | 27 | /// Called when the RPC receives an error from Discord. 28 | /// The connection will be terminated immediately. 29 | /// - Parameters: 30 | /// - rpc: The current RPC to work with. 31 | /// - code: The error code as provided by Discord. 32 | /// - msg: The error message as provided by Discord. 33 | func rpcDidReceiveError( 34 | _ rpc: SwordRPC, 35 | code: Int, 36 | message msg: String 37 | ) 38 | 39 | /// Called when Discord notifies us a user joined a game. 40 | /// - Parameters: 41 | /// - rpc: The current RPC to work with. 42 | /// - secret: The join secret for the invite. 43 | func rpcDidJoinGame( 44 | _ rpc: SwordRPC, 45 | secret: String 46 | ) 47 | 48 | /// Called when Discord notifies us a client is spectating a game. 49 | /// - Parameters: 50 | /// - rpc: The current RPC to work with. 51 | /// - secret: The spectate secret for the invite. 52 | func rpcDidSpectateGame( 53 | _ rpc: SwordRPC, 54 | secret: String 55 | ) 56 | 57 | /// Called when Discord notifies us the client received a join request. 58 | /// - Parameters: 59 | /// - rpc: The current RPC to work with. 60 | /// - user: The user requesting an invite. 61 | /// - secret: The spectate secret for the request. 62 | func rpcDidReceiveJoinRequest( 63 | _ rpc: SwordRPC, 64 | user: PartialUser, 65 | secret: String 66 | ) 67 | } 68 | 69 | /// A dummy extension providing empty, default functions for our protocol. 70 | /// We do this to avoid using optional, as it forces our functions to be @objc. 71 | public extension SwordRPCDelegate { 72 | func rpcDidConnect(_: SwordRPC) {} 73 | func rpcDidDisconnect(_: SwordRPC, code _: Int?, message _: String?) {} 74 | func rpcDidReceiveError(_: SwordRPC, code _: Int, message _: String) {} 75 | func rpcDidJoinGame(_: SwordRPC, secret _: String) {} 76 | func rpcDidSpectateGame(_: SwordRPC, secret _: String) {} 77 | func rpcDidReceiveJoinRequest(_: SwordRPC, user _: PartialUser, secret _: String) {} 78 | } 79 | -------------------------------------------------------------------------------- /SwordRPC/Presence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Presence.swift 3 | // SwordRPC 4 | // 5 | // Created by Spotlight Deveaux on 3/26/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension SwordRPC { 11 | /// Sets the presence for this RPC connection. 12 | /// The presence is guaranteed to be set within 15 seconds of call 13 | /// in accordance with Discord ratelimits. 14 | /// 15 | /// If the presence is set before RPC is connected, it is discarded. 16 | /// 17 | /// - Parameter presence: The presence to display. 18 | func setPresence(_ presence: RichPresence) { 19 | self.currentPresence.send(presence) 20 | } 21 | 22 | func clearPresence() { 23 | self.currentPresence.send(nil) 24 | } 25 | 26 | /// Sends a command to clear the current presence. 27 | internal func sendEmptyPresence() { 28 | log.notice("Sending an empty presence.") 29 | 30 | // We send SET_ACTIVITY with no activity payload to clear our presence. 31 | let command = Command(cmd: .setActivity, args: [ 32 | "pid": .int(Int(pid)), 33 | ]) 34 | try? send(command) 35 | } 36 | 37 | /// Sends a command to set the current activity. 38 | internal func sendPresence(_ presence: RichPresence) throws { 39 | log.notice("Sending new presence now: \(String(describing: presence))") 40 | 41 | let command = Command(cmd: .setActivity, args: [ 42 | "pid": .int(Int(self.pid)), 43 | "activity": .activity(presence), 44 | ]) 45 | 46 | try self.send(command) 47 | } 48 | 49 | internal func startPresenceUpdater() { 50 | log.notice("Starting presence updater.") 51 | 52 | self.presenceUpdater = self.currentPresence.throttle( 53 | for: .seconds(3), 54 | scheduler: self.worker, 55 | latest: true 56 | ).sink { presence in 57 | if let presence = presence { 58 | try? self.sendPresence(presence) 59 | } else { 60 | self.sendEmptyPresence() 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SwordRPC/RPC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RPC.swift 3 | // SwordRPC 4 | // 5 | // Created by Alejandro Alonso 6 | // Copyright © 2017 Alejandro Alonso. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension SwordRPC { 12 | /// Sends a handshake to begin RPC interaction. 13 | func handshake() throws { 14 | let response = AuthorizationRequest(version: 1, clientId: appId) 15 | try send(response, opcode: .handshake) 16 | } 17 | 18 | /// Emits a subscribe request for the given command type. 19 | /// https://discord.com/developers/docs/topics/rpc#subscribe 20 | /// - Parameter type: The event type to subscribe for. 21 | func subscribe(_ type: EventType) { 22 | let command = Command(cmd: .subscribe, evt: type) 23 | try? send(command) 24 | } 25 | 26 | /// Handles incoming events from Discord. 27 | /// - Parameter payload: JSON given over IPC. 28 | func handleEvent(_ payload: String) { 29 | var data = decode(payload) 30 | 31 | guard let evt = data["evt"] as? String, 32 | let event = EventType(rawValue: evt) 33 | else { 34 | // We'll treat this as a close. 35 | // ...hopefully. 36 | delegate?.rpcDidDisconnect(self, code: data["code"] as? Int, message: data["message"] as? String) 37 | return 38 | } 39 | 40 | data = data["data"] as! [String: Any] 41 | 42 | switch event { 43 | case .error: 44 | let code = data["code"] as! Int 45 | let message = data["message"] as! String 46 | delegate?.rpcDidReceiveError(self, code: code, message: message) 47 | 48 | case .join: 49 | let secret = data["secret"] as! String 50 | delegate?.rpcDidJoinGame(self, secret: secret) 51 | 52 | case .joinRequest: 53 | let user = data["user"] as! [String: String] 54 | 55 | // TODO: can we properly decode this without doing this manually? 56 | let joinRequest = PartialUser( 57 | avatar: user["avatar"]!, 58 | discriminator: user["discriminator"]!, 59 | userId: user["id"]!, 60 | username: user["username"]! 61 | ) 62 | 63 | let secret = data["secret"] as! String 64 | delegate?.rpcDidReceiveJoinRequest(self, user: joinRequest, secret: secret) 65 | 66 | case .ready: 67 | delegate?.rpcDidConnect(self) 68 | startPresenceUpdater() 69 | 70 | case .spectate: 71 | let secret = data["secret"] as! String 72 | delegate?.rpcDidSpectateGame(self, secret: secret) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SwordRPC/SwordRPC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwordRPC.swift 3 | // SwordRPC 4 | // 5 | // Created by Alejandro Alonso 6 | // Copyright © 2017 Alejandro Alonso. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import os.log 11 | import Combine 12 | 13 | public class SwordRPC { 14 | // MARK: App Info 15 | 16 | public let appId: String 17 | public var handlerInterval: Int 18 | public let autoRegister: Bool 19 | 20 | // MARK: Technical stuff 21 | 22 | let pid: Int32 23 | var client: ConnectionClient? 24 | let worker: DispatchQueue 25 | var log: Logger 26 | let encoder = JSONEncoder() 27 | let decoder = JSONDecoder() 28 | let currentPresence = CurrentValueSubject(nil) 29 | var presenceUpdater: AnyCancellable! 30 | 31 | // MARK: Presence-related metadata 32 | 33 | var presence: RichPresence? 34 | 35 | // MARK: Event Handlers 36 | 37 | public weak var delegate: SwordRPCDelegate? 38 | 39 | public init(appId: String, handlerInterval: Int = 1000, autoRegister: Bool = true) { 40 | self.appId = appId 41 | self.handlerInterval = handlerInterval 42 | self.autoRegister = autoRegister 43 | 44 | pid = ProcessInfo.processInfo.processIdentifier 45 | log = Logger(subsystem: "space.joscomputing.swordrpc.\(pid)", category: "rpc") 46 | worker = DispatchQueue( 47 | label: "com.auroraeditor.\(pid)", 48 | qos: .background 49 | ) 50 | encoder.dateEncodingStrategy = .secondsSince1970 51 | } 52 | 53 | public func connect() { 54 | let tempDir = NSTemporaryDirectory() 55 | 56 | for ipcPort in 0 ..< 10 { 57 | let socketPath = tempDir + "discord-ipc-\(ipcPort)" 58 | let localClient = ConnectionClient(pipe: socketPath) 59 | do { 60 | try localClient.connect() 61 | 62 | // Set handlers 63 | localClient.textHandler = handleEvent 64 | localClient.disconnectHandler = handleEvent 65 | 66 | client = localClient 67 | // Attempt handshaking 68 | try handshake() 69 | } catch { 70 | // If an error occurrs, we should not log it. 71 | // We must iterate through all 10 ports before logging. 72 | continue 73 | } 74 | 75 | subscribe(.join) 76 | subscribe(.spectate) 77 | subscribe(.joinRequest) 78 | return 79 | } 80 | 81 | print("[SwordRPC] Discord not detected") 82 | } 83 | 84 | /// Replies to an activity join request. 85 | /// - Parameters: 86 | /// - user: The user making the request 87 | /// - reply: Whether to accept or decline the request. 88 | public func reply(to user: PartialUser, with reply: JoinReply) { 89 | var type: CommandType 90 | 91 | switch reply { 92 | case .yes: 93 | type = .sendActivityJoinInvite 94 | case .ignore, .no: 95 | type = .closeActivityJoinRequest 96 | } 97 | 98 | // We must give Discord the requesting user's ID to handle. 99 | let command = Command(cmd: type, args: [ 100 | "user_id": .string(user.userId), 101 | ]) 102 | 103 | try? send(command) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /SwordRPC/Types/Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enums.swift 3 | // SwordRPC 4 | // 5 | // Created by Alejandro Alonso 6 | // Copyright © 2017 Alejandro Alonso. All rights reserved. 7 | // 8 | 9 | /// Command types to send over RPC. 10 | /// https://discord.com/developers/docs/topics/rpc#commands-and-events-rpc-commands 11 | enum CommandType: String, Codable { 12 | case dispatch = "DISPATCH" 13 | case authorize = "AUTHORIZE" 14 | case subscribe = "SUBSCRIBE" 15 | case setActivity = "SET_ACTIVITY" 16 | case sendActivityJoinInvite = "SEND_ACTIVITY_JOIN_INVITE" 17 | case closeActivityJoinRequest = "CLOSE_ACTIVITY_JOIN_REQUEST" 18 | } 19 | 20 | /// Possible event types. 21 | /// https://discord.com/developers/docs/topics/rpc#commands-and-events-rpc-events 22 | enum EventType: String, Codable { 23 | case error = "ERROR" 24 | case join = "ACTIVITY_JOIN" 25 | case joinRequest = "ACTIVITY_JOIN_REQUEST" 26 | case ready = "READY" 27 | case spectate = "ACTIVITY_SPECTATE" 28 | } 29 | 30 | /// An enum for a reply to a join request, defining yes or ignore/no types. 31 | public enum JoinReply { 32 | case no 33 | case yes 34 | case ignore 35 | } 36 | -------------------------------------------------------------------------------- /SwordRPC/Types/JoinRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JoinRequest.swift 3 | // SwordRPC 4 | // 5 | // Created by Alejandro Alonso 6 | // Copyright © 2017 Alejandro Alonso. All rights reserved. 7 | // 8 | 9 | /// Used to represent a partial user given by Discord. 10 | /// For example: https://discord.com/developers/docs/topics/rpc#activityjoinrequest-example-activity-join-request-dispatch-payload 11 | public struct PartialUser: Decodable { 12 | let avatar: String 13 | let discriminator: String 14 | let userId: String 15 | let username: String 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case avatar 19 | case discriminator 20 | case userId = "id" 21 | case username 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SwordRPC/Types/Requests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Requests.swift 3 | // 4 | // 5 | // Created by Spotlight Deveaux on 2022-01-17. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Describes the format needed for an authorization request. 11 | /// https://discord.com/developers/docs/topics/rpc#authenticating-rpc-authorize-example 12 | struct AuthorizationRequest: Encodable { 13 | let version: Int 14 | let clientId: String 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case version = "v" 18 | case clientId = "client_id" 19 | } 20 | } 21 | 22 | /// RequestArg permits a union-like type for arguments to encode. 23 | enum RequestArg: Encodable { 24 | /// An integer value. 25 | case int(Int) 26 | /// A string value. 27 | case string(String) 28 | /// An activity value. 29 | case activity(RichPresence) 30 | 31 | func encode(to encoder: Encoder) throws { 32 | var container = encoder.singleValueContainer() 33 | switch self { 34 | case let .int(int): 35 | try container.encode(int) 36 | case let .string(string): 37 | try container.encode(string) 38 | case let .activity(presence): 39 | try container.encode(presence) 40 | } 41 | } 42 | } 43 | 44 | /// A generic format for a payload with a command, possibly used for an event. 45 | struct Command: Encodable { 46 | /// The type of command to issue to Discord. For normal events, this should be .dispatch. 47 | let cmd: CommandType 48 | /// The nonce for this command. It should typically be an automatically generated UUID. 49 | let nonce: String = UUID().uuidString 50 | /// Arguments sent alongside the command. 51 | var args: [String: RequestArg]? 52 | /// The event type this command pertains to, if needed. 53 | var evt: EventType? 54 | } 55 | 56 | /// A generic format for sending an event. 57 | struct Event: Encodable { 58 | /// The event type to handle. 59 | let eventType: EventType 60 | /// Arguments sent alongside the event. 61 | var args: [String: RequestArg]? 62 | 63 | /// Convenience initializer to create an event with the given type. 64 | init(_ event: EventType) { 65 | eventType = event 66 | } 67 | 68 | /// Convenience initializer to create an event with the given type and arguments. 69 | init(_ event: EventType, args: [String: RequestArg]?) { 70 | eventType = event 71 | self.args = args 72 | } 73 | 74 | func encode(to encoder: Encoder) throws { 75 | // All events are dispatched. 76 | var command = Command(cmd: .dispatch, args: args) 77 | command.evt = eventType 78 | 79 | try command.encode(to: encoder) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /SwordRPC/Types/RichPresence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichPresence.swift 3 | // SwordRPC 4 | // 5 | // Created by Alejandro Alonso 6 | // Copyright © 2017 Alejandro Alonso. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct RichPresence: Encodable { 12 | public var assets = Assets() 13 | public var details = "" 14 | public var instance = true 15 | public var party = Party() 16 | public var secrets = Secrets() 17 | public var state = "" 18 | public var timestamps = Timestamps() 19 | 20 | public init() {} 21 | } 22 | 23 | public extension RichPresence { 24 | struct Timestamps: Encodable { 25 | public var end: Date? 26 | public var start: Date? 27 | 28 | enum CodingKeys: String, CodingKey { 29 | case end 30 | case start 31 | } 32 | 33 | public func encode(to encoder: Encoder) throws { 34 | var container = encoder.container(keyedBy: CodingKeys.self) 35 | 36 | try container.encodeIfPresent(start.map { Int($0.timeIntervalSince1970 * 1000) }, forKey: .start) 37 | try container.encodeIfPresent(end.map { Int($0.timeIntervalSince1970 * 1000) }, forKey: .end) 38 | } 39 | } 40 | 41 | struct Assets: Encodable { 42 | public var largeImage: String? 43 | public var largeText: String? 44 | public var smallImage: String? 45 | public var smallText: String? 46 | 47 | enum CodingKeys: String, CodingKey { 48 | case largeImage = "large_image" 49 | case largeText = "large_text" 50 | case smallImage = "small_image" 51 | case smallText = "small_text" 52 | } 53 | } 54 | 55 | struct Party: Encodable { 56 | public var id: String? 57 | public var max: Int? 58 | public var size: Int? 59 | 60 | enum CodingKeys: String, CodingKey { 61 | case id 62 | case size 63 | } 64 | 65 | public func encode(to encoder: Encoder) throws { 66 | var container = encoder.container(keyedBy: CodingKeys.self) 67 | try container.encodeIfPresent(id, forKey: .id) 68 | 69 | guard let max = self.max, let size = size else { 70 | return 71 | } 72 | 73 | try container.encode([size, max], forKey: .size) 74 | } 75 | } 76 | 77 | struct Secrets: Encodable { 78 | public var join: String? 79 | public var match: String? 80 | public var spectate: String? 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /SwordRPC/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // SwordRPC 4 | // 5 | // Created by Alejandro Alonso 6 | // Copyright © 2017 Alejandro Alonso. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension SwordRPC { 12 | /// Decodes the given string as a JSON object. 13 | func decode(_ json: String) -> [String: Any] { 14 | decode(json.data(using: .utf8)!) 15 | } 16 | 17 | /// Decodes the given data as a JSON object. 18 | func decode(_ json: Data) -> [String: Any] { 19 | do { 20 | return try JSONSerialization.jsonObject(with: json, options: []) as! [String: Any] 21 | } catch { 22 | return [:] 23 | } 24 | } 25 | 26 | /// Serializes and sends the given object as JSON. 27 | func send(_ response: Encodable) throws { 28 | try send(response, opcode: .frame) 29 | } 30 | 31 | /// Sends the given JSON string with the given opcode. 32 | func send(_ response: Encodable, opcode: IPCOpcode) throws { 33 | let data = try response.toJSON() 34 | try client?.send(data: data, opcode: opcode) 35 | } 36 | } 37 | 38 | extension Encodable { 39 | func toJSON() throws -> String { 40 | let result = try JSONEncoder().encode(self) 41 | return String(bytes: result, encoding: .utf8) ?? "" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SwordRPC/WebSockets/ConnectionClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionClient.swift 3 | // SwordRPC 4 | // 5 | // Created by Spotlight Deveaux on 2022-01-17. 6 | // 7 | 8 | import Foundation 9 | import NIOCore 10 | import NIOPosix 11 | 12 | class ConnectionClient: ChannelInboundHandler { 13 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 14 | var channel: Channel? 15 | let pipePath: String 16 | 17 | /// Initializes a new socket client for the given pipe. 18 | init(pipe pipePath: String) { 19 | self.pipePath = pipePath 20 | } 21 | 22 | /// Called upon a disconnect. 23 | var disconnectHandler: ((_ text: String) -> Void)? 24 | /// Called upon a text event. 25 | var textHandler: ((_ text: String) -> Void)? 26 | 27 | /// Connects to the configured pipe socket. 28 | /// This call is intentionally blocking, in order to ensure connection 29 | /// success over a local UNIX socket. 30 | func connect() throws { 31 | let bootstrap = ClientBootstrap(group: group) 32 | // Enable SO_REUSEADDR. 33 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 34 | .channelInitializer { channel in 35 | // We add our custom inbound/outbound coders. 36 | channel.pipeline.addHandlers([ 37 | ByteToMessageHandler(IPCInboundHandler()), 38 | IPCOutboundHandler(), 39 | self, 40 | ]) 41 | } 42 | 43 | let future = bootstrap.connect(unixDomainSocketPath: pipePath) 44 | // We will willingly block connection, as this is to a local UNIX socket. 45 | let localChannel = try future.wait() 46 | 47 | channel = localChannel 48 | } 49 | 50 | func close() { 51 | try? group.syncShutdownGracefully() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SwordRPC/WebSockets/ConnectionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionHandler.swift 3 | // SwordRPC 4 | // 5 | // Created by Spotlight Deveaux on 2022-01-17. 6 | // 7 | 8 | import Foundation 9 | import NIOCore 10 | 11 | extension ConnectionClient { 12 | typealias InboundIn = IPCPayload 13 | typealias OutboundOut = IPCPayload 14 | 15 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 16 | let frame = unwrapInboundIn(data) 17 | 18 | switch frame.opcode { 19 | case .ping: 20 | // We are expected to respond to all pings. 21 | ping(context: context, frame: frame) 22 | case .frame: 23 | receivedData(frame: frame) 24 | case .close: 25 | receivedClose(context: context, frame: frame) 26 | default: 27 | // Handle unknown frames as errors. 28 | closeOnError(context: context) 29 | } 30 | } 31 | 32 | public func channelReadComplete(context: ChannelHandlerContext) { 33 | context.flush() 34 | } 35 | 36 | private func receivedClose(context: ChannelHandlerContext, frame: IPCPayload) { 37 | // We should have recieved data from this close, as Discord provides us with such. 38 | let data = frame.payload 39 | 40 | // Close the connection, as requested. 41 | context.close(promise: nil) 42 | 43 | // Call back if possible. 44 | disconnectHandler?(data) 45 | } 46 | 47 | private func receivedData(frame: IPCPayload) { 48 | textHandler?(frame.payload) 49 | } 50 | 51 | private func ping(context: ChannelHandlerContext, frame: IPCPayload) { 52 | // Write back the given ping data for a pong. 53 | let data = frame.payload 54 | let frame = IPCPayload(opcode: .pong, payload: data) 55 | context.write(wrapOutboundOut(frame), promise: nil) 56 | } 57 | 58 | private func closeOnError(context: ChannelHandlerContext) { 59 | // We have hit an error, so we want to close. 60 | // We do that by sending a close frame and then shutting down the write side of the connection. 61 | // The server will respond with a close of its own. 62 | let frame = IPCPayload(opcode: .close, payload: "") 63 | context.write(wrapOutboundOut(frame)).whenComplete { (_: Result) in 64 | context.close(mode: .output, promise: nil) 65 | } 66 | } 67 | 68 | /// Sends the given data for the given opcode to the connected WebSocket. 69 | func send(data: String, opcode: IPCOpcode) throws { 70 | let frame = IPCPayload(opcode: opcode, payload: data) 71 | try channel!.writeAndFlush(wrapOutboundOut(frame)).wait() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SwordRPC/WebSockets/IPCHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IPCHandler.swift 3 | // SwiftRPC 4 | // 5 | // Created by Spotlight Deveaux on 2022-01-17. 6 | // 7 | 8 | import Foundation 9 | import NIOCore 10 | 11 | enum IPCHandlingError: Error { 12 | case unknownOpcode 13 | case payloadTooShort 14 | } 15 | 16 | final class IPCInboundHandler: ByteToMessageDecoder { 17 | public typealias InboundIn = ByteBuffer 18 | public typealias InboundOut = IPCPayload 19 | 20 | func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState { 21 | guard let opcodeInt = buffer.getInteger(at: 0, endianness: .little, as: UInt32.self) else { 22 | return .needMoreData 23 | } 24 | guard let opcode = IPCOpcode(rawValue: opcodeInt) else { 25 | throw IPCHandlingError.unknownOpcode 26 | } 27 | 28 | guard let size = buffer.getInteger(at: 4, endianness: .little, as: UInt32.self).map({ Int($0) }) else { 29 | return .needMoreData 30 | } 31 | 32 | if buffer.readableBytes - 8 < size { 33 | return .needMoreData 34 | } 35 | 36 | guard let payload = buffer.getString(at: 8, length: size) else { 37 | throw IPCHandlingError.payloadTooShort 38 | } 39 | 40 | let result = IPCPayload(opcode: opcode, payload: payload) 41 | context.fireChannelRead(self.wrapInboundOut(result)) 42 | buffer.moveReaderIndex(to: 8 + size) 43 | return .needMoreData 44 | } 45 | } 46 | 47 | final class IPCOutboundHandler: ChannelOutboundHandler { 48 | typealias OutboundIn = IPCPayload 49 | public typealias OutboundOut = ByteBuffer 50 | 51 | public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 52 | let data = unwrapOutboundIn(data) 53 | 54 | // Synthesize a buffer. 55 | var buffer = ByteBuffer() 56 | // Our payload's size is + + . 57 | let payloadSize = data.payload.lengthOfBytes(using: .utf8) 58 | 59 | // Write contents. 60 | buffer.writeInteger(UInt32(data.opcode.rawValue), endianness: .little, as: UInt32.self) 61 | buffer.writeInteger(UInt32(payloadSize), endianness: .little, as: UInt32.self) 62 | buffer.writeString(data.payload) 63 | 64 | context.write(wrapOutboundOut(buffer), promise: promise) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SwordRPC/WebSockets/IPCPayload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IPCHandler.swift 3 | // SwiftNIO 4 | // 5 | // Created by Spotlight Deveaux on 2022-01-17. 6 | // 7 | 8 | import Foundation 9 | 10 | /// All observed IPC opcodes from Discord. 11 | enum IPCOpcode: UInt32 { 12 | case handshake = 0 13 | case frame = 1 14 | case close = 2 15 | case ping = 3 16 | case pong = 4 17 | } 18 | 19 | /// A structure for the IPC payload. 20 | struct IPCPayload { 21 | let opcode: IPCOpcode 22 | let payload: String 23 | } 24 | --------------------------------------------------------------------------------