├── 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 |
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 |
--------------------------------------------------------------------------------