diff --git a/.travis.yml b/.travis.yml index a8712d2..5cc5d32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,4 @@ sudo: required dist: trusty osx_image: xcode8 script: - - eval "$(curl -sL https://raw.githubusercontent.com/ChameleonBot/Scripts/master/ci_3.0.1)" + - eval "$(curl -sL https://raw.githubusercontent.com/ChameleonBot/Scripts/master/ci)" diff --git a/Package.swift b/Package.swift index d61b37b..e024ba8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,16 +1,11 @@ +// swift-tools-version:3.1 + import PackageDescription let package = Package( name: "Camille", - targets: [ - Target( - name: "Camille", - dependencies: [] - ) - ], dependencies: [ - .Package(url: "https://github.com/ChameleonBot/Bot.git", majorVersion: 0, minor: 2), - .Package(url: "https://github.com/ChameleonBot/Sugar.git", majorVersion: 0, minor: 2) + .Package(url: "https://github.com/ChameleonBot/Chameleon.git", majorVersion: 1), ], exclude: [ "XcodeProject" diff --git a/Sources/Camille/Configs/Configs+CrossPostService.swift b/Sources/Camille/Configs/Configs+CrossPostService.swift index d8d1b32..dd29beb 100644 --- a/Sources/Camille/Configs/Configs+CrossPostService.swift +++ b/Sources/Camille/Configs/Configs+CrossPostService.swift @@ -1,21 +1,21 @@ -import Sugar - -extension Configs { - static let CrossPost = CrossPostServiceConfig( - timeSpan: 60 * 2, - includeMessage: { message in - return message.text.components(separatedBy: " ").count > 5 - }, - reportingTarget: "admins", - publicWarning: { channel, user in - return try SlackMessage() - .line(user, " cross posting is discouraged.") - .makeChatPostMessage(target: channel) - }, - privateWarning: { im in - return try SlackMessage() - .line("Please refrain from cross posting, it is discouraged here.") - .makeChatPostMessage(target: im) - } - ) -} +//import Sugar +// +//extension Configs { +// static let CrossPost = CrossPostServiceConfig( +// timeSpan: 60 * 2, +// includeMessage: { message in +// return message.text.components(separatedBy: " ").count > 5 +// }, +// reportingTarget: "admins", +// publicWarning: { channel, user in +// return try SlackMessage() +// .line(user, " cross posting is discouraged.") +// .makeChatPostMessage(target: channel) +// }, +// privateWarning: { im in +// return try SlackMessage() +// .line("Please refrain from cross posting, it is discouraged here.") +// .makeChatPostMessage(target: im) +// } +// ) +//} diff --git a/Sources/Camille/Configs/Configs+KarmaService.swift b/Sources/Camille/Configs/Configs+KarmaService.swift index c9c199e..73823a2 100644 --- a/Sources/Camille/Configs/Configs+KarmaService.swift +++ b/Sources/Camille/Configs/Configs+KarmaService.swift @@ -1,16 +1,16 @@ -import Sugar - -extension Configs { - static let Karma = KarmaService.Config( - topUsersLimit: 20, - karmaAdjusters: [("++", 1), ("--", -1)], - textDistanceThreshold: 4, - allowedBufferCharacters: [" ", ":"], - positiveMessage: { user, total in - return ["\(user.name) you rock!: \(total)"] - }, - negativeMessage: { user, total in - return ["Boooo \(user.name)!: \(total)"] - } - ) -} +//import Sugar +// +//extension Configs { +// static let Karma = KarmaService.Config( +// topUsersLimit: 20, +// karmaAdjusters: [("++", 1), ("--", -1)], +// textDistanceThreshold: 4, +// allowedBufferCharacters: [" ", ":"], +// positiveMessage: { user, total in +// return ["\(user.name) you rock!: \(total)"] +// }, +// negativeMessage: { user, total in +// return ["Boooo \(user.name)!: \(total)"] +// } +// ) +//} diff --git a/Sources/Camille/Configs/Configs+TopicService.swift b/Sources/Camille/Configs/Configs+TopicService.swift index 0b3db2a..4984205 100644 --- a/Sources/Camille/Configs/Configs+TopicService.swift +++ b/Sources/Camille/Configs/Configs+TopicService.swift @@ -1,14 +1,14 @@ -import Sugar - -extension Configs { - static let Topic = TopicServiceConfig( - userAllowed: { user in - return user.is_admin - }, - warning: { channel, user in - return try SlackMessage() - .line("I can't let you do that, ", user, ". Only admins are allowed to change the topic.") - .makeChatPostMessage(target: channel) - } - ) -} +//import Sugar +// +//extension Configs { +// static let Topic = TopicServiceConfig( +// userAllowed: { user in +// return user.is_admin +// }, +// warning: { channel, user in +// return try SlackMessage() +// .line("I can't let you do that, ", user, ". Only admins are allowed to change the topic.") +// .makeChatPostMessage(target: channel) +// } +// ) +//} diff --git a/Sources/Camille/Configs/Configs+UserJoinService.swift b/Sources/Camille/Configs/Configs+UserJoinService.swift index e12ff1a..498dcf2 100644 --- a/Sources/Camille/Configs/Configs+UserJoinService.swift +++ b/Sources/Camille/Configs/Configs+UserJoinService.swift @@ -1,9 +1,9 @@ -import Sugar - -extension Configs { - static let UserJoin = UserJoinConfig(newUserAnnouncement: { im in - return try SlackMessage() - .line("Hi, ", im.user, ", welcome to the ios-developer slack team!") - .makeChatPostMessage(target: im) - }) -} +//import Sugar +// +//extension Configs { +// static let UserJoin = UserJoinConfig(newUserAnnouncement: { im in +// return try SlackMessage() +// .line("Hi, ", im.user, ", welcome to the ios-developer slack team!") +// .makeChatPostMessage(target: im) +// }) +//} diff --git a/Sources/Camille/Configs/Configs.swift b/Sources/Camille/Configs/Configs.swift index 98c4ff9..fcf0640 100644 --- a/Sources/Camille/Configs/Configs.swift +++ b/Sources/Camille/Configs/Configs.swift @@ -1,2 +1,2 @@ - -enum Configs { } +// +//enum Configs { } diff --git a/Sources/Camille/CrossPost/CrossPostButton.swift b/Sources/Camille/CrossPost/CrossPostButton.swift index c7f7b12..47e9cdb 100644 --- a/Sources/Camille/CrossPost/CrossPostButton.swift +++ b/Sources/Camille/CrossPost/CrossPostButton.swift @@ -1,24 +1,24 @@ - -enum CrossPostButton: String { - case privateWarning - case publicWarning - case removeAll - - var text: String { - switch self { - case .privateWarning: return "Private Warning" - case .publicWarning: return "Public Warning" - case .removeAll: return "Remove all posts" - } - } - - var afterExecuted: [CrossPostButton] { - switch self { - case .privateWarning: return [.removeAll] - case .publicWarning: return [.removeAll] - case .removeAll: return [.privateWarning, .publicWarning] - } - } - - static var all: [CrossPostButton] { return [.privateWarning, .publicWarning, .removeAll] } -} +// +//enum CrossPostButton: String { +// case privateWarning +// case publicWarning +// case removeAll +// +// var text: String { +// switch self { +// case .privateWarning: return "Private Warning" +// case .publicWarning: return "Public Warning" +// case .removeAll: return "Remove all posts" +// } +// } +// +// var afterExecuted: [CrossPostButton] { +// switch self { +// case .privateWarning: return [.removeAll] +// case .publicWarning: return [.removeAll] +// case .removeAll: return [.privateWarning, .publicWarning] +// } +// } +// +// static var all: [CrossPostButton] { return [.privateWarning, .publicWarning, .removeAll] } +//} diff --git a/Sources/Camille/CrossPost/CrossPostService.swift b/Sources/Camille/CrossPost/CrossPostService.swift index 02fdf31..4670629 100644 --- a/Sources/Camille/CrossPost/CrossPostService.swift +++ b/Sources/Camille/CrossPost/CrossPostService.swift @@ -1,179 +1,179 @@ -import Bot -import Sugar -import Foundation - -//MARK: - Service -final class CrossPostService: SlackMessageService, SlackInteractiveButtonResponderService { - //MARK: - Properties - let buttonResponder = SlackInteractiveButtonResponder() - fileprivate let config: CrossPostServiceConfig - private var timer: TimerService? - private var messages: [MessageDecorator] = [] - - //MARK: - Lifecycle - init(config: CrossPostServiceConfig) { - self.config = config - } - - //MARK: - Event Routing - func configureEvents(slackBot: SlackBot, webApi: WebAPI, dispatcher: SlackRTMEventDispatcher) { - self.configureMessageEvent(slackBot: slackBot, webApi: webApi, dispatcher: dispatcher) - self.timer = TimerService(id: "crossPosting", interval: self.config.timeSpan, storage: slackBot.storage, dispatcher: dispatcher) { [weak self] pong in - guard let `self` = self else { return } - - //get rid of messages older than config.timeSpan - let activeMessages = self.messages.filter(self.newerThan(timestamp: pong.timestamp, lifespan: self.config.timeSpan)) - self.messages = activeMessages - - guard let message = self.makeCrossPostWarning(pong: pong, messages: self.messages, webApi: webApi) else { return } - - self.messages = [] - - guard - let target = slackBot.target(nameOrId: self.config.reportingTarget) - else { return } - - try webApi.execute(message.makeChatPostMessage(target: target)) - } - } - func messageEvent(slackBot: SlackBot, webApi: WebAPI, message: MessageDecorator, previous: MessageDecorator?) throws { - //only add new _channel_ messages and ones that meet the requirements - guard - message.target?.channel != nil, - previous == nil, - self.config.includeMessage(message) - else { return } - - self.messages.append(message) - } -} - -//MARK: - CrossPost Check -fileprivate extension CrossPostService { - func makeCrossPostWarning(pong: Pong, messages: [MessageDecorator], webApi: WebAPI) -> SlackMessage? { - let duplicates = self.findCrossPosts(in: messages) - guard !duplicates.isEmpty else { return nil } - - let message = SlackMessage() - .line("Cross Post Alert:".bold) - .line("The following cross posts have been detected:") - .attachments(for: duplicates) { builder, dupes in - dupes.addAttachment( - with: builder, - buttonResponder: self, - handler: self.buttonHandler(messages: dupes, webApi: webApi) - ) - } - - return message - } -} - -//MARK: - CrossPost Data -fileprivate extension CrossPostService { - func uniqueMessageKey(message: MessageDecorator) -> String { - let userId = message.sender?.id ?? "" - return "\(userId)\(message.text.hashValue)" - } - func potentialDuplicates(messages: [MessageDecorator]) -> Bool { - guard - messages.count > 1, - let first = messages.first, - first.sender != nil - else { return false } - - return !first.text.isEmpty - } - func postedInMultipleChannels(messages: [MessageDecorator]) -> Bool { - return messages - .grouped { $0.target?.name ?? "" } - .values.count > 1 - } - func findCrossPosts(in messages: [MessageDecorator]) -> [[MessageDecorator]] { - let potentialDuplicates = messages - .grouped(by: self.uniqueMessageKey) - .values - .filter(self.potentialDuplicates) - .filter(self.postedInMultipleChannels) - - return Array(potentialDuplicates) - } -} - -//MARK: - Cross Post Button Responder -fileprivate extension CrossPostService { - func buttonHandler(messages: [MessageDecorator], webApi: WebAPI) -> (InteractiveButtonResponse) throws -> Void { - return { response in - guard - let name = response.actions.first?.name, - let action = CrossPostButton(rawValue: name) - else { return } - - switch action { - case .publicWarning: try self.publicWarning(messages: messages, webApi: webApi) - case .privateWarning: try self.privateWarning(messages: messages, webApi: webApi) - case .removeAll: try self.removeAll(messages: messages, webApi: webApi) - } - - try self.updateWarning( - message: response.original_message, - after: action, - from: response, - webApi: webApi - ) - } - } - - private func publicWarning(messages: [MessageDecorator], webApi: WebAPI) throws { - guard - let user = messages.first?.sender, - let target = messages.first?.target - else { return } - - let message = try self.config.publicWarning(target, user) - try webApi.execute(message) - } - private func privateWarning(messages: [MessageDecorator], webApi: WebAPI) throws { - guard let user = messages.first?.sender else { return } - - let openIm = IMOpen(user: user) - let im = try webApi.execute(openIm) - - let message = try self.config.privateWarning(im) - try webApi.execute(message) - } - private func removeAll(messages: [MessageDecorator], webApi: WebAPI) throws { - for message in messages { - let delete = ChatDelete(message: message.message) - try webApi.execute(delete) - } - } -} - -//MARK: - Helpers -fileprivate extension CrossPostService { - func newerThan(timestamp: Int, lifespan: TimeInterval) -> (MessageDecorator) -> Bool { - return { message in - guard let messageTimestamp = Int(message.message.timestamp) else { return true } - return (timestamp - messageTimestamp) < Int(lifespan) - } - } -} - -fileprivate extension CrossPostService { - func updateWarning(message: Message, after action: CrossPostButton, from interactiveButton: InteractiveButtonResponse, webApi: WebAPI) throws { - - let update = SlackMessage(message: message) - .updateAttachment(buttonResponse: interactiveButton) { builder in - builder.updateOrAddField(titled: "Actions Taken") { field in - var values = field.value.components(separatedBy: "\n") - values.append("<@\(interactiveButton.user.id)> chose: \(action.text)") - field.value = values.joined(separator: "\n") - } - builder.removeButtons(matching: { !action.afterExecuted.map({ $0.rawValue }).contains($0.name) }) - } - - try webApi.execute(try update.makeChatUpdate(to: message, in: interactiveButton.channel)) - } -} - +//import Bot +//import Sugar +//import Foundation +// +////MARK: - Service +//final class CrossPostService: SlackMessageService, SlackInteractiveButtonResponderService { +// //MARK: - Properties +// let buttonResponder = SlackInteractiveButtonResponder() +// fileprivate let config: CrossPostServiceConfig +// private var timer: TimerService? +// private var messages: [MessageDecorator] = [] +// +// //MARK: - Lifecycle +// init(config: CrossPostServiceConfig) { +// self.config = config +// } +// +// //MARK: - Event Routing +// func configureEvents(slackBot: SlackBot, webApi: WebAPI, dispatcher: SlackRTMEventDispatcher) { +// self.configureMessageEvent(slackBot: slackBot, webApi: webApi, dispatcher: dispatcher) +// self.timer = TimerService(id: "crossPosting", interval: self.config.timeSpan, storage: slackBot.storage, dispatcher: dispatcher) { [weak self] pong in +// guard let `self` = self else { return } +// +// //get rid of messages older than config.timeSpan +// let activeMessages = self.messages.filter(self.newerThan(timestamp: pong.timestamp, lifespan: self.config.timeSpan)) +// self.messages = activeMessages +// +// guard let message = self.makeCrossPostWarning(pong: pong, messages: self.messages, webApi: webApi) else { return } +// +// self.messages = [] +// +// guard +// let target = slackBot.target(nameOrId: self.config.reportingTarget) +// else { return } +// +// try webApi.execute(message.makeChatPostMessage(target: target)) +// } +// } +// func messageEvent(slackBot: SlackBot, webApi: WebAPI, message: MessageDecorator, previous: MessageDecorator?) throws { +// //only add new _channel_ messages and ones that meet the requirements +// guard +// message.target?.channel != nil, +// previous == nil, +// self.config.includeMessage(message) +// else { return } +// +// self.messages.append(message) +// } +//} +// +////MARK: - CrossPost Check +//fileprivate extension CrossPostService { +// func makeCrossPostWarning(pong: Pong, messages: [MessageDecorator], webApi: WebAPI) -> SlackMessage? { +// let duplicates = self.findCrossPosts(in: messages) +// guard !duplicates.isEmpty else { return nil } +// +// let message = SlackMessage() +// .line("Cross Post Alert:".bold) +// .line("The following cross posts have been detected:") +// .attachments(for: duplicates) { builder, dupes in +// dupes.addAttachment( +// with: builder, +// buttonResponder: self, +// handler: self.buttonHandler(messages: dupes, webApi: webApi) +// ) +// } +// +// return message +// } +//} +// +////MARK: - CrossPost Data +//fileprivate extension CrossPostService { +// func uniqueMessageKey(message: MessageDecorator) -> String { +// let userId = message.sender?.id ?? "" +// return "\(userId)\(message.text.hashValue)" +// } +// func potentialDuplicates(messages: [MessageDecorator]) -> Bool { +// guard +// messages.count > 1, +// let first = messages.first, +// first.sender != nil +// else { return false } +// +// return !first.text.isEmpty +// } +// func postedInMultipleChannels(messages: [MessageDecorator]) -> Bool { +// return messages +// .grouped { $0.target?.name ?? "" } +// .values.count > 1 +// } +// func findCrossPosts(in messages: [MessageDecorator]) -> [[MessageDecorator]] { +// let potentialDuplicates = messages +// .grouped(by: self.uniqueMessageKey) +// .values +// .filter(self.potentialDuplicates) +// .filter(self.postedInMultipleChannels) +// +// return Array(potentialDuplicates) +// } +//} +// +////MARK: - Cross Post Button Responder +//fileprivate extension CrossPostService { +// func buttonHandler(messages: [MessageDecorator], webApi: WebAPI) -> (InteractiveButtonResponse) throws -> Void { +// return { response in +// guard +// let name = response.actions.first?.name, +// let action = CrossPostButton(rawValue: name) +// else { return } +// +// switch action { +// case .publicWarning: try self.publicWarning(messages: messages, webApi: webApi) +// case .privateWarning: try self.privateWarning(messages: messages, webApi: webApi) +// case .removeAll: try self.removeAll(messages: messages, webApi: webApi) +// } +// +// try self.updateWarning( +// message: response.original_message, +// after: action, +// from: response, +// webApi: webApi +// ) +// } +// } +// +// private func publicWarning(messages: [MessageDecorator], webApi: WebAPI) throws { +// guard +// let user = messages.first?.sender, +// let target = messages.first?.target +// else { return } +// +// let message = try self.config.publicWarning(target, user) +// try webApi.execute(message) +// } +// private func privateWarning(messages: [MessageDecorator], webApi: WebAPI) throws { +// guard let user = messages.first?.sender else { return } +// +// let openIm = IMOpen(user: user) +// let im = try webApi.execute(openIm) +// +// let message = try self.config.privateWarning(im) +// try webApi.execute(message) +// } +// private func removeAll(messages: [MessageDecorator], webApi: WebAPI) throws { +// for message in messages { +// let delete = ChatDelete(message: message.message) +// try webApi.execute(delete) +// } +// } +//} +// +////MARK: - Helpers +//fileprivate extension CrossPostService { +// func newerThan(timestamp: Int, lifespan: TimeInterval) -> (MessageDecorator) -> Bool { +// return { message in +// guard let messageTimestamp = Int(message.message.timestamp) else { return true } +// return (timestamp - messageTimestamp) < Int(lifespan) +// } +// } +//} +// +//fileprivate extension CrossPostService { +// func updateWarning(message: Message, after action: CrossPostButton, from interactiveButton: InteractiveButtonResponse, webApi: WebAPI) throws { +// +// let update = SlackMessage(message: message) +// .updateAttachment(buttonResponse: interactiveButton) { builder in +// builder.updateOrAddField(titled: "Actions Taken") { field in +// var values = field.value.components(separatedBy: "\n") +// values.append("<@\(interactiveButton.user.id)> chose: \(action.text)") +// field.value = values.joined(separator: "\n") +// } +// builder.removeButtons(matching: { !action.afterExecuted.map({ $0.rawValue }).contains($0.name) }) +// } +// +// try webApi.execute(try update.makeChatUpdate(to: message, in: interactiveButton.channel)) +// } +//} +// diff --git a/Sources/Camille/CrossPost/CrossPostServiceConfig.swift b/Sources/Camille/CrossPost/CrossPostServiceConfig.swift index 6cf6938..9ecf447 100644 --- a/Sources/Camille/CrossPost/CrossPostServiceConfig.swift +++ b/Sources/Camille/CrossPost/CrossPostServiceConfig.swift @@ -1,11 +1,11 @@ -import Bot -import Sugar -import Foundation - -struct CrossPostServiceConfig { - let timeSpan: TimeInterval - let includeMessage: (MessageDecorator) -> Bool - let reportingTarget: String - let publicWarning: (SlackTargetType, User) throws -> ChatPostMessage - let privateWarning: (IM) throws -> ChatPostMessage -} +//import Bot +//import Sugar +//import Foundation +// +//struct CrossPostServiceConfig { +// let timeSpan: TimeInterval +// let includeMessage: (MessageDecorator) -> Bool +// let reportingTarget: String +// let publicWarning: (SlackTargetType, User) throws -> ChatPostMessage +// let privateWarning: (IM) throws -> ChatPostMessage +//} diff --git a/Sources/Camille/CrossPost/Sequence+MessageDecorator.swift b/Sources/Camille/CrossPost/Sequence+MessageDecorator.swift index 8357c37..f084890 100644 --- a/Sources/Camille/CrossPost/Sequence+MessageDecorator.swift +++ b/Sources/Camille/CrossPost/Sequence+MessageDecorator.swift @@ -1,23 +1,23 @@ -import Sugar - -extension Collection where Iterator.Element == MessageDecorator { - func addAttachment(with builder: SlackMessageAttachmentBuilder, buttonResponder: SlackInteractiveButtonResponderService, handler: @escaping InteractiveButtonResponseHandler) { - guard let message = self.first, let user = message.sender else { return } - let channels = self - .flatMap { $0.target?.channel } - .map { "<#\($0.id)>" } - .joined(separator: ", ") - - builder.field(short: true, title: "User", value: "<@\(user.id)>") - builder.field(short: true, title: "Channels", value: channels) - builder.field(short: false, title: "Message Preview", value: message.text.substring(to: 50)) - for button in CrossPostButton.all { - builder.button( - name: button.rawValue, - text: button.text, - responder: buttonResponder, - handler: handler - ) - } - } -} +//import Sugar +// +//extension Collection where Iterator.Element == MessageDecorator { +// func addAttachment(with builder: SlackMessageAttachmentBuilder, buttonResponder: SlackInteractiveButtonResponderService, handler: @escaping InteractiveButtonResponseHandler) { +// guard let message = self.first, let user = message.sender else { return } +// let channels = self +// .flatMap { $0.target?.channel } +// .map { "<#\($0.id)>" } +// .joined(separator: ", ") +// +// builder.field(short: true, title: "User", value: "<@\(user.id)>") +// builder.field(short: true, title: "Channels", value: channels) +// builder.field(short: false, title: "Message Preview", value: message.text.substring(to: 50)) +// for button in CrossPostButton.all { +// builder.button( +// name: button.rawValue, +// text: button.text, +// responder: buttonResponder, +// handler: handler +// ) +// } +// } +//} diff --git a/Sources/Camille/HelloService.swift b/Sources/Camille/HelloService.swift index a7d37dc..0af4130 100644 --- a/Sources/Camille/HelloService.swift +++ b/Sources/Camille/HelloService.swift @@ -1,25 +1,38 @@ -import Bot -import Sugar +import Chameleon -final class HelloService: SlackMessageService { - func messageEvent(slackBot: SlackBot, webApi: WebAPI, message: MessageDecorator, previous: MessageDecorator?) throws { - guard let target = message.target, let sender = message.sender else { return } - - try message.routeText( - to: self.sayHello(to: sender, in: target, with: webApi), - matching: Greeting(name: "greeting"), slackBot.me - ) +final class HelloService: SlackBotMessageService { + func configure(slackBot: SlackBot) { + configureMessageService(slackBot: slackBot) + + slackBot.registerHelp(item: Patterns.greeting(slackBot)) + } + func onMessage(slackBot: SlackBot, message: MessageDecorator, previous: MessageDecorator?) throws { + try slackBot.route(message, matching: Patterns.greeting(slackBot), to: sendGreeting) + } + + private func sendGreeting(bot: SlackBot, message: MessageDecorator, match: PatternMatch) throws -> Void { + let response = try message + .respond() + .text(["well", try match.value(key: "greeting"), "back at you", try message.sender()]) + .makeChatMessage() + + try bot.send(response) } } -fileprivate extension HelloService { - func sayHello(to sender: User, in target: SlackTargetType, with webApi: WebAPI) -> (PatternMatchResult) throws -> Void { - return { match in - let message = try SlackMessage() - .line(match.value(named: "greeting"), " ", sender) - .makeChatPostMessage(target: target) - - try webApi.execute(message) +private enum Patterns: HelpRepresentable { + case greeting(SlackBot) + + var topic: String { + return "Greetings" + } + var description: String { + return "Greet the bot" + } + + var pattern: [Matcher] { + switch self { + case .greeting(let bot): return [["hey", "hi", "hello"].any.using(key: "greeting"), bot.me] } } } diff --git a/Sources/Camille/Karma/KarmaAdjustable.swift b/Sources/Camille/Karma/KarmaAdjustable.swift deleted file mode 100644 index 6f0399e..0000000 --- a/Sources/Camille/Karma/KarmaAdjustable.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Models - -protocol KarmaAdjustable { - var karmaValue: String { get } -} - -extension String: KarmaAdjustable { - var karmaValue: String { return self } -} - -extension Emoji: KarmaAdjustable { - var karmaValue: String { - return self.emojiSymbol - } -} diff --git a/Sources/Camille/Karma/KarmaService+Adjustments.swift b/Sources/Camille/Karma/KarmaService+Adjustments.swift index 8cc0824..53399b3 100644 --- a/Sources/Camille/Karma/KarmaService+Adjustments.swift +++ b/Sources/Camille/Karma/KarmaService+Adjustments.swift @@ -1,74 +1,72 @@ -import Bot -import Sugar +import Chameleon +import Foundation + +private typealias PartialUpdate = (user: ModelPointer, operation: Operation) +private typealias Update = (ModelPointer, (current: Int, change: Int)) + +private enum Operation: String { + case plus = "++" + case minus = "--" + + func update(value: Int) -> Int { + switch self { + case .plus: return value + 1 + case .minus: return value - 1 + } + } +} + +private let trimCharacterSet = CharacterSet.whitespaces.union(CharacterSet(charactersIn: ">:")) extension KarmaService { - func adjustKarma( - in message: MessageDecorator, - from sender: User, - in target: SlackTargetType, - storage: Storage, - webApi: WebAPI) throws -> Void { - - //find any karma adjustments - let adjustments: [(user: User, amount: Int)] = message.mentioned_users.flatMap { link in - //user can't adjust their own karma - guard link.value != sender else { return nil } - - //Move past the `>` and get the rest of the message - let start = message.text.index(after: link.range.upperBound) - let text = message.text.substring(from: start) - - //iterate forward until we pass the threshold or hit the end of the string - for index in (0..>) throws -> PartialUpdate? { + guard try link.value.id != message.sender().id else { return nil } + + guard let operation = Operation(rawValue: message.text + .substring(from: link.range.upperBound) + .trimmingCharacters(in: trimCharacterSet) + .components(separatedBy: .whitespaces) + .first ?? "") + else { return nil } + + return (link.value, operation) } - - guard !adjustments.isEmpty else { return } - - //consolidate adjustments - let consolidated: [(user: User, change: Int, total: Int)] = adjustments - .grouped(by: { $0.user }) - .flatMap { user, changes in - let current: Int = storage.get(.in("Karma"), key: user.id, or: 0) - let change = changes.reduce(0) { $0 + $1.amount } - guard change != 0 else { return nil } - let newTotal = current + change - return (user, change, newTotal) + + func consolidatePartialUpdates(for user: ModelPointer, partials: [PartialUpdate]) throws -> Update { + let count: Int = try storage.get(key: user.id, from: Keys.namespace, or: 0) + let change = partials.reduce(0) { $1.operation.update(value: $0) } + return (user, (count, change)) } - - //make adjustments - for adjustment in consolidated where adjustment.change != 0 { - try storage.set(.in("Karma"), key: adjustment.user.id, value: adjustment.total) + + let updates = try message + .mentionedUsers + .flatMap(partialUpdate) + .group(by: { $0.user }) + .map(consolidatePartialUpdates) + .filter { $0.value.change != 0 } + + guard !updates.isEmpty else { return } + + let response = try message.respond() + + for (user, data) in updates { + let newTotal = data.current + data.change + storage.set(value: newTotal, forKey: user.id, in: Keys.namespace) + + let comment = (data.change > 0 + ? config.positiveComments.randomElement + : config.negativeComments.randomElement + ) ?? "" + + response + .text([user, comment, newTotal]) + .newLine() } - //build a message - let response = consolidated - .flatMap { adjustment in - if adjustment.change > 0 { - return self.config.positiveMessage(adjustment.user, adjustment.total).randomElement - } else if adjustment.change < 0 { - return self.config.negativeMessage(adjustment.user, adjustment.total).randomElement - } - return nil - } - .joined(separator: "\n") - - guard !response.isEmpty else { return } - - //respond - let request = ChatPostMessage(target: target, text: response) - try webApi.execute(request) + try bot.send(response.makeChatMessage()) } } + diff --git a/Sources/Camille/Karma/KarmaService+Config.swift b/Sources/Camille/Karma/KarmaService+Config.swift index 2d9768b..951da97 100644 --- a/Sources/Camille/Karma/KarmaService+Config.swift +++ b/Sources/Camille/Karma/KarmaService+Config.swift @@ -1,13 +1,22 @@ -import Sugar -import Models extension KarmaService { - struct Config { - let topUsersLimit: Int - let karmaAdjusters: [(adjuster: KarmaAdjustable, amount: Int)] - let textDistanceThreshold: Int - let allowedBufferCharacters: Set - let positiveMessage: (_ user: User, _ total: Int) -> [String] - let negativeMessage: (_ user: User, _ total: Int) -> [String] + public struct Config { + let topUserLimit: Int + let positiveComments: [String] + let negativeComments: [String] + + public init(topUserLimit: Int, positiveComments: [String], negativeComments: [String]) { + self.topUserLimit = topUserLimit + self.positiveComments = positiveComments + self.negativeComments = negativeComments + } + + public static func `default`() -> Config { + return Config( + topUserLimit: 10, + positiveComments: ["you rock!"], + negativeComments: ["booooo!"] + ) + } } } diff --git a/Sources/Camille/Karma/KarmaService+Patterns.swift b/Sources/Camille/Karma/KarmaService+Patterns.swift new file mode 100644 index 0000000..8b124f5 --- /dev/null +++ b/Sources/Camille/Karma/KarmaService+Patterns.swift @@ -0,0 +1,33 @@ +import Chameleon + +extension KarmaService { + enum Patterns: HelpRepresentable { + case topUsers + case myCount + case userCount + case adjustment + + var topic: String { + return "Karma" + } + var description: String { + switch self { + case .topUsers: return "Find out who's on top!" + case .myCount: return "See how much karma you have" + case .userCount: return "See how much karma someone else has" + case .adjustment: return "Give or take karma from someone" + } + } + + var pattern: [Matcher] { + switch self { + case .topUsers: return ["top", Int.any.using(key: Keys.count)] + case .myCount: return ["how much karma do I have"] + case .userCount: return ["how much karma does", User.any.using(key: Keys.user), "have"] + case .adjustment: return [User.any, ["++", "--"].any] + } + } + + var strict: Bool { return false } + } +} diff --git a/Sources/Camille/Karma/KarmaService+TopUsers.swift b/Sources/Camille/Karma/KarmaService+TopUsers.swift index 131b9ef..99bdc9c 100644 --- a/Sources/Camille/Karma/KarmaService+TopUsers.swift +++ b/Sources/Camille/Karma/KarmaService+TopUsers.swift @@ -1,51 +1,52 @@ -import Bot -import Sugar +import Chameleon extension KarmaService { - func showTopUsers( - from storage: Storage, in target: SlackTargetType, with webApi: WebAPI, - users: @autoclosure @escaping () -> [User]) -> (PatternMatchResult) throws -> Void { - - return { match in - let count: Int = match.value(named: "count") - - guard count > 0 else { - let message = SlackMessage().line("Top ", count, "? You must work in QA.") - return try webApi.execute(message.makeChatPostMessage(target: target)) - } - - func karmaFor(user: User) -> Int { - return storage.get(.in("Karma"), key: user.id, or: 0) - } - - let karmaUsers = storage.allKeys(.in("Karma")) - .flatMap { id in users().filter({ $0.id == id }) } - .map { (user: $0, karma: karmaFor(user: $0)) } - .sorted { $0.karma > $1.karma } - - let actualCount: Int - let prefixText: String - - if count > self.config.topUsersLimit { - actualCount = max(karmaUsers.count, self.config.topUsersLimit) - prefixText = "Yeah, that's too many. Here's the top " - - } else if count > karmaUsers.count { - actualCount = karmaUsers.count - prefixText = "We only have " - - } else { - actualCount = count - prefixText = "Top " - } - - let message = SlackMessage() - .line(prefixText, actualCount, ":") - .lines(for: karmaUsers) { message, entry in - return message.line(entry.user.name.bold, ": ", entry.karma) + func topUsers(bot: SlackBot, message: MessageDecorator, match: PatternMatch) throws { + let count: Int = try match.value(key: Keys.count) + + guard count > 0 else { + let response = try message + .respond() + .text(["Top", count, "? You must work in QA."]) + .makeChatMessage() + + return try bot.send(response) + } + + let leaderboard = try storage + .keys(in: Keys.namespace) + .map { userId in + return (ModelPointer(id: userId), try storage.get(key: userId, from: Keys.namespace, or: 0)) } - - try webApi.execute(message.makeChatPostMessage(target: target)) + .sorted(by: { $0.1 > $1.1 }) + + guard leaderboard.count > 0 else { + let response = try message + .respond() + .text(["No one has any karma yet."]) + .makeChatMessage() + + return try bot.send(response) + } + + let prefix: String + if count > config.topUserLimit { prefix = "Yeah, that's too many. Here's the top" } + else if leaderboard.count < count { prefix = "We only have" } + else { prefix = "Top" } + + let actualCount = min(leaderboard.count, config.topUserLimit) + + let response = try message + .respond() + .text([prefix, actualCount]) + .newLine() + + for (position, entry) in leaderboard.prefix(actualCount).enumerated() { + try response + .text(["\(position + 1))", entry.0.value().bold, ": ", entry.1]) + .newLine() } + + try bot.send(response.makeChatMessage()) } } diff --git a/Sources/Camille/Karma/KarmaService+User.swift b/Sources/Camille/Karma/KarmaService+User.swift index 421120d..1de0cca 100644 --- a/Sources/Camille/Karma/KarmaService+User.swift +++ b/Sources/Camille/Karma/KarmaService+User.swift @@ -1,20 +1,31 @@ -import Bot -import Sugar +import Chameleon extension KarmaService { - func showUserKarma(user: User, from storage: Storage, in target: SlackTargetType, - with webApi: WebAPI) -> (PatternMatchResult) throws -> Void { - - return { match in - let count: Int = storage.get(.in("Karma"), key: user.id, or: 0) - - let message = SlackMessage() - .line(count == 0 - ? ["It doesn't look like you have any karma yet ", user, ". Helping people out is a great way to get some!"] - : ["You have ", count, " karma ", user] + func userCount(bot: SlackBot, message: MessageDecorator, match: PatternMatch) throws { + let user: ModelPointer = try match.value(key: Keys.user) + + let count = try storage.get(key: user.id, from: Keys.namespace, or: 0) + + let response = try message + .respond() + .text(count == 0 + ? ["It doesn't look like", user, "has any karma yet"] + : [user, "has", count, "karma"] ) - - try webApi.execute(message.makeChatPostMessage(target: target)) - } + + try bot.send(response.makeChatMessage()) + } + + func senderCount(bot: SlackBot, message: MessageDecorator, match: PatternMatch) throws { + let count = try storage.get(key: message.sender().id, from: Keys.namespace, or: 0) + + let response = try message + .respond() + .text(count == 0 + ? ["It doesn't look like you have any karma yet"] + : ["You have", count, "karma"] + ) + + try bot.send(response.makeChatMessage()) } } diff --git a/Sources/Camille/Karma/KarmaService.swift b/Sources/Camille/Karma/KarmaService.swift index 30c4ae8..069a39c 100644 --- a/Sources/Camille/Karma/KarmaService.swift +++ b/Sources/Camille/Karma/KarmaService.swift @@ -1,51 +1,41 @@ -import Bot -import Sugar +import Chameleon -class KarmaService: SlackMessageService { - //MARK: - Properties +final class KarmaService: SlackBotMessageService { + // MARK: - Properties + let storage: Storage let config: Config - - //MARK: - Lifecycle - init(config: Config) { + + enum Keys { + static let namespace = "Karma" + static let count = "count" + static let user = "user" + } + + // MARK: - Lifecycle + init(config: Config = Config.default(), storage: Storage) { self.config = config + self.storage = storage + } + + // MARK: - Public Functions + func configure(slackBot: SlackBot) { + configureMessageService(slackBot: slackBot) + + slackBot + .registerHelp(item: Patterns.topUsers) + .registerHelp(item: Patterns.myCount) + .registerHelp(item: Patterns.userCount) + .registerHelp(item: Patterns.adjustment) } - - //MARK: - Routing - func messageEvent(slackBot: SlackBot, webApi: WebAPI, message: MessageDecorator, previous: MessageDecorator?) throws { - guard let sender = message.sender, let target = message.target else { return } - - let isDirectMessage = message.message.channel?.instantMessage != nil - - //Top Users - /* - let topUserPattern = (isDirectMessage - ? [String.any, "top", Int.any(name: "count")] - : [slackBot.me, String.any, "top", Int.any(name: "count")] - ) - try message.routeText( - to: self.showTopUsers( - from: slackBot.storage, - in: target, - with: webApi, - users: slackBot.currentSlackModelData().users - ), - matching: topUserPattern - ) - */ - - //Users Karma - let userKarmaPattern: [PartialPatternMatcher] = (isDirectMessage - ? ["how much karma do i have"] - : [slackBot.me, "how much karma do i have"] - ) - try message.routeText( - to: self.showUserKarma(user: sender, from: slackBot.storage, in: target, with: webApi), - allowingRemainder: true, - matching: userKarmaPattern - ) - - //Karma Action - guard !isDirectMessage else { return } - try self.adjustKarma(in: message, from: sender, in: target, storage: slackBot.storage, webApi: webApi) + func onMessage(slackBot: SlackBot, message: MessageDecorator, previous: MessageDecorator?) throws { + try slackBot + .route(message, matching: Patterns.topUsers, to: topUsers) + .route(message, matching: Patterns.myCount, to: senderCount) + .route(message, matching: Patterns.userCount, to: userCount) + .route(message, matching: Patterns.adjustment, to: noop) + + try adjust(bot: slackBot, message: message) } + + private func noop(bot: SlackBot, message: MessageDecorator, match: PatternMatch) throws { } } diff --git a/Sources/Camille/TimedMessageService.swift b/Sources/Camille/TimedMessageService.swift index 25a3127..d7ac072 100644 --- a/Sources/Camille/TimedMessageService.swift +++ b/Sources/Camille/TimedMessageService.swift @@ -1,35 +1,35 @@ -import Bot -import Sugar -import Foundation - -struct TimedMessageConfig { - let interval: TimeInterval - let target: String - let announcement: (SlackTargetType) throws -> ChatPostMessage -} - -final class TimedMessageService: SlackRTMEventService { - private let config: TimedMessageConfig - private var timer: TimerService? - - //MARK: - Lifecycle - init(config: TimedMessageConfig) { - self.config = config - } - - //MARK: - Event Dispatch - func configureEvents(slackBot: SlackBot, webApi: WebAPI, dispatcher: SlackRTMEventDispatcher) { - self.timer = TimerService(id: "timedMessage", interval: self.config.interval, storage: slackBot.storage, dispatcher: dispatcher) { _ in - try self.pongEvent(slackBot: slackBot, webApi: webApi) - } - } - func pongEvent(slackBot: SlackBot, webApi: WebAPI) throws { - let data = slackBot.currentSlackModelData() - guard let channel = data.channels.filter({ $0.name == config.target }).first - else { return } - - let message = try config.announcement(channel) - - try webApi.execute(message) - } -} +//import Bot +//import Sugar +//import Foundation +// +//struct TimedMessageConfig { +// let interval: TimeInterval +// let target: String +// let announcement: (SlackTargetType) throws -> ChatPostMessage +//} +// +//final class TimedMessageService: SlackRTMEventService { +// private let config: TimedMessageConfig +// private var timer: TimerService? +// +// //MARK: - Lifecycle +// init(config: TimedMessageConfig) { +// self.config = config +// } +// +// //MARK: - Event Dispatch +// func configureEvents(slackBot: SlackBot, webApi: WebAPI, dispatcher: SlackRTMEventDispatcher) { +// self.timer = TimerService(id: "timedMessage", interval: self.config.interval, storage: slackBot.storage, dispatcher: dispatcher) { _ in +// try self.pongEvent(slackBot: slackBot, webApi: webApi) +// } +// } +// func pongEvent(slackBot: SlackBot, webApi: WebAPI) throws { +// let data = slackBot.currentSlackModelData() +// guard let channel = data.channels.filter({ $0.name == config.target }).first +// else { return } +// +// let message = try config.announcement(channel) +// +// try webApi.execute(message) +// } +//} diff --git a/Sources/Camille/TopicService.swift b/Sources/Camille/TopicService.swift index 5fec876..6c50735 100644 --- a/Sources/Camille/TopicService.swift +++ b/Sources/Camille/TopicService.swift @@ -1,72 +1,72 @@ -import Bot -import Sugar - -typealias UserAllowedClosure = (User) -> Bool -typealias TopicWarningClosure = (Channel, User) throws -> ChatPostMessage - -struct TopicServiceConfig { - let userAllowed: UserAllowedClosure - let warning: TopicWarningClosure? -} - -final class TopicService: SlackRTMEventService, SlackConnectionService { - //MARK: - Properties - fileprivate let config: TopicServiceConfig - - //MARK: - Lifecycle - init(config: TopicServiceConfig) { - self.config = config - } - - //MARK: - Connection Event - func connected(slackBot: SlackBot, botUser: BotUser, team: Team, users: [User], channels: [Channel], groups: [Group], ims: [IM]) throws { - try self.updateTopics(for: channels, in: slackBot.storage) - } - - //MARK: - RTMAPI Events - func configureEvents(slackBot: SlackBot, webApi: WebAPI, dispatcher: SlackRTMEventDispatcher) { - dispatcher.onEvent(message.self) { data in - try self.messageEvent(slackBot: slackBot, webApi: webApi, message: data.message) - } - dispatcher.onEvent(channel_joined.self) { channel in - try self.updateTopics(for: [channel], in: slackBot.storage) - } - } -} - -//MARK: - Topic Sync -fileprivate extension TopicService { - func updateTopics(for channels: [Channel], in storage: Storage) throws { - for channel in channels { - try storage.set(.in("topics"), key: channel.id, value: channel.topic?.value) - } - } -} - -//MARK: - Topic Updates -fileprivate extension TopicService { - func messageEvent(slackBot: SlackBot, webApi: WebAPI, message: Message) throws { - guard - let subtype = message.subtype, - subtype == .channel_topic, - let topic = message.topic, - let user = message.user, - let channel = message.channel?.channel - else { return } - - if (self.config.userAllowed(user)) { - //update the stored topic - try slackBot.storage.set(.in("topics"), key: channel.id, value: topic) - - } else if let lastTopic: String = slackBot.storage.get(.in("topics"), key: channel.id) { - //reset the topic to the previous one set by an authorised user - let setTopic = ChannelSetTopic(channel: channel, topic: lastTopic) - _ = try webApi.execute(setTopic) - - //warn user if needed - guard let warning = self.config.warning else { return } - let message = try warning(channel, user) - _ = try webApi.execute(message) - } - } -} +//import Bot +//import Sugar +// +//typealias UserAllowedClosure = (User) -> Bool +//typealias TopicWarningClosure = (Channel, User) throws -> ChatPostMessage +// +//struct TopicServiceConfig { +// let userAllowed: UserAllowedClosure +// let warning: TopicWarningClosure? +//} +// +//final class TopicService: SlackRTMEventService, SlackConnectionService { +// //MARK: - Properties +// fileprivate let config: TopicServiceConfig +// +// //MARK: - Lifecycle +// init(config: TopicServiceConfig) { +// self.config = config +// } +// +// //MARK: - Connection Event +// func connected(slackBot: SlackBot, botUser: BotUser, team: Team, users: [User], channels: [Channel], groups: [Group], ims: [IM]) throws { +// try self.updateTopics(for: channels, in: slackBot.storage) +// } +// +// //MARK: - RTMAPI Events +// func configureEvents(slackBot: SlackBot, webApi: WebAPI, dispatcher: SlackRTMEventDispatcher) { +// dispatcher.onEvent(message.self) { data in +// try self.messageEvent(slackBot: slackBot, webApi: webApi, message: data.message) +// } +// dispatcher.onEvent(channel_joined.self) { channel in +// try self.updateTopics(for: [channel], in: slackBot.storage) +// } +// } +//} +// +////MARK: - Topic Sync +//fileprivate extension TopicService { +// func updateTopics(for channels: [Channel], in storage: Storage) throws { +// for channel in channels { +// try storage.set(.in("topics"), key: channel.id, value: channel.topic?.value) +// } +// } +//} +// +////MARK: - Topic Updates +//fileprivate extension TopicService { +// func messageEvent(slackBot: SlackBot, webApi: WebAPI, message: Message) throws { +// guard +// let subtype = message.subtype, +// subtype == .channel_topic, +// let topic = message.topic, +// let user = message.user, +// let channel = message.channel?.channel +// else { return } +// +// if (self.config.userAllowed(user)) { +// //update the stored topic +// try slackBot.storage.set(.in("topics"), key: channel.id, value: topic) +// +// } else if let lastTopic: String = slackBot.storage.get(.in("topics"), key: channel.id) { +// //reset the topic to the previous one set by an authorised user +// let setTopic = ChannelSetTopic(channel: channel, topic: lastTopic) +// _ = try webApi.execute(setTopic) +// +// //warn user if needed +// guard let warning = self.config.warning else { return } +// let message = try warning(channel, user) +// _ = try webApi.execute(message) +// } +// } +//} diff --git a/Sources/Camille/UserJoinService.swift b/Sources/Camille/UserJoinService.swift index b3c9bdf..a6ade48 100644 --- a/Sources/Camille/UserJoinService.swift +++ b/Sources/Camille/UserJoinService.swift @@ -1,33 +1,33 @@ -import Bot -import Sugar - -struct UserJoinConfig { - let newUserAnnouncement: (IM) throws -> ChatPostMessage -} - -final class UserJoinService: SlackRTMEventService { - private let config: UserJoinConfig - - //MARK: - Lifecycle - init(config: UserJoinConfig) { - self.config = config - } - - //MARK: - Event Dispatch - func configureEvents(slackBot: SlackBot, webApi: WebAPI, dispatcher: SlackRTMEventDispatcher) { - dispatcher.onEvent(team_join.self) { user in - try self.teamJoinEvent(slackBot: slackBot, - webApi: webApi, - user: user) - } - } - func teamJoinEvent(slackBot: SlackBot, webApi: WebAPI, user: User) throws { - guard !user.is_bot else { return } - - let imOpenRequest = IMOpen(user: user) - let channel = try webApi.execute(imOpenRequest) - let message = try config.newUserAnnouncement(channel) - - try webApi.execute(message) - } -} +//import Bot +//import Sugar +// +//struct UserJoinConfig { +// let newUserAnnouncement: (IM) throws -> ChatPostMessage +//} +// +//final class UserJoinService: SlackRTMEventService { +// private let config: UserJoinConfig +// +// //MARK: - Lifecycle +// init(config: UserJoinConfig) { +// self.config = config +// } +// +// //MARK: - Event Dispatch +// func configureEvents(slackBot: SlackBot, webApi: WebAPI, dispatcher: SlackRTMEventDispatcher) { +// dispatcher.onEvent(team_join.self) { user in +// try self.teamJoinEvent(slackBot: slackBot, +// webApi: webApi, +// user: user) +// } +// } +// func teamJoinEvent(slackBot: SlackBot, webApi: WebAPI, user: User) throws { +// guard !user.is_bot else { return } +// +// let imOpenRequest = IMOpen(user: user) +// let channel = try webApi.execute(imOpenRequest) +// let message = try config.newUserAnnouncement(channel) +// +// try webApi.execute(message) +// } +//} diff --git a/Sources/Camille/main.swift b/Sources/Camille/main.swift index a7ccc22..82cb112 100644 --- a/Sources/Camille/main.swift +++ b/Sources/Camille/main.swift @@ -1,22 +1,23 @@ -import Bot -import Sugar +import Chameleon -#if os(Linux) -let StorageProvider = RedisStorage.self -#else -let StorageProvider = PlistStorage.self -#endif +let env = Environment() +let scopes: String = try env.get(forKey: "SCOPES") +let auth = OAuthAuthenticator( + network: NetworkProvider(), + clientId: try env.get(forKey: "CLIENT_ID"), + clientSecret: try env.get(forKey: "CLIENT_SECRET"), + scopes: Set(scopes.components(separatedBy: ",").flatMap(WebAPI.Scope.init)) +) +let keyValueStore = RedisKeyValueStore(url: try env.get(forKey: "STORAGE_URL")) +let storage = RedisStorage(url: try env.get(forKey: "STORAGE_URL")) -let bot = try SlackBot( - configDataSource: DefaultConfigDataSource, - authenticator: OAuthAuthentication.self, - storage: StorageProvider, +let bot = SlackBot( + authenticator: auth, services: [ - //CrossPostService(config: Configs.CrossPost), - //TopicService(config: Configs.Topic), + SlackBotHelpService(), + SlackBotErrorService(store: keyValueStore), HelloService(), - KarmaService(config: Configs.Karma), - //UserJoinService(config: Configs.UserJoin) + KarmaService(storage: storage) ] ) diff --git a/Tests/CamilleTests/CamilleTests.swift b/Tests/CamilleTests/CamilleTests.swift new file mode 100644 index 0000000..ec36352 --- /dev/null +++ b/Tests/CamilleTests/CamilleTests.swift @@ -0,0 +1,13 @@ +import XCTest +@testable import Camille + +class CamilleTests: XCTestCase { + func testExample() { + // + } + + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..85e728e --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,6 @@ +import XCTest +@testable import CamilleTests + +XCTMain([ + testCase(CamilleTests.allTests), +])