From 8a7e4ec6a0e737058251ae4b8d1c1eaf4b712d8d Mon Sep 17 00:00:00 2001
From: Joe Rayhawk <jrayhawk+git@omgwallhack.org>
Date: Wed, 16 Mar 2022 19:05:20 -0700
Subject: crystal/irc.cr: add obs integration and parallelize everything

---
 crystal/irc.cr | 664 +++++++++++++++++++++++++++++++++++----------------------
 1 file changed, 412 insertions(+), 252 deletions(-)

diff --git a/crystal/irc.cr b/crystal/irc.cr
index ceddd56..391f2c8 100644
--- a/crystal/irc.cr
+++ b/crystal/irc.cr
@@ -7,6 +7,12 @@ require "crystal_mpd"
 
 STDOUT.sync = true
 
+struct Nil
+  def as_s?
+    self
+  end
+end
+
 settings = Hash(String, String).new
 
 settings["home"] = Path.home.to_s
@@ -18,6 +24,9 @@ settings["home"] = Path.home.to_s
   end
 end
 
+obsipc = Channel( String ).new
+ircipc = Channel( Tuple( String, String ) ).new
+
 snifflast = Int64.new(0)
 
 client = Twitcr::Client.new( settings )
@@ -113,6 +122,7 @@ def getvoice( settings : Hash(String, String), userdir : String, chatuser : Stri
   return( [namesub, voice_setting, voice_output ] )
 end
 
+# text2speech
 def t2s( settings : Hash(String, String), userdir : String, chatuser : String, text : String )
   if ( text !~ /^ *(!|\|)/ )
     namesub, voice_setting, voice = getvoice( settings, userdir, chatuser )
@@ -132,281 +142,431 @@ end
 
 lastvoice = Array(String).new
 
-bot = Twitch::IRC::Client.new( nick: "bungmonkey", token: "oauth:" + settings["access_token"], log_mode: true )
-bot.tags = [ "membership", "tags", "commands" ]
-
-# Reconnect on IO::Error?
+# OBS
+spawn do
+  loop do
+    begin
 
-# Create a handler to process incoming messages
-bot.on_message do |message|
-  spawn do
-  next unless ( userlogreturn = userlog( settings, message ) )
-  chatuser, uid, userdir = userlogreturn
-  if ( chatuser == "pipne" ) && ( ( Time.utc.to_unix - snifflast ) >= 14400 )
-    snifflast = Time.utc.to_unix
-    t2smsg( chatuser + " sniff null" )
-  end
-  if ( t2sreturn = t2s( settings, userdir, chatuser, message.params[1] ) )
-    lastvoice.insert( 0, t2sreturn )
-    lastvoice = lastvoice[0..4]
+    obs_pubsub = HTTP::WebSocket.new( URI.parse( "ws://127.0.0.1:4444/" ), HTTP::Headers{"Cookie" => "SESSIONID=1234"} )
+    # Outgoing
+    spawn do
+      while msg = obsipc.receive
+        pp msg
+        obs_pubsub.send( msg )
+      end
+    end
+    # Incoming
+    obs_pubsub.on_message do | message |
+      json = JSON.parse( message )
+      if json["error"]?
+        puts json["error"]
+        ircipc.send( { "#" + settings["channel"], "| obs: #{json["error"]}" } )
+        next
+      end
+      case json["update-type"]?
+      when "StreamStatus" # these are a bit noisy,
+        next              # so skip them
+      when "SwitchScenes"
+        ircipc.send( { "#" + settings["channel"], "| obs: switched scene to " + ( json["scene-name"]?.as_s? || "unknown" ) } )
+      when "MediaEnded"
+        request = Hash( String, String | Bool ){
+          "request-type" => "SetSceneItemProperties",
+          "message-id" => "SetSceneItemProperties",
+          "item" => json["sourceName"].as_s,
+          "visible" => false
+        }
+        #ircipc.send( { "##{settings["channel"]}", "Disabling #{json["sourceName"].as_s}" } )
+        obsipc.send( request.to_json )
+      when "SourceFilterVisibilityChanged"
+        ircipc.send( { "##{settings["channel"]}", "| obs: source #{json["sourceName"]} filter #{json["filterName"]} visibility is now #{json["filterEnabled"]}" } )
+      end
+      print( "RECEIVED: ")
+      print( json.to_pretty_json )
+      print( "\n")
+      case json["message-id"]?
+      when "GetCurrentScene"
+        ircipc.send( { "#" + settings["channel"], "| obs: " + json["sources"].as_a.map{ |source| source["name"] }.join(", ") } )
+      when "GetSourceFilters"
+        ircipc.send( { "#" + settings["channel"], "| obs: " + json["filters"].as_a.map{ |filter| filter["name"] }.join(", ") } )
+      when "GetSceneList"
+        ircipc.send( { "#" + settings["channel"], "| obs: " + json["scenes"].as_a.map{ |scene| scene["name"] }.join(", ") } )
+      when "toggle-filter"
+        name = json["filters"][0]["name"].as_s
+        visible = ( json["filters"][0]["enabled"].as_bool == false )
+        ircipc.send( { "#" + settings["channel"], "| obs: Setting visibility of filter #{name} to #{visible}" } )
+        obsipc.send( "{ \"request-type\": \"SetSourceFilterVisibility\", \"message-id\": \"SetSourceFilterVisibility\", \"sourceName\": \"#{name}\", \"visible\": #{visible} }" )
+       #This is a dumb hack to toggle SetSceneItemProperty visibility
+      when "toggle-source"
+        name = json["name"].as_s
+        visible = ( json["visible"].as_bool == false )
+        ircipc.send( { "#" + settings["channel"], "| obs: Setting visibility of source #{name} to #{visible}" } )
+        obsipc.send( "{ \"request-type\": \"SetSceneItemProperties\", \"message-id\": \"SetSceneItemProperties\", \"item\": \"#{name}\", \"visible\": #{visible} }" )
+      end
+    end
+      obs_pubsub.run
+    rescue ex : Socket::ConnectError
+      # these are a bit noisy
+      pp ex
+      obs_pubsub && obs_pubsub.close
+    rescue ex
+      ircmsg( settings["home"], settings["channel"], "irc.cr: " + ex.to_s.gsub(/\r|\n/, ' ') )
+      puts ex
+      obs_pubsub && obs_pubsub.close
+    end
+    sleep 10
+    next
   end
-  next unless ( ( match = message.params[1].match(/^ *!([A-Za-z]+) (([a-zA-Z0-9= _\:,.&'\/?;\\\(\)\[\]+\-]|!)+)/) || message.params[1].match(/^ *!([A-Za-z]+)/) ) )
-  cmd = match[1]
-  own = ( message.tags["room-id"] == message.tags["user-id"] )
-  vip = ( message.tags["badges"].to_s.matches?( /(^|,)vip\// ) )
-  mod = ( message.tags["mod"] == "1" )
-  sub = ( message.tags["subscriber"] == "1" )
-  if ( ( cmd =~ /^(substitute|voicesub)$/ ) && ( mod || sub ) )
-    case message.params[1].split( " " ).size
-    when 1
-      if File.exists?( userdir + "/voicesub" )
-        bot.message( "#bungmonkey", "| Current name substitution is \"#{File.read( userdir + "/voicesub" )}\"." )
-      else
-        bot.message( "#bungmonkey", "| Current name substitution is disabled." )
+end
+
+# IRC
+loop do
+  begin
+
+    bot = Twitch::IRC::Client.new( nick: settings["channel"], token: "oauth:" + settings["access_token"], log_mode: true )
+    bot.tags = [ "membership", "tags", "commands" ]
+
+    # Outgoing
+    spawn do
+      while msgtuple = ircipc.receive
+bot.message( msgtuple[0], msgtuple[1][0..480] )
       end
-    else
-      if match[2]?
-          voicesub = match[2].downcase
-        if voicesub =~ /^(disabled|null|disable|none)$/
+    end
+
+    # Create a handler to process incoming messages
+    bot.on_message do |message|
+      spawn do
+      next unless ( userlogreturn = userlog( settings, message ) )
+      chatuser, uid, userdir = userlogreturn
+      if ( chatuser == "pipne" ) && ( ( Time.utc.to_unix - snifflast ) >= 14400 )
+        snifflast = Time.utc.to_unix
+        t2smsg( chatuser + " sniff null" )
+      end
+      if ( t2sreturn = t2s( settings, userdir, chatuser, message.params[1] ) )
+        lastvoice.insert( 0, t2sreturn )
+        lastvoice = lastvoice[0..4]
+      end
+      next unless ( ( match = message.params[1].match(/^ *!([A-Za-z]+) (([a-zA-Z0-9= _\:,.&'\/?;\\\(\)\[\]+\-]|!)+)/) || message.params[1].match(/^ *!([A-Za-z]+)/) ) )
+      cmd = match[1]
+      own = ( message.tags["room-id"] == message.tags["user-id"] )
+      vip = ( message.tags["badges"].to_s.matches?( /(^|,)vip\// ) )
+      mod = ( message.tags["mod"] == "1" )
+      sub = ( message.tags["subscriber"] == "1" )
+      if ( ( cmd =~ /^(substitute|voicesub)$/ ) && ( mod || sub ) )
+        case message.params[1].split( " " ).size
+        when 1
           if File.exists?( userdir + "/voicesub" )
-            File.delete( userdir + "/voicesub" )
-            bot.message( "#bungmonkey", "| Name substitution for #{chatuser} is now disabled." )
+            bot.message( "#bungmonkey", "| Current name substitution is \"#{File.read( userdir + "/voicesub" )}\"." )
           else
-            bot.message( "#bungmonkey", "| Name substitution for #{chatuser} is already disabled." )
+            bot.message( "#bungmonkey", "| Current name substitution is disabled." )
           end
         else
-          File.directory?( userdir ) || Dir.mkdir( userdir )
-          File.write( userdir + "/voicesub", voicesub )
-          bot.message( "#bungmonkey", "| Name substitution for #{chatuser} is now \"#{File.read( userdir + "/voicesub" )}\"." )
+          if match[2]?
+              voicesub = match[2].downcase
+            if voicesub =~ /^(disabled|null|disable|none)$/
+              if File.exists?( userdir + "/voicesub" )
+                File.delete( userdir + "/voicesub" )
+                bot.message( "#bungmonkey", "| Name substitution for #{chatuser} is now disabled." )
+              else
+                bot.message( "#bungmonkey", "| Name substitution for #{chatuser} is already disabled." )
+              end
+            else
+              File.directory?( userdir ) || Dir.mkdir( userdir )
+              File.write( userdir + "/voicesub", voicesub )
+              bot.message( "#bungmonkey", "| Name substitution for #{chatuser} is now \"#{File.read( userdir + "/voicesub" )}\"." )
+            end
+          end
         end
-      end
-    end
-  elsif ( cmd =~ /^(commands|help)$/ )
-    bot.message( "#bungmonkey", "| https://bungmonkey.omgwallhack.org/txt/commands.txt" )
-  elsif ( ( cmd == "lastvoice" ) && ( mod || sub ) )
-    unless lastvoice.empty?
-    bot.message( "#bungmonkey", "| Last voices were " + lastvoice.join( ", " ) )
-    else
-      bot.message( "#bungmonkey", "| No voices used so far." )
-    end
-  elsif ( ( cmd =~ /^(voices|voicelist)$/ ) && ( mod || sub ) )
-    bot.message( "#bungmonkey", "| https://bungmonkey.omgwallhack.org/voicelist.txt" )
-  elsif ( ( cmd =~ /^(setvoice|voice)$/ ) && ( mod || sub ) )
-    case message.params[1].split( " " ).size
-    when 1
-      namesub, voice_setting, voice_output = getvoice( settings, userdir, chatuser )
-      bot.message( "#bungmonkey", "| Current voice is #{voice_setting}" )
-    when 2
-      if match[2]?
-        voice = match[2].downcase
-        if voice =~ /disabled|null|disable|none|random/
-          if File.exists?( userdir + "/voice" )
-            File.delete( userdir + "/voice" )
-            bot.message( "#bungmonkey", "| Voice for #{chatuser} is now random." )
+      elsif ( cmd =~ /^(commands|help)$/ )
+        bot.message( "#bungmonkey", "| https://bungmonkey.omgwallhack.org/txt/commands.txt" )
+      elsif ( cmd =~ /^(dexem)$/ )
+        ircipc.send( { "#bungmonkey", "| You're doing great work, Dexem!" } )
+      elsif ( ( cmd == "lastvoice" ) && ( mod || sub ) )
+        unless lastvoice.empty?
+        bot.message( "#bungmonkey", "| Last voices were " + lastvoice.join( ", " ) )
+        else
+          bot.message( "#bungmonkey", "| No voices used so far." )
+        end
+      elsif ( ( cmd =~ /^(voices|voicelist)$/ ) && ( mod || sub ) )
+        bot.message( "#bungmonkey", "| https://bungmonkey.omgwallhack.org/voicelist.txt" )
+      elsif ( ( cmd =~ /^(setvoice|voice)$/ ) && ( mod || sub ) )
+        case message.params[1].split( " " ).size
+        when 1
+          namesub, voice_setting, voice_output = getvoice( settings, userdir, chatuser )
+          bot.message( "#bungmonkey", "| Current voice is #{voice_setting}" )
+        when 2
+          if match[2]?
+            voice = match[2].downcase
+            if voice =~ /disabled|null|disable|none|random/
+              if File.exists?( userdir + "/voice" )
+                File.delete( userdir + "/voice" )
+                bot.message( "#bungmonkey", "| Voice for #{chatuser} is now random." )
+              else
+                bot.message( "#bungmonkey", "| Voice for #{chatuser} is already random." )
+              end
+            elsif voices.has_key?( voice )
+              csvoice = voices[voice]
+              File.directory?( userdir ) || Dir.mkdir( userdir )
+              File.write( userdir + "/voice", csvoice )
+              pp userdir
+              bot.message( "#bungmonkey", "| Voice for #{chatuser} is now #{File.read( userdir + "/voice" )}." )
+              # TODO: make separate script to print streamelements URLs to client machine
+            else
+              pp ( match )
+              bot.message( "#bungmonkey", "| Invalid voice. To see list, use !voices" )
+            end
+          end
+        when 3
+        else
+        end
+      elsif ( ( cmd =~ /^scene$/ ) && ( mod || own ) )
+        if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ )
+          obsipc.send( "{ \"request-type\": \"SetCurrentScene\", \"message-id\": \"SetCurrentScene\", \"scene-name\": \"" + match[2] + "\" }" )
+        else
+          obsipc.send( "{ \"request-type\": \"GetSceneList\", \"message-id\": \"GetSceneList\" }" )
+        end
+      elsif ( ( cmd =~ /^source$/ ) && ( mod || own ) )
+        request = Hash( String, String | Bool ).new
+        if ( match[2]? ) && ( sourceargs = match[2].match( /^([a-zA-Z0-9-_]+) +(true|false)/ ) )
+          request["request-type"] = "SetSceneItemProperties"
+          request["message-id"] = "SetSceneItemProperties"
+          request["item"] = sourceargs[1]
+          sourceargs[2] == "true" && ( request["visible"] = true )
+          sourceargs[2] == "false" && ( request["visible"] = false )
+
+          ircipc.send( { "##{settings["channel"]}", "Setting source #{sourceargs[1]} visibility to #{sourceargs[2]}" } )
+          obsipc.send( request.to_json )
+        elsif ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ )
+          request["request-type"] = "GetSceneItemProperties"
+          request["message-id"] = "toggle-source"
+          request["item"] = match[2]
+          obsipc.send( request.to_json )
+        else
+          request["request-type"] = "GetCurrentScene"
+          request["message-id"] = "GetCurrentScene"
+          obsipc.send( request.to_json )
+        end
+      elsif ( ( cmd =~ /^filter$/ ) && ( mod || own ) )
+        request = Hash( String, String | Bool ){
+          "request-type" => "GetSourceFilters"
+        }
+        if ( match[2]? ) && ( filterargs = match[2].match( /^([a-zA-Z0-9-_]+) +([a-zA-Z0-9-_]+) (true|false)/ ) )
+          request["request-type"] = "SetSourceFilterVisibility"
+          request["message-id"] = "SetSourceFilterVisibility"
+          request["sourceName"] = filterargs[1]
+          request["filterName"] = filterargs[2]
+          #request["filterEnabled"] = filterargs[3]
+          filterargs[3] == "true" && ( request["filterEnabled"] = true )
+          filterargs[3] == "false" && ( request["filterEnabled"] = false )
+          obsipc.send( request.to_json )
+        elsif ( match[2]? ) && ( filterargs = match[2].match( /^([a-zA-Z0-9-_]+) +([a-zA-Z0-9-_]+)/ ) )
+          request["message-id"] = "toggle-filter"
+          request["sourceName"] = filterargs[1]
+          request["filterName"] = filterargs[2]
+          obsipc.send( request.to_json )
+        elsif ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ )
+          request["sourceName"] = match[2]
+          request["message-id"] = "GetSourceFilters"
+          obsipc.send( request.to_json )
+        else
+          ircipc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument, and optionally one filter name." } )
+        end
+      elsif ( cmd =~ /^(metaminute|youdied)$/ )
+        request = Hash( String, String | Bool ).new
+        request["request-type"] = "SetSceneItemProperties"
+        request["message-id"] = "SetSceneItemProperties"
+        request["item"] = "media-" + cmd
+        # This breaks playback (???)
+        #request["visible"] = false
+        #obsipc.send( request.to_json )
+        #sleep 0.5
+        # I should probably set this up to add the source, and then delete the source at the end of playback.
+        request["visible"] = true
+        ircipc.send( { "##{settings["channel"]}", "Playing media-#{cmd}" } )
+        obsipc.send( request.to_json )
+      elsif ( ( cmd =~ /^(status|title)$/ ) && ( mod || own ) )
+        begin
+          if match[2]?
+            client.put_channel!( settings["channel_id"].to_u64, title: match[2] )
+            json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
+            ircmsg( settings["home"], settings["channel"], "Title is now \"#{ json["data"][0]["title"] }\"")
+          else
+            json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
+            ircmsg( settings["home"], settings["channel"], "| Title is currently \"#{ json["data"][0]["title"] }\"")
+          end
+        rescue ex
+          ircmsg( settings["home"], settings["channel"], "| An error occurred! " + ex.message.to_s )
+        end
+      elsif ( ( cmd =~ /^(game|category)$/ ) && ( mod || own ) )
+        begin
+          if match[2]?
+              puts "2 matches"
+            client.put_channel!( settings["channel_id"].to_u64, game: match[2] )
+            json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
+            ircmsg( settings["home"], settings["channel"], "Game is now \"#{ json["data"][0]["game_name"] }\"")
           else
-            bot.message( "#bungmonkey", "| Voice for #{chatuser} is already random." )
+            puts "1 matches"
+            json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
+            ircmsg( settings["home"], settings["channel"], "| Game is currently \"#{ json["data"][0]["game_name"] }\"")
           end
-        elsif voices.has_key?( voice )
-          csvoice = voices[voice]
-          File.directory?( userdir ) || Dir.mkdir( userdir )
-          File.write( userdir + "/voice", csvoice )
-          pp userdir
-          bot.message( "#bungmonkey", "| Voice for #{chatuser} is now #{File.read( userdir + "/voice" )}." )
-          # TODO: make separate script to print streamelements URLs to client machine
+        rescue ex
+          ircmsg( settings["home"], settings["channel"], "| An error occurred! " + ex.message.to_s )
+        end
+      elsif ( ( cmd == "urban" ) && ( mod || own || sub || vip ) )
+        if match[2]? && match[2] =~ /^([a-zA-Z0-9 -])+$/
+          definition = urbandef( match[2] )
+          ircmsg( settings["home"], settings["channel"], definition[0,400] )
         else
-          pp ( match )
-          bot.message( "#bungmonkey", "| Invalid voice. To see list, use !voices" )
+          ircmsg( settings["home"], settings["channel"], "| Urban Dictionary search term should consist of letters, numbers, spaces, and/or hyphens." )
         end
-      end
-    when 3
-    else
-    end
-  elsif ( ( cmd =~ /^(status|title)$/ ) && ( mod || own ) )
-    begin
-      if match[2]?
-        client.put_channel!( settings["channel_id"].to_u64, title: match[2] )
-        json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
-        ircmsg( settings["home"], settings["channel"], "Title is now \"#{ json["data"][0]["title"] }\"")
-      else
-        json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
-        ircmsg( settings["home"], settings["channel"], "| Title is currently \"#{ json["data"][0]["title"] }\"")
-      end
-    rescue ex
-      ircmsg( settings["home"], settings["channel"], "| An error occurred! " + ex.message.to_s )
-    end
-  elsif ( ( cmd =~ /^(game|category)$/ ) && ( mod || own ) )
-    begin
-      if match[2]?
-          puts "2 matches"
-        client.put_channel!( settings["channel_id"].to_u64, game: match[2] )
-        json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
-        ircmsg( settings["home"], settings["channel"], "Game is now \"#{ json["data"][0]["game_name"] }\"")
-      else
-        puts "1 matches"
-        json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
-        ircmsg( settings["home"], settings["channel"], "| Game is currently \"#{ json["data"][0]["game_name"] }\"")
-      end
-    rescue ex
-      ircmsg( settings["home"], settings["channel"], "| An error occurred! " + ex.message.to_s )
-    end
-  elsif ( ( cmd == "urban" ) && ( mod || own || sub || vip ) )
-    if match[2]? && match[2] =~ /^([a-zA-Z0-9 -])+$/
-      definition = urbandef( match[2] )
-      ircmsg( settings["home"], settings["channel"], definition[0,400] )
-    else
-      ircmsg( settings["home"], settings["channel"], "| Urban Dictionary search term should consist of letters, numbers, spaces, and/or hyphens." )
-    end
-  elsif ( cmd == "matrix" )
-    effectsmsg( "overlay glmatrix" )
-  elsif ( cmd =~ /juggle|juggler/ )
-    effectsmsg( "overlay juggler3d" )
-  elsif ( cmd =~ /fireworks|firework/ )
-    effectsmsg( "overlay fireworkx" )
-  elsif ( cmd == "pipes" )
-    if match[2]? && match[2] =~ /^(fast|faster)$/
-      effectsmsg( "overlay pipes " + match[2] )
-    else
-      effectsmsg( "overlay pipes" )
-    end
-  elsif ( cmd == "jellyfish" )
-    effectsmsg( "overlay hydrostat" )
-  elsif ( cmd == "gluten" )
-    effectsmsg( "overlay flyingtoasters" )
-  elsif ( cmd =~ /^(glsnake|glmatrix|gibson|xmatrix|flyingtoasters|moebiusgears|fireworkx|hydrostat|hypertorus|jigsaw|juggler3d|kaleidocycle|kumppa|molecule|noof|polyhedra)$/ )
-    effectsmsg( "overlay " + cmd )
-  elsif ( cmd =~ /gltext|cowsay|xcowsay/ )
-    if ( match[2]? ) && ( gltextargs = match[2].match( /^([0-9]+) +(.+)$/ ) )
-      seconds=UInt64.new( 1 )
-      seconds=gltextargs[1].to_u64
-      if ( own || mod || vip )
-        effectsmsg( "overlay #{cmd} #{match[2]}" )
-        puts "matched #{cmd} #{match[2]}"
-      elsif ( sub )
-        if ( seconds > 20 )
-          seconds=20
+      elsif ( cmd == "matrix" )
+        effectsmsg( "overlay glmatrix" )
+      elsif ( cmd =~ /juggle|juggler/ )
+        effectsmsg( "overlay juggler3d" )
+      elsif ( cmd =~ /fireworks|firework/ )
+        effectsmsg( "overlay fireworkx" )
+      elsif ( cmd == "pipes" )
+        if match[2]? && match[2] =~ /^(fast|faster)$/
+          effectsmsg( "overlay pipes " + match[2] )
+        else
+          effectsmsg( "overlay pipes" )
         end
-        effectsmsg( "overlay #{cmd} #{seconds} #{gltextargs[2]}" )
-        puts "matched #{cmd} #{seconds} #{gltextargs[2]}"
-      else
-        if ( seconds > 5 )
-          seconds=5
+      elsif ( cmd == "jellyfish" )
+        effectsmsg( "overlay hydrostat" )
+      elsif ( cmd == "gluten" )
+        effectsmsg( "overlay flyingtoasters" )
+      elsif ( cmd =~ /^(glsnake|glmatrix|gibson|xmatrix|flyingtoasters|moebiusgears|fireworkx|hydrostat|hypertorus|jigsaw|juggler3d|kaleidocycle|kumppa|molecule|noof|polyhedra)$/ )
+        effectsmsg( "overlay " + cmd )
+      elsif ( cmd =~ /gltext|cowsay|xcowsay/ )
+        if ( match[2]? ) && ( gltextargs = match[2].match( /^([0-9]+) +(.+)$/ ) )
+          seconds=UInt64.new( 1 )
+          seconds=gltextargs[1].to_u64
+          if ( own || mod || vip )
+            effectsmsg( "overlay #{cmd} #{match[2]}" )
+            puts "matched #{cmd} #{match[2]}"
+          elsif ( sub )
+            if ( seconds > 20 )
+              seconds=20
+            end
+            effectsmsg( "overlay #{cmd} #{seconds} #{gltextargs[2]}" )
+            puts "matched #{cmd} #{seconds} #{gltextargs[2]}"
+          else
+            if ( seconds > 5 )
+              seconds=5
+            end
+            effectsmsg( "overlay #{cmd} #{seconds} #{gltextargs[2]}" )
+            puts "matched #{cmd} #{seconds} #{gltextargs[2]}"
+          end
+        elsif match[2]? && match[2] =~ /^.+$/
+          effectsmsg( "overlay #{cmd} #{match[2]}" )
+          puts "matched #{cmd} #{match[2]}"
+        else
+          effectsmsg( "overlay #{cmd}" )
+          puts "failed to match gltext"
         end
-        effectsmsg( "overlay #{cmd} #{seconds} #{gltextargs[2]}" )
-        puts "matched #{cmd} #{seconds} #{gltextargs[2]}"
-      end
-    elsif match[2]? && match[2] =~ /^.+$/
-      effectsmsg( "overlay #{cmd} #{match[2]}" )
-      puts "matched #{cmd} #{match[2]}"
-    else
-      effectsmsg( "overlay #{cmd}" )
-      puts "failed to match gltext"
-    end
-  elsif ( cmd =~ /cow|cowabunga|holycow/ )
-    if match[2]? && match[2] =~ /^([0-9])+$/
-      effectsmsg( "overlay bouncingcow #{match[2]}" )
-    else
-      effectsmsg( "overlay bouncingcow" )
-    end
-  elsif ( cmd == "overlay" )
-    if match[2]? && match[2] =~ /^[a-z]+$/
-        ircmsg( settings["home"], settings["channel"], "| overlay requires an argument consisting wholly of lower case characters.")
-      effectsmsg( "overlay #{match[2]}" )
-    else
-    end
-  elsif ( cmd == "hackerman" )
-    ircmsg( settings["home"], settings["channel"], "| https://bungmonkey.omgwallhack.org/img/hackerman.jpg" )
-  elsif ( cmd =~ /^(songrequest|sr)$/ ) && match[2]?
-    puts ("song detected: #{match[2]}")
-    if ( ( match[2] =~ /list=/ ) && ( match[2] !~ /v=/ ) )
-      ircmsg( settings["home"], settings["channel"], "| Lists are not accepted.\n" )
-    elsif Process.run( "sraddsong.sh", {match[2]} )
-      m = MPD::Client.new
-      currentsong = m.currentsong
-      if ( currentsong ) && ( currentsong["file"].to_s == "http://music/music.ogg" )
+      elsif ( cmd =~ /cow|cowabunga|holycow/ )
+        if match[2]? && match[2] =~ /^([0-9])+$/
+          effectsmsg( "overlay bouncingcow #{match[2]}" )
+        else
+          effectsmsg( "overlay bouncingcow" )
+        end
+      elsif ( cmd == "overlay" )
+        if match[2]? && match[2] =~ /^[a-z]+$/
+            ircmsg( settings["home"], settings["channel"], "| overlay requires an argument consisting wholly of lower case characters.")
+          effectsmsg( "overlay #{match[2]}" )
+        else
+        end
+      elsif ( cmd == "hackerman" )
+        ircmsg( settings["home"], settings["channel"], "| https://bungmonkey.omgwallhack.org/img/hackerman.jpg" )
+      elsif ( cmd =~ /^(songrequest|sr)$/ ) && match[2]?
+        puts ("song detected: #{match[2]}")
+        if ( ( match[2] =~ /list=/ ) && ( match[2] !~ /v=/ ) )
+          ircmsg( settings["home"], settings["channel"], "| Lists are not accepted.\n" )
+        elsif Process.run( "sraddsong.sh", {match[2]} )
+          m = MPD::Client.new
+          currentsong = m.currentsong
+          if ( currentsong ) && ( currentsong["file"].to_s == "http://music/music.ogg" )
+            m.next
+          end
+          if ( status = m.status ) && ( playlistinfo = m.playlistinfo )
+            ircmsg( settings["home"], settings["channel"], "| " + playlistinfo[ status["playlistlength"].to_i - 1 ]["file"].to_s + " added in position " + status["playlistlength"].to_s )
+          else
+            ircmsg( settings["home"], settings["channel"], "| A failure occured." )
+          end
+          m.disconnect
+        else
+        end
+      elsif ( cmd =~ /^current(|song)$/ )
+        m = MPD::Client.new
+        if ( currentsong = m.currentsong )
+            ircmsg( settings["home"], settings["channel"], "| Currently playing: " + currentsong["file"].to_s )
+        else
+          ircmsg( settings["home"], settings["channel"], "| A failure occured." )
+        end
+        m.disconnect
+      elsif ( cmd =~ /^seek$/ )
+        if ( ( match[2] ) && ( match[2] =~ /([+-]|)[0-9]/ ) )
+          m = MPD::Client.new
+          m.seekcur( match[2] )
+          if ( ( musicstatus = m.status ) && ( pos = musicstatus["elapsed"].to_f.to_u64 ) )
+            min=( pos / 60 ).to_u64
+            sec=( pos % 60 ).to_u64
+            ircmsg( settings["home"], settings["channel"], "| " + sprintf( "2%d:2%d", min, sec ) )
+          else
+            ircmsg( settings["home"], settings["channel"], "| An error occurred. " )
+          end
+          m.disconnect
+        else
+          ircmsg( settings["home"], settings["channel"], "| Seek requires an argument of an absolute position in integer number of seconds, or a relative position in in signed integer number of seconds such as +60" )
+        end
+      elsif ( cmd =~ /^next(|song)$/ )
+        m = MPD::Client.new
         m.next
-      end
-      if ( status = m.status ) && ( playlistinfo = m.playlistinfo )
-        ircmsg( settings["home"], settings["channel"], "| " + playlistinfo[ status["playlistlength"].to_i - 1 ]["file"].to_s + " added in position " + status["playlistlength"].to_s )
-      else
-        ircmsg( settings["home"], settings["channel"], "| A failure occured." )
-      end
-      m.disconnect
-    else
-    end
-  elsif ( cmd =~ /^current(|song)$/ )
-    m = MPD::Client.new
-    if ( currentsong = m.currentsong )
-        ircmsg( settings["home"], settings["channel"], "| Currently playing: " + currentsong["file"].to_s )
-    else
-      ircmsg( settings["home"], settings["channel"], "| A failure occured." )
-    end
-    m.disconnect
-  elsif ( cmd =~ /^seek$/ )
-    if ( ( match[2] ) && ( match[2] =~ /([+-]|)[0-9]/ ) )
-      m = MPD::Client.new
-      m.seekcur( match[2] )
-      if ( ( musicstatus = m.status ) && ( pos = musicstatus["elapsed"].to_f.to_u64 ) )
-        min=( pos / 60 ).to_u64
-        sec=( pos % 60 ).to_u64
-        ircmsg( settings["home"], settings["channel"], "| " + sprintf( "2%d:2%d", min, sec ) )
-      else
-        ircmsg( settings["home"], settings["channel"], "| An error occurred. " )
-      end
-      m.disconnect
-    else
-      ircmsg( settings["home"], settings["channel"], "| Seek requires an argument of an absolute position in integer number of seconds, or a relative position in in signed integer number of seconds such as +60" )
-    end
-  elsif ( cmd =~ /^next(|song)$/ )
-    m = MPD::Client.new
-    m.next
-    if ( status = m.status ) && ( status["playlistlength"].to_i > 0 )
-      if ( currentsong = m.nextsong )
-        ircmsg( settings["home"], settings["channel"], "| Currently playing: " + currentsong["file"].to_s )
-      else
-        ircmsg( settings["home"], settings["channel"], "| A failure occured." )
-      end
-    else
-      ircmsg( settings["home"], settings["channel"], "| Playlist is now empty." )
-    end
-    m.disconnect
-  elsif ( cmd == "followage" )
-    begin
-      if match[2]?
-        args = match[2].split(/\s/)
-        if args[1]?
-          json = JSON.parse( client.get_user_follows( from: client.user_id( args[0] ).to_u64 , to: client.user_id( args[1] ).to_u64 ) )
-          puts client.user_id( args[0] ).to_s
-          puts client.user_id( args[1] ).to_s
-          puts json
-          ircmsg( settings["home"], settings["channel"], "| " + json["data"][0]["followed_at"].to_s )
-        elsif args[0]?
-          json = JSON.parse( client.get_user_follows( from: client.user_id( args[0] ).to_u64 , to: settings["channel_id"].to_u64 ) )
-          puts client.user_id( args[0] ).to_s
-          puts json
-          ircmsg( settings["home"], settings["channel"], "| " + json["data"][0]["followed_at"].to_s )
+        if ( status = m.status ) && ( status["playlistlength"].to_i > 0 )
+          if ( currentsong = m.nextsong )
+            ircmsg( settings["home"], settings["channel"], "| Currently playing: " + currentsong["file"].to_s )
+          else
+            ircmsg( settings["home"], settings["channel"], "| A failure occured." )
+          end
+        else
+          ircmsg( settings["home"], settings["channel"], "| Playlist is now empty." )
+        end
+        m.disconnect
+      elsif ( cmd == "followage" )
+        begin
+          if match[2]?
+            args = match[2].split(/\s/)
+            if args[1]?
+              json = JSON.parse( client.get_user_follows( from: client.user_id( args[0] ).to_u64 , to: client.user_id( args[1] ).to_u64 ) )
+              puts client.user_id( args[0] ).to_s
+              puts client.user_id( args[1] ).to_s
+              puts json
+              ircmsg( settings["home"], settings["channel"], "| " + json["data"][0]["followed_at"].to_s )
+            elsif args[0]?
+              json = JSON.parse( client.get_user_follows( from: client.user_id( args[0] ).to_u64 , to: settings["channel_id"].to_u64 ) )
+              puts client.user_id( args[0] ).to_s
+              puts json
+              ircmsg( settings["home"], settings["channel"], "| " + json["data"][0]["followed_at"].to_s )
+            end
+          else
+            json = JSON.parse( client.get_user_follows( from: uid.to_u64 , to: settings["channel_id"].to_u64 ) )
+            puts json
+            ircmsg( settings["home"], settings["channel"], "| " + json["data"][0]["followed_at"].to_s )
+          end
+        rescue ex
+          ircmsg( settings["home"], settings["channel"], "An error occurred! " + ex.message.to_s )
         end
-      else
-        json = JSON.parse( client.get_user_follows( from: uid.to_u64 , to: settings["channel_id"].to_u64 ) )
-        puts json
-        ircmsg( settings["home"], settings["channel"], "| " + json["data"][0]["followed_at"].to_s )
       end
-    rescue ex
-      ircmsg( settings["home"], settings["channel"], "An error occurred! " + ex.message.to_s )
+      end
     end
-  end
-  end
-end
-
 
-rooms = Array( String ).new
-rooms = [ "#bungmonkey", "kr3wzz" ]
+    rooms = Array( String ).new
+    rooms = [ "#bungmonkey", "kr3wzz" ]
 
-while true
-  begin
-# Connect to Twitch
+    # Connect to Twitch
     bot.run( rooms.map{ | room | room.sub( /^#/, "") } )
   rescue ex : IO::Error
     pp ex
+    sleep 1
     # loop to reconnect
   rescue ex
     pp ex
-- 
cgit v1.2.3