Skip to content

Commit

Permalink
Switch to a more async approach (#7)
Browse files Browse the repository at this point in the history
* Futures

* Cleanup

* Command fixes

* Prefix updates

* Fixup tests
  • Loading branch information
MrLotU authored Jun 19, 2020
1 parent 5aed5a1 commit 2c7f3a2
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 117 deletions.
109 changes: 70 additions & 39 deletions Sources/SwiftHooks/Commands/Command.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import class NIO.EventLoopFuture

public protocol AnyFuture {
func toVoidFuture() -> EventLoopFuture<Void>
}

extension EventLoopFuture: AnyFuture {
@inlinable
public func toVoidFuture() -> EventLoopFuture<Void> {
self.map { _ in }
}
}

/// Base command
public struct Command: ExecutableCommand {
public let name: String
public let group: String?
public let alias: [String]
public let hookWhitelist: [HookID]
public let permissionChecks: [CommandPermissionChecker]
public let closure: (SwiftHooks, CommandEvent) throws -> Void
public let closure: (CommandEvent) -> AnyFuture

public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws {
try closure(hooks, event)
public func invoke(on event: CommandEvent) -> EventLoopFuture<Void> {
return closure(event).toVoidFuture()
}

public func copyWith(name: String, group: String?, alias: [String], hookWhitelist: [HookID], permissionChecks: [CommandPermissionChecker], closure: @escaping Execute) -> Self {
Expand All @@ -34,7 +47,7 @@ public struct Command: ExecutableCommand {
self.alias = []
self.hookWhitelist = []
self.permissionChecks = []
self.closure = { _, _ in }
self.closure = { e in e.eventLoop.makeSucceededFuture(()) }
}

public func validate() throws { }
Expand All @@ -55,19 +68,16 @@ public struct Command: ExecutableCommand {
alias: alias,
hookWhitelist: hookWhitelist,
permissionChecks: permissionChecks,
closure: { _, _, _ in },
closure: { e, _ in e.eventLoop.makeSucceededFuture(()) },
arg: x
)
}
}

fileprivate extension ExecutableCommand {
func getArg<T>(_ t: T.Type = T.self, _ index: inout Int, for arg: CommandArgument, on event: CommandEvent) throws -> T.ResolvedArgument where T: CommandArgumentConvertible {
func parse(_ s: String) throws -> T.ResolvedArgument {
if !arg.isOptional && s.isEmpty {
throw CommandError.ArgumentNotFound(name)
}
let t = try T.resolveArgument(s, on: event)
func parse(_ s: String?) throws -> T.ResolvedArgument {
let t = try T.resolveArgument(s, arg: arg, on: event)
if (t as? AnyOptionalType)?.isNil ?? false {
return t
}
Expand All @@ -78,10 +88,7 @@ fileprivate extension ExecutableCommand {
let s = event.args[index...].joined(separator: " ")
return try parse(s)
}
guard let s = event.args[safe:index] else {
throw CommandError.ArgumentNotFound(arg.componentName)
}
return try parse(s)
return try parse(event.args[safe:index])
}
}

Expand All @@ -92,7 +99,7 @@ public struct OneArgCommand<A>: ExecutableCommand where A: CommandArgumentConver
public let alias: [String]
public let hookWhitelist: [HookID]
public let permissionChecks: [CommandPermissionChecker]
public let closure: (SwiftHooks, CommandEvent, A.ResolvedArgument) throws -> Void
public let closure: (CommandEvent, A.ResolvedArgument) -> AnyFuture
public var readableArguments: String? {
arg.description
}
Expand All @@ -115,10 +122,16 @@ public struct OneArgCommand<A>: ExecutableCommand where A: CommandArgumentConver

public func validate() throws { }

public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws {
var idx = 0
let a = try getArg(A.self, &idx, for: arg, on: event)
try closure(hooks, event, a)
public func invoke(on event: CommandEvent) -> EventLoopFuture<Void> {
let p = event.eventLoop.makePromise(of: Void.self)
do {
var idx = 0
let a = try getArg(A.self, &idx, for: arg, on: event)
closure(event, a).toVoidFuture().cascade(to: p)
} catch {
p.fail(error)
}
return p.futureResult
}

/// Add an argument to this command
Expand All @@ -136,7 +149,7 @@ public struct OneArgCommand<A>: ExecutableCommand where A: CommandArgumentConver
alias: alias,
hookWhitelist: hookWhitelist,
permissionChecks: permissionChecks,
closure: { _, _, _, _ in },
closure: { e, _, _ in e.eventLoop.makeSucceededFuture(()) },
argOne: arg,
argTwo: GenericCommandArgument<B>(componentType: B.typedName, componentName: name)
)
Expand All @@ -150,7 +163,7 @@ public struct TwoArgCommand<A, B>: ExecutableCommand where A: CommandArgumentCon
public let alias: [String]
public let hookWhitelist: [HookID]
public let permissionChecks: [CommandPermissionChecker]
public let closure: (SwiftHooks, CommandEvent, A.ResolvedArgument, B.ResolvedArgument) throws -> Void
public let closure: (CommandEvent, A.ResolvedArgument, B.ResolvedArgument) -> AnyFuture
public var readableArguments: String? {
[argOne, argTwo].map(\.description).joined(separator: " ")
}
Expand Down Expand Up @@ -179,11 +192,17 @@ public struct TwoArgCommand<A, B>: ExecutableCommand where A: CommandArgumentCon
}
}

public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws {
var idx = 0
let a = try getArg(A.self, &idx, for: argOne, on: event)
let b = try getArg(B.self, &idx, for: argTwo, on: event)
try self.closure(hooks, event, a, b)
public func invoke(on event: CommandEvent) -> EventLoopFuture<Void> {
let p = event.eventLoop.makePromise(of: Void.self)
do {
var idx = 0
let a = try getArg(A.self, &idx, for: argOne, on: event)
let b = try getArg(B.self, &idx, for: argTwo, on: event)
self.closure(event, a, b).toVoidFuture().cascade(to: p)
} catch {
p.fail(error)
}
return p.futureResult
}

/// Add an argument to this command
Expand All @@ -201,7 +220,7 @@ public struct TwoArgCommand<A, B>: ExecutableCommand where A: CommandArgumentCon
alias: alias,
hookWhitelist: hookWhitelist,
permissionChecks: permissionChecks,
closure: { _, _, _, _, _ in },
closure: { e, _, _, _ in e.eventLoop.makeSucceededFuture(()) },
argOne: argOne,
argTwo: argTwo,
argThree: GenericCommandArgument<C>(componentType: C.typedName, componentName: name)
Expand All @@ -216,7 +235,7 @@ public struct ThreeArgCommand<A, B, C>: ExecutableCommand where A: CommandArgume
public let alias: [String]
public let hookWhitelist: [HookID]
public let permissionChecks: [CommandPermissionChecker]
public let closure: (SwiftHooks, CommandEvent, A.ResolvedArgument, B.ResolvedArgument, C.ResolvedArgument) throws -> Void
public let closure: (CommandEvent, A.ResolvedArgument, B.ResolvedArgument, C.ResolvedArgument) -> AnyFuture
public var readableArguments: String? {
[argOne, argTwo, argThree].map(\.description).joined(separator: " ")
}
Expand Down Expand Up @@ -250,12 +269,18 @@ public struct ThreeArgCommand<A, B, C>: ExecutableCommand where A: CommandArgume
}
}

public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws {
var idx = 0
let a = try getArg(A.self, &idx, for: argOne, on: event)
let b = try getArg(B.self, &idx, for: argTwo, on: event)
let c = try getArg(C.self, &idx, for: argThree, on: event)
try self.closure(hooks, event, a, b, c)
public func invoke(on event: CommandEvent) -> EventLoopFuture<Void> {
let p = event.eventLoop.makePromise(of: Void.self)
do {
var idx = 0
let a = try getArg(A.self, &idx, for: argOne, on: event)
let b = try getArg(B.self, &idx, for: argTwo, on: event)
let c = try getArg(C.self, &idx, for: argThree, on: event)
self.closure(event, a, b, c).toVoidFuture().cascade(to: p)
} catch {
p.fail(error)
}
return p.futureResult
}

/// Add an argument to this command
Expand All @@ -273,7 +298,7 @@ public struct ThreeArgCommand<A, B, C>: ExecutableCommand where A: CommandArgume
alias: alias,
hookWhitelist: hookWhitelist,
permissionChecks: permissionChecks,
closure: { _, _, _ in },
closure: { e, _ in e.eventLoop.makeSucceededFuture(()) },
arguments: [argOne, argTwo, argThree, GenericCommandArgument<T>(componentType: T.typedName, componentName: name)]
)
}
Expand All @@ -286,7 +311,7 @@ public struct ArrayArgCommand: ExecutableCommand {
public let alias: [String]
public let hookWhitelist: [HookID]
public let permissionChecks: [CommandPermissionChecker]
public let closure: (SwiftHooks, CommandEvent, Arguments) throws -> Void
public let closure: (CommandEvent, Arguments) throws -> AnyFuture
/// Arguments for this command.
public let arguments: [CommandArgument]

Expand Down Expand Up @@ -316,8 +341,14 @@ public struct ArrayArgCommand: ExecutableCommand {
}
}

public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws {
try closure(hooks, event, Arguments(arguments))
public func invoke(on event: CommandEvent) -> EventLoopFuture<Void> {
let p = event.eventLoop.makePromise(of: Void.self)
do {
try closure(event, Arguments(arguments)).toVoidFuture().cascade(to: p)
} catch {
p.fail(error)
}
return p.futureResult
}

/// Add an argument to this command
Expand All @@ -335,7 +366,7 @@ public struct ArrayArgCommand: ExecutableCommand {
alias: alias,
hookWhitelist: hookWhitelist,
permissionChecks: permissionChecks,
closure: { _, _, _ in },
closure: { e, _ in e.eventLoop.makeSucceededFuture(()) },
arguments: arguments + GenericCommandArgument<T>(componentType: T.typedName, componentName: name)
)
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/SwiftHooks/Commands/Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ public struct Group: Commands {
return commands.executables()
}

/// NOTE: Only one group prefix is supported. So calling `.group(_:)` on a Group will not do anything
/// Add a group prefix to these commands.
///
/// **NOTE**: Only one group prefix is supported. So calling `.group(_:)` on a Group will not do anything
public func group(_ group: String) -> Group {
return self
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/SwiftHooks/Commands/ExecutableCommand.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import class NIO.EventLoopFuture

/// Base `ExecutableCommand`
public protocol _ExecutableCommand: Commands {
/// Help description of the command. Used to explain usage to users.
Expand All @@ -22,7 +24,7 @@ public protocol _ExecutableCommand: Commands {
/// Validates if a command is valid. See also: `CommandError`
func validate() throws
/// Invokes a command on given event.
func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws
func invoke(on event: CommandEvent) -> EventLoopFuture<Void>
}

/// Base `ExecutableCommand`
Expand Down
62 changes: 43 additions & 19 deletions Sources/SwiftHooks/Commands/SwiftHooks+Commands.swift
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
import NIO
import struct Dispatch.DispatchTime
import Logging
import Metrics

extension Userable {
var prefix: String {
return mention + " "
}
}

extension SwiftHooks {
func handleMessage(_ message: Messageable, from h: _Hook) {
guard config.commands.enabled, message.content.starts(with: self.config.commands.prefix) else { return }
let foundCommands = self.findCommands(for: message)
func handleMessage(_ message: Messageable, from h: _Hook, on eventLoop: EventLoop) {
let p: String?
switch self.config.commands.prefix {
case .mention: p = h.user?.prefix
case .string(let s): p = s
}
guard let prefix = p, config.commands.enabled, message.content.starts(with: prefix) else { return }
let foundCommands = self.findCommands(for: message, withPrefix: prefix)

foundCommands.forEach { (command) in
guard command.hookWhitelist.isEmpty || command.hookWhitelist.contains(h.id) else { return }
let event = CommandEvent(hooks: self, cmd: command, msg: message, for: h)
let event = CommandEvent(hooks: self, cmd: command, msg: message, for: h, on: eventLoop)

do {
event.logger.debug("Invoking command")
event.logger.trace("Full message: \(message.content)")
try Timer.measure(label: "command_duration", dimensions: [("command", command.fullTrigger)]) {
try command.invoke(on: event, using: self)
}
event.logger.debug("Command succesfully invoked.")
} catch let e {
event.message.error(e, on: command)
event.logger.error("\(e.localizedDescription)")
Counter(label: "command_failure", dimensions: [("command", command.fullTrigger)]).increment()
event.logger.debug("Invoking command")
event.logger.trace("Full message: \(message.content)")
let timer = Timer(label: "command_duration", dimensions: [("command", command.fullTrigger)])
let start = DispatchTime.now().uptimeNanoseconds
command.invoke(on: event)
.flatMapErrorThrowing({ (e) in
event.message.error(e, on: command)
throw e
})
.whenComplete { result in
let delta = DispatchTime.now().uptimeNanoseconds - start
timer.recordNanoseconds(delta)
switch result {
case .success(_):
event.logger.debug("Command succesfully invoked.")
Counter(label: "command_finish", dimensions: [("command", command.fullTrigger), ("status", "success")]).increment()
case .failure(let e):
event.logger.error("\(e.localizedDescription)")
Counter(label: "command_finish", dimensions: [("command", command.fullTrigger), ("status", "failure")]).increment()
}
}
Counter(label: "command_success", dimensions: [("command", command.fullTrigger)]).increment()
}
}

func findCommands(for message: Messageable) -> [_ExecutableCommand] {
return self.commands.compactMap { return message.content.starts(with: self.config.commands.prefix + $0.fullTrigger) ? $0 : nil }
func findCommands(for message: Messageable, withPrefix prefix: String) -> [_ExecutableCommand] {
return self.commands.compactMap { return message.content.starts(with: prefix + $0.fullTrigger) ? $0 : nil }
}
}

Expand Down Expand Up @@ -81,6 +102,8 @@ public struct CommandEvent {
public let hook: _Hook
/// Command specific logger. Has command trigger set as command metadata by default.
public private(set) var logger: Logger
/// EventLoop this Command runs on.
public let eventLoop: EventLoop

/// Create a new `CommandEvent`
///
Expand All @@ -89,7 +112,7 @@ public struct CommandEvent {
/// - cmd: Command this event is wrapping.
/// - msg: Message that executed the command.
/// - h: `_Hook` that originally dispatched this command.
public init(hooks: SwiftHooks, cmd: _ExecutableCommand, msg: Messageable, for h: _Hook) {
public init(hooks: SwiftHooks, cmd: _ExecutableCommand, msg: Messageable, for h: _Hook, on loop: EventLoop) {
self.logger = Logger(label: "SwiftHooks.Command")
self.hooks = hooks
self.user = msg.gAuthor
Expand All @@ -103,6 +126,7 @@ public struct CommandEvent {
self.name = name
self.args = comps.map(String.init)
self.hook = h
self.eventLoop = loop
self.logger[metadataKey: "command"] = "\(cmd.fullTrigger)"
}
}
19 changes: 15 additions & 4 deletions Sources/SwiftHooks/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,32 @@ import Foundation
/// SwiftHooks configuration. Use `.default` for default config.
public struct SwiftHooksConfig {
public struct Commands {
public let prefix: String
public enum Prefix {
/// Use a static String as prefix. For example `!` or `/`
case string(String)
/// Use a mention as prefix.
case mention
}

/// Command prefix used.
public let prefix: Prefix
/// Wether or not to enable commands.
public let enabled: Bool

/// - parameters:
/// - prefix: Command prefix to use. Default is `!`
/// - prefix: Command prefix to use. Default is mentioning your bot.
/// - enabled: Wether or not to enable commands. Default is `true`
public init(prefix: String, enabled: Bool) {
public init(prefix: Prefix, enabled: Bool) {
self.prefix = prefix
self.enabled = enabled
}
}

/// Commands config
public let commands: Commands

public static let `default`: SwiftHooksConfig = .init(commands: .init(prefix: "!", enabled: true))
/// Default config
public static let `default`: SwiftHooksConfig = .init(commands: .init(prefix: .mention, enabled: true))

/// - parameters:
/// - commands: Commands configuration.
Expand Down
Loading

0 comments on commit 2c7f3a2

Please sign in to comment.