From 67f3927471b70b73676cd62002a1b04898d0369a Mon Sep 17 00:00:00 2001 From: Zach Ward Date: Sat, 16 Apr 2022 15:30:15 -0400 Subject: [PATCH 1/7] slack command executable target --- Package.resolved | 9 ++++ Package.swift | 10 +++- Sources/SlackCommand/File.swift | 96 +++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 Sources/SlackCommand/File.swift diff --git a/Package.resolved b/Package.resolved index 65d674c..ad0066a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GoodNotes/CryptoSwift.git", + "state" : { + "branch" : "swiftwasm-support", + "revision" : "a8bc733bc578312b1a507da3417f92d311e3143d" + } + }, { "identity" : "swift-compute-runtime", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 2c583a4..0ee86fe 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/AndrewBarba/swift-compute-runtime", branch: "main"), + .package(url: "https://github.com/GoodNotes/CryptoSwift.git", branch: "swiftwasm-support"), ], targets: [ .executableTarget( @@ -22,6 +23,13 @@ let package = Package( .executableTarget( name: "Rest", dependencies: [.product(name: "Compute", package: "swift-compute-runtime")] - ) + ), + .executableTarget( + name: "SlackCommand", + dependencies: [ + .product(name: "Compute", package: "swift-compute-runtime"), + .product(name: "CryptoSwift", package: "CryptoSwift"), + ] + ), ] ) diff --git a/Sources/SlackCommand/File.swift b/Sources/SlackCommand/File.swift new file mode 100644 index 0000000..9600ed2 --- /dev/null +++ b/Sources/SlackCommand/File.swift @@ -0,0 +1,96 @@ +import Compute +import Foundation +import CryptoSwift + +extension String: Error {} + +struct MessagePayload: Codable { + var text: String? + var responseType: String? +} + +@main +struct App { + static func main() async throws { + try await onIncomingRequest(router.run) + } + + static let router = Router() + .get("/status") { req, res in + try await res.status(.ok).send("OK") + } + + .post("/webhook") { req, res in + let signature = req.headers.get("X-Slack-Signature") + let timestamp = req.headers.get("X-Slack-Request-Timestamp") + let version = "v0" // slack version number is always "v0" right now + + print("X-Slack-Request-Timestamp -> \(timestamp ?? "")") + + let env = try Compute.Dictionary(name: "env") + let secret = env.get("SLACK_SIGNING_SECRET") + let rawBody = try await req.body.text() + + func validateSignature(payload: [UInt8]) async throws -> Bool { + guard let secret = secret else { + // noop for development + return false + } + let hmac = try HMAC(key: secret, variant: .sha2(.sha256)) + let expected = try "\(version)=" + hmac.authenticate(payload).toHexString() + + print("[validateSignature] expected -> \(expected)") + print("[validateSignature] signature -> \(signature ?? "")") + return signature == expected + } + + let payload = "\(version):\(timestamp!):\(rawBody)" + + guard try await validateSignature(payload: payload.bytes) else { + return try await res.status(.unauthorised).send("Invalid request") + } + + // very simple approach to parsing application/x-www-form-urlencoded values into + // a dictionary + let formParts = rawBody.components(separatedBy: "&").map { entry in + return entry.components(separatedBy: "=") + } + var formDict: [String:String] = [:] + for entry in formParts { + formDict[entry[0].removingPercentEncoding!] = entry[1].removingPercentEncoding! + } + + let slackWebhookURL = formDict["response_url"] + + print("slackWebhookURL -> \(slackWebhookURL ?? "")") + + if (slackWebhookURL != nil) { + let url = URL(string: slackWebhookURL!)! + let response = try await fetch( + url.absoluteString, + .options( + method: .post, + body: .json([ + "text": "Hello, Slack Commands!", + "response_type": "in_channel" + ]), + headers: [ + "Content-Type": "application/json" + ], + backend: "hooks.slack.com" + ) + ) + } + + try await res.status(.ok).send() + } + + .get("/test") { req, res in + let messageBody = MessagePayload(text: "Test", responseType: "ephemeral") + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + let encodedMessageBody = try encoder.encode(messageBody) + try await res.status(.ok).send(messageBody, encoder: encoder) + } + +} From 66c46ca44d9ab922596b6e985e31085638694741 Mon Sep 17 00:00:00 2001 From: Zach Ward Date: Sat, 16 Apr 2022 15:48:33 -0400 Subject: [PATCH 2/7] add comment --- Sources/SlackCommand/File.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SlackCommand/File.swift b/Sources/SlackCommand/File.swift index 9600ed2..d9d8a2d 100644 --- a/Sources/SlackCommand/File.swift +++ b/Sources/SlackCommand/File.swift @@ -65,7 +65,8 @@ struct App { print("slackWebhookURL -> \(slackWebhookURL ?? "")") if (slackWebhookURL != nil) { - let url = URL(string: slackWebhookURL!)! + // not sure about all this force-unwrapping + let url = URL(string: slackWebhookURL)! let response = try await fetch( url.absoluteString, .options( From 1e246b4c14872819bb550e79273387679b09c3d2 Mon Sep 17 00:00:00 2001 From: Zach Ward Date: Sat, 16 Apr 2022 15:49:04 -0400 Subject: [PATCH 3/7] force unwrap slackWebookUrl --- Sources/SlackCommand/File.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SlackCommand/File.swift b/Sources/SlackCommand/File.swift index d9d8a2d..7f3c3a0 100644 --- a/Sources/SlackCommand/File.swift +++ b/Sources/SlackCommand/File.swift @@ -66,7 +66,7 @@ struct App { if (slackWebhookURL != nil) { // not sure about all this force-unwrapping - let url = URL(string: slackWebhookURL)! + let url = URL(string: slackWebhookURL!)! let response = try await fetch( url.absoluteString, .options( From c6b3949e097a4b5dd61e8d3bbad2956611a21d25 Mon Sep 17 00:00:00 2001 From: Zach Ward Date: Sat, 16 Apr 2022 16:49:07 -0400 Subject: [PATCH 4/7] update deps, use guard around slackWebhookURL --- Package.resolved | 2 +- Sources/SlackCommand/File.swift | 42 +++++++++++---------------------- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/Package.resolved b/Package.resolved index ad0066a..e39abef 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,7 +15,7 @@ "location" : "https://github.com/AndrewBarba/swift-compute-runtime", "state" : { "branch" : "main", - "revision" : "8c84156b144fc44ce758ea7745f9d0c902520904" + "revision" : "184a97d61249cb274491e2bde455667bc521ca47" } } ], diff --git a/Sources/SlackCommand/File.swift b/Sources/SlackCommand/File.swift index 7f3c3a0..050ec65 100644 --- a/Sources/SlackCommand/File.swift +++ b/Sources/SlackCommand/File.swift @@ -49,39 +49,25 @@ struct App { guard try await validateSignature(payload: payload.bytes) else { return try await res.status(.unauthorised).send("Invalid request") } + + let formDict = try await req.body.formValues() - // very simple approach to parsing application/x-www-form-urlencoded values into - // a dictionary - let formParts = rawBody.components(separatedBy: "&").map { entry in - return entry.components(separatedBy: "=") - } - var formDict: [String:String] = [:] - for entry in formParts { - formDict[entry[0].removingPercentEncoding!] = entry[1].removingPercentEncoding! + guard let slackWebhookURL = formDict["response_url"] else { + return try await res.status(.badRequest).send() } - let slackWebhookURL = formDict["response_url"] - - print("slackWebhookURL -> \(slackWebhookURL ?? "")") + print("slackWebhookURL -> \(slackWebhookURL)") - if (slackWebhookURL != nil) { - // not sure about all this force-unwrapping - let url = URL(string: slackWebhookURL!)! - let response = try await fetch( - url.absoluteString, - .options( - method: .post, - body: .json([ - "text": "Hello, Slack Commands!", - "response_type": "in_channel" - ]), - headers: [ - "Content-Type": "application/json" - ], - backend: "hooks.slack.com" - ) + let response = try await fetch( + slackWebhookURL, + .options( + method: .post, + body: .json([ + "text": "Hello, Slack Commands!", + "response_type": "in_channel" + ]) ) - } + ) try await res.status(.ok).send() } From 68639964a270c42e3b60a360ff36483c7aeec749 Mon Sep 17 00:00:00 2001 From: Zach Ward Date: Sat, 16 Apr 2022 17:45:36 -0400 Subject: [PATCH 5/7] add message to bad request response --- Sources/SlackCommand/File.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SlackCommand/File.swift b/Sources/SlackCommand/File.swift index 050ec65..0e1675c 100644 --- a/Sources/SlackCommand/File.swift +++ b/Sources/SlackCommand/File.swift @@ -53,7 +53,7 @@ struct App { let formDict = try await req.body.formValues() guard let slackWebhookURL = formDict["response_url"] else { - return try await res.status(.badRequest).send() + return try await res.status(.badRequest).send("Missing response_url") } print("slackWebhookURL -> \(slackWebhookURL)") From 7cae28bb9849c94f00599bc4f87f71f7a44a11aa Mon Sep 17 00:00:00 2001 From: Zach Ward Date: Sat, 16 Apr 2022 18:06:39 -0400 Subject: [PATCH 6/7] copy change --- Sources/SlackCommand/File.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SlackCommand/File.swift b/Sources/SlackCommand/File.swift index 0e1675c..7e0fb57 100644 --- a/Sources/SlackCommand/File.swift +++ b/Sources/SlackCommand/File.swift @@ -63,7 +63,7 @@ struct App { .options( method: .post, body: .json([ - "text": "Hello, Slack Commands!", + "text": "Hello, Slack", "response_type": "in_channel" ]) ) From 25f4185c2d71ac89111a9a189dac4d171d676db0 Mon Sep 17 00:00:00 2001 From: Zach Ward Date: Sat, 16 Apr 2022 22:35:16 -0400 Subject: [PATCH 7/7] return username in message body text --- Sources/SlackCommand/File.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/SlackCommand/File.swift b/Sources/SlackCommand/File.swift index 7e0fb57..def87de 100644 --- a/Sources/SlackCommand/File.swift +++ b/Sources/SlackCommand/File.swift @@ -55,15 +55,20 @@ struct App { guard let slackWebhookURL = formDict["response_url"] else { return try await res.status(.badRequest).send("Missing response_url") } + + guard let userName = formDict["user_name"] else { + return try await res.status(.badRequest).send("Missing user_name") + } print("slackWebhookURL -> \(slackWebhookURL)") - + print("userName -> \(userName)") + let response = try await fetch( slackWebhookURL, .options( method: .post, body: .json([ - "text": "Hello, Slack", + "text": "Hello, \(userName.capitalized)", "response_type": "in_channel" ]) )