summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/twitch/irc.cr65
-rw-r--r--src/twitch/rate_limiter.cr39
2 files changed, 104 insertions, 0 deletions
diff --git a/src/twitch/irc.cr b/src/twitch/irc.cr
new file mode 100644
index 0000000..f5fccfc
--- /dev/null
+++ b/src/twitch/irc.cr
@@ -0,0 +1,65 @@
+require "socket"
+require "fast_irc"
+require "./rate_limiter"
+
+class Twitch::IRC
+ private getter socket : IO
+ private getter limiter : RateLimiter(String)
+
+ def self.new(nick, token)
+ new(TCPSocket.new("irc.chat.twitch.tv", 6667), nick, token)
+ end
+
+ def initialize(@socket : IO, @nick : String, @token : String)
+ @limiter = RateLimiter(String).new
+ limiter.bucket(:join, 50_u32, 15.seconds)
+ end
+
+ def run(channels)
+ authenticate
+ spawn join_channels(channels)
+ @connected = true
+ FastIRC.parse(socket) do |message|
+ spawn dispatch(message)
+ end
+ end
+
+ def on_message(&@on_message : FastIRC::Message ->)
+ end
+
+ def join_channel(channel)
+ return unless @connected
+ wait = limiter.rate_limited?(:join, "")
+ sleep(wait) if wait.is_a?(Time::Span)
+ raw_write("JOIN", ["##{channel}"])
+ end
+
+ def dispatch(message)
+ puts message.inspect
+ case message.command
+ when "PING"
+ pong
+ when "PRIVMSG"
+ @on_message.try &.call(message)
+ end
+ end
+
+ private def authenticate
+ raw_write("PASS", [@token])
+ raw_write("NICK", [@nick])
+ end
+
+ private def join_channels(channels)
+ channels.each do |channel|
+ join_channel(channel)
+ end
+ end
+
+ private def pong
+ raw_write("PONG", ["tmi.twitch.tv"])
+ end
+
+ private def raw_write(command, params, prefix = nil, tags = nil)
+ FastIRC::Message.new(command, params, prefix: prefix, tags: tags).to_s(socket)
+ end
+end
diff --git a/src/twitch/rate_limiter.cr b/src/twitch/rate_limiter.cr
new file mode 100644
index 0000000..929dbc0
--- /dev/null
+++ b/src/twitch/rate_limiter.cr
@@ -0,0 +1,39 @@
+require "rate_limiter"
+
+class RateLimiter(T)
+ class Bucket(K)
+ def initialize(@limit : UInt32, @time_span : Time::Span,
+ @delay : Time::Span = 0.seconds)
+ @bucket = {} of K => Deque(Time)
+ end
+
+ def rate_limited?(key : K, rate_limit_time = Time.now)
+ queue = @bucket[key]?
+
+ unless queue
+ @bucket[key] = Deque(Time).new(1, rate_limit_time)
+ return false
+ end
+
+ first_time = queue[0]
+ last_time = queue[-1]
+
+ if @limit && (queue.size + 1) > @limit
+ return (first_time + @time_span) - rate_limit_time if (first_time + @time_span) > rate_limit_time
+
+ clean_queue(queue, rate_limit_time - @time_span)
+ end
+
+ return (last_time + @delay) - rate_limit_time if @delay && (last_time + @delay) > rate_limit_time
+
+ queue.push(rate_limit_time)
+ false
+ end
+
+ def clean_queue(queue, rate_limit_time = Time.now)
+ while queue[0] < rate_limit_time
+ queue.shift
+ end
+ end
+ end
+end