Skip to content
This repository has been archived by the owner. It is now read-only.
Switch branches/tags
Go to file
…fee, added HUBOT_RATE_LIMIT_NOTIFY_MSG, fix for review comment
4 contributors

Users who have contributed to this file

@michaelansel @geoffreyanderson @ehammersley @TobiTenno
# Description
# Middleware for adding rate limits to commands
# Configuration:
# HUBOT_RATE_LIMIT_NOTIFY_PERIOD - how frequently to put rate limiting messages into chat (accounting done by listener)
# HUBOT_RATE_LIMIT_CMD_PERIOD - how frequently to execute any single listener (can be overridden by the listener)
# HUBOT_RATE_LIMIT_SILENT - (Optional) Setting this environment variable to any truthy value will supress rate limit exceeded feedback messages in chat.
# HUBOT_RATE_LIMIT_NOTIFY_MSG - (Optional) message to be sent when user has exceeded rate limit
# Commands:
# Notes:
# <optional notes required for the script>
# Author:
# Michael Ansel <>
# Geoffrey Anderson <>
module.exports = (robot) ->
# Map of listener ID to last time it was executed
lastExecutedTime = {}
# Map of listener ID to last time a reply was sent
lastNotifiedTime = {}
# Interval between mentioning that execution is rate limited
notifyPeriodMs = parseInt(process.env.HUBOT_RATE_LIMIT_NOTIFY_PERIOD)*1000
notifyPeriodMs = 10*1000 # default: 10s
robot.respond /debug rate limits/, {rateLimits:{minPeriodMs:0}}, (response) ->
response.reply('lastExecutedTime: ' + JSON.stringify(lastExecutedTime))
response.reply('lastNotifiedTime: ' + JSON.stringify(lastNotifiedTime))
robot.listenerMiddleware (context, next, done) ->
# Retrieve the listener id. If one hasn't been registered, fallback
# to using the regex to uniquely identify the listener (even though
# it is dirty).
listenerID = context.listener.options?.id or context.listener.regex
# Bail on unknown because we can't reliably track listeners
return unless listenerID?
# Default to 1s unless listener or environment variable provides a
# different minimum period (listener overrides win here).
if context.listener.options?.rateLimits?.minPeriodMs?
minPeriodMs = context.listener.options.rateLimits.minPeriodMs
else if process.env.HUBOT_RATE_LIMIT_CMD_PERIOD?
minPeriodMs = parseInt(process.env.HUBOT_RATE_LIMIT_CMD_PERIOD)*1000
minPeriodMs = 1*1000
rateLimitMsg = process.env.HUBOT_RATE_LIMIT_NOTIFY_MSG || "Rate limit hit! Please wait #{minPeriodMs/1000} seconds before trying again."
# Grab the room or user name that fired this listener.
roomOrUser =
roomOrUser =
# Construct a key to rate limit on. If the response was from a room
# then append the room name to the key. Otherwise, append the user name.
listenerAndRoom = listenerID + "_" + roomOrUser
# See if command has been executed recently in the same room (or with the same user)
if lastExecutedTime.hasOwnProperty(listenerAndRoom) and
lastExecutedTime[listenerAndRoom] > - minPeriodMs
# Command is being executed too quickly!
robot.logger.debug "Rate limiting " + listenerID + " in " + roomOrUser + "; #{minPeriodMs} > #{ - lastExecutedTime[listenerAndRoom]}"
# Notify at least once per rate limiting event
myNotifyPeriodMs = minPeriodMs if notifyPeriodMs > minPeriodMs
# If no notification sent recently
if (lastNotifiedTime.hasOwnProperty(listenerAndRoom) and
lastNotifiedTime[listenerAndRoom] < - myNotifyPeriodMs) or
not lastNotifiedTime.hasOwnProperty(listenerAndRoom)
if not process.env.HUBOT_RATE_LIMIT_SILENT?
context.response.reply rateLimitMsg
lastNotifiedTime[listenerAndRoom] =
# Bypass executing the listener callback
next () ->
lastExecutedTime[listenerAndRoom] =
catch err
robot.emit('error', err, context.response)