Package.swift
:
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "SlackBot",
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0-rc"),
// 🔵 Swift ORM (queries, models, relations, etc) built on SQLite 3.
.package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0-rc"),
// Websockets
.package(url: "https://github.com/vapor/websocket.git", from: "1.0.0"),
// Jobs / Timers
.package(url: "https://github.com/BrettRToomey/Jobs.git", from: "1.1.1")
],
targets: [
.target(name: "App", dependencies: ["FluentSQLite", "WebSocket", "Jobs", "Vapor"]),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"])
]
)
main.swift
:
import App
import Service
import Vapor
import Foundation
import Jobs
// Add your bot token here:
let slackBotToken = "xoxb-....YOUR-TOKEN...."
// see https://api.slack.com/methods/rtm.start
// see https://api.slack.com/methods/rtm.connect
public struct SlackDetails: Codable {
var ok: Bool
var url: URL
enum CodingKeys: String, CodingKey {
case ok
case url
}
}
public enum SlackNotificationType: String, Codable {
case hello = "hello"
case message = "message"
case ping = "ping"
case pong = "pong"
}
public struct SlackNotification: Codable {
var type: SlackNotificationType
}
public struct SlackMessage: Codable {
var id: Int64?
var type: SlackNotificationType
var channel: String?
var user: String?
var sourceTeam: String?
var team: String?
var text: String?
enum CodingKeys: String, CodingKey {
case id
case type
case channel
case user
case team
case text
}
init(id: Int64, type: SlackNotificationType) {
self.id = id
self.type = type
}
mutating func stripDetails() {
user = nil
sourceTeam = nil
team = nil
text = nil
}
}
public final class MessageFactory {
public static let shared = MessageFactory()
private var identifier: Int64 = -1
private var id: Int64 {
get {
self.identifier += 1
return identifier
}
}
private init() {
print("initialized message factory")
}
public func response(for message: SlackMessage) -> SlackMessage {
var response = message
response.id = id
response.stripDetails()
if let user = message.user {
response.text = "Hello <@\(user)>"
} else if let text = message.text {
response.text = String(text.reversed())
} else {
response.text = "Hi!"
}
return response
}
public func ping() -> SlackMessage {
return SlackMessage(id: id, type: .ping)
}
}
extension WebSocket {
public func send(message: SlackMessage, promise: Promise<Void>? = nil) {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(message)
guard let json = String(bytes: data, encoding: .utf8) else { return }
print("--> \(json) (\(data))")
//send(text: json, promise: promise)
send(data, promise: promise)
} catch {
print("ERROR: \(error.localizedDescription)")
}
}
}
// The contents of main are wrapped in a do/catch block because any errors that get raised to the top level will crash Xcode
do {
var config = Config.default()
var env = try Environment.detect()
var services = Services.default()
try App.configure(&config, &env, &services)
let app = try Application(
config: config,
environment: env,
services: services
)
try App.boot(app)
// Create websocket worker
let worker = MultiThreadedEventLoopGroup(numThreads: 1)
// Whether or not we're connected
var connected = false
// Get the websocket url from Slack
var connectHeaders = HTTPHeaders()
connectHeaders.add(name: .accept, value: "application/json")
let connectURL = "https://slack.com/api/rtm.connect?token=\(slackBotToken)"
let details = try app.make(Client.self).get(connectURL, headers: connectHeaders).map(to: SlackDetails.self) { (response) -> SlackDetails in
guard let data = response.http.body.data else {
fatalError("No response")
}
let decoder = JSONDecoder()
return try decoder.decode(SlackDetails.self, from: data)
}.wait()
print("ws url: \(details.url)")
// Setup the RTM Websocket
let ws = try HTTPClient.webSocket(scheme: .wss, hostname: details.url.host!, port: details.url.port, path: details.url.path, on: worker).wait()
// let promise = worker.eventLoop.newPromise(Void.self)
print("Established Real Time Messaging (RTM) websocket connection")
ws.onError { (ws, error) in
print("error: \(error.localizedDescription)")
}
// ws.onBinary { (ws, data) in
// print("<-- \(data)")
// }
ws.onText { (ws, text) in
print("<-- \(text)")
// promise.succeed(result: text)
let data = text.convertToData()
let decoder = JSONDecoder()
do {
let notification = try decoder.decode(SlackNotification.self, from: data)
switch notification.type {
case .hello:
print("Connected")
connected = true
break
// case .ping:
// ws.send(message: MessageFactory.shared.pong())
case .pong:
print("<-- PONG!")
break
case .message:
let message = try decoder.decode(SlackMessage.self, from: data)
print("<-- \(text)")
ws.send(message: MessageFactory.shared.response(for: message))
//ws.send(message: message, promise: promise)
break
default:
break
}
} catch {
// unsupported notification type
//print("error: \(error.localizedDescription)")
//print("json: \(text)")
}
}
Jobs.add(interval: .seconds(10)) {
//print("ws closed: \(ws.isClosed), connected: \(connected)")
guard !ws.isClosed, connected else { return }
// send keepalive ping
ws.send(message: MessageFactory.shared.ping())
}
try ws.onClose.always {
connected = false
}.wait()
print("WebSocket connection closed, is closed: \(ws.isClosed)")
try app.run()
} catch {
print(error)
exit(1)
}