340 lines
10 KiB
Swift
340 lines
10 KiB
Swift
import Foundation
|
|
import SwiftZulipAPI
|
|
|
|
//: An error that occurs during bot handling.
|
|
public enum BotHandlerError: Error {
|
|
/*:
|
|
An error that occurs when the current user's full name cannot be
|
|
determnied (for use in detecting mentions).
|
|
*/
|
|
case invalidMention
|
|
|
|
//: An error that occurs when a queue's ID cannot be determined.
|
|
case invalidQueueID
|
|
|
|
//: An error that occurs when an event's message cannot be determined.
|
|
case invalidMessage
|
|
|
|
//: An error that occurs when an event's ID cannot be determined.
|
|
case invalidEventID
|
|
|
|
//: An error that occurs when events are in an invalid format.
|
|
case invalidEvents
|
|
|
|
//: An error that occurs when a message content cannot be determined.
|
|
case invalidMessageContent
|
|
|
|
//: An error that occurs when a message type cannot be determined.
|
|
case invalidMessageType
|
|
|
|
//: An error that occurs when a message recipient cannot be determined.
|
|
case invalidMessageTo
|
|
|
|
//: An error that occurs when a message subject cannot be determined.
|
|
case invalidMessageSubject
|
|
}
|
|
|
|
/*:
|
|
Handles bots, including starting, stopping, and handling new messages.
|
|
*/
|
|
public class BotHandler {
|
|
private var bot: Bot
|
|
private var botFullName: String?
|
|
private var zulip: Zulip
|
|
|
|
/*:
|
|
Initializes a bot handler.
|
|
|
|
- Parameters:
|
|
- bot: The `Bot` to handle.
|
|
*/
|
|
public init(bot: Bot, zulipConfig: Config) {
|
|
self.bot = bot
|
|
self.zulip = Zulip(config: zulipConfig)
|
|
}
|
|
|
|
/*:
|
|
Starts listening to and handling messages where the bot is mentioned.
|
|
*/
|
|
public func startMentionedMessageHandling() {
|
|
let users = self.zulip.users()
|
|
let events = self.zulip.events()
|
|
|
|
users.getCurrent(
|
|
clientGravatar: false,
|
|
callback: { (profile, error) in
|
|
if let error = error {
|
|
self.handleError(error: error)
|
|
return
|
|
}
|
|
|
|
guard let botFullName = profile?["full_name"] as? String else {
|
|
self.handleError(error: BotHandlerError.invalidMention)
|
|
return
|
|
}
|
|
|
|
self.botFullName = botFullName
|
|
|
|
events.register(
|
|
eventTypes: ["message"],
|
|
allPublicStreams: true,
|
|
narrow: [],
|
|
callback: { (queue, error) in
|
|
if let error = error {
|
|
self.handleError(error: error)
|
|
return
|
|
}
|
|
|
|
guard let queueID = queue?["queue_id"] as? String else {
|
|
self.handleError(
|
|
error: BotHandlerError.invalidQueueID
|
|
)
|
|
return
|
|
}
|
|
|
|
self.loopMentionedMessageHandling(
|
|
queueID: queueID,
|
|
lastEventID: -1
|
|
)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
|
|
/*:
|
|
Listens to and handles messages on an event queue. Recursively loops
|
|
until the process is stopped.
|
|
|
|
- Parameters:
|
|
- queueID: The queue ID to listen to messages on.
|
|
- lastEventID: The ID of the last event to acknowledge.
|
|
*/
|
|
private func loopMentionedMessageHandling(
|
|
queueID: String,
|
|
lastEventID: Int
|
|
) {
|
|
self.zulip.events().get(
|
|
queueID: queueID,
|
|
lastEventID: lastEventID,
|
|
dontBlock: true,
|
|
callback: { (events, error) in
|
|
if let error = error {
|
|
self.handleError(error: error)
|
|
return
|
|
}
|
|
|
|
guard let events = events else {
|
|
self.handleError(error: BotHandlerError.invalidEvents)
|
|
return
|
|
}
|
|
|
|
var newLastEventID = lastEventID
|
|
|
|
for event in events {
|
|
if
|
|
let result = event["result"] as? String,
|
|
result == "error",
|
|
let errorMessage = event["msg"] as? String
|
|
{
|
|
self.handleError(
|
|
error: ZulipError.error(message: errorMessage)
|
|
)
|
|
continue
|
|
}
|
|
|
|
guard
|
|
let message = event["message"] as? [String: Any]
|
|
else {
|
|
// This means that it was likely a heartbeat event.
|
|
continue
|
|
}
|
|
|
|
guard
|
|
let messageContent = message["content"] as? String
|
|
else {
|
|
self.handleError(
|
|
error: BotHandlerError.invalidMessageContent
|
|
)
|
|
continue
|
|
}
|
|
|
|
guard let botFullName = self.botFullName else {
|
|
self.handleError(error: BotHandlerError.invalidMention)
|
|
continue
|
|
}
|
|
|
|
if (!messageContent.starts(
|
|
with: "@**" + botFullName + "**"
|
|
)) {
|
|
continue
|
|
}
|
|
|
|
guard let newEventID = event["id"] as? Int else {
|
|
self.handleError(
|
|
error: BotHandlerError.invalidEventID
|
|
)
|
|
continue
|
|
}
|
|
|
|
newLastEventID = max(lastEventID, newEventID)
|
|
|
|
self.handleMessage(message: message)
|
|
}
|
|
|
|
/*
|
|
Wait before the next call in order to not exceed the rate
|
|
limit.
|
|
*/
|
|
sleep(2)
|
|
|
|
self.loopMentionedMessageHandling(
|
|
queueID: queueID,
|
|
lastEventID: newLastEventID
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
/*:
|
|
Handles a message using the bot's `handleMessage` method after some
|
|
preparation.
|
|
|
|
- Parameters:
|
|
- message: The message to handle.
|
|
*/
|
|
private func handleMessage(message: [String: Any]) {
|
|
guard let botFullName = self.botFullName else {
|
|
self.handleError(error: BotHandlerError.invalidMention)
|
|
return
|
|
}
|
|
|
|
guard let content = message["content"] as? String else {
|
|
self.handleError(error: BotHandlerError.invalidMessageContent)
|
|
return
|
|
}
|
|
|
|
var contentWithoutMention = content
|
|
|
|
// First check if there is a space after the mention.
|
|
if content.hasPrefix("@**" + botFullName + "** ") {
|
|
contentWithoutMention = String(
|
|
content.dropFirst(botFullName.count + 6)
|
|
)
|
|
/*
|
|
If not, check if there is a mention at all (there should be one no
|
|
matter what, but it's better to be safe).
|
|
*/
|
|
} else if content.hasPrefix("@**" + botFullName + "**") {
|
|
contentWithoutMention = String(
|
|
content.dropFirst(botFullName.count + 5)
|
|
)
|
|
}
|
|
|
|
self.bot.handleMessage(
|
|
message: message,
|
|
contentWithoutMention: contentWithoutMention,
|
|
callback: { (messageContent, error) in
|
|
if let error = error {
|
|
self.handleError(error: error)
|
|
return
|
|
}
|
|
|
|
guard let messageContent = messageContent else {
|
|
self.handleError(
|
|
error: BotHandlerError.invalidMessageContent
|
|
)
|
|
return
|
|
}
|
|
|
|
self.sendResponse(
|
|
content: messageContent,
|
|
originalMessage: message
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
/*:
|
|
Sends a response to a message.
|
|
|
|
- Parameters:
|
|
- content: The content of the response.
|
|
- originalMessage: The message to respond to.
|
|
*/
|
|
private func sendResponse(content: String, originalMessage: [String: Any]) {
|
|
let messages = self.zulip.messages()
|
|
|
|
guard let typeString = originalMessage["type"] as? String else {
|
|
self.handleError(error: BotHandlerError.invalidMessageType)
|
|
return
|
|
}
|
|
|
|
if typeString == "stream" {
|
|
guard let subject = originalMessage["subject"] as? String else {
|
|
self.handleError(error: BotHandlerError.invalidMessageSubject)
|
|
return
|
|
}
|
|
|
|
guard
|
|
let to = originalMessage["display_recipient"] as? String
|
|
else {
|
|
self.handleError(error: BotHandlerError.invalidMessageTo)
|
|
return
|
|
}
|
|
|
|
messages.send(
|
|
messageType: MessageType.streamMessage,
|
|
to: to,
|
|
subject: subject,
|
|
content: content,
|
|
callback: { (id, error) in
|
|
if let error = error {
|
|
self.handleError(error: error)
|
|
}
|
|
}
|
|
)
|
|
} else {
|
|
guard
|
|
let to = originalMessage[
|
|
"display_recipient"
|
|
] as? [[String: Any]]
|
|
else {
|
|
self.handleError(error: BotHandlerError.invalidMessageTo)
|
|
return
|
|
}
|
|
|
|
let toString = to.compactMap({ recipient in
|
|
guard let email = recipient["email"] as? String else {
|
|
return nil
|
|
}
|
|
|
|
return email
|
|
}).joined(separator: ",")
|
|
|
|
messages.send(
|
|
messageType: MessageType.privateMessage,
|
|
to: toString,
|
|
subject: nil,
|
|
content: content,
|
|
callback: { (id, error) in
|
|
if let error = error {
|
|
self.handleError(error: error)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
/*:
|
|
Handles an error that occurs during bot handling.
|
|
|
|
- Parameters:
|
|
- error: The error to handle.
|
|
*/
|
|
private func handleError(error: Error) {
|
|
print("Error: " + String(describing: error))
|
|
}
|
|
}
|