Dactyloidae iOS initial commit

This commit is contained in:
wuggy 2026-06-26 21:04:09 -07:00
commit 7154a0497e
2123 changed files with 197052 additions and 0 deletions

View file

@ -0,0 +1,24 @@
<?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>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>10.6</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View file

@ -0,0 +1,170 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Alamofire
import Shared
import JSONSchema
import Deferred
// MARK: Ping Centre Client
public protocol PingCentreClient {
@discardableResult func sendPing(_ data: [String: Any], validate: Bool) -> Success
@discardableResult func sendBatch(_ data: [[String: Any]], validate: Bool) -> Success
}
/*
* A Ping Centre Topic has a name and an associated JSON schema describing the ping data.
*/
public struct PingCentreTopic {
public let name: String
public let schema: Schema
public init(name: String, schema: Schema) {
self.name = name
self.schema = schema
}
}
public struct PingCentre {
public static func clientForTopic(_ topic: PingCentreTopic, clientID: String) -> PingCentreClient {
guard !AppConstants.IsRunningTest else {
return DefaultPingCentreImpl(topic: topic, endpoint: .staging, clientID: clientID)
}
switch AppConstants.BuildChannel {
case .developer:
return DefaultPingCentreImpl(topic: topic, endpoint: .staging, clientID: clientID)
case .beta:
fallthrough
case .release:
return DefaultPingCentreImpl(topic: topic, endpoint: .production, clientID: clientID)
}
}
}
public struct PingValidationError: MaybeErrorType {
public let errors: [String]
public var description: String {
return "Ping JSON validation failed with the following errors: \(errors)"
}
}
public struct PingJSONError: MaybeErrorType {
public let error: Error
public var description: String {
return "Failed to serialize JSON ping into NSData format -- \(error)"
}
}
enum Endpoint {
case staging
case production
var url: URL {
switch self {
case .staging:
return URL(string: "https://onyx_tiles.stage.mozaws.net/v3/links/ping-centre")!
case .production:
return URL(string: "https://tiles.services.mozilla.com/v3/links/ping-centre")!
}
}
}
class DefaultPingCentreImpl: PingCentreClient {
fileprivate let topic: PingCentreTopic
fileprivate let clientID: String
fileprivate let endpoint: Endpoint
fileprivate let manager: SessionManager
fileprivate let validationQueue: DispatchQueue
fileprivate static let queueLabel = "org.mozilla.pingcentre.validationQueue"
init(topic: PingCentreTopic, endpoint: Endpoint, clientID: String,
validationQueue: DispatchQueue = DispatchQueue(label: queueLabel),
manager: SessionManager = SessionManager()) {
self.topic = topic
self.clientID = clientID
self.endpoint = endpoint
self.manager = manager
self.validationQueue = validationQueue
}
public func sendPing(_ data: [String: Any], validate: Bool) -> Success {
return (validate ? validatePayload(data, schema: topic.schema) : succeed())
>>> {
do {
let request = try self.singleRequestFor(payload: data)
return self.send(request: request)
} catch let e {
return deferMaybe(PingJSONError(error: e))
}
}
}
public func sendBatch(_ data: [[String: Any]], validate: Bool) -> Success {
// Ignore call if we don't have anything to send!
guard !data.isEmpty else {
return succeed()
}
// Walk through all the pings if we need to validate
return (validate ? walk(data) { self.validatePayload($0, schema: self.topic.schema) } : succeed())
>>> {
do {
let request = try self.batchRequestFor(payloads: data)
return self.send(request: request)
} catch let e {
return deferMaybe(PingJSONError(error: e))
}
}
}
fileprivate func singleRequestFor(payload: [String: Any]) throws -> URLRequest {
var request = URLRequest(url: endpoint.url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
var root = payload
root["topic"] = topic.name
root["client_id"] = clientID
request.httpBody = try JSONSerialization.data(withJSONObject: root, options: [])
return request
}
fileprivate func batchRequestFor(payloads: [[String: Any]]) throws -> URLRequest {
var request = URLRequest(url: endpoint.url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let root: [String: Any] = [
"topic": self.topic.name,
"batch-mode": true,
"payloads": payloads
]
request.httpBody = try JSONSerialization.data(withJSONObject: root, options: [])
return request
}
fileprivate func send(request: URLRequest) -> Success {
let deferred = Deferred<Maybe<()>>()
self.manager.request(request as URLRequestConvertible)
.validate(statusCode: 200..<300)
.response(queue: DispatchQueue.global()) { (response) in
if let e = response.error {
NSLog("Failed to send ping to ping centre -- topic: \(self.topic.name), error: \(e)")
deferred.fill(Maybe(failure: e as MaybeErrorType))
return
}
deferred.fill(Maybe(success: ()))
}
return deferred
}
fileprivate func validatePayload(_ payload: [String: Any], schema: Schema) -> Success {
return deferDispatchAsync(validationQueue) {
let errors = schema.validate(payload).errors ?? []
guard errors.isEmpty else {
return deferMaybe(PingValidationError(errors: errors))
}
return succeed()
}
}
}

View file

@ -0,0 +1,15 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#import <UIKit/UIKit.h>
//! Project version number for SyncTelemetry.
FOUNDATION_EXPORT double TelemetryVersionNumber;
//! Project version string for SyncTelemetry.
FOUNDATION_EXPORT const unsigned char TelemetryVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Telemetry/PublicHeader.h>

View file

@ -0,0 +1,120 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Alamofire
import Foundation
import XCGLogger
import SwiftyJSON
import Shared
private let log = Logger.browserLogger
private let ServerURL = "https://incoming.telemetry.mozilla.org".asURL!
private let AppName = "Fennec"
public enum TelemetryDocType: String {
case core = "core"
case sync = "sync"
}
public protocol SyncTelemetryEvent {
func record(_ prefs: Prefs)
}
open class SyncTelemetry {
private static var prefs: Prefs?
private static var telemetryVersion: Int = 4
open class func initWithPrefs(_ prefs: Prefs) {
assert(self.prefs == nil, "Prefs already initialized")
self.prefs = prefs
}
open class func recordEvent(_ event: SyncTelemetryEvent) {
guard let prefs = prefs else {
assertionFailure("Prefs not initialized")
return
}
event.record(prefs)
}
open class func send(ping: SyncTelemetryPing, docType: TelemetryDocType) {
let docID = UUID().uuidString
let appVersion = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
let buildID = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as! String
let channel = AppConstants.BuildChannel.rawValue
let path = "/submit/telemetry/\(docID)/\(docType.rawValue)/\(AppName)/\(appVersion)/\(channel)/\(buildID)"
let url = ServerURL.appendingPathComponent(path)
var request = URLRequest(url: url)
log.debug("Ping URL: \(url)")
log.debug("Ping payload: \(ping.payload.stringValue() ?? "")")
// Don't add the common ping format for the mobile core ping.
let pingString: String?
if docType != .core {
var json = JSON(commonPingFormat(forType: docType))
json["payload"] = ping.payload
pingString = json.stringValue()
} else {
pingString = ping.payload.stringValue()
}
guard let body = pingString?.data(using: String.Encoding.utf8) else {
log.error("Invalid data!")
assertionFailure()
return
}
guard channel != "default" else {
log.debug("Non-release build; not sending ping")
return
}
request.httpMethod = "POST"
request.httpBody = body
request.addValue(Date().toRFC822String(), forHTTPHeaderField: "Date")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
SessionManager.default.request(request).response { response in
log.debug("Ping response: \(response.response?.statusCode ?? -1).")
}
}
private static func commonPingFormat(forType type: TelemetryDocType) -> [String: Any] {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
let date = formatter.string(from: NSDate() as Date)
let displayVersion = [
AppInfo.appVersion,
"b",
AppInfo.buildNumber
].joined()
let version = ProcessInfo.processInfo.operatingSystemVersion
let osVersion = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
return [
"type": type.rawValue,
"id": UUID().uuidString,
"creationDate": date,
"version": SyncTelemetry.telemetryVersion,
"application": [
"architecture": "arm",
"buildId": AppInfo.buildNumber,
"name": AppInfo.displayName,
"version": AppInfo.appVersion,
"displayVersion": displayVersion,
"platformVersion": osVersion,
"channel": AppConstants.BuildChannel.rawValue
]
]
}
}
public protocol SyncTelemetryPing {
var payload: JSON { get }
}

View file

@ -0,0 +1,98 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Shared
import SwiftyJSON
private let log = Logger.browserLogger
public typealias IdentifierString = String
public extension IdentifierString {
func validate() -> Bool {
// Regex located here: http://gecko.readthedocs.io/en/latest/toolkit/components/telemetry/telemetry/collection/events.html#limits
let regex = try! NSRegularExpression(pattern: "^[a-zA-Z][a-zA-Z0-9_.]*[a-zA-Z0-9]$", options: [])
return regex.matches(in: self, options: [], range: NSRange(location: 0, length: self.characters.count)).count > 0
}
}
// Telemetry Events
// Documentation: http://gecko.readthedocs.io/en/latest/toolkit/components/telemetry/telemetry/collection/events.html#events
public struct Event {
let timestamp: Timestamp
let category: IdentifierString
let method: IdentifierString
let object: IdentifierString
let value: String?
let extra: [String: String]?
public init(category: IdentifierString,
method: IdentifierString,
object: IdentifierString,
value: String? = nil,
extra: [String: String]? = nil) {
self.init(timestamp: .uptimeInMilliseconds(),
category: category,
method: method,
object: object,
value: value,
extra: extra)
}
init(timestamp: Timestamp,
category: IdentifierString,
method: IdentifierString,
object: IdentifierString,
value: String? = nil,
extra: [String: String]? = nil) {
self.timestamp = timestamp
self.category = category
self.method = method
self.object = object
self.value = value
self.extra = extra
}
public func validate() -> Bool {
let results = [category, method, object].map { $0.validate() }
// Fold down the results into false if any of the results is false.
return results.reduce(true) { $0 ? $1 :$0 }
}
public func pickle() -> Data? {
do {
return try JSONSerialization.data(withJSONObject: toArray(), options: [])
} catch let error {
log.error("Error pickling telemetry event. Error: \(error), Event: \(self)")
return nil
}
}
public static func unpickle(_ data: Data) -> Event? {
do {
let array = try JSONSerialization.jsonObject(with: data, options: []) as! [Any]
return Event(
timestamp: Timestamp(array[0] as! UInt64),
category: array[1] as! String,
method: array[2] as! String,
object: array[3] as! String,
value: array[4] as? String,
extra: array[5] as? [String: String]
)
} catch let error {
log.error("Error unpickling telemetry event: \(error)")
return nil
}
}
public func toArray() -> [Any] {
return [timestamp, category, method, object, value ?? NSNull(), extra ?? NSNull()]
}
}
extension Event: CustomDebugStringConvertible {
public var debugDescription: String {
return toArray().description
}
}