~pixelinc/Slack-RTM

ref: 12f3e6940048710458f7d2a9367b13301b65e38e Slack-RTM/src/slack_rtm/client.cr -rw-r--r-- 4.6 KiB
12f3e694PixeL update name other places 1 year, 9 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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