commit 731add8c807f472a774fd4bb637fe9c081d6b9fd
Author: Jordan Koch <me@jordanko.ch>
Date:   Thu Jul 18 10:49:28 2019 -0400

    initial

diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..217a369
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+*~
+*~.*
+.DS_Store
+.LSOverride
+*.mode1v3
+*.mode2v3
+*.pbxuser
+*.perspectivev3
+*.pyc
+.tmp
+build/
+xcuserdata
+*.xcworkspace
diff --git a/Arxius-Bridging-Header.h b/Arxius-Bridging-Header.h
new file mode 100755
index 0000000..7013c1c
--- /dev/null
+++ b/Arxius-Bridging-Header.h
@@ -0,0 +1 @@
+#import "Textual.h"
diff --git a/Arxius.swift b/Arxius.swift
new file mode 100755
index 0000000..38a7959
--- /dev/null
+++ b/Arxius.swift
@@ -0,0 +1,224 @@
+import Foundation
+
+@objc
+class Plugin: NSObject, THOPluginProtocol {
+    var apikey = ""
+    var theClient:IRCClient? = nil
+    var currentAlbum = ""
+    var uploadQueue:[URL] = []
+    var isUploading = false
+    
+    var task:URLSessionDataTask?
+
+    @objc
+    let subscribedUserInputCommands = ["arxiuskey", "upload", "cancel"]
+    
+    @objc func pluginLoadedIntoMemory() {
+        if (apikey.count < 1) {
+            if let key = TPCPreferencesUserDefaults.shared().value(forKey: "arxiusAPIKey") {
+                apikey = key as! String
+            }
+        }
+    }
+
+    @objc
+    func userInputCommandInvoked(on client: IRCClient, command commandString: String, messageString: String) {
+        theClient = client
+    
+        performBlock(onMainThread: {
+            switch commandString {
+                case "ARXIUSKEY":
+                    if (messageString.count == 32) {
+                        apikey = messageString
+                        TPCPreferencesUserDefaults.shared().setValue(apikey, forKey: "arxiusAPIKey")
+                        debug("Your API key is set")
+                    }
+                    else if (messageString.count == 0 && apikey.count > 0) {
+                        apikey = ""
+                        TPCPreferencesUserDefaults.shared().removeObject(forKey: "arxiusAPIKey")
+                        debug("Your API key is unset")
+                    }
+                    else if (messageString.count == 0) {
+                        debug("Enter your API key like this: /arxiuskey abcd1234abcd1234abcd1234abcd1234")
+                    }
+                    else {
+                        debug("That's not a valid API key")
+                    }
+                
+                case "UPLOAD":
+                    let dialog = NSOpenPanel();
+                    
+                    dialog.title                   = "Select files to upload";
+                    dialog.showsResizeIndicator    = true;
+                    dialog.showsHiddenFiles        = false;
+                    dialog.canChooseDirectories    = false;
+                    dialog.canCreateDirectories    = true;
+                    dialog.allowsMultipleSelection = true;
+                    
+                    if (dialog.runModal() == NSApplication.ModalResponse.OK) {
+                        let files = dialog.urls
+                        
+                        if (files.count > 0) {
+                            for file in files {
+                                let path = file.absoluteURL
+                                uploadQueue.append(path)
+                            }
+                            
+                            makeAlbumIfNeeded()
+                        }
+                    }
+                
+                case "CANCEL":
+                    task?.cancel()
+                    uploadQueue = []
+                    debug("Upload cancelled")
+                    
+                    break
+                
+                default:
+                    break
+            }
+        })
+    }
+    
+    func debug(_ message: Any) {
+        self.theClient!.printDebugInformation("\u{02}[Arxius]\u{02} \(message)", in: self.theClient!.lastSelectedChannel!)
+    }
+    
+    func startUploading() {
+        uploadFiles()
+    }
+    
+    func uploadFiles() {
+        if (uploadQueue.count > 0) {
+            upload()
+        }
+        else {
+            currentAlbum = ""
+        }
+    }
+    
+    func upload() {
+        let file = uploadQueue[0]
+        uploadQueue.remove(at: 0)
+
+        let url = URL(string: "https://arxius.io/")!
+        var request = URLRequest(url: url)
+        request.httpMethod = "POST"
+        var parameters: [String:String] = [:]
+        
+        if (apikey.count > 0) {
+            parameters["apikey"] = apikey
+        }
+        
+        if (currentAlbum.count > 0) {
+            parameters["album"] = currentAlbum
+        }
+        
+        var last = false
+        if (uploadQueue.count < 1) {
+            last = true
+            parameters["last"] = "true"
+        }
+
+        let boundary = "Boundary-\(UUID().uuidString)"
+        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
+        
+        do {
+            let fileData = try Data(contentsOf: file)
+            
+            request.httpBody = createBody(parameters: parameters, boundary: boundary, data: fileData, mimeType: "application/octet-stream", filename: file.lastPathComponent)
+            
+            task = URLSession.shared.dataTask(with: request) { data, response, error in
+                guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
+                    return
+                }
+                
+                guard (200 ... 299) ~= response.statusCode else {
+                    return
+                }
+                
+                if (last) {
+                    if let link = String(data: data, encoding: .utf8) {
+                        self.debug("Upload complete: \(link)")
+                    }
+                }
+            }
+            task!.resume()
+            
+            debug("Uploading: \(file.path)")
+            
+            uploadFiles()
+        }
+        catch {}
+    }
+    
+    func makeAlbumIfNeeded() {
+        if (uploadQueue.count > 1) {
+            let url = URL(string: "https://arxius.io/api/create/album")!
+            var request = URLRequest(url: url)
+            request.httpMethod = "POST"
+            var parameters: [String:String] = [:]
+            
+            if (apikey.count > 0) {
+                parameters["apikey"] = apikey
+            }
+            
+            request.httpBody = parameters.percentEscaped().data(using: .utf8)
+            
+            let task = URLSession.shared.dataTask(with: request) { data, response, error in
+                guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
+                    return
+                }
+                
+                guard (200 ... 299) ~= response.statusCode else {
+                    return
+                }
+                
+                do {
+                    let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! [String: Any]
+                    
+                    if (json.index(forKey: "album") != nil) {
+                        let album = json["album"] as! String
+                        self.currentAlbum = album
+                        self.debug("Created album: \(album)")
+                        
+                        self.startUploading()
+                    }
+                    else {
+                        self.debug("Error creating album")
+                    }
+                } catch {}
+            }
+            task.resume()
+        }
+        else {
+            self.startUploading()
+        }
+    }
+    
+    func createBody(parameters: [String: String],
+                    boundary: String,
+                    data: Data,
+                    mimeType: String,
+                    filename: String) -> Data {
+        let body = NSMutableData()
+        
+        let boundaryPrefix = "--\(boundary)\r\n"
+        
+        for (key, value) in parameters {
+            body.appendString(boundaryPrefix)
+            body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
+            body.appendString("\(value)\r\n")
+        }
+        
+        body.appendString(boundaryPrefix)
+        body.appendString("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n")
+        body.appendString("Content-Type: \(mimeType)\r\n\r\n")
+        body.append(data)
+        body.appendString("\r\n")
+        body.appendString("--".appending(boundary.appending("--")))
+        
+        return body as Data
+    }
+}
diff --git a/Arxius.xcodeproj/project.pbxproj b/Arxius.xcodeproj/project.pbxproj
new file mode 100755
index 0000000..c2979bc
--- /dev/null
+++ b/Arxius.xcodeproj/project.pbxproj
@@ -0,0 +1,305 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		07E88365214F2BAE001982D7 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E88364214F2BAE001982D7 /* Extensions.swift */; };
+		4C38AD881946BB2C00B4A7AB /* Arxius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C38AD871946BB2C00B4A7AB /* Arxius.swift */; };
+		4C51BE9412D0471600E79CEB /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C51BE9312D0471600E79CEB /* Cocoa.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		07E88364214F2BAE001982D7 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
+		4C38AD861946BB2B00B4A7AB /* Arxius-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Arxius-Bridging-Header.h"; sourceTree = "<group>"; };
+		4C38AD871946BB2C00B4A7AB /* Arxius.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Arxius.swift; sourceTree = "<group>"; };
+		4C51BE9312D0471600E79CEB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
+		8D576316048677EA00EA77CD /* Arxius.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Arxius.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
+		8D576317048677EA00EA77CD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		8D576313048677EA00EA77CD /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				4C51BE9412D0471600E79CEB /* Cocoa.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		089C166AFE841209C02AAC07 /* PreferencePaneExample */ = {
+			isa = PBXGroup;
+			children = (
+				08FB77AFFE84173DC02AAC07 /* Source */,
+				089C167CFE841241C02AAC07 /* Resources */,
+				089C1671FE841209C02AAC07 /* Frameworks */,
+				19C28FB6FE9D52B211CA2CBB /* Products */,
+			);
+			name = PreferencePaneExample;
+			sourceTree = "<group>";
+		};
+		089C1671FE841209C02AAC07 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				4C51BE9312D0471600E79CEB /* Cocoa.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		089C167CFE841241C02AAC07 /* Resources */ = {
+			isa = PBXGroup;
+			children = (
+				4C51BE9912D0472300E79CEB /* Documents */,
+			);
+			name = Resources;
+			sourceTree = "<group>";
+		};
+		08FB77AFFE84173DC02AAC07 /* Source */ = {
+			isa = PBXGroup;
+			children = (
+				07E88364214F2BAE001982D7 /* Extensions.swift */,
+				4C38AD871946BB2C00B4A7AB /* Arxius.swift */,
+				4C38AD861946BB2B00B4A7AB /* Arxius-Bridging-Header.h */,
+			);
+			name = Source;
+			sourceTree = "<group>";
+		};
+		19C28FB6FE9D52B211CA2CBB /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				8D576316048677EA00EA77CD /* Arxius.bundle */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		4C51BE9912D0472300E79CEB /* Documents */ = {
+			isa = PBXGroup;
+			children = (
+				8D576317048677EA00EA77CD /* Info.plist */,
+			);
+			name = Documents;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		8D57630D048677EA00EA77CD /* Arxius */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Arxius" */;
+			buildPhases = (
+				8D57630F048677EA00EA77CD /* Resources */,
+				8D576311048677EA00EA77CD /* Sources */,
+				8D576313048677EA00EA77CD /* Frameworks */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = Arxius;
+			productInstallPath = "$(HOME)/Library/Bundles";
+			productName = PreferencePaneExample;
+			productReference = 8D576316048677EA00EA77CD /* Arxius.bundle */;
+			productType = "com.apple.product-type.bundle";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		089C1669FE841209C02AAC07 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastSwiftUpdateCheck = 0700;
+				LastUpgradeCheck = 1000;
+				TargetAttributes = {
+					8D57630D048677EA00EA77CD = {
+						LastSwiftMigration = 1000;
+					};
+				};
+			};
+			buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Arxius" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 1;
+			knownRegions = (
+				English,
+				Japanese,
+				French,
+				German,
+				Base,
+			);
+			mainGroup = 089C166AFE841209C02AAC07 /* PreferencePaneExample */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				8D57630D048677EA00EA77CD /* Arxius */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		8D57630F048677EA00EA77CD /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		8D576311048677EA00EA77CD /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				4C38AD881946BB2C00B4A7AB /* Arxius.swift in Sources */,
+				07E88365214F2BAE001982D7 /* Extensions.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		1DEB911B08733D790010E9CD /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				BUNDLE_LOADER = /Applications/Textual.app/Contents/MacOS/Textual;
+				COMBINE_HIDPI_IMAGES = YES;
+				FRAMEWORK_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Frameworks/**\"";
+				HEADER_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Headers/**\"";
+				INFOPLIST_FILE = Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 10.9;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.jordankoch.textual-arxius";
+				PRODUCT_NAME = Arxius;
+				SWIFT_OBJC_BRIDGING_HEADER = "Arxius-Bridging-Header.h";
+				SWIFT_SWIFT3_OBJC_INFERENCE = Default;
+				SWIFT_VERSION = 5.0;
+				WRAPPER_EXTENSION = bundle;
+			};
+			name = Debug;
+		};
+		1DEB911C08733D790010E9CD /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				BUNDLE_LOADER = /Applications/Textual.app/Contents/MacOS/Textual;
+				COMBINE_HIDPI_IMAGES = YES;
+				FRAMEWORK_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Frameworks/**\"";
+				HEADER_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Headers/**\"";
+				INFOPLIST_FILE = Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 10.9;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.jordankoch.textual-arxius";
+				PRODUCT_NAME = Arxius;
+				SWIFT_OBJC_BRIDGING_HEADER = "Arxius-Bridging-Header.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_SWIFT3_OBJC_INFERENCE = Default;
+				SWIFT_VERSION = 5.0;
+				WRAPPER_EXTENSION = bundle;
+			};
+			name = Release;
+		};
+		1DEB911F08733D790010E9CD /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		1DEB912008733D790010E9CD /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Arxius" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1DEB911B08733D790010E9CD /* Debug */,
+				1DEB911C08733D790010E9CD /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Arxius" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1DEB911F08733D790010E9CD /* Debug */,
+				1DEB912008733D790010E9CD /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 089C1669FE841209C02AAC07 /* Project object */;
+}
diff --git a/Arxius.xcodeproj/xcshareddata/xcschemes/Arxius.xcscheme b/Arxius.xcodeproj/xcshareddata/xcschemes/Arxius.xcscheme
new file mode 100644
index 0000000..42f94d4
--- /dev/null
+++ b/Arxius.xcodeproj/xcshareddata/xcschemes/Arxius.xcscheme
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1020"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "8D57630D048677EA00EA77CD"
+               BuildableName = "Arxius.bundle"
+               BlueprintName = "Arxius"
+               ReferencedContainer = "container:Arxius.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+      </Testables>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "8D57630D048677EA00EA77CD"
+            BuildableName = "Arxius.bundle"
+            BlueprintName = "Arxius"
+            ReferencedContainer = "container:Arxius.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "8D57630D048677EA00EA77CD"
+            BuildableName = "Arxius.bundle"
+            BlueprintName = "Arxius"
+            ReferencedContainer = "container:Arxius.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Extensions.swift b/Extensions.swift
new file mode 100755
index 0000000..237c20e
--- /dev/null
+++ b/Extensions.swift
@@ -0,0 +1,37 @@
+//
+//  Extensions.swift
+//  Arxius
+//
+//  Created by Jordan Koch on 9/16/18.
+//
+
+import Foundation
+
+extension Dictionary {
+    func percentEscaped() -> String {
+        return map { (key, value) in
+            let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
+            let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
+            return escapedKey + "=" + escapedValue
+            }
+            .joined(separator: "&")
+    }
+}
+
+extension CharacterSet {
+    static let urlQueryValueAllowed: CharacterSet = {
+        let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
+        let subDelimitersToEncode = "!$&'()*+,;="
+        
+        var allowed = CharacterSet.urlQueryAllowed
+        allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
+        return allowed
+    }()
+}
+
+extension NSMutableData {
+    func appendString(_ string: String) {
+        let data = string.data(using: String.Encoding.utf8, allowLossyConversion: false)
+        append(data!)
+    }
+}
diff --git a/Info.plist b/Info.plist
new file mode 100755
index 0000000..db7db66
--- /dev/null
+++ b/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>English</string>
+	<key>CFBundleExecutable</key>
+	<string>${EXECUTABLE_NAME}</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleName</key>
+	<string>${PRODUCT_NAME}</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleVersion</key>
+	<string>1.0.1</string>
+	<key>MinimumTextualVersion</key>
+	<string>6.0.0</string>
+	<key>NSPrincipalClass</key>
+	<string>$(PRODUCT_NAME).Plugin</string>
+</dict>
+</plist>
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..18b2a87
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+## Download
+
+https://arxius.io/textual
+
+## Install
+
+Unzip & double click `Arxius.bundle`
+
+## Usage
+
+Upload a file
+
+`/upload`
+
+Set your API key
+
+`/arxiuskey abcd1234abcd1234abcd1234abcd1234`
+
+Unset your API key
+
+`/arxiuskey`
\ No newline at end of file