diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..b423f24 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,13 @@ +# file options + +--exclude .build +--exclude Tests/OneSignalTests/XCTestManifests.swift + +# format options + +--self insert +--patternlet inline +--stripunusedargs unnamed-only +--comments ignore + +# rules diff --git a/Package.resolved b/Package.resolved index 39380fd..7d53ea4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -2,75 +2,12 @@ "object": { "pins": [ { - "package": "Console", - "repositoryURL": "https://github.com/vapor/console.git", + "package": "async-http-client", + "repositoryURL": "https://github.com/swift-server/async-http-client.git", "state": { "branch": null, - "revision": "d6cf07af59ae63cd95c4b5f98cf1f25627750fd1", - "version": "3.1.0" - } - }, - { - "package": "Core", - "repositoryURL": "https://github.com/vapor/core.git", - "state": { - "branch": null, - "revision": "1794ff138bd669175a2528d27695028d7cb30471", - "version": "3.5.0" - } - }, - { - "package": "Crypto", - "repositoryURL": "https://github.com/vapor/crypto.git", - "state": { - "branch": null, - "revision": "bce9ac891c9b33fc045deda713e0d976d13b749a", - "version": "3.3.1" - } - }, - { - "package": "DatabaseKit", - "repositoryURL": "https://github.com/vapor/database-kit.git", - "state": { - "branch": null, - "revision": "3557894af50914e134803a684b42a9ea6eefaea2", - "version": "1.3.2" - } - }, - { - "package": "HTTP", - "repositoryURL": "https://github.com/vapor/http.git", - "state": { - "branch": null, - "revision": "6973bf50dab8dd00e4daf8cb82ca72b33f5db016", - "version": "3.1.6" - } - }, - { - "package": "Multipart", - "repositoryURL": "https://github.com/vapor/multipart.git", - "state": { - "branch": null, - "revision": "e57007c23a52b68e44ebdfc70cbe882a7c4f1ec3", - "version": "3.0.2" - } - }, - { - "package": "Routing", - "repositoryURL": "https://github.com/vapor/routing.git", - "state": { - "branch": null, - "revision": "3219e328491b0853b8554c5a694add344d2c6cfb", - "version": "3.0.1" - } - }, - { - "package": "Service", - "repositoryURL": "https://github.com/vapor/service.git", - "state": { - "branch": null, - "revision": "281a70b69783891900be31a9e70051b6fe19e146", - "version": "1.0.0" + "revision": "64851a1a0a2a9e8fa7ae7b3508ce46a1da4a2e1d", + "version": "1.0.0-alpha.2" } }, { @@ -78,8 +15,8 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "a20e129c22ad00a51c902dca54a5456f90664780", - "version": "1.12.0" + "revision": "790827800d6af12ca6a8b1dca7a9072606fd2a1e", + "version": "2.7.0" } }, { @@ -87,71 +24,8 @@ "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", "state": { "branch": null, - "revision": "0f3999f3e3c359cc74480c292644c3419e44a12f", - "version": "1.4.0" - } - }, - { - "package": "swift-nio-ssl-support", - "repositoryURL": "https://github.com/apple/swift-nio-ssl-support.git", - "state": { - "branch": null, - "revision": "c02eec4e0e6d351cd092938cf44195a8e669f555", - "version": "1.0.0" - } - }, - { - "package": "swift-nio-zlib-support", - "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", - "state": { - "branch": null, - "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", - "version": "1.0.0" - } - }, - { - "package": "TemplateKit", - "repositoryURL": "https://github.com/vapor/template-kit.git", - "state": { - "branch": null, - "revision": "aff2d6fc65bfd04579b0201b31a8d6720239c1cf", - "version": "1.1.1" - } - }, - { - "package": "URLEncodedForm", - "repositoryURL": "https://github.com/vapor/url-encoded-form.git", - "state": { - "branch": null, - "revision": "932024f363ee5ff59059cf7d67194a1c271d3d0c", - "version": "1.0.5" - } - }, - { - "package": "Validation", - "repositoryURL": "https://github.com/vapor/validation.git", - "state": { - "branch": null, - "revision": "4de213cf319b694e4ce19e5339592601d4dd3ff6", - "version": "2.1.1" - } - }, - { - "package": "Vapor", - "repositoryURL": "https://github.com/vapor/vapor.git", - "state": { - "branch": null, - "revision": "54cbf396147aba77ecb32e7fc75a3d13251551b1", - "version": "3.1.1" - } - }, - { - "package": "WebSocket", - "repositoryURL": "https://github.com/vapor/websocket.git", - "state": { - "branch": null, - "revision": "21eb4773e25a8ff96fe347a31fe106900a69fa6a", - "version": "1.1.1" + "revision": "f5dd7a60ff56f501ff7bf9be753e4b1875bfaf20", + "version": "2.4.0" } } ] diff --git a/Package.swift b/Package.swift index 4d4f435..10b3d56 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.0 // Managed by ice import PackageDescription @@ -9,10 +9,10 @@ let package = Package( .library(name: "OneSignal", targets: ["OneSignal"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0") + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0-alpha.1"), ], targets: [ - .target(name: "OneSignal", dependencies: ["Vapor"]), + .target(name: "OneSignal", dependencies: ["AsyncHTTPClient"]), .testTarget(name: "OneSignalTests", dependencies: ["OneSignal"]), ] ) diff --git a/README.md b/README.md index ae0ac4c..441d624 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,14 @@ Using OneSignal on Vapor. For sending to an array of device tokens: ```swift +let apiKey = "YourApiKey" +let appId = "YourAppId" + let deviceTokens = ["foo...", "bar..."] let message = OneSignalMessage("Hello Vapor!") -let notif = OneSignalNotification(message: message, iosDeviceTokens: [deviceTokens]) -let app = OneSignalApp(apiKey: Environment.get("ONESIGNAL_API_KEY") ?? "", appId: Environment.get("ONESIGNAL_APP_ID") ?? "") -let result = try OneSignal.makeService(for: request).send(notification: notif, toApp: app) +let notif = OneSignalNotification(message: message, users: deviceTokens) +let app = OneSignalApp(apiKey: apiKey, appId: appId) + +let eventLoop = MultiThreadedEventLoopGroup(numberOfThreads: 1).next() +let resultFuture = try OneSignal(on: eventLoop).send(notification: notif, toApp: app) ``` diff --git a/Sources/OneSignal/OneSignal.swift b/Sources/OneSignal/OneSignal.swift index 7fe67b7..199be2a 100644 --- a/Sources/OneSignal/OneSignal.swift +++ b/Sources/OneSignal/OneSignal.swift @@ -4,31 +4,34 @@ // // Created by Anthony Castelli on 9/5/18. // + +import AsyncHTTPClient import Foundation -import Vapor - -public final class OneSignal: ServiceType { - - var worker: Container - var client: FoundationClient - - public static func makeService(for worker: Container) throws -> OneSignal { - return try OneSignal(worker: worker) +import NIO + +public final class OneSignal { + let httpClient: HTTPClient + + public init(httpClient: HTTPClient) { + self.httpClient = httpClient } - - public init(worker: Container) throws { - self.worker = worker - self.client = try FoundationClient.makeService(for: worker) + + public init(on eventLoop: EventLoop) { + self.httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoop)) } - + + deinit { + try? httpClient.syncShutdown() + } + /// Send the message - public func send(notification: OneSignalNotification, toApp app: OneSignalApp) throws -> Future { - return try self.client.send(notification.generateRequest(on: self.worker, for: app)).map(to: OneSignalResult.self) { response in - guard let body = response.http.body.data else { + public func send(notification: OneSignalNotification, toApp app: OneSignalApp) throws -> EventLoopFuture { + return try self.sendRaw(notification: notification, toApp: app).map { response in + guard var responseBody = response.body, let body = responseBody.readBytes(length: responseBody.readableBytes) else { return OneSignalResult.error(error: OneSignalError.internal) } - - guard response.http.status == .ok else { + + guard response.status == .ok else { if let message = String(bytes: body, encoding: .utf8) { return OneSignalResult.error(error: OneSignalError.requestError(value: message)) } @@ -37,9 +40,8 @@ public final class OneSignal: ServiceType { return OneSignalResult.success } } - - public func sendRaw(notification: OneSignalNotification, toApp app: OneSignalApp) throws -> Future { - return try self.client.send(notification.generateRequest(on: self.worker, for: app)) + + public func sendRaw(notification: OneSignalNotification, toApp app: OneSignalApp) throws -> EventLoopFuture { + return try self.httpClient.execute(request: notification.generateRequest(for: app)) } - } diff --git a/Sources/OneSignal/OneSignalApp.swift b/Sources/OneSignal/OneSignalApp.swift index 56e0b8b..94e7786 100644 --- a/Sources/OneSignal/OneSignalApp.swift +++ b/Sources/OneSignal/OneSignalApp.swift @@ -12,10 +12,10 @@ public struct OneSignalApp: Codable { case apiKey = "api_key" case appId = "app_id" } - + public var apiKey: String public var appId: String - + public init(apiKey: String, appId: String) { self.apiKey = apiKey self.appId = appId diff --git a/Sources/OneSignal/OneSignalMessage.swift b/Sources/OneSignal/OneSignalMessage.swift index 81e823c..1c6f273 100644 --- a/Sources/OneSignal/OneSignalMessage.swift +++ b/Sources/OneSignal/OneSignalMessage.swift @@ -8,13 +8,13 @@ import Foundation public struct OneSignalMessage: Codable { - internal var messages: [String : String] - + internal var messages: [String: String] + public init(_ message: String) { // English is required by `OneSignal` self.messages = ["en": message] } - + public subscript(key: String) -> String? { get { return self.messages[key] diff --git a/Sources/OneSignal/OneSignalNotification.swift b/Sources/OneSignal/OneSignalNotification.swift index 2a1eea1..bc6d848 100644 --- a/Sources/OneSignal/OneSignalNotification.swift +++ b/Sources/OneSignal/OneSignalNotification.swift @@ -5,98 +5,98 @@ // Created by Anthony Castelli on 9/5/18. // +import AsyncHTTPClient import Foundation -import Vapor public struct OneSignalNotification: Codable { enum CodingKeys: String, CodingKey { case users case deviceTokens - + case title case subtitle case message - + case category = "ios_category" - + case sound = "ios_sound" case sendAfter = "send_after" case additionalData = "data" case attachments = "ios_attachments" - + case isContentAvailable = "content_available" case isContentMutable = "mutable_content" } - + /** RECOMMENDED - Specific players to send your notification to. Does not require API Auth Key. Do not combine with other targeting parameters. Not compatible with any other targeting parameters. - + Example: `["1dd608f2-c6a1-11e3-851d-000c2940e62c"]` */ public var users: [String] = [] - + /** NOT RECOMMENDED - Please consider using include_player_ids instead. Target using iOS device tokens. Warning: Only works with Production tokens. All non-alphanumeric characters must be removed from each token. If a token does not correspond to an existing user, a new user will be created. - + Example: `ce777617da7f548fe7a9ab6febb56cf39fba6d38203...` */ public var deviceTokens: [String]? - + /** The notification's title, a map of language codes to text for each language. Each hash must have a language code string for a key, mapped to the localized text you would like users to receive for that language. This field supports inline substitutions. - + Example: `{"en": "English Title", "es": "Spanish Title"}` */ public var title: OneSignalMessage? - + /** The notification's subtitle, a map of language codes to text for each language. Each hash must have a language code string for a key, mapped to the localized text you would like users to receive for that language. - + This field supports inline substitutions https://documentation.onesignal.com/docs/tag-variable-substitution. */ public var subtitle: OneSignalMessage? - + /** REQUIRED unless content_available=true or template_id is set. - + The notification's content (excluding the title), a map of language codes to text for each language. - + Each hash must have a language code string for a key, mapped to the localized text you would like users to receive for that language. This field supports inline substitutions. English must be included in the hash. - + Example: `{"en": "English Message", "es": "Spanish Message"}` */ public var message: OneSignalMessage - + /** Category APS payload, use with `registerUserNotificationSettings:categories` in your Objective-C / Swift code. - + Example: calendar category which contains actions like accept and decline iOS 10+ This will trigger your `UNNotificationContentExtension` whose ID matches this category. */ public var category: String? - + /** Sound file that is included in your app to play instead of the default device notification sound. Pass nil to disable vibration and sound for the notification. - + Example: `"notification.wav"` */ public var sound: String? - + /** Schedule notification for future delivery. - + Examples: All examples are the exact same date & time. `"Thu Sep 24 2015 14:00:00 GMT-0700 (PDT)"` `"September 24th 2015, 2:00:00 pm UTC-07:00"` @@ -105,59 +105,58 @@ public struct OneSignalNotification: Codable { `"Thu Sep 24 2015 14:00:00 GMT-0700 (Pacific Daylight Time)"` */ public var sendAfter: String? - + /** A custom map of data that is passed back to your app. - + Example: `{"abc": "123", "foo": "bar"}` */ - public var additionalData: [String : String]? - + public var additionalData: [String: String]? + /** Adds media attachments to notifications. Set as JSON object, key as a media id of your choice and the value as a valid local filename or URL. User must press and hold on the notification to view. - + Do not set `mutable_content` to download attachments. The OneSignal SDK does this automatically */ - public var attachments: [String : String]? - + public var attachments: [String: String]? + /** Sending true wakes your app from background to run custom native code (Apple interprets this as content-available=1). - + Note: Not applicable if the app is in the "force-quit" state (i.e app was swiped away). Omit the contents field to prevent displaying a visible notification. */ public var isContentAvailable: Bool? - + /** Sending true allows you to change the notification content in your app before it is displayed. Triggers `didReceive(_:withContentHandler:)` on your `UNNotificationServiceExtension.` */ public var isContentMutable: Bool? - - + public init(message: String) { self.message = OneSignalMessage(message) self.users = [] } - + public init(message: OneSignalMessage) { self.message = message self.users = [] } - + public init(message: String, users: [String]) { self.message = OneSignalMessage(message) self.users = users } - + public init(message: OneSignalMessage, users: [String]) { self.message = message self.users = users } - - public init(title: String?, subtitle: String?, body: String, users: [String], deviceTokens: [String]? = nil, sound: String? = nil, category: String? = nil, sendAfter: String? = nil, additionalData: [String : String]? = nil, attachments: [String : String]? = nil) { + + public init(title: String?, subtitle: String?, body: String, users: [String], deviceTokens: [String]? = nil, sound: String? = nil, category: String? = nil, sendAfter: String? = nil, additionalData: [String: String]? = nil, attachments: [String: String]? = nil) { if let title = title { self.title = OneSignalMessage(title) } if let subtitle = subtitle { self.subtitle = OneSignalMessage(subtitle) } self.message = OneSignalMessage(body) @@ -174,7 +173,7 @@ extension OneSignalNotification { public mutating func addUser(_ id: String) { self.users.append(id) } - + public mutating func addMessage(_ message: String, language: String = "en") { self.message[language] = message } @@ -184,37 +183,34 @@ extension OneSignalNotification { public mutating func setTitle(_ title: String) { self.setTitle(OneSignalMessage(title)) } - + public mutating func setTitle(_ message: OneSignalMessage) { self.title = message } - + public mutating func setSubtitle(_ subtitle: String) { self.setSubtitle(OneSignalMessage(subtitle)) } - + public mutating func setSubtitle(_ message: OneSignalMessage) { self.subtitle = message } - + public mutating func setContentAvailable(_ isContentAvailable: Bool?) { self.isContentAvailable = isContentAvailable } - + public mutating func setContentMutability(_ isContentMutable: Bool?) { self.isContentMutable = isContentMutable } } extension OneSignalNotification { - internal func generateRequest(on container: Container, for app: OneSignalApp) throws -> Request { - let request = Request(using: container) - request.http.method = .POST - - request.http.headers.add(name: .connection, value: "Keep-Alive") - request.http.headers.add(name: .authorization, value: "Basic \(app.apiKey)") - request.http.headers.add(name: .contentType, value: "applicaiton/json") - + internal func generateRequest(for app: OneSignalApp) throws -> HTTPClient.Request { + guard let url = URL(string: "https://onesignal.com/api/v1/notifications") else { + throw OneSignalError.invalidURL + } + let payload = OneSignalPayload( appId: app.appId, playerIds: self.users, @@ -230,14 +226,29 @@ extension OneSignalNotification { contentAvailable: self.isContentAvailable, mutableContent: self.isContentMutable ) - - try request.content.encode(payload) - - guard let url = URL(string: "https://onesignal.com/api/v1/notifications") else { - throw OneSignalError.invalidURL + + let bodyData: Data + do { + bodyData = try JSONEncoder().encode(payload) + } catch { + throw OneSignalError.makeRequestBodyFailed(error) } - request.http.url = url - + + let request: HTTPClient.Request + do { + request = try HTTPClient.Request( + url: url, + method: .POST, + headers: .init([ + ("Authorization", "Basic \(app.apiKey)"), + ("Content-Type", "application/json"), + ]), + body: .data(bodyData) + ) + } catch { + throw OneSignalError.makeRequestFailed(error) + } + return request } } diff --git a/Sources/OneSignal/OneSignalPayload.swift b/Sources/OneSignal/OneSignalPayload.swift index 755d852..35956d0 100644 --- a/Sources/OneSignal/OneSignalPayload.swift +++ b/Sources/OneSignal/OneSignalPayload.swift @@ -5,47 +5,46 @@ // Created by Anthony Castelli on 9/5/18. // +import AsyncHTTPClient import Foundation -import Vapor -public struct OneSignalPayload: Content { +public struct OneSignalPayload: Encodable { enum CodingKeys: String, CodingKey { case appId = "app_id" case playerIds = "include_player_ids" case iosDeviceTokens = "include_ios_tokens" - case contents = "contents" - case headings = "headings" - case subtitle = "subtitle" - + case contents + case headings + case subtitle + case category = "ios_category" - + case sound = "ios_sound" case sendAfter = "send_after" case additionalData = "data" case attachments = "ios_attachments" - + case contentAvailable = "content_available" case mutableContent = "mutable_content" } - + public var appId: String public var playerIds: [String] public var iosDeviceTokens: [String]? public var contents: [String: String] - + public var headings: [String: String]? public var subtitle: [String: String]? - + public var category: String? public var sound: String? public var sendAfter: String? - public var additionalData: [String : String]? - public var attachments: [String : String]? - + public var additionalData: [String: String]? + public var attachments: [String: String]? + public var contentAvailable: Bool? public var mutableContent: Bool? } - diff --git a/Sources/OneSignal/OneSignalResult.swift b/Sources/OneSignal/OneSignalResult.swift index 8db7f8e..01311c4 100644 --- a/Sources/OneSignal/OneSignalResult.swift +++ b/Sources/OneSignal/OneSignalResult.swift @@ -10,6 +10,8 @@ import Foundation public enum OneSignalError: Swift.Error { case `internal` case invalidURL + case makeRequestBodyFailed(Error) + case makeRequestFailed(Error) case apiKeyNotSet case appIDNotSet case requestError(value: String) diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index fa8374c..3861989 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -3,5 +3,6 @@ import XCTest import OneSignalTests var tests = [XCTestCaseEntry]() -tests += OneSignalTests.allTests() -XCTMain(tests) \ No newline at end of file +tests += OneSignalTests.__allTests() + +XCTMain(tests) diff --git a/Tests/OneSignalTests/OneSignalTests.swift b/Tests/OneSignalTests/OneSignalTests.swift index d1f1f99..22073bf 100644 --- a/Tests/OneSignalTests/OneSignalTests.swift +++ b/Tests/OneSignalTests/OneSignalTests.swift @@ -1,16 +1,23 @@ -import XCTest @testable import OneSignal +import XCTest final class OneSignalTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(OneSignal().text, "Hello, World!") - } + func testOneSignalNotificationGenerateRequest() throws { + let apiKey = "YourApiKey" + let appId = "YourAppId" + + let deviceTokens = ["foo...", "bar..."] + let message = OneSignalMessage("Hello Vapor!") + let notif = OneSignalNotification(message: message, users: deviceTokens) + let app = OneSignalApp(apiKey: apiKey, appId: appId) + let request = try notif.generateRequest(for: app) - static var allTests = [ - ("testExample", testExample), - ] + XCTAssertEqual(request.url.absoluteString, "https://onesignal.com/api/v1/notifications") + + XCTAssertEqual(request.headers["Authorization"].first, "Basic YourApiKey") + XCTAssertEqual(request.headers["Content-Type"].first, "application/json") + + XCTAssertEqual(request.body?.length, #"{"contents":{"en":"Hello Vapor!"},"app_id":"YourAppId","include_player_ids":["foo...","bar..."]}"# .count) + } } diff --git a/Tests/OneSignalTests/XCTestManifests.swift b/Tests/OneSignalTests/XCTestManifests.swift index 6b47862..88b29b6 100644 --- a/Tests/OneSignalTests/XCTestManifests.swift +++ b/Tests/OneSignalTests/XCTestManifests.swift @@ -1,9 +1,18 @@ +#if !canImport(ObjectiveC) import XCTest -#if !os(macOS) -public func allTests() -> [XCTestCaseEntry] { +extension OneSignalTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__OneSignalTests = [ + ("testOneSignalNotificationGenerateRequest", testOneSignalNotificationGenerateRequest), + ] +} + +public func __allTests() -> [XCTestCaseEntry] { return [ - testCase(OneSignalTests.allTests), + testCase(OneSignalTests.__allTests__OneSignalTests), ] } -#endif \ No newline at end of file +#endif