diff --git a/Package.resolved b/Package.resolved index d2a9577e88..22de37a8d2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,21 +1,21 @@ { "pins" : [ { - "identity" : "swift-docc-plugin", + "identity" : "swift-log", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-plugin", + "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", - "version" : "1.3.0" + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" } }, { - "identity" : "swift-docc-symbolkit", + "identity" : "swift-log-oslog", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", + "location" : "https://github.com/MasterJ93/swift-log-oslog.git", "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" + "branch" : "master", + "revision" : "b3d6886e0e236036f83b5b8966827b707e174e81" } }, { diff --git a/Package.swift b/Package.swift index 048cb0633f..8cd6ee8577 100644 --- a/Package.swift +++ b/Package.swift @@ -19,16 +19,20 @@ let package = Package( targets: ["ATProtoKit"]), ], dependencies: [ + .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.7.0"), + .package(url: "https://github.com/MasterJ93/swift-log-oslog.git", branch: "master"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), - .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.7.0") + .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( name: "ATProtoKit", - dependencies: ["SwiftSoup"] - ), + dependencies: [ + "SwiftSoup", + "Logging" + ]) // .testTarget( // name: "ATProtoKitTests", // dependencies: ["ATProtoKit"]), diff --git a/Sources/ATProtoKit/ATProtoKit.swift b/Sources/ATProtoKit/ATProtoKit.swift index 116b456deb..152dbf07e7 100644 --- a/Sources/ATProtoKit/ATProtoKit.swift +++ b/Sources/ATProtoKit/ATProtoKit.swift @@ -1,13 +1,49 @@ import Foundation +import Logging /// Defines a protocol for configurations in the `ATProtoKit` API library. /// /// `ATProtoKitConfiguration` defines the basic requirements for any configuration class or structure /// within `ATProtoKit`. Any class that conforms to this protocol must be geared for sending API calls to the AT Protocol. Creating a class /// that conforms to this is useful if you have additional lexicons specific to the service you're running. +/// +/// For logging-related tasks, make sure you set up the logging instide the `init()` method and attach it to the `logger` property. +/// ```swift +/// public init(session: UserSession? = nil, logIdentifier: String? = nil, logCategory: String?, logLevel: Logger.Level? = .info) { +/// self.session = session +/// self.logIdentifier = logIdentifier ?? Bundle.main.bundleIdentifier ?? "com.cjrriley.ATProtoKit" +/// self.logCategory = logCategory ?? "ATProtoKit" +/// self.logLevel = logLevel +/// +/// #if canImport(os) +/// LoggingSystem.bootstrap { label in +/// ATLogHandler(subsystem: label, category: logCategory ?? "ATProtoKit") +/// } +/// #else +/// LoggingSystem.bootstrap(StreamLogHandler.standardOutput) +/// #endif +/// +/// logger = Logger(label: logIdentifier ?? "com.cjrriley.ATProtoKit") +/// logger.logLevel = logLevel ?? .info +/// } +/// ``` public protocol ATProtoKitConfiguration { /// Represents an authenticated user session within the AT Protocol. Optional. var session: UserSession? { get } + /// Specifies the logger that will be used for emitting log messages. + /// + /// - Note: Be sure to create an instance inside the `init()` method. This is important + var logger: Logger { get } + /// Specifies the identifier for managing log outputs. Optional. + /// + /// This should default to the bundle identifier if it's in an Apple platform (`CFBundleIdentifier`). + var logIdentifier: String? { get } + /// Specifies the category name the logs in the logger within ATProtoKit will be in. Optional. + var logCategory: String? { get } + /// Specifies the highest level of logs that will be outputted. Optional. + /// + /// This should default to `.info` + var logLevel: Logger.Level? { get } /// Prepares an authorization value for API requests based on `session` and `pdsURL`. /// /// This determines whether the "Authorization" header will be included in the request payload. It takes both `shouldAuthenticate` and `pdsURL` into account if @@ -88,12 +124,42 @@ extension ATProtoKitConfiguration { public class ATProtoKit: ATProtoKitConfiguration { /// Represents an authenticated user session within the AT Protocol. Optional. public let session: UserSession? + /// Specifies the logger that will be used for emitting log messages. + public private(set) var logger: Logger + /// Specifies the identifier for managing log outputs. Optional. Defaults to the project's `CFBundleIdentifier`. + public let logIdentifier: String? + /// Specifies the category name the logs in the logger within ATProtoKit will be in. Optional. Defaults to `ATProtoKit`. + /// + /// - Note: This property is ignored if you're using `StreamLogHandler`. + public let logCategory: String? + /// Specifies the highest level of logs that will be outputted. Optional. Defaults to `.info`. + public let logLevel: Logger.Level? /// Initializes a new instance of `ATProtoKit`. + /// + /// This will also handle some of the logging-related setup. The identifier will either be your project's `CFBundleIdentifier` or an identifier named + /// `com.cjrriley.ATProtoKit`. However, you can manually override this. /// - Parameters: /// - session: The authenticated user session within the AT Protocol. Optional. - public init(session: UserSession? = nil) { + /// - logIdentifier: Specifies the identifier for managing log outputs. Optional. Defaults to the project's `CFBundleIdentifier`. + /// - logCategory: Specifies the category name the logs in the logger within ATProtoKit will be in. Optional. Defaults to `ATProtoKit`. + /// - logLevel: Specifies the highest level of logs that will be outputted. Optional. Defaults to `.info`. + public init(session: UserSession? = nil, logIdentifier: String? = nil, logCategory: String? = nil, logLevel: Logger.Level? = .info) { self.session = session + self.logIdentifier = logIdentifier ?? Bundle.main.bundleIdentifier ?? "com.cjrriley.ATProtoKit" + self.logCategory = logCategory ?? "ATProtoKit" + self.logLevel = logLevel + + #if canImport(os) + LoggingSystem.bootstrap { label in + ATLogHandler(subsystem: label, category: logCategory ?? "ATProtoKit") + } + #else + LoggingSystem.bootstrap(StreamLogHandler.standardOutput) + #endif + + logger = Logger(label: logIdentifier ?? "com.cjrriley.ATProtoKit") + logger.logLevel = logLevel ?? .info } /// Determines the appropriate Personal Data Server (PDS) URL. @@ -137,12 +203,39 @@ public class ATProtoKit: ATProtoKitConfiguration { public class ATProtoAdmin: ATProtoKitConfiguration { /// Represents an authenticated user session within the AT Protocol. Optional. public let session: UserSession? + /// Specifies the logger that will be used for emitting log messages. + public private(set) var logger: Logger + /// Specifies the identifier for managing log outputs. Optional. Defaults to the project's `CFBundleIdentifier`. + public let logIdentifier: String? + /// Specifies the category name the logs in the logger within ATProtoKit will be in. Optional. Defaults to `ATProtoKit`. + /// + /// - Note: This property is ignored if you're using `StreamLogHandler`. + public let logCategory: String? + /// Specifies the highest level of logs that will be outputted. Optional. Defaults to `.info`. + public let logLevel: Logger.Level? /// Initializes a new instance of `ATProtoAdmin`. /// - Parameters: /// - session: The authenticated user session within the AT Protocol. - public init(session: UserSession? = nil) { + /// - logIdentifier: Specifies the identifier for managing log outputs. Optional. Defaults to the project's `CFBundleIdentifier`. + /// - logCategory: Specifies the category name the logs in the logger within ATProtoKit will be in. Optional. Defaults to `ATProtoKit`. + /// - logLevel: Specifies the highest level of logs that will be outputted. Optional. Defaults to `.info`. + public init(session: UserSession? = nil, logIdentifier: String? = nil, logCategory: String? = nil, logLevel: Logger.Level? = .info) { self.session = session + self.logIdentifier = logIdentifier ?? Bundle.main.bundleIdentifier ?? "com.cjrriley.ATProtoKit" + self.logCategory = logCategory ?? "ATProtoKit" + self.logLevel = logLevel + + #if canImport(os) + LoggingSystem.bootstrap { label in + ATLogHandler(subsystem: label, category: logCategory ?? "ATProtoKit") + } + #else + LoggingSystem.bootstrap(StreamLogHandler.standardOutput) + #endif + + logger = Logger(label: logIdentifier ?? "com.cjrriley.ATProtoKit") + logger.logLevel = logLevel ?? .info } } diff --git a/Sources/ATProtoKit/Models/Lexicons/com.atproto/Label/AtprotoLabelDefs.swift b/Sources/ATProtoKit/Models/Lexicons/com.atproto/Label/AtprotoLabelDefs.swift index fb420c237c..bfd056548e 100644 --- a/Sources/ATProtoKit/Models/Lexicons/com.atproto/Label/AtprotoLabelDefs.swift +++ b/Sources/ATProtoKit/Models/Lexicons/com.atproto/Label/AtprotoLabelDefs.swift @@ -173,7 +173,7 @@ public struct SelfLabel: Codable { } } -/// A data model definition for +/// A data model definition for labeler-created labels. /// /// - Note: According to the AT Protocol specifications: "Declares a label value and its expected interpertations and behaviors." /// diff --git a/Sources/ATProtoKit/Networking/CoreAPI/CreateInviteCode.swift b/Sources/ATProtoKit/Networking/CoreAPI/CreateInviteCode.swift index b416dbe952..c2f72559cc 100644 --- a/Sources/ATProtoKit/Networking/CoreAPI/CreateInviteCode.swift +++ b/Sources/ATProtoKit/Networking/CoreAPI/CreateInviteCode.swift @@ -10,8 +10,8 @@ import Foundation extension ATProtoKit { /// Creates an invite code. /// - /// - Note: If you need to create multiple invite codes at once, please use ``create`` instead. - /// + /// - Note: If you need to create multiple invite codes at once, please use ``createInviteCodes(_:for:)`` instead. + /// /// - Note: According to the AT Protocol specifications: "Create an invite code." /// /// - SeeAlso: This is based on the [`com.atproto.server.createInviteCode`][github] lexicon. diff --git a/Sources/ATProtoKit/Networking/PlatformAPI/CreatePost.swift b/Sources/ATProtoKit/Networking/PlatformAPI/CreatePost.swift index 339a28bc4a..843bd472dd 100644 --- a/Sources/ATProtoKit/Networking/PlatformAPI/CreatePost.swift +++ b/Sources/ATProtoKit/Networking/PlatformAPI/CreatePost.swift @@ -23,7 +23,8 @@ extension ATProtoKit { /// - swapCommit: Swaps out an operation based on the CID. Optional. /// - Returns: A strong reference, which contains the newly-created record's URI and CID hash. public func createPostRecord(text: String, locales: [Locale] = [], replyTo: String? = nil, embed: EmbedIdentifier? = nil, - labels: FeedLabelUnion? = nil, tags: [String]? = nil, creationDate: Date = Date.now, recordKey: String? = nil, shouldValidate: Bool? = true, swapCommit: String? = nil) async -> Result { + labels: FeedLabelUnion? = nil, tags: [String]? = nil, creationDate: Date = Date.now, recordKey: String? = nil, + shouldValidate: Bool? = true, swapCommit: String? = nil) async -> Result { guard let session else { return .failure(ATRequestPrepareError.missingActiveSession) @@ -158,8 +159,8 @@ extension ATProtoKit { /// /// `EmbedIdentifier` provides a unified interface for specifying embeddable content, simplifying the process of attaching /// images, external links, other post records, or media to a post. By abstracting the details of each embed type, it allows methods - /// like ``createPostRecord(text:locales:replyTo:embed:labels:tags:creationDate:)`` to handle the - /// necessary operations (e.g., uploading, grabbing metadata, validation, etc.) behind the scenes, streamlining the embedding process. + /// like ``createPostRecord(text:locales:replyTo:embed:labels:tags:creationDate:recordKey:shouldValidate:swapCommit:)`` + /// to handle the necessary operations (e.g., uploading, grabbing metadata, validation, etc.) behind the scenes, streamlining the embedding process. public enum EmbedIdentifier { /// Represents a set of images to be embedded in the post. /// - Parameter images: An array of `ImageQuery` objects, each containing the image data, metadata, and filenames of the image. diff --git a/Sources/ATProtoKit/Networking/PlatformAPI/DescribeFeedGenerator.swift b/Sources/ATProtoKit/Networking/PlatformAPI/DescribeFeedGenerator.swift index 34b93da9ac..30442ba4a7 100644 --- a/Sources/ATProtoKit/Networking/PlatformAPI/DescribeFeedGenerator.swift +++ b/Sources/ATProtoKit/Networking/PlatformAPI/DescribeFeedGenerator.swift @@ -17,7 +17,7 @@ extension ATProtoKit { /// /// [github]: https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/describeFeedGenerator.json /// - /// - Returns: A `Result`, containing either a ``FeedDescribeFeedGenerator`` if successful, or an `Error` if not. + /// - Returns: A `Result`, containing either a ``FeedDescribeFeedGeneratorOutput`` if successful, or an `Error` if not. public func describeFeedGenerator(pdsURL: String? = nil) async throws -> Result { guard let sessionURL = pdsURL != nil ? pdsURL : session?.pdsURL, let requestURL = URL(string: "\(sessionURL)/app.bsky.feed.describeFeedGenerator") else { diff --git a/Sources/ATProtoKit/Networking/PlatformAPI/GetActorFeeds.swift b/Sources/ATProtoKit/Networking/PlatformAPI/GetActorFeeds.swift index 64abc8456f..6cdf906fc5 100644 --- a/Sources/ATProtoKit/Networking/PlatformAPI/GetActorFeeds.swift +++ b/Sources/ATProtoKit/Networking/PlatformAPI/GetActorFeeds.swift @@ -20,7 +20,7 @@ extension ATProtoKit { /// - actorDID: The decentralized identifier (DID) of the user who created the feeds. /// - limit: The number of items that can be in the list. Optional. Defaults to `50`. /// - cursor: The mark used to indicate the starting point for the next set of result. Optional. - /// - Returns: A `Result`, containing either a ``FeedGetActorFeeds`` if successful, or an `Error` if not. + /// - Returns: A `Result`, containing either a ``FeedGetActorFeedsOutput`` if successful, or an `Error` if not. public func getActorFeeds(by actorDID: String, limit: Int? = 50, cursor: String? = nil) async throws -> Result { guard session != nil, let accessToken = session?.accessToken else { diff --git a/Sources/ATProtoKit/Networking/PlatformAPI/GetActorLikes.swift b/Sources/ATProtoKit/Networking/PlatformAPI/GetActorLikes.swift index b6728f5982..a2ca40b224 100644 --- a/Sources/ATProtoKit/Networking/PlatformAPI/GetActorLikes.swift +++ b/Sources/ATProtoKit/Networking/PlatformAPI/GetActorLikes.swift @@ -14,7 +14,7 @@ extension ATProtoKit { /// (and therefore, the documentation is outdated) or unintentional (in this case, the underlying implementation is outdated). For now, this method will act as if auth is required until Bluesky clarifies their position. /// /// - Important: This will only be able to get like records for the authenticated account. This won't work for any other user account. If you need to grab the like records for user accounts other than the - /// authenticated one, use ``listRecords`` instead. + /// authenticated one, use ``listRecords(from:collection:limit:cursor:isArrayReverse:pdsURL:)`` instead. /// /// - Note: According to the AT Protocol specifications: "Get a list of posts liked by an actor. Does not require auth." /// diff --git a/Sources/ATProtoKit/Networking/PlatformAPI/GetProfile.swift b/Sources/ATProtoKit/Networking/PlatformAPI/GetProfile.swift index 4a33c2e87c..136b40b707 100644 --- a/Sources/ATProtoKit/Networking/PlatformAPI/GetProfile.swift +++ b/Sources/ATProtoKit/Networking/PlatformAPI/GetProfile.swift @@ -15,8 +15,8 @@ extension ATProtoKit { /// - Note: If your Personal Data Server's (PDS) URL is something other than `https://bsky.social` and you're not using authentication, be sure to change it if the normal URL isn't used /// for unauthenticated API calls.\ ///\ - /// If you need profiles of several users, it's best to use ``getProfiles(_:accessToken:pdsURL:)``. - /// + /// If you need profiles of several users, it's best to use ``getProfiles(_:pdsURL:shouldAuthenticate:)``. + /// /// - Note: According to the AT Protocol specifications: "Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth." /// /// - SeeAlso: This is based on the [`app.bsky.actor.getProfile`][github] lexicon. diff --git a/Sources/ATProtoKit/Networking/PlatformAPI/GetProfiles.swift b/Sources/ATProtoKit/Networking/PlatformAPI/GetProfiles.swift index 00b387c562..0a528f62ae 100644 --- a/Sources/ATProtoKit/Networking/PlatformAPI/GetProfiles.swift +++ b/Sources/ATProtoKit/Networking/PlatformAPI/GetProfiles.swift @@ -15,7 +15,7 @@ extension ATProtoKit { /// - Note: If your Personal Data Server's (PDS) URL is something other than `https://bsky.social` and you're not using authentication, be sure to change it if the normal URL isn't used /// for unauthenticated API calls.\ /// \ - /// If you need a profile of just one user, it's best to use ``getProfile(_:accessToken:pdsURL:)`` + /// If you need a profile of just one user, it's best to use ``getProfile(_:pdsURL:shouldAuthenticate:)``. /// /// - Note: According to the AT Protocol specifications: "Get detailed profile views of multiple actors." /// diff --git a/Sources/ATProtoKit/Utilities/ATImageProcessing.swift b/Sources/ATProtoKit/Utilities/ATImageProcessing.swift index 869b84bd9e..e78e1bd16a 100644 --- a/Sources/ATProtoKit/Utilities/ATImageProcessing.swift +++ b/Sources/ATProtoKit/Utilities/ATImageProcessing.swift @@ -45,7 +45,7 @@ protocol ImageProtocol { /// - Important: `stripMetadata(from:)` is an important method to create as, according to the AT Protocol documentation, the protocol may be more strict about stripping metadata in the future.\ /// \ /// Also, this should be an `internal` method, as it will be part of `convertToImageQuery(image:altText:targetFileSize)`. It's recommended that it's called before -/// ``convertToImageQuery(image:altText:targetFileSize)`` attempts to access the image. +/// ``convertToImageQuery(imagePath:altText:targetFileSize:)-2fma7`` attempts to access the image. /// /// ### Example /// Below is a sample implementation showcasing how to conform to `ATImageProcessable` for a custom image type: diff --git a/Sources/ATProtoKit/Utilities/ExtensionHelpers.swift b/Sources/ATProtoKit/Utilities/ExtensionHelpers.swift index ab7b94eb5a..c7296eef9f 100644 --- a/Sources/ATProtoKit/Utilities/ExtensionHelpers.swift +++ b/Sources/ATProtoKit/Utilities/ExtensionHelpers.swift @@ -6,6 +6,10 @@ // import Foundation +//#if canImport(os) +//import Logging +//import os.log +//#endif // MARK: - String Extension extension String: Truncatable { @@ -113,3 +117,45 @@ extension UInt64 { return encoded } } + +//// MARK: - Logging.Logger Extension +//#if canImport(os) +//extension Logging.Logger { +// enum PrivacyAwareMetadataValue { +// case string(String, OSLogPrivacy) +// case stringConvertible(CustomStringConvertible, OSLogPrivacy) +// +// var privacy: OSLogPrivacy { +// switch self { +// case .string(_, let privacy), .stringConvertible(_, let privacy): +// return privacy +// } +// } +// +// var value: String { +// switch self { +// case .string(let value, _): +// return value +// case .stringConvertible(let value, _): +// return value.description +// } +// } +// } +//} +//#endif +// +//// MARK: - DefaultStringInterpolation Extension +//extension DefaultStringInterpolation { +// mutating func appendInterpolation(_ message: OSLogMessage) { +// #if canImport(os) +// appendInterpolation(value) +// appendLiteral("") +// #else +// appendLiteral(value) +// #endif +// } +// +// private mutating func wrapMessage(_ value: OSLogMessage) { +// appendInterpolation(<#T##OSLogMessage#>) +// } +//} diff --git a/Sources/ATProtoKit/Utilities/Logging/Logging.swift b/Sources/ATProtoKit/Utilities/Logging/Logging.swift new file mode 100644 index 0000000000..2dbad6dc19 --- /dev/null +++ b/Sources/ATProtoKit/Utilities/Logging/Logging.swift @@ -0,0 +1,60 @@ +// +// Logging.swift +// +// +// Created by Christopher Jr Riley on 2024-04-04. +// + +import Logging + +#if canImport(os) +import os + +struct ATLogHandler: LogHandler { + public let subsystem: String + public let category: String + public var logLevel: Logging.Logger.Level = .info + public var metadata: Logging.Logger.Metadata = [:] + private var appleLogger: os.Logger + + init(subsystem: String, category: String? = nil) { + self.subsystem = subsystem + self.category = category ?? "ATProtoKit" + self.appleLogger = Logger(subsystem: subsystem, category: category ?? "ATProtoKit") + } + + public func log(level: Logging.Logger.Level, + message: Logging.Logger.Message, + metadata explicitMetadata: Logging.Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt) { + let allMetadata = self.metadata.merging(metadata ?? [:]) { _, new in new } + var messageMetadata = [String: Any]() + var privacySettings = [String: OSLogPrivacy]() + +// appleLogger(level: level, message: formattedMessage) +// appleLogger.log(level: .info, "\(formattedMessage)") + switch level { + case .trace, .debug: + appleLogger.log(level: .debug, "\(message, privacy: .auto)") + case .info: + appleLogger.log(level: .info, "\(message, privacy: .auto)") + case .notice: + appleLogger.log(level: .default, "\(message, privacy: .auto)") + case .warning: + appleLogger.log(level: .error, "\(message, privacy: .auto)") + case .error: + appleLogger.log(level: .error, "\(message, privacy: .auto)") + case .critical: + appleLogger.log(level: .fault, "\(message, privacy: .auto)") + } + } + + subscript(metadataKey key: String) -> Logging.Logger.Metadata.Value? { + get { metadata[key] } + set { metadata[key] = newValue } + } +} +#endif diff --git a/Sources/ATProtoKit/Utilities/Logging/LoggingBootStrapping.swift b/Sources/ATProtoKit/Utilities/Logging/LoggingBootStrapping.swift new file mode 100644 index 0000000000..02c85e08ee --- /dev/null +++ b/Sources/ATProtoKit/Utilities/Logging/LoggingBootStrapping.swift @@ -0,0 +1,39 @@ +// +// LoggingBootstrapping.swift +// +// +// Created by Christopher Jr Riley on 2024-04-04. +// + +//import Foundation +//import Logging +// +//struct ATLogging { +// public func bootstrap() { +// func bootstrapWithOSLog(subsystem: String?) { +// LoggingSystem.bootstrap { label in +// #if canImport(os) +// OSLogHandler(subsystem: subsystem ?? defaultIdentifier(), category: label) +// #else +// StreamLogHandler.standardOutput(label: label) +// #endif +// } +// } +// } +// +// #if canImport(os) +// private func defaultIdentifier() -> String { +// return Bundle.main.bundleIdentifier ?? "com.cjrriley.ATProtoKit" +// } +// #endif +// +// public func handleBehavior(_ behavior: HandleBehavior = .default) { +// +// } +// +// public enum HandleBehavior { +// case `default` +// case osLog +// case swiftLog +// } +//}