Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Multiple Teams #20

Merged
merged 4 commits into from
Nov 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Changelog

## [Unreleased]
* [#20](https://github.com/blackjacx/assist/pull/20): Support for Multiple Teams - [@blackjacx](https://github.com/blackjacx).

## [0.0.2] - 2020-09-25
* [#17](https://github.com/blackjacx/assist/pull/8): Implementing Screenshot Tool - [@blackjacx](https://github.com/blackjacx).
* [#17](https://github.com/blackjacx/assist/pull/17): Implementing Screenshot Tool - [@blackjacx](https://github.com/blackjacx).

## [0.0.1] - 2020-09-16
* [#8](https://github.com/blackjacx/assist/pull/8): Switch to JWTKit - [@blackjacx](https://github.com/blackjacx).
31 changes: 27 additions & 4 deletions Sources/ASC/AscResource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,33 @@ enum AscResource {

extension AscResource: Resource {

static var token: String?
static let service: Service = ASCService()
static let apiVersion: String = "v1"


private static let apiVersion: String = "v1"
private static var apiKey: ApiKey?
private static func determineToken() throws -> String {

if apiKey == nil {
let op = ApiKeysOperation(.list)
ASC.queue.addOperations([op], waitUntilFinished: true)
let apiKeys = try op.result.get()

switch apiKeys.count {
case 0: throw AscError.noApiKeysSpecified
case 1: apiKey = apiKeys[0]
default:
print("Please choose one of the registered API keys:")
apiKeys.enumerated().forEach { print("\t \($0). \($1.name) (\($1.keyId))") }

guard let input = readLine(), let index = Int(input), (0..<apiKeys.count).contains(index) else {
throw AscError.invalidInput("Please enter the specified number of the key.")
}
apiKey = apiKeys[index]
}
}
return try JSONWebToken.tokenAsc(keyFile: apiKey!.path, kid: apiKey!.keyId, iss: apiKey!.issuerId)
}

var host: String { "api.appstoreconnect.apple.com" }

var port: Int? { nil }
Expand Down Expand Up @@ -76,7 +99,7 @@ extension AscResource: Resource {

if shouldAuthorize {
do {
let token = try JSONWebToken.tokenAsc()
let token = try Self.determineToken()
headers["Authorization"] = "Bearer \(token)"
} catch {
print(error)
Expand Down
10 changes: 6 additions & 4 deletions Sources/ASC/commands/ASC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import Core
/// The main class for the App Store Connect command line tool.
public final class ASC: ParsableCommand {

/// Concurrent operation queue
static let queue = OperationQueue()
/// The API key chosen by the user. If only one key is registered this one is automatically used.
static var apiKey: ApiKey?

public static var configuration = CommandConfiguration(
// Optional abstracts and discussions are used for help output.
abstract: "A utility for accessing the App Store Connect API.",
Expand All @@ -22,7 +27,7 @@ public final class ASC: ParsableCommand {
// Pass an array to `subcommands` to set up a nested tree of subcommands.
// With language support for type-level introspection, this could be
// provided by automatically finding nested `ParsableCommand` types.
subcommands: [Groups.self, Apps.self, AppStoreVersions.self, BetaTesters.self],
subcommands: [ApiKeys.self, Groups.self, Apps.self, AppStoreVersions.self, BetaTesters.self],

// A default subcommand, when provided, is automatically selected if a
// subcommand is not given on the command line.
Expand All @@ -37,9 +42,6 @@ struct Options: ParsableArguments {
@Flag(name: .shortAndLong, help: "Activate verbose logging.")
var verbose: Int

@Option(name: .shortAndLong, help: "Filter which is set as part of the request. See https://developer.apple.com/documentation/appstoreconnectapi for possible values.")
var filters: [Filter] = []

mutating func validate() throws {
// Misusing validate to set the received flag globally
Network.verbosityLevel = verbose
Expand Down
88 changes: 88 additions & 0 deletions Sources/ASC/commands/sub/ApiKeys.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// RegisterApiKey.swift
// ASC
//
// Created by Stefan Herold on 17.11.20.
//

import Foundation
import ArgumentParser

extension ASC {

/// Lists, registers and deletes App Store Connect API keys locally.
struct ApiKeys: ParsableCommand {

static var configuration = CommandConfiguration(
abstract: "Lists, registers and deletes App Store Connect API keys on your Mac.",
subcommands: [List.self, Register.self, Delete.self],
defaultSubcommand: List.self)
}
}

extension ASC.ApiKeys {

/// List locally stored App Store Connect API keys.
struct List: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "List locally stored App Store Connect API keys keys.")

// The `@OptionGroup` attribute includes the flags, options, and arguments defined by another
// `ParsableArguments` type.
@OptionGroup()
var options: Options

func run() throws {
let op = ApiKeysOperation(.list)
ASC.queue.addOperations([op], waitUntilFinished: true)
try op.result.get().forEach { print($0) }
}
}

/// Registers App Store Connect API keys locally.
struct Register: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "Registers App Store Connect API keys locally.")

// The `@OptionGroup` attribute includes the flags, options, and arguments defined by another
// `ParsableArguments` type.
@OptionGroup()
var options: Options

@Option(name: .shortAndLong, help: "The name of the key.")
var name: String

@Option(name: .shortAndLong, help: "The absolute path to the p8 key file.")
var path: String

@Option(name: .shortAndLong, help: "Key key's id.")
var keyId: String

@Option(name: .shortAndLong, help: "The id of the key issuer.")
var issuerId: String

func run() throws {
let key = ApiKey(name: name, path: path, keyId: keyId, issuerId: issuerId)
let op = ApiKeysOperation(.register(key: key))
ASC.queue.addOperations([op], waitUntilFinished: true)
try op.result.get().forEach { print($0) }
}
}

/// Delete locally stored App Store Connect API keys.
struct Delete: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "Delete locally stored App Store Connect API keys.")

// The `@OptionGroup` attribute includes the flags, options, and arguments defined by another
// `ParsableArguments` type.
@OptionGroup()
var options: Options

@Option(name: .shortAndLong, help: "Key key's id.")
var keyId: String

func run() throws {
let op = ApiKeysOperation(.delete(keyId: keyId))
ASC.queue.addOperations([op], waitUntilFinished: true)
try op.result.get().forEach { print($0) }
}
}
}
5 changes: 4 additions & 1 deletion Sources/ASC/commands/sub/AppStoreVersions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ extension ASC.AppStoreVersions {
@OptionGroup()
var options: Options

@Option(name: .shortAndLong, help: "Filter which is set as part of the request. See https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_store_versions_for_an_app for possible values.")
var filters: [Filter] = []

@Option(name: .shortAndLong, parsing: .upToNextOption, help: "The IDs of the apps you want to get the versions from.")
var appIds: [String] = []

@Argument(help: "The attribute you are interested in. [attributes] (default: id).")
var attribute: String?

func run() throws {
let result = try ASCService.listAppStoreVersions(appIds: appIds, filters: options.filters)
let result = try ASCService.listAppStoreVersions(appIds: appIds, filters: filters)
for item in result {
if let readyForSaleVersion = item.versions.filter({ $0.attributes.appStoreState == .readyForSale }).first {
print("\(item.app.name.padding(toLength: 30, withPad: " ", startingAt: 0)): \(readyForSaleVersion.attributes.versionString)")
Expand Down
5 changes: 4 additions & 1 deletion Sources/ASC/commands/sub/Apps.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ extension ASC.Apps {
@OptionGroup()
var options: Options

@Option(name: .shortAndLong, help: "Filter which is set as part of the request. See https://developer.apple.com/documentation/appstoreconnectapi/list_apps for possible values.")
var filters: [Filter] = []

@Argument(help: "The attribute you want to get. [name | bundleId | locale | attributes] (default: id).")
var attribute: String?

func run() throws {
let apps = try ASCService.listApps(filters: options.filters)
let apps = try ASCService.listApps(filters: filters)
apps.out(attribute)
}
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/ASC/commands/sub/BetaTesters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ extension ASC.BetaTesters {
@OptionGroup()
var options: Options

@Option(name: .shortAndLong, help: "Filter which is set as part of the request. See https://developer.apple.com/documentation/appstoreconnectapi/list_beta_testers for possible values.")
var filters: [Filter] = []

@Argument(help: "The attribute you are interested in. [firstName | lastName | email | attributes] (default: id).")
var attribute: String?

func run() throws {
let result = try ASCService.listBetaTester(filters: options.filters)
let result = try ASCService.listBetaTester(filters: filters)
result.out(attribute)
}
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/ASC/commands/sub/Groups.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ extension ASC.Groups {
@OptionGroup()
var options: Options

@Option(name: .shortAndLong, help: "Filter which is set as part of the request. See https://developer.apple.com/documentation/appstoreconnectapi/list_beta_groups for possible values.")
var filters: [Filter] = []

@Argument(help: "The attribute you are interested in. [firstName | lastName | email | attributes] (default: id).")
var attribute: String?

func run() throws {
let groups = try ASCService.listBetaGroups(filters: options.filters)
let groups = try ASCService.listBetaGroups(filters: filters)
groups.out(attribute)
}
}
Expand Down
15 changes: 15 additions & 0 deletions Sources/ASC/models/ApiKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// ApiKey.swift
//
//
// Created by Stefan Herold on 17.11.20.
//

import Foundation

struct ApiKey: Codable {
var name: String
var path: String
var keyId: String
var issuerId: String
}
3 changes: 3 additions & 0 deletions Sources/ASC/models/enums/AscError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ import Foundation
enum AscError: Error {
case noDataProvided(_ type: String)
case noUserFound(_ email: String)
case noApiKeysSpecified
case invalidInput(_ message: String)
case apiKeyNotFound(_ keyId: String)
case requestFailed(underlyingErrors: [Error])
}
53 changes: 53 additions & 0 deletions Sources/ASC/service/ApiKeysOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// ApiKeysOperation.swift
// ASC
//
// Created by Stefan Herold on 18.11.20.
//

import Foundation
import Core

final class ApiKeysOperation: AsyncOperation {

enum SubCommand {
case list
case register(key: ApiKey)
case delete(keyId: String)
}

/// Collection of registered API keys
@UserDefault("\(ProcessInfo.processId).apiKeys", defaultValue: []) private static var apiKeys: [ApiKey]

var result: Result<[ApiKey], AscError>!

private let subcommand: SubCommand

init(_ subcommand: SubCommand) {
self.subcommand = subcommand
}

override func main() {

defer {
self.state = .finished
}

switch subcommand {
case .list:
result = .success(Self.apiKeys)

case .register(let key):
Self.apiKeys.append(key)
result = .success([key])

case .delete(let keyId):
guard let key = Self.apiKeys.first(where: { keyId == $0.keyId }) else {
result = .failure(.apiKeyNotFound(keyId))
return
}
Self.apiKeys = Self.apiKeys.filter { $0.keyId != keyId }
result = .success([key])
}
}
}
61 changes: 61 additions & 0 deletions Sources/Core/AsyncOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// AsyncOperation.swift
// ASC
//
// Created by Stefan Herold on 18.11.20.
//

import Foundation

/// Operation subclass that can be used as base for any async operation. Just subclass this AsyncOperation, execute
/// your async task and set state = .finished when finished. Doing so enables you to extract any async task into its
/// own operation object and just add the operation to an OperationQueue. See how ReverseGeocodeOperation is
/// implemented and used to get an idea.
/// - https://withintent.com/blog/basics-of-operations-and-operation-queues-in-ios/
/// - https://www.avanderlee.com/swift/asynchronous-operations/
/// - https://gist.github.com/Sorix/57bc3295dc001434fe08acbb053ed2bc
/// - https://www.raywenderlich.com/5293-operation-and-operationqueue-tutorial-in-swift
/// - https://aplus.rs/2018/asynchronous-operation/
open class AsyncOperation: Operation {

public enum State: String {
case ready, executing, finished

fileprivate var keyPath: String {
"is" + rawValue.capitalized
}
}

public override var isAsynchronous: Bool { true }
public override var isExecuting: Bool { state == .executing }
public override var isFinished: Bool { state == .finished }

private let stateQueue = DispatchQueue(label: "AsyncOperation State Queue", attributes: .concurrent)

/// Non thread-safe state storage, use only with locks
private var stateStore: State = .ready

/// Thread-safe computed state value
public var state: State {
get {
stateQueue.sync { stateStore }
}
set {
let oldValue = state
willChangeValue(forKey: state.keyPath)
willChangeValue(forKey: newValue.keyPath)
stateQueue.sync(flags: .barrier) {
stateStore = newValue
}
didChangeValue(forKey: state.keyPath)
didChangeValue(forKey: oldValue.keyPath)
}
}

public override func start() {

guard !isCancelled else { state = .finished; return }
state = .executing
main()
}
}
2 changes: 1 addition & 1 deletion Sources/Core/Extensions/FileManager+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
public extension FileManager {

static func createTemporaryDirectory() throws -> URL {
let uuid = "com.stherold.\(ProcessInfo.processInfo.processName).\(NSUUID().uuidString)"
let uuid = "\(ProcessInfo.processId).\(NSUUID().uuidString)"
let tmpDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(uuid)
try FileManager.default.createDirectory(at: tmpDirectoryURL, withIntermediateDirectories: true, attributes: nil)
return tmpDirectoryURL
Expand Down
Loading