From 30bce2853215912afbaa18a6e16e11f1edb04f80 Mon Sep 17 00:00:00 2001 From: Khoa Pham Date: Mon, 30 Dec 2019 12:21:34 +0100 Subject: [PATCH] Send message to Slack --- Documentation/GettingStarted.md | 2 +- Documentation/Tasks/Slack.md | 18 +++ Example/TestPuma/TestPuma/main.swift | 15 +++ Sources/Core/Library/Console/Console.swift | 4 + Sources/Core/Library/PumaError.swift | 11 +- Sources/Core/Tasks/Slack/Slack.swift | 122 +++++++++++++++++++++ 6 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 Documentation/Tasks/Slack.md create mode 100644 Sources/Core/Tasks/Slack/Slack.swift diff --git a/Documentation/GettingStarted.md b/Documentation/GettingStarted.md index 515050f..9e49d44 100644 --- a/Documentation/GettingStarted.md +++ b/Documentation/GettingStarted.md @@ -128,6 +128,7 @@ Contains the core utilities, Task protocol and some core tasks - Concurrent: run sub tasks in parallel - DownloadFile: Download and save file - MoveFile: Move file to another location +- [Slack](Tasks/Slack.md): send message as a bot to Slack ### PumaiOS @@ -149,7 +150,6 @@ Contains Android related tasks. TBD Contains extra tasks -- Slack: interact with Slack - AppStoreConnect: interact with AppStore Connect - AppDistribution: interact with Firebase AppDistribution - Crashlytics: interact with Firebase Crashlytics \ No newline at end of file diff --git a/Documentation/Tasks/Slack.md b/Documentation/Tasks/Slack.md new file mode 100644 index 0000000..d9c90e5 --- /dev/null +++ b/Documentation/Tasks/Slack.md @@ -0,0 +1,18 @@ +## Slack + +### Post message to a channel in Slack + +Follow [Create a bot for your workspace](https://slack.com/intl/en-no/help/articles/115005265703-create-a-bot-for-your-workspace) to create, add a bot to your workspace. Obtain `Bot User OAuth Access Token` to use to authenticate + +```swift +Slack { + $0.post( + message: .init( + token: ProcessInfo().environment["slackBotToken"]!, + channel: "random", + text: "Hello from Puma", + username: "onmyway133" + ) + ) +} +``` \ No newline at end of file diff --git a/Example/TestPuma/TestPuma/main.swift b/Example/TestPuma/TestPuma/main.swift index 85d2ee7..5cc50f8 100644 --- a/Example/TestPuma/TestPuma/main.swift +++ b/Example/TestPuma/TestPuma/main.swift @@ -56,6 +56,7 @@ func testDrive() { } Screenshot { + $0.isEnabled = false $0.configure( projectType: .project("TestApp"), appScheme: "TestApp", @@ -86,6 +87,7 @@ func testDrive() { } Archive { + $0.isEnabled = false $0.configure( projectType: .project("TestApp"), scheme: "TestApp", @@ -94,6 +96,7 @@ func testDrive() { } ExportArchive { + $0.isEnabled = false $0.configure( projectType: .project("TestApp"), archivePath: Directory.downloads.appendingPathComponent("TestApp.xcarchive").path, @@ -110,6 +113,7 @@ func testDrive() { } UploadApp { + $0.isEnabled = false $0.authenticate( username: ProcessInfo().environment["username"]!, password: ProcessInfo().environment["password"]! @@ -119,6 +123,17 @@ func testDrive() { ipaPath: Directory.downloads.appendingPathComponent("TestApp.ipa").path ) } + + Slack { + $0.post( + message: .init( + token: ProcessInfo().environment["slackBotToken"]!, + channel: "random", + text: "Hello from Puma", + username: "onmyway133" + ) + ) + } } workflow.workingDirectory = Directory.home.appendingPathComponent("XcodeProject2/Puma/Example/TestApp").path diff --git a/Sources/Core/Library/Console/Console.swift b/Sources/Core/Library/Console/Console.swift index 306ef5d..6c43191 100644 --- a/Sources/Core/Library/Console/Console.swift +++ b/Sources/Core/Library/Console/Console.swift @@ -29,6 +29,10 @@ open class Console { print("❌ \(text)".foreground.Red) } + open func success(_ text: String) { + print("👍 \(text)".foreground.Green) + } + open func warn(_ text: String) { print("⚠️ \(text)".foreground.Magenta) } diff --git a/Sources/Core/Library/PumaError.swift b/Sources/Core/Library/PumaError.swift index 57ffd4d..b5b508c 100644 --- a/Sources/Core/Library/PumaError.swift +++ b/Sources/Core/Library/PumaError.swift @@ -8,8 +8,15 @@ import Foundation public enum PumaError: Error { - case unknown case invalid - case validate(String) + case string(String) case process(terminationStatus: Int32, output: String, error: String) + + static func from(string: String?) -> PumaError { + if let string = string { + return PumaError.string(string) + } else { + return PumaError.invalid + } + } } diff --git a/Sources/Core/Tasks/Slack/Slack.swift b/Sources/Core/Tasks/Slack/Slack.swift new file mode 100644 index 0000000..72c3f69 --- /dev/null +++ b/Sources/Core/Tasks/Slack/Slack.swift @@ -0,0 +1,122 @@ +// +// Slack.swift +// PumaCore +// +// Created by khoa on 30/12/2019. +// + +import Foundation + +public class Slack { + public var name: String = "Send message to Slack" + public var isEnabled = true + + private var message: Message? + + public init(_ closure: (Slack) -> Void = { _ in }) { + closure(self) + } +} + +public extension Slack { + struct Message { + let token: String + let channel: String + let text: String + let username: String? + + public init( + token: String, + channel: String, + text: String, + username: String + ) { + self.token = token + self.channel = channel + self.text = text + self.username = username + } + } + + func post(message: Message) { + self.message = message + } +} + +extension Slack: Task { + public func run(workflow: Workflow, completion: @escaping TaskCompletion) { + guard let message = message else { + completion(.failure(PumaError.invalid)) + return + } + + let sender = MessageSender() + sender.send(message: message, completion: { result in + switch result { + case .success: + Deps.console.success("Posted: \(message.text)") + case .failure(let error): + Deps.console.error("Failed: \(error.localizedDescription)") + } + completion(result) + }) + } +} + +private class MessageSender { + struct Response: Decodable { + let ok: Bool + let error: String? + } + + func send(message: Slack.Message, completion: @escaping (Result<(), Error>) -> Void) { + guard let baseUrl = URL(string: "https://slack.com/api/chat.postMessage") else { + completion(.failure(PumaError.invalid)) + return + } + + var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: false) + components?.queryItems = [ + URLQueryItem(name: "token", value: message.token), + URLQueryItem(name: "channel", value: message.channel), + URLQueryItem(name: "text", value: message.text), + URLQueryItem(name: "pretty", value: "1") + ] + + if let username = message.username { + components?.queryItems?.append( + URLQueryItem(name: "username", value: username) + ) + } + + guard let requestUrl = components?.url else { + completion(.failure(PumaError.invalid)) + return + } + + var request = URLRequest(url: requestUrl) + request.allHTTPHeaderFields = [ + "Accept": "application/json" + ] + + let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in + guard let data = data else { + completion(.failure(error ?? PumaError.invalid)) + return + } + + do { + let response = try JSONDecoder().decode(Response.self, from: data) + if response.ok { + completion(.success(())) + } else { + completion(.failure(PumaError.from(string: response.error))) + } + } catch { + completion(.failure(error)) + } + }) + + task.resume() + } +}