-
Notifications
You must be signed in to change notification settings - Fork 265
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
227 changed files
with
12,158 additions
and
2,951 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
AIRSHIP_VERSION="18.3.1" | ||
AIRSHIP_VERSION="18.4.0" | ||
|
||
Pod::Spec.new do |s| | ||
s.version = AIRSHIP_VERSION | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
Airship/AirshipAutomation/Source/AudienceCheck/AdditionalAudienceCheckerApiClient.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/* Copyright Airship and Contributors */ | ||
|
||
import Foundation | ||
#if canImport(AirshipCore) | ||
import AirshipCore | ||
#endif | ||
|
||
protocol AdditionalAudienceCheckerAPIClientProtocol: Sendable { | ||
func resolve( | ||
info: AdditionalAudienceCheckResult.Request | ||
) async throws -> AirshipHTTPResponse<AdditionalAudienceCheckResult> | ||
} | ||
|
||
struct AdditionalAudienceCheckResult: Codable, Sendable, Equatable { | ||
let isMatched: Bool | ||
let cacheTTL: TimeInterval | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case isMatched = "allowed" | ||
case cacheTTL = "cache_seconds" | ||
} | ||
|
||
struct Request: Sendable { | ||
let url: URL | ||
let channelID: String | ||
let contactID: String | ||
let namedUserID: String? | ||
let context: AirshipJSON? | ||
} | ||
} | ||
|
||
final class AdditionalAudienceCheckerAPIClient: AdditionalAudienceCheckerAPIClientProtocol { | ||
private let config: RuntimeConfig | ||
private let session: AirshipRequestSession | ||
private let encoder: JSONEncoder | ||
|
||
init(config: RuntimeConfig, session: AirshipRequestSession, encoder: JSONEncoder = JSONEncoder()) { | ||
self.config = config | ||
self.session = session | ||
self.encoder = encoder | ||
} | ||
|
||
convenience init(config: RuntimeConfig) { | ||
self.init( | ||
config: config, | ||
session: config.requestSession | ||
) | ||
} | ||
|
||
func resolve( | ||
info: AdditionalAudienceCheckResult.Request | ||
) async throws -> AirshipHTTPResponse<AdditionalAudienceCheckResult> { | ||
|
||
let body = RequestBody( | ||
channelID: info.channelID, | ||
contactID: info.contactID, | ||
namedUserID: info.namedUserID, | ||
context: info.context | ||
) | ||
|
||
let request = AirshipRequest( | ||
url: info.url, | ||
headers: [ | ||
"Content-Type": "application/json", | ||
"Accept": "application/vnd.urbanairship+json; version=3;", | ||
"X-UA-Contact-ID": info.contactID, | ||
"X-UA-Device-Family": "ios", | ||
], | ||
method: "POST", | ||
auth: .contactAuthToken(identifier: info.contactID), | ||
body: try encoder.encode(body) | ||
) | ||
|
||
AirshipLogger.trace("Performing additional audience check with request \(request), body: \(body)") | ||
|
||
return try await session.performHTTPRequest(request) { data, response in | ||
AirshipLogger.debug("Additional audience check response finished with response: \(response)") | ||
|
||
guard (200..<300).contains(response.statusCode) else { | ||
return nil | ||
} | ||
|
||
guard let data = data else { | ||
throw AirshipErrors.error("Invalid response body \(String(describing: data))") | ||
} | ||
|
||
return try JSONDecoder().decode(AdditionalAudienceCheckResult.self, from: data) | ||
} | ||
} | ||
|
||
fileprivate struct RequestBody: Encodable { | ||
let channelID: String | ||
let contactID: String | ||
let namedUserID: String? | ||
let context: AirshipJSON? | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case channelID = "channel_id" | ||
case contactID = "contact_id" | ||
case namedUserID = "named_user_id" | ||
case context | ||
} | ||
} | ||
} |
139 changes: 139 additions & 0 deletions
139
Airship/AirshipAutomation/Source/AudienceCheck/AdditionalAudienceCheckerResolver.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/* Copyright Airship and Contributors */ | ||
|
||
import Foundation | ||
#if canImport(AirshipCore) | ||
import AirshipCore | ||
#endif | ||
|
||
protocol AdditionalAudienceCheckerResolverProtocol: AnyActor { | ||
func resolve( | ||
deviceInfoProvider: AudienceDeviceInfoProvider, | ||
additionalAudienceCheckOverrides: AdditionalAudienceCheckOverrides? | ||
) async throws -> Bool | ||
} | ||
|
||
actor AdditionalAudienceCheckerResolver: AdditionalAudienceCheckerResolverProtocol { | ||
private let cache: AirshipCache | ||
private let apiClient: AdditionalAudienceCheckerAPIClientProtocol | ||
|
||
private let date: AirshipDateProtocol | ||
private var inProgress: Task<Bool, Error>? | ||
private let configProvider: () -> RemoteConfig.AdditionalAudienceCheckConfig? | ||
|
||
private var additionalAudienceConfig: RemoteConfig.AdditionalAudienceCheckConfig? { | ||
get { | ||
configProvider() | ||
} | ||
} | ||
|
||
init( | ||
config: RuntimeConfig, | ||
cache: AirshipCache, | ||
date: AirshipDateProtocol = AirshipDate.shared | ||
) { | ||
self.cache = cache | ||
self.apiClient = AdditionalAudienceCheckerAPIClient(config: config) | ||
self.date = date | ||
self.configProvider = { | ||
config.remoteConfig.iaaConfig?.additionalAudienceConfig | ||
} | ||
} | ||
|
||
/// Testing | ||
init( | ||
cache: AirshipCache, | ||
apiClient: AdditionalAudienceCheckerAPIClientProtocol, | ||
date: AirshipDateProtocol, | ||
configProvider: @escaping () -> RemoteConfig.AdditionalAudienceCheckConfig? | ||
) { | ||
self.cache = cache | ||
self.apiClient = apiClient | ||
self.date = date | ||
self.configProvider = configProvider | ||
} | ||
|
||
func resolve( | ||
deviceInfoProvider: AudienceDeviceInfoProvider, | ||
additionalAudienceCheckOverrides: AdditionalAudienceCheckOverrides? | ||
) async throws -> Bool { | ||
|
||
guard | ||
let config = additionalAudienceConfig, | ||
config.isEnabled | ||
else { | ||
return true | ||
} | ||
|
||
guard | ||
let urlString = additionalAudienceCheckOverrides?.url ?? config.url, | ||
let url = URL(string: urlString) | ||
else { | ||
AirshipLogger.warn("Failed to parse additional audience check url " + | ||
String(describing: additionalAudienceCheckOverrides) + ", " + | ||
String(describing: config) + ")") | ||
throw AirshipErrors.error("Missing additional audience check url") | ||
} | ||
|
||
guard additionalAudienceCheckOverrides?.bypass != true else { | ||
AirshipLogger.trace("Additional audience check is bypassed") | ||
return true | ||
} | ||
let context = additionalAudienceCheckOverrides?.context ?? config.context | ||
|
||
_ = try? await inProgress?.value | ||
let task = Task { | ||
return try await doResolve( | ||
url: url, | ||
context: context, | ||
deviceInfoProvider: deviceInfoProvider | ||
) | ||
} | ||
|
||
inProgress = task | ||
return try await task.value | ||
} | ||
|
||
private func doResolve( | ||
url: URL, | ||
context: AirshipJSON?, | ||
deviceInfoProvider: AudienceDeviceInfoProvider | ||
) async throws -> Bool { | ||
|
||
let channelID = try await deviceInfoProvider.channelID | ||
let contactInfo = await deviceInfoProvider.stableContactInfo | ||
|
||
let cacheKey = try cacheKey( | ||
url: url.absoluteString, | ||
context: context ?? .null, | ||
contactID: contactInfo.contactID, | ||
channelID: channelID | ||
) | ||
|
||
if let cached: AdditionalAudienceCheckResult = await cache.getCachedValue(key: cacheKey) { | ||
return cached.isMatched | ||
} | ||
|
||
let request = AdditionalAudienceCheckResult.Request( | ||
url: url, | ||
channelID: channelID, | ||
contactID: contactInfo.contactID, | ||
namedUserID: contactInfo.namedUserID, | ||
context: context | ||
) | ||
|
||
let response = try await apiClient.resolve(info: request) | ||
|
||
if response.isSuccess, let result = response.result { | ||
await cache.setCachedValue(result, key: cacheKey, ttl: result.cacheTTL) | ||
return result.isMatched | ||
} else if response.isServerError { | ||
throw AirshipErrors.error("Failed to perform additional check due to server error \(response)") | ||
} else { | ||
return false | ||
} | ||
} | ||
|
||
private func cacheKey(url: String, context: AirshipJSON, contactID: String, channelID: String) throws -> String { | ||
return String([url, try context.toString(), contactID, channelID].joined(separator: ":")) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.