Class: Lich::DragonRealms::SlackBot

Inherits:
Object
  • Object
show all
Defined in:
documented/dragonrealms/commons/slackbot.rb

Overview

Represents a Slack bot for interacting with the Slack API.

Examples:

Creating a new Slack bot instance

bot = Lich::DragonRealms::SlackBot.new

Defined Under Namespace

Classes: ApiError, Error, NetworkError, ThrottlingError

Constant Summary collapse

LNET_CONNECTION_TIMEOUT =

The timeout duration for lnet connection attempts (in seconds).

30
LNET_ACTIVITY_TIMEOUT =

Consider lnet connection stale if no activity for 2 minutes The duration to consider lnet connection stale if no activity occurs (in seconds).

120
BASE_RETRY_DELAY_SECONDS =

Base delay for exponential backoff on API retries. Slack recommends waiting at least 30 seconds before retrying after rate limits. See: api.slack.com/docs/rate-limits The base delay for exponential backoff on API retries (in seconds).

30
MAX_RETRY_DELAY_SECONDS =

Maximum delay before giving up on retries (2 minutes) The maximum delay before giving up on retries (in seconds).

120

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSlackBot

Initializes a new instance of SlackBot.

Raises:

  • (Error)

    if lnet connection fails or Slack token is not found.



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
# File 'documented/dragonrealms/commons/slackbot.rb', line 42

def initialize
  @api_url = 'https://slack.com/api/'
  @initialized = false
  @error_message = nil

  unless authed?(UserVars.slack_token)
    unless lnet_connected?
      unless Script.running?('lnet')
        unless Script.exists?('lnet')
          @error_message = "lnet.lic not found - cannot retrieve Slack token"
          return
        end
        start_script('lnet')
      end
      start_time = Time.now
      until lnet_connected?
        if (Time.now - start_time) > LNET_CONNECTION_TIMEOUT
          @error_message = "lnet did not connect within #{LNET_CONNECTION_TIMEOUT} seconds."
          return
        end
        pause 1
      end
    end

    @lnet = (Script.running + Script.hidden).find { |val| val.name == 'lnet' }
    unless find_token
      @error_message = "Unable to locate a Slack token"
      return
    end
  end

  begin
    @users_list = post('users.list', { 'token' => UserVars.slack_token })
  rescue ApiError => e
    Lich.log "error fetching user list: #{e.message}"
    @users_list = { 'members' => [] }
  end

  @initialized = true
end

Instance Attribute Details

#error_messageObject (readonly)

Returns the value of attribute error_message.



20
21
22
# File 'documented/dragonrealms/commons/slackbot.rb', line 20

def error_message
  @error_message
end

Instance Method Details

#authed?(token) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
107
108
109
110
111
# File 'documented/dragonrealms/commons/slackbot.rb', line 104

def authed?(token)
  return false unless token
  begin
    post('auth.test', { 'token' => token })['ok']
  rescue ApiError, NetworkError
    false
  end
end

#direct_message(username, message) ⇒ void

This method returns an undefined value.

Sends a direct message to a specified user on Slack.

Parameters:

  • username (String)

    The username of the recipient.

  • message (String)

    The message to send.

Raises:

  • (Error)

    if the DM channel cannot be found.



212
213
214
215
216
217
218
219
220
221
222
# File 'documented/dragonrealms/commons/slackbot.rb', line 212

def direct_message(username, message)
  begin
    dm_channel = get_dm_channel(username)
    raise Error, "Could not find DM channel for #{username}" unless dm_channel

    params = { 'token' => UserVars.slack_token, 'channel' => dm_channel, 'text' => "#{checkname}: #{message}", 'as_user' => true }
    post('chat.postMessage', params)
  rescue Error => e
    Lich.log "Failed to send Slack message to #{username}: #{e.message}"
  end
end

#find_tokenBoolean

Attempts to find a valid Slack token from known lichbots.

Returns:

  • (Boolean)

    true if a valid token is found, false otherwise.



137
138
139
140
141
142
143
144
145
146
147
148
# File 'documented/dragonrealms/commons/slackbot.rb', line 137

def find_token
  lichbots = %w[Quilsilgas]
  echo 'Looking for a token...'
  pause until @lnet

  lichbots.any? do |bot|
    token = request_token(bot)
    authed = authed?(token) if token
    UserVars.slack_token = token if token && authed
    authed
  end
end

#get_dm_channel(username) ⇒ String?

Retrieves the direct message channel ID for a specified user.

Parameters:

  • username (String)

    The username of the user.

Returns:

  • (String, nil)

    The DM channel ID if found, nil otherwise.



227
228
229
230
# File 'documented/dragonrealms/commons/slackbot.rb', line 227

def get_dm_channel(username)
  user = @users_list['members'].find { |u| u['name'] == username }
  user ? user['id'] : nil
end

#initialized?Boolean

Checks if the Slack bot has been initialized.

Returns:

  • (Boolean)

    true if initialized, false otherwise.



85
86
87
# File 'documented/dragonrealms/commons/slackbot.rb', line 85

def initialized?
  @initialized
end

#lnet_connected?Boolean

Returns:

  • (Boolean)


89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'documented/dragonrealms/commons/slackbot.rb', line 89

def lnet_connected?
  return false unless defined?(LNet)
  return false unless LNet.server
  return false if LNet.server.closed?

  # Check last activity if method exists
  if LNet.respond_to?(:last_recv) && LNet.last_recv
    return false if (Time.now - LNet.last_recv) > LNET_ACTIVITY_TIMEOUT
  end

  true
rescue IOError, Errno::EBADF, Errno::EPIPE, NoMethodError
  false
end

#post(method, params) ⇒ Hash

Sends a POST request to the Slack API.

Parameters:

  • method (String)

    The API method to call.

  • params (Hash)

    The parameters to send with the request.

Returns:

  • (Hash)

    The response body from the Slack API.

Raises:



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'documented/dragonrealms/commons/slackbot.rb', line 156

def post(method, params)
  retries = 0
  max_retries = 5

  begin
    uri = URI.parse("#{@api_url}#{method}")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
    req = Net::HTTP::Post.new(uri.path)
    req.set_form_data(params)

    res = http.request(req)

    if res.code == '429'
      retry_after = res['Retry-After']&.to_i
      raise ThrottlingError.new("Throttled by Slack API", retry_after)
    end

    raise NetworkError, "HTTP Error: #{res.code} #{res.message}" unless res.is_a?(Net::HTTPSuccess)

    body = JSON.parse(res.body)
    raise ApiError, "Slack API Error: #{body['error']}" unless body['ok']

    return body
  rescue JSON::ParserError => e
    raise ApiError, "Failed to parse Slack API response: #{e.message}"
  rescue ThrottlingError => e
    raise ApiError, "Throttled by Slack API. Max retries (#{max_retries}) exceeded." if retries >= max_retries
    delay = e.retry_after || (BASE_RETRY_DELAY_SECONDS * (2**retries))
    if delay > MAX_RETRY_DELAY_SECONDS
      raise ApiError, "Throttled by Slack API. Retry delay (#{delay}s) exceeds maximum."
    end
    Lich.log "Throttled by Slack API. Retrying in #{delay} seconds..."
    sleep delay
    retries += 1
    retry
  rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
         Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError => e
    raise NetworkError, "Network error. Max retries (#{max_retries}) exceeded." if retries >= max_retries
    delay = BASE_RETRY_DELAY_SECONDS * (2**retries)
    if delay > MAX_RETRY_DELAY_SECONDS
      raise NetworkError, "Network error. Retry delay (#{delay}s) exceeds maximum."
    end
    Lich.log "Network error: #{e.message}. Retrying in #{delay} seconds..."
    sleep delay
    retries += 1
    retry
  end
end

#request_token(lichbot) ⇒ String, false

Requests a Slack token from the specified lichbot.

Parameters:

  • lichbot (String)

    The name of the lichbot to request the token from.

Returns:

  • (String, false)

    The Slack token if found, false otherwise.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'documented/dragonrealms/commons/slackbot.rb', line 116

def request_token(lichbot)
  ttl = 10
  send_time = Time.now
  @lnet.unique_buffer.push("chat to #{lichbot} RequestSlackToken")
  loop do
    line = get
    pause 0.05
    return false if Time.now - send_time > ttl

    case line
    when /\[Private\]-.*:#{lichbot}: "slack_token: (.*)"/
      msg = Regexp.last_match(1)
      return msg != 'Not Found' ? msg : false
    when /\[server\]: "no user .*/
      return false
    end
  end
end