require "json"
require "./rest"
module Slack
class Client
include REST
getter websocket : Slack::WebSocket do
initialize_websocket
end
property cache : Cache?
def initialize(@token : String)
@heartbeat_acked = true
@heartbeat_interval = 45_000
setup_heartbeats
end
def run
loop do
begin
websocket.run
rescue ex
Log.error(exception: ex) { "Exception in websocket#run" }
Log.error { ex.inspect_with_backtrace }
end
Log.info { "Reconnecting..." }
@websocket = initialize_websocket
end
end
private def initialize_websocket
# hit slack websocket url to get our connection
websocket_info = get_websocket_url
raise "fuck you" if websocket_info.url.nil?
websocket_info.channels.try &.each do |channel|
@cache.try &.cache(channel)
end
websocket_info.users.try &.each do |user|
@cache.try &.cache(user)
end
websocket = Slack::WebSocket.new(
websocket_info.url.not_nil!
)
websocket.on_message(&->on_message(String))
websocket.on_close(&->on_close(HTTP::WebSocket::CloseCode, String))
websocket
end
private def on_close(code : HTTP::WebSocket::CloseCode, message : String)
reason = message.empty? ? "(none)" : message
Log.warn { "Websocket closed with code: #{code}, reason: #{reason}" }
end
macro call_event(name, payload)
{% p!(name) %}
@on_{{name}}_handlers.try &.each do |handler|
begin
handler.call({{payload}})
rescue ex
Log.error(exception: ex) { "An exception occurred in a user-defined event handler!" }
Log.error { ex.inspect_with_backtrace }
end
end
end
private def on_message(message : String)
spawn do
begin
event = RTM::Packet.from_json(message)
case event
when RTM::Hello then call_event hello, event
when RTM::Goodbye then call_event goodbye, event
when RTM::Pong
handle_heartbeat_ack
call_event pong, event
when RTM::Message then call_event message, event
when RTM::UserTyping then call_event user_typing, event
when RTM::UserChange then call_event user_change, event
when RTM::ChannelJoined then call_event channel_joined, event
when RTM::MemberJoinedChannel then call_event member_joined, event
else
Log.warn { "Unsupported event: #{event.type}" }
end
rescue ex : JSON::MappingError
Log.warn { "An unsupported event came in, dropping it! " }
Log.warn { "Raw: #{message}" }
rescue ex : JSON::ParseException
Log.error { "An error occurred while parsing messages on #{message}" }
Log.error { ex.inspect_with_backtrace }
rescue ex
Log.error(exception: ex) { "A miscellaneous exception occurred during message handling." }
Log.error { ex.inspect_with_backtrace }
end
end
end
private def handle_heartbeat_ack
Log.debug { "Heartbeat ACK Received" }
@heartbeat_acked = true
end
private def setup_heartbeats
Log.debug { "Setting up heartbeater..." }
spawn do
loop do
Log.debug { "Sleeping for #{@heartbeat_interval.milliseconds} before heartbeating..." }
sleep @heartbeat_interval.milliseconds
unless @heartbeat_acked
Log.warn { "Heartbeat not acknowledged, reconnecting..." }
@heartbeat_acked = true
websocket.close(4000)
next
end
Log.debug { "Sending heartbeat" }
begin
# TODO: randomized id, or perhaps custom sequence?
websocket.send({type: "ping", id: 2525}.to_json)
@heartbeat_acked = false
rescue ex
Log.error(exception: ex) { "Heartbeat failed!" }
Log.error { ex.inspect_with_backtrace }
end
end
end
end
macro event(name, payload_type)
def on_{{name}}(&handler : {{payload_type}} ->)
(@on_{{name}}_handlers ||= [] of {{payload_type}} ->) << handler
end
end
# Various events that the user can subscribe to
event message, RTM::Message
event hello, RTM::Hello
event goodbye, RTM::Goodbye
event pong, RTM::Pong
event user_typing, RTM::UserTyping
event channel_joined, RTM::ChannelJoined
event member_joined, RTM::MemberJoinedChannel
event user_change, RTM::UserChange
end
end