From 1fbe697f7ae040bfe7bd24925c805e7d5019d2ba Mon Sep 17 00:00:00 2001 From: Joe Rayhawk Date: Sat, 29 Oct 2022 16:56:39 -0700 Subject: bungmobott.cr: irresponsible omnibus commit. Finishes migration to OBSWebSocket. Fiberizes OBS events and chat better. Adds bungmoBoom effects. --- crystal/bungmobott.cr | 1194 +++++++++++++++---------------------------------- 1 file changed, 371 insertions(+), 823 deletions(-) (limited to 'crystal/bungmobott.cr') diff --git a/crystal/bungmobott.cr b/crystal/bungmobott.cr index 1d91bf7..aad49b9 100755 --- a/crystal/bungmobott.cr +++ b/crystal/bungmobott.cr @@ -4,8 +4,10 @@ require "uri" require "twitcr" require "json" require "crystal_mpd" +require "obswebsocket" STDOUT.sync = true +STDOUT.flush_on_newline = true struct Nil def as_s? @@ -67,8 +69,8 @@ else end # enable explosions? +explosions = Hash( String, String ).new if File.exists?( settings["configdir"] + "/explosionlist.txt" ) - explosions = Hash(String, String).new File.each_line( settings["configdir"] + "/explosionlist.txt" ) do |line| explosions[ line.downcase ] = line end @@ -105,12 +107,66 @@ else settings["chat_user"] = settings["channel"] end -obs4ipc = Channel( String ).new -obs5ipc = Channel( String ).new +# fiber communication channels ircipc = Channel( Tuple( String, String ) ).new t2sipc = Channel( Tuple( String, String ) ).new twitchipc = Channel( Tuple( String, Union( String, UInt64 ) ) ).new +obs = OBSWebSocket::Server.new( "ws://127.0.0.1:4455/" ) + +# OBS event thread +spawn do + evchan = Channel( JSON::Any ).new + obs.scenes["meta-foreground"].to_h.each_key do | key | + if key =~ /^media-temporary-/ + obs.inputs[key].delete! + end + end + obs.reqchan.send( { evchan, "subscribe events" } ) + while json = evchan.receive + # A Fiber.yield occurs after this to make sure "json" doesn't get overwritten before we can use it. + spawn do + d = json # Copy *immediately* + case d["eventType"].as_s + when "CurrentProgramSceneChanged" + ircipc.send( { "#" + settings["channel"], "| obs: switched scene to " + ( d["eventData"]["sceneName"]?.as_s? || "unknown" ) } ) + when "MediaInputPlaybackEnded" + if d["eventData"]["inputName"].as_s =~ /^media-temporary-/ + obs.reqchan.send( { nil, OBSWebSocket.req( "RemoveInput", UUID.random.to_s, JSON.parse({ "inputName" => d["eventData"]["inputName"].as_s }.to_json) ) } ) + end + when "SceneItemEnableStateChanged" + edata = d["eventData"] + name = obs.scenes[ edata["sceneName"].as_s ][ edata["sceneItemId"].as_i64 ].name + if name !~ /media-temporary/ + ircipc.send( { "##{settings["channel"]}", "| obs: source #{name} visibility is now #{edata["sceneItemEnabled"].as_bool}" } ) + end + when "SceneItemTransformChanged" + edata = d["eventData"] + sceneitem = obs.scenes[edata["sceneName"].as_s][edata["sceneItemId"].as_i64] + t = edata["sceneItemTransform"] + if ( sceneitem.name =~ /^media-temporary-/ ) + spx = t["positionX" ].as_f.to_i64 + spy = t["positionY" ].as_f.to_i64 + sdx = t["sourceHeight"].as_f.to_i64 + sdy = t["sourceWidth" ].as_f.to_i64 + if ( spx == 0 && spy == 0 && sdx != 0 && sdy != 0 ) + # source position randomizer + bx = obs.video.to_h["baseWidth" ].as(Int64 | Float64).to_i64 + by = obs.video.to_h["baseHeight"].as(Int64 | Float64).to_i64 + spx = ( rand(bx) - (sdx / 2) ) + spy = ( rand(by) - (sdy / 2) ) + sceneitem.transform( { "positionX" => spx, "positionY" => spy } ) + end + end + when "SourceFilterEnableStateChanged" + edata = d["eventData"] + ircipc.send( { "##{settings["channel"]}", "| obs: source #{edata["sourceName"].as_s} filter #{edata["filterName"].as_s} visibility is currently #{edata["filterEnabled"].as_s}" } ) + end + end + Fiber.yield + end +end + # Twitch API request handling thread spawn do loop do @@ -119,7 +175,7 @@ spawn do cmd, arg = [ *twitchtuple ] case cmd when "get_user" - userinfo = JSON.parse( client.get_user( arg.to_u64 ) )["data"][0] + userinfo = JSON.parse( client.get_user( arg ) )["data"][0] pp userinfo unless userinfo["broadcaster_type"].as_s.blank? puts "\033[38;5;12m#{userinfo["login"]} is #{userinfo["broadcaster_type"]}\033[0m" @@ -139,16 +195,31 @@ spawn do end end - -snifflast = Int64.new(0) - +def obstemporarymediacreate( obs : OBSWebSocket::Server, sname : String, iname, path : String ) + iname = "media-temporary-explosion-#{iname}" + isettings = Hash( String, String | Bool | Int64 | Float64 ){ + "advanced" => true, + "clear_on_media_end" => true, + "color_range" => 0.to_i64, + "is_local_file" => true, + "looping" => false, + "restart_on_activate" => true, + "local_file" => path, + } + response = obs.scenes[sname].createinput( iname, "ffmpeg_source", isettings ) + # Skip ORM stuff and configure the SceneItem as fast as we possibly can + if ( rdata = response["responseData"]? ) && ( goodtransform = obs.sources.last_known_real_transform?( iname ) ) + siid = rdata["sceneItemId"].as_i64 + obs.reqchan.send( { nil, OBSWebSocket.req( "SetSceneItemTransform", UUID.random.to_s, JSON.parse( { "sceneName" => sname, "sceneItemId" => siid, "sceneItemTransform" => { "positionX" => goodtransform.to_h["positionX"], "positionY" => goodtransform.to_h["positionY"] } }.to_json ) ) } ) + end +end def urbandef( term : String ) ssl_context = OpenSSL::SSL::Context::Client.new {% if flag?(:windows) %} ssl_context.verify_mode = OpenSSL::SSL::VerifyMode::NONE {% end %} - #http://api.urbandictionary.com/v0/define?term=waifu + #https://api.urbandictionary.com/v0/define?term=waifu response = HTTP::Client.exec( "GET", "https://api.urbandictionary.com/v0/define?term=#{term}", tls: ssl_context ) puts response.status_code json = JSON.parse( response.body ) @@ -293,29 +364,6 @@ def t2s( t2sipc : Channel, settings : Hash(String, String), userdir : String, ch end end -def reversesource( scenes : Hash( String, Hash( String, Hash( String, Int64 | Float64 | Bool | String ) ) ), currentscene : String ) -# "meta-cam-front" => { -# "vidcap-c920" => { -# "render" => true, "type" => "dshow_input", "x" => 0.0, "y" => 0.0 -# }, -# "gst-bungfront" => { -# "type" => "gstreamer-source", "x" => 0.0, "y" => 0.0 -# } -# } -# # scenes[currentscene][source]["type"] => "scene" - sourcemap = Hash( String, String ).new - scenes[currentscene].each do | source, sourcedata | - # maybe make this recurse later - sourcemap[source.to_s] = currentscene - if ( ( ! sourcedata.has_key?("type") ) || ( sourcedata["type"] == "scene" ) ) - scenes[source.to_s].each do | metasource, metasourcedata | - sourcemap[metasource.to_s] = source.to_s - end - end - end - return sourcemap -end - def playaudiofile( filepath : String ) p = Process.new( "powershell.exe", @@ -485,505 +533,6 @@ spawn do end end -def obsrequest( type : String, id : String, data : ( String | Nil | JSON::Any ) = nil ) - request = JSON.build do |json| - json.object do - json.field "op", 6 - json.field "d" do - json.object do - json.field "requestType", type - json.field "requestId", id - if data - json.field "requestData", data - end - end - end - end - end - return request.to_s -end - -# obs-websocket-5 thread -spawn do - loop do - begin - - obs5_pubsub = HTTP::WebSocket.new( URI.parse( "ws://127.0.0.1:4455/" ), HTTP::Headers{"Cookie" => "SESSIONID=1235", "Sec-WebSocket-Protocol" => "obswebsocket.json"} ) - # Outgoing - # state - currentscene = "unknown" - canvas = { 1280, 720 } - - # inputname => { property => value } - inputs = Hash( String, Hash( String, String | Int64 | Float64 | Bool ) ) - # GetInputList - # GetInputSettings - - # scenename => { sceneitemid => { property => value } - scenes = Hash( String, Hash( String, Hash( String, Int64 | Float64 | Bool | String ) ) ).new - # GetSceneList - # GetSceneItemList - # GetSceneItemTransform - - spawn do - while msg = obs5ipc.receive - pp msg - if ( match = msg.match( /^\x10source(-notransition|) ([a-z0-9-_]+)/ ) ) - transition = ( match[1] != "-notransition" ) - source = match[2] - sources = reversesource( scenes, currentscene ) - pp sources - unless ( scene = sources[source]? ) - ircipc.send( { "#" + settings["channel"], "| obs5: source not found: #{source}" } ) - next - end - obs5_pubsub.send( obsrequest( "GetSceneItemEnabled", "GetSceneItemTransform", JSON.parse( { "sceneName" => scene, "sceneItemId" => scenes[scene][source]["id"] }.to_json ) ) ) - if ( match = msg.match( /^\x10source[-a-zA-Z0-9_]* ([a-z0-9-_]+) (true|false)/ ) ) - obs5_pubsub.send( obsrequest( "SetSceneItemEnabled", "SetSceneItemEnabled", JSON.parse( { "sceneName" => scene, "sceneItemId" => scenes[scene][source]["id"], "sceneItemEnabled" => ( match[2] == "true" ) }.to_json ) ) ) - elsif ( match = msg.match( /^\x10source[-a-zA-Z0-9_]* ([a-z0-9-_]+)/ ) ) - obs5_pubsub.send( obsrequest( "SetSceneItemEnabled", "SetSceneItemEnabled", JSON.parse( { "sceneName" => scene, "sceneItemId" => scenes[scene][source]["id"], "sceneItemEnabled" => ! scenes[scene][source]["render"] }.to_json ) ) ) - end -# if transition -# request = Hash( String, String | Bool ).new -# request["request-type"] = "SetPreviewScene" -# request["message-id"] = "SetPreviewScene" -# request["scene-name"] = currentscene -# obs4_pubsub.send( request.to_json ) -# sleep 0.1 # somehow these get out-of-order otherwise? >:( -# request = Hash( String, String ).new -# request["request-type"] = "TransitionToProgram" -# request["message-id"] = "TransitionToProgram" -# obs4_pubsub.send( request.to_json ) -# end - elsif ( match = msg.match( /^\x10source/ ) ) - pp scenes - pp currentscene - sources = reversesource( scenes, currentscene ) - #ircipc.send( { "#" + settings["channel"], "| obs: " + sources.keys.join(", ") } ) - obs5_pubsub.send( obsrequest( "GetSceneItemList", "GetSceneItemList", JSON.parse( { "sceneName" => currentscene }.to_json ) ) ) - else - obs5_pubsub.send( msg ) - end - end - end - # Incoming - obs5_pubsub.on_message do | message | - json = JSON.parse( message ) - if json["error"]? - puts json["error"] - ircipc.send( { "#" + settings["channel"], "| obs5: #{json["error"]}" } ) - next - end - case json["op"].as_i - when 0 # hello - json_text = %({"op":1,"d":{"rpcVersion":1}}) - obs5_pubsub.send( json_text ) - when 5 # event - edata = json["d"]["eventData"] - case json["d"]["eventType"].as_s - when "CurrentProgramSceneChanged" - ircipc.send( { "#" + settings["channel"], "| obs5: switched scene to " + ( edata["sceneName"]?.as_s? || "unknown" ) } ) - puts "SwitchScenes: currentscene is " + currentscene - currentscene = edata["sceneName"].as_s - puts "SwitchScenes: currentscene is " + currentscene - when "SceneItemEnableStateChanged" - # FIXME: No sceneItemName? need to convert scenes/sceneitems into a proper object interface or build a new index so we can lookup name by id - edata["sceneItemEnabled"].as_bool - edata["sceneItemId"].as_i - edata["sceneName"].as_s - end - when 7 # requestResponse - case json["d"]["requestType"].as_s - when "GetStats" # FIXME: RequestBatch this with GetOutputStatus? - streamstatus = json["d"]["responseData"].as_h - - obs5_pubsub.send( obsrequest( "GetOutputList", "GetOutputList" )) - when "GetOutputList" - output_name = String.new - json["d"]["responseData"]["outputs"].as_a.each do | output | - if output["outputKind"].as_s == "rtmp_output" - output_name = output["outputName"].as_s - end - end - unless output_name.empty? - obs5_pubsub.send( obsrequest( "GetOutputStatus", "GetOutputStatus", JSON.parse( { "outputName" => output_name }.to_json ) )) - end - when "GetOutputStatus" - streamstatus = streamstatus.merge( json["d"]["responseData"].as_h ) - ircipc.send( { "##{settings["channel"]}", "| #{streamstatus["outputTimecode"].as_s[0..7]} #{streamstatus["activeFps"].as_f.to_s[0,2]}fps Usage: #{streamstatus["cpuUsage"].as_f.to_i64}% #{streamstatus["memoryUsage"].as_f.to_i64}MiB Frame losses: #{streamstatus["outputSkippedFrames"].as_i64} #{streamstatus["outputSkippedFrames"].as_i64}" } ) - when /^GetSceneItemList/ - pp json - rdata = json["d"]["responseData"] - ( match = json["d"]["requestId"].as_s.match( /^GetSceneItemList-([A-Za-z0-9-_\/]+)/ ) ) || next - # scenename => { sourcename => { property => value } - # scenes = Hash( String, Hash( String, Hash( String, Int64 | Float64 | Bool | String ) ) ).new - rdata["sceneItems"].as_a.each do | source | - - scenes[match[1]][source["sourceName"]]? || ( scenes[match[1]][source["sourceName"].as_s] = Hash( String, Int64 | Float64 | Bool | String ).new ) - source["sceneItemEnabled"].as_bool != Nil && ( scenes[match[1]][source["sourceName"].as_s]["render"] = source["sceneItemEnabled"].as_bool ) - source["sceneItemId"].as_i64? && ( scenes[match[1]][source["sourceName"].as_s]["id"] = source["sceneItemId"].as_i64 ) - source["inputKind"].as_s? && ( scenes[match[1]][source["sourceName"].as_s]["type"] = source["inputKind"].as_s ) - source["sceneItemTransform"]["positionX"].as_f? && ( scenes[match[1]][source["sourceName"].as_s]["x"] = source["sceneItemTransform"]["positionX"].as_f ) - source["sceneItemTransform"]["positionY"].as_f? && ( scenes[match[1]][source["sourceName"].as_s]["y"] = source["sceneItemTransform"]["positionY"].as_f ) - source["sceneItemTransform"]["sourceWidth"].as_f? && ( scenes[match[1]][source["sourceName"].as_s]["source_cx"] = source["sceneItemTransform"]["sourceWidth"].as_f ) - source["sceneItemTransform"]["sourceHeight"].as_f? && ( scenes[match[1]][source["sourceName"].as_s]["source_cy"] = source["sceneItemTransform"]["sourceHeight"].as_f ) - if source["sceneItemTransform"]["width"].as_i64? - scenes[match[1]][source["sourceName"].as_s]["cx"] = source["sceneItemTransform"]["cx"].as_i64 - else - scenes[match[1]][source["sourceName"].as_s]["cx"] = 0.to_i64 - end - if source["sceneItemTransform"]["height"].as_i64? - scenes[match[1]][source["sourceName"].as_s]["cy"] = source["sceneItemTransform"]["cy"].as_i64 - else - scenes[match[1]][source["sourceName"].as_s]["cy"] = 0.to_i64 - end - end - when "GetSceneList" - rdata = json["d"]["responseData"] - currentscene = rdata["currentProgramSceneName"].as_s - pp rdata["scenes"] - rdata["scenes"].as_a.each do |scene| - scenes[scene["sceneName"].as_s] = Hash( String, Hash( String, Int64 | Float64 | Bool | String ) ).new - end - if json["d"]["requestId"].as_s =~ /Quietly/ # We need to populate the scenes table once at startup - scenes.keys.each do |scene| - obs5_pubsub.send( obsrequest( "GetSceneItemList", "GetSceneItemList-#{scene}", JSON.parse( { "sceneName" => scene }.to_json ) )) - end - else # Loudly - ircipc.send( { "#" + settings["channel"], "| obs5: " + rdata["scenes"].as_a.map{ |scene| scene["sceneName"] }.join(", ") } ) - end - when "GetVideoSettings" - rdata = json["d"]["responseData"] - canvas = { rdata["baseWidth"].as_i.to_u64 , rdata["baseHeight"].as_i.to_u64 } - pp canvas - end - end - print( "5 RECEIVED: ") - print( json.to_pretty_json ) - print( "\n") - end - spawn do - # authentication - sleep 1 - obs5_pubsub.send( obsrequest( "GetVideoSettings", "GetVideoSettings" )) - obs5_pubsub.send( obsrequest( "GetStats", "GetStats" )) - sleep 1 - obs5_pubsub.send( obsrequest( "GetSceneList", "GetSceneList Quietly" )) - end - obs5_pubsub.run - rescue ex : Socket::ConnectError - # these are a bit noisy - #pp ex - obs5_pubsub && obs5_pubsub.close - rescue ex : IO::Error - pp ex - ircipc.send( { "#" + settings["channel"], "obs.cr:obs: " + ex.to_s.gsub(/\r|\n/, ' ') + ": Maybe try again in 10 seconds?" } ) - rescue ex - pp ex - pp ex.backtrace? - ircipc.send( { "#" + settings["channel"], "irc.cr:obs: " + ex.to_s.gsub(/\r|\n/, ' ') } ) - obs5_pubsub && obs5_pubsub.close - end - sleep 10 - next - end -end - -# obs-websocket-4 thread -spawn do - loop do - begin - - obs4_pubsub = HTTP::WebSocket.new( URI.parse( "ws://127.0.0.1:4444/" ), HTTP::Headers{"Cookie" => "SESSIONID=1234"} ) - # Outgoing - # state - currentscene = "unknown" - canvas = { 1280, 720 } - # scenename => { sourcename => { property => value } - scenes = Hash( String, Hash( String, Hash( String, Int64 | Float64 | Bool | String ) ) ).new - spawn do - while msg = obs4ipc.receive - pp msg - if ( match = msg.match( /^\x10source(-notransition|) ([a-z0-9-_]+)/ ) ) - transition = ( match[1] != "-notransition" ) - source = match[2] - sources = reversesource( scenes, currentscene ) - pp sources - request = Hash( String, String | Bool ).new - request["request-type"] = "SetSceneItemProperties" - request["message-id"] = "SetSceneItemProperties" - unless ( scene = sources[source]? ) - ircipc.send( { "#" + settings["channel"], "| obs4: source not found: #{source}" } ) - next - end - request["scene-name"] = scene - request["item"] = source - if ( match = msg.match( /^\x10source[-a-zA-Z0-9_]* ([a-z0-9-_]+) (true|false)/ ) ) - request["visible"] = ( match[2] == "true" ) - elsif ( match = msg.match( /^\x10source[-a-zA-Z0-9_]* ([a-z0-9-_]+)/ ) ) - pp scenes[scene][source]["render"] - request["visible"] = ( scenes[scene][source]["render"]? != true ) - # use a random similarly named source instead? - randsources = sources.keys.select!{ |x| x =~ /^#{source}/} - if ( randsources.size > 1 ) - #if Random.new.next_bool # 50/50 - if Random.new.rand(5) == 0 # 20/80 - source = randsources.sample - scene = sources[source] - request["scene-name"] = scene - request["item"] = source - end - end - end - obs4_pubsub.send( request.to_json ) - puts "x10source: currentscene is " + currentscene - if transition - request = Hash( String, String | Bool ).new - request["request-type"] = "SetPreviewScene" - request["message-id"] = "SetPreviewScene" - request["scene-name"] = currentscene - obs4_pubsub.send( request.to_json ) - sleep 0.1 # somehow these get out-of-order otherwise? >:( - request = Hash( String, String ).new - request["request-type"] = "TransitionToProgram" - request["message-id"] = "TransitionToProgram" - obs4_pubsub.send( request.to_json ) - end - elsif ( match = msg.match( /^\x10sceneitem ([a-z0-9-_]+)/ ) ) - source = match[1] - sources = reversesource( scenes, currentscene ) - pp sources - request = Hash( String, String | Bool ).new - request["request-type"] = "GetSceneItemProperties" - request["message-id"] = "GetSceneItemProperties" - unless ( scene = sources[source]? ) - ircipc.send( { "#" + settings["channel"], "| obs4: source not found: #{source}" } ) - next - end - request["scene-name"] = scene - request["item"] = source - obs4_pubsub.send( request.to_json ) - elsif ( match = msg.match( /^\x10source/ ) ) - request = Hash( String, String ){ - "request-type" => "GetCurrentScene", - "message-id" => "GetCurrentScene", - } - obs4_pubsub.send( request.to_json ) - elsif ( match = msg.match( /^\x10newsource(-temporary|) ([a-zA-Z0-9-_]+)/ ) ) - temporary = ( match[1]? ) - - sourcename = "media#{temporary}-#{match[2]}" - - request = Hash( String, String | Hash( String, String | Bool ) ){ - "request-type" => "CreateSource", - "message-id" => "CreateSource", - "sourceName" => sourcename, - "sourceKind" => "ffmpeg_source", - "sceneName" => "meta-foreground", - #"setVisible" => "false", # this somehow triggers one or more MediaEnded events? - "sourceSettings" => { - "clear_on_media_end"=> true, - "close_when_inactive"=> false, - "is_local_file"=> true, - "local_file"=> "c:/cygwin64/home/user/effects/explosions/#{match[2]}.webm", - "looping"=> false - }, - } - ircipc.send( { "##{settings["channel"]}", "Creating #{sourcename}" } ) - obs4_pubsub.send( request.to_json ) - else - obs4_pubsub.send( msg ) - end - end - end - # Incoming - obs4_pubsub.on_message do | message | - json = JSON.parse( message ) - if json["error"]? - puts json["error"] - ircipc.send( { "#" + settings["channel"], "| obs4: #{json["error"]}" } ) - next - end - case json["update-type"]? - when "StreamStatus" # these are a bit noisy, - streamstatus = json.as_h - next # so skip them - when "SwitchScenes" - ircipc.send( { "#" + settings["channel"], "| obs4: switched scene to " + ( json["scene-name"]?.as_s? || "unknown" ) } ) - puts "SwitchScenes: currentscene is " + currentscene - currentscene = json["scene-name"].as_s - puts "SwitchScenes: currentscene is " + currentscene - when "MediaEnded" - pp json - source = json["sourceName"].as_s - sources = reversesource( scenes, currentscene ) - if scene = sources[source]? - if ( scenes[scene][source]["render"] == true ) && ( source =~ /^media-/ ) && ( source !~ /media-temporary/ ) - request = Hash( String, String | Bool ){ - "request-type" => "SetSceneItemProperties", - "message-id" => "SetSceneItemProperties", - "scene-name" => scene, - "item" => source, - "visible" => false - } - ircipc.send( { "##{settings["channel"]}", "| obs4: Media ended; disabling #{source}" } ) - obs4ipc.send( request.to_json ) - end - end - if ( source =~ /^media-temporary-/ ) - request = Hash( String, String | Hash( String, String ) ){ - "scene" => "meta-foreground", - "request-type" => "DeleteSceneItem", - "message-id" => "DeleteSceneItem", - "item" => { "name" => json["sourceName"].as_s }, - } - ircipc.send( { "##{settings["channel"]}", "Deleting #{source}" } ) - obs4ipc.send( request.to_json ) - end - when "SourceFilterVisibilityChanged" - ircipc.send( { "##{settings["channel"]}", "| obs4: source #{json["sourceName"]} filter #{json["filterName"]} visibility is currently #{json["filterEnabled"]}" } ) - when "SceneItemAdded" - request = Hash( String, String ){ - "request-type" => "GetCurrentScene", - "message-id" => "GetCurrentScene Quietly", - } - obs4ipc.send( request.to_json) - request = Hash( String, String ){ - "request-type" => "GetSceneList", - "message-id" => "GetSceneList Quietly", - } - obs4ipc.send( request.to_json ) - - if json["item-name"].as_s =~ /media-temporary/ - request = Hash( String, String | Bool ).new - request["request-type"] = "GetSceneItemProperties" - request["message-id"] = "GetSceneItemProperties" - request["scene-name"] = json["scene-name"].as_s - request["item"] = json["item-name"].as_s - obs4_pubsub.send( request.to_json ) - end - #scenes[json["scene-name"].as_s][json["item-name"].as_s] - when "SourceRenamed" - sourceprev = json["previousName"].as_s - sourcenew = json["newName"].as_s - # does not tell us the scenes involved? - # Will have to iterate through them. - scenes.keys.each{ | scene | - if scenes[scene][sourceprev]? - scenes[scene][sourcenew] = scenes[scene][sourceprev] - scenes[scene].delete(sourceprev) - end - } - when "SceneItemVisibilityChanged" - print( json.to_pretty_json ) - if json["item-name"].as_s !~ /media-temporary/ - scenes[json["scene-name"].as_s][json["item-name"].as_s]["render"] = json["item-visible"].as_bool - ircipc.send( { "##{settings["channel"]}", "| obs4: source #{json["item-name"]} visibility is now #{json["item-visible"].as_bool}" } ) - end - end - print( "4 RECEIVED: ") - print( json.to_pretty_json ) - print( "\n") - case json["message-id"]?.as_s? - when "GetSceneItemProperties" - if json["name"].as_s =~ /media-temporary/ - size = { json["sourceWidth"].as_i, json["sourceHeight"].as_i } - request = Hash( String, String | Bool | Hash( String, Float64) ).new - request["request-type"] = "SetSceneItemProperties" - request["message-id"] = "SetSceneItemProperties" - request["scene-name"] = "meta-foreground" # why isn't this returned? - request["item"] = json["name"].as_s - # FIXME we're not getting real values for sourceWidth and sourceHeight until playback begins, but dropping frames would be horrible. - # We should probably just preprocess these values into explosionslist :/ - # ffprobe -show_entries stream=width,height big_explosion_1.webm - request["position"] = { - "x" => (rand(canvas[0]) - ( size[0] / 2 ) ).to_f, - "y" => (rand(canvas[1]) - ( size[1] / 2 ) ).to_f, - } - obs4ipc.send( request.to_json ) - #ircipc.send( { "##{settings["channel"]}", "| obs4: #{request.to_json}" } ) - - end - when /^GetVideoInfo/ - canvas = { json["baseWidth"].as_i64, json["baseHeight"].as_i64 } - pp canvas - when /^GetCurrentScene/ - currentscene = json["name"].as_s - puts "GetCurrentScene: currentscene is " + currentscene - if json["message-id"].as_s? !~ /Quietly/ - sources = reversesource( scenes, currentscene ) - ircipc.send( { "#" + settings["channel"], "| obs4: " + sources.keys.join(", ") } ) - end - when "GetSourceFilters" - ircipc.send( { "#" + settings["channel"], "| obs4: " + json["filters"].as_a.map{ |filter| "#{filter["name"].as_s}: #{filter["enabled"].as_bool}" }.join(", ") } ) - when /^GetSceneList/ - if json["message-id"].as_s? !~ /Quietly/ - ircipc.send( { "#" + settings["channel"], "| obs4: " + json["scenes"].as_a.map{ |scene| scene["name"] }.join(", ") } ) - end - json["scenes"].as_a.each{ |scene| - scenes[scene["name"].as_s]? || ( scenes[scene["name"].as_s] = Hash( String, Hash( String, Int64 | Float64 | Bool | String ) ).new ) - scene["sources"].as_a.each{ |source| - scenes[scene["name"].as_s][source["name"]]? || ( scenes[scene["name"].as_s][source["name"].as_s] = Hash( String, Int64 | Float64 | Bool | String ).new ) - source["render"].as_bool != Nil && ( scenes[scene["name"].as_s][source["name"].as_s]["render"] = source["render"].as_bool ) - source["type"].as_s? && ( scenes[scene["name"].as_s][source["name"].as_s]["type"] = source["type"].as_s ) - source["x"].as_f? && ( scenes[scene["name"].as_s][source["name"].as_s]["x"] = source["x"].as_f ) - source["y"].as_f? && ( scenes[scene["name"].as_s][source["name"].as_s]["y"] = source["y"].as_f ) - source["source_cx"].as_f? && ( scenes[scene["name"].as_s][source["name"].as_s]["source_cx"] = source["source_cx"].as_f ) - source["source_cy"].as_f? && ( scenes[scene["name"].as_s][source["name"].as_s]["source_cy"] = source["source_cy"].as_f ) - if source["cx"].as_i64? - scenes[scene["name"].as_s][source["name"].as_s]["cx"] = source["cx"].as_i64 - else - scenes[scene["name"].as_s][source["name"].as_s]["cx"] = 0.to_i64 - end - if source["cy"].as_i64? - scenes[scene["name"].as_s][source["name"].as_s]["cy"] = source["cy"].as_i64 - else - scenes[scene["name"].as_s][source["name"].as_s]["cy"] = 0.to_i64 - end - } - } - pp scenes - end - end - spawn do - sleep 1 - - request = Hash( String, String ){ - "request-type" => "GetCurrentScene", - "message-id" => "GetCurrentScene Quietly", - } - obs4ipc.send( request.to_json ) - request = Hash( String, String ){ - "request-type" => "GetSceneList", - "message-id" => "GetSceneList Quietly", - } - obs4ipc.send( request.to_json) - sleep 1 - request = Hash( String, String ){ - "request-type" => "GetVideoInfo", - "message-id" => "GetVideoInfo Quietly", - } - obs4ipc.send( request.to_json) - end - obs4_pubsub.run - rescue ex : Socket::ConnectError - # these are a bit noisy - #pp ex - obs4_pubsub && obs4_pubsub.close - rescue ex : IO::Error - pp ex - ircipc.send( { "#" + settings["channel"], "obs.cr:obs4: " + ex.to_s.gsub(/\r|\n/, ' ') + ": Maybe try again in 10 seconds?" } ) - rescue ex - pp ex - pp ex.backtrace? - ircipc.send( { "#" + settings["channel"], "irc.cr:obs4: " + ex.to_s.gsub(/\r|\n/, ' ') } ) - obs4_pubsub && obs4_pubsub.close - end - sleep 10 - next - end -end - # main thread: IRC loop do begin @@ -1004,182 +553,187 @@ loop do # Create a handler to process incoming messages bot.on_message do |message| spawn do - next unless ( userlogreturn = userlog( ircipc, settings, message ) ) - chatuser, uid, userdir, lastseen = userlogreturn - if ( t2sreturn = t2s( t2sipc, settings, userdir, chatuser, message.params[1] ) ) - lastvoice.insert( 0, t2sreturn ) - lastvoice = lastvoice[0..4] - end - # Have we seen this user lately? - if ( ( Time.utc.to_unix - lastseen ) >= 14400 ) - twitchipc.send( { "get_user", uid.to_u64 } ) - twitchipc.send( { "get_followers", uid.to_u64 } ) - prevnames = Array( String ).new - if ( prevnames = Dir.children( settings["statedir"] + "/uids/" + uid + "/names" ) ) && ( prevnames.size > 1 ) - prevnames.delete( chatuser ) - puts "\033[38;5;14m#{chatuser} previous names: #{prevnames.join(", ")}\033[0m" - end - # play random fanfare if available. - {% if flag?(:windows) %} - # This file hierarchy gets manually set up for now. - # Maybe someday let mods do something like: - # !fanfare add pipne https://pip.ne/sniff.mp3 - if File.exists?( userdir + "/fanfare/" ) - playaudiofile( userdir + "/fanfare/" + Dir.children( userdir + "/fanfare/" ).sample ) + next unless ( userlogreturn = userlog( ircipc, settings, message ) ) + chatuser, uid, userdir, lastseen = userlogreturn + if ( t2sreturn = t2s( t2sipc, settings, userdir, chatuser, message.params[1] ) ) + lastvoice.insert( 0, t2sreturn ) + lastvoice = lastvoice[0..4] + end + # Have we seen this user lately? + if ( ( Time.utc.to_unix - lastseen ) >= 14400 ) + twitchipc.send( { "get_user", uid.to_u64 } ) + twitchipc.send( { "get_followers", uid.to_u64 } ) + prevnames = Array( String ).new + if ( prevnames = Dir.children( settings["statedir"] + "/uids/" + uid + "/names" ) ) && ( prevnames.size > 1 ) + prevnames.delete( chatuser ) + puts "\033[38;5;14m#{chatuser} previous names: #{prevnames.join(", ")}\033[0m" end - {% else %} - t2smsg( settings, "fanfare #{uid} #{chatuser}" ) - {% end %} - 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( "##{settings["channel"]}", "| Current name substitution is \"#{File.read( userdir + "/voicesub" )}\"." ) - else - bot.message( "##{settings["channel"]}", "| Current name substitution is disabled." ) + # play random fanfare if available. + {% if flag?(:windows) %} + # This file hierarchy gets manually set up for now. + # Maybe someday let mods do something like: + # !fanfare add pipne https://pip.ne/sniff.mp3 + if File.exists?( userdir + "/fanfare/" ) + playaudiofile( userdir + "/fanfare/" + Dir.children( userdir + "/fanfare/" ).sample ) + end + {% else %} + t2smsg( settings, "fanfare #{uid} #{chatuser}" ) + {% end %} + end + 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" ) + # bungmoBoom + # message.tags["emotes"] =~ /^322820/ + bungmobooms = Array( String ).new + if ( message.tags["emotes"]? ) && + ( message.tags["emotes"] ) && + ( bungmobooms = message.tags["emotes"].not_nil!.split("/").select( /^322820[:_]/ ).join(",").split(",") ) && + ( ! bungmobooms[0].empty? ) && + ( explosions.size > 0 ) + explosions.values.sample( bungmobooms.size ).each do | explosion | + obstemporarymediacreate( obs, "meta-foreground", explosion, "C:/cygwin64/home/user/effects/explosions/#{explosion}.webm" ) end - else - if match[2]? - voicesub = match[2].downcase - if voicesub =~ /^(disabled|null|disable|none)$/ - if File.exists?( userdir + "/voicesub" ) - File.delete( userdir + "/voicesub" ) - bot.message( "##{settings["channel"]}", "| Name substitution for #{chatuser} is now disabled." ) + 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] + if ( ( cmd =~ /^(substitute|voicesub)$/ ) && ( mod || sub ) ) + case message.params[1].split( " " ).size + when 1 + if File.exists?( userdir + "/voicesub" ) + bot.message( "##{settings["channel"]}", "| Current name substitution is \"#{File.read( userdir + "/voicesub" )}\"." ) + else + bot.message( "##{settings["channel"]}", "| Current name substitution is disabled." ) + end + else + if match[2]? + voicesub = match[2].downcase + if voicesub =~ /^(disabled|null|disable|none)$/ + if File.exists?( userdir + "/voicesub" ) + File.delete( userdir + "/voicesub" ) + bot.message( "##{settings["channel"]}", "| Name substitution for #{chatuser} is now disabled." ) + else + bot.message( "##{settings["channel"]}", "| Name substitution for #{chatuser} is already disabled." ) + end else - bot.message( "##{settings["channel"]}", "| Name substitution for #{chatuser} is already disabled." ) + Dir.mkdir_p( userdir ) + File.write( userdir + "/voicesub", voicesub ) + bot.message( "##{settings["channel"]}", "| Name substitution for #{chatuser} is now \"#{File.read( userdir + "/voicesub" )}\"." ) end - else - Dir.mkdir_p( userdir ) - File.write( userdir + "/voicesub", voicesub ) - bot.message( "##{settings["channel"]}", "| Name substitution for #{chatuser} is now \"#{File.read( userdir + "/voicesub" )}\"." ) end end - end - elsif ( cmd =~ /^(commands|help)$/ ) - bot.message( "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/txt/commands.txt" ) - elsif ( cmd =~ /^(dexem)$/ ) - ircipc.send( { "##{settings["channel"]}", "| You're doing great work, Dexem!" } ) - elsif ( ( cmd == "lastvoice" ) && ( mod || sub ) ) - unless lastvoice.empty? - bot.message( "##{settings["channel"]}", "| Last voices were " + lastvoice.join( ", " ) ) - else - bot.message( "##{settings["channel"]}", "| No voices used so far." ) - end - elsif ( ( cmd == "regeneratevoicelist" ) && ( own ) ) - voices = regeneratevoicelist( settings, aws, gcloud ) - ircipc.send( { "##{settings["channel"]}", "| Regenerated voicelist." } ) - elsif ( ( cmd =~ /^(voices|voice(s|)list|listvoice(s|))$/ ) && ( mod || sub ) ) - bot.message( "##{settings["channel"]}", "| 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( "##{settings["channel"]}", "| 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( "##{settings["channel"]}", "| Voice for #{chatuser} is now random." ) + elsif ( cmd =~ /^(commands|help)$/ ) + bot.message( "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/txt/commands.txt" ) + elsif ( cmd =~ /^(dexem)$/ ) + ircipc.send( { "##{settings["channel"]}", "| You're doing great work, Dexem!" } ) + elsif ( ( cmd == "lastvoice" ) && ( mod || sub ) ) + unless lastvoice.empty? + bot.message( "##{settings["channel"]}", "| Last voices were " + lastvoice.join( ", " ) ) + else + bot.message( "##{settings["channel"]}", "| No voices used so far." ) + end + elsif ( ( cmd == "regeneratevoicelist" ) && ( own ) ) + voices = regeneratevoicelist( settings, aws, gcloud ) + ircipc.send( { "##{settings["channel"]}", "| Regenerated voicelist." } ) + elsif ( ( cmd =~ /^(voices|voice(s|)list|listvoice(s|))$/ ) && ( mod || sub ) ) + bot.message( "##{settings["channel"]}", "| 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( "##{settings["channel"]}", "| 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( "##{settings["channel"]}", "| Voice for #{chatuser} is now random." ) + else + bot.message( "##{settings["channel"]}", "| Voice for #{chatuser} is already random." ) + end + elsif voices.has_key?( voice ) + csvoice = voices[voice] + Dir.mkdir_p( userdir ) + File.write( userdir + "/voice", csvoice ) + pp userdir + bot.message( "##{settings["channel"]}", "| Voice for #{chatuser} is now #{File.read( userdir + "/voice" )}." ) else - bot.message( "##{settings["channel"]}", "| Voice for #{chatuser} is already random." ) + pp ( match ) + bot.message( "##{settings["channel"]}", "| Invalid voice. To see list, use !voices" ) end - elsif voices.has_key?( voice ) - csvoice = voices[voice] - Dir.mkdir_p( userdir ) - File.write( userdir + "/voice", csvoice ) - pp userdir - bot.message( "##{settings["channel"]}", "| Voice for #{chatuser} is now #{File.read( userdir + "/voice" )}." ) - # TODO: make separate script to print streamelements URLs to client machine + end + when 3 + else + end + elsif ( cmd =~ /^uptime$/ ) + stats = obs.stats.to_h + ostatus = obs.outputs["adv_stream"].status.to_h + ircipc.send( { "##{settings["channel"]}", "| #{ostatus["outputTimecode"].to_s[0..7]} #{stats["activeFps"].to_s[0,2]}fps Usage: #{stats["cpuUsage"].as(Float64).to_i64}% #{stats["memoryUsage"].as(Float64).to_i64}MiB Frame losses: #{stats["outputSkippedFrames"].as(Int64)} #{stats["renderSkippedFrames"].as(Int64)}" } ) + # !newimage [name] [url] (random|top|bottom|left|right|center) + # download url, verify mimetype with libmagic, duplicate existing medialoop source, change "file" SourceSetting + elsif ( ( cmd =~ /^inputsetting(|s)$/ ) && ( mod || own || vip ) ) + if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) + ircipc.send( { "##{settings["channel"]}", "| #{obs.inputs[match[2]].settings.to_h.to_s} " } ) + end + elsif ( ( cmd =~ /^input(|s)$/ ) && ( mod || own || vip ) ) + ircipc.send( { "##{settings["channel"]}", "| inputs: #{obs.inputs.to_h.keys.join(" ")}" } ) + elsif ( ( cmd =~ /^scene(|s)$/ ) && ( mod || own || vip ) ) + if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) + obs.scenes[match[2]].program! + else + ircipc.send( { "##{settings["channel"]}", "| scenes: #{obs.scenes.to_h.keys.join(" ")}" } ) + end + elsif ( ( cmd =~ /^source(|s)$/ ) && ( mod || own || vip ) ) + request = Hash( String, String | Bool ).new + if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) + obs.scenes.current.metascene[match[2]].toggle! + # in studio mode, direct Scene->SceneItem toggles require a transition + obs.scenes.current.preview! + obs.transition! + else + ircipc.send( { "##{settings["channel"]}", "| current sources: #{obs.scenes.current.metascene.keys.join(" ")}" } ) + end + elsif ( ( cmd =~ /^filter(|s)$/ ) && ( mod || own ) ) + if ( match[2]? ) && ( filterargs = match[2].match( /^([a-zA-Z0-9-_]+) +([a-zA-Z0-9-_]+)/ ) ) + obs.sources.to_h[filterargs[1]].filters[filterargs[2]].toggle! + elsif ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) + ircipc.send( { "##{settings["channel"]}", "| #{match[2]} filters: #{obs.sources.to_h[match[2]].filters.to_h.keys.join(" ")}" } ) + else + ircipc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument, and optionally one filter name to toggle on or off." } ) + end + elsif ( cmd == "create" && ( mod || own || vip ) ) + if ( match[2]? ) && ( match[2] =~ /^([a-zA-Z0-9-_]+)/ ) + obstemporarymediacreate( obs, "meta-foreground", match[2], "C:/cygwin64/home/user/effects/explosions/#{match[2]}.webm" ) + else + ircipc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument." } ) + end + elsif ( cmd =~ /^(metaminute|youdied)$/ ) + obs.scenes.current.metascene["media-#{cmd}"].enable! + elsif ( cmd =~ /^explosion$/ ) && ( sub || mod || own || vip ) + if explosions + if ( match[2]? ) && ( match[2].match( /^([0-9]+)/ ) && ( match[2].to_i > 1 ) && ( match[2].to_i <= 32 ) ) + count = match[2].to_i else - pp ( match ) - bot.message( "##{settings["channel"]}", "| Invalid voice. To see list, use !voices" ) + count = 1 + end + explosions.values.sample( count ).each do | explosion | + obstemporarymediacreate( obs, "meta-foreground", explosion, "C:/cygwin64/home/user/effects/explosions/#{explosion}.webm" ) end + else + puts "explosions undefined" end - when 3 - else - end - elsif ( cmd =~ /^uptime$/ ) - obs5ipc.send( obsrequest( "GetStats", "GetStats" )) - # !newimage [name] [url] (random|top|bottom|left|right|center) - # download url, verify mimetype with libmagic, duplicate existing medialoop source, change "file" SourceSetting - elsif ( ( cmd =~ /^sourcesettings$/ ) && ( mod || own || vip ) ) - if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) - - obs5ipc.send( obsrequest( "GetInputSettings", "GetInputSettings", JSON.parse( { "inputName" => match[2] }.to_json ) )) - end - elsif ( ( cmd =~ /^inputlist$/ ) && ( mod || own || vip ) ) - obs5ipc.send( obsrequest( "GetInputList", "GetInputList" ) ) - elsif ( ( cmd =~ /^scene$/ ) && ( mod || own || vip ) ) - if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) - obs5ipc.send( obsrequest( "SetCurrentProgramScene", "SetCurrentProgramScene", JSON.parse( { "sceneName" => match[2] }.to_json ) )) - else - obs5ipc.send( obsrequest( "GetSceneList", "GetSceneList" ) ) - end - elsif ( ( cmd =~ /^source$/ ) && ( mod || own || vip ) ) - request = Hash( String, String | Bool ).new - if ( match[2]? ) && ( sourceargs = match[2].match( /^([a-zA-Z0-9-_]+) +(true|false)$/ ) ) - obs5ipc.send( "\x10source #{match[2]}" ) - elsif ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) - obs5ipc.send( "\x10source #{match[2]}" ) - else - obs5ipc.send( "\x10source" ) - 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 ) - obs4ipc.send( request.to_json ) - elsif ( match[2]? ) && ( filterargs = match[2].match( /^([a-zA-Z0-9-_]+) +([a-zA-Z0-9-_]+)/ ) ) - ircipc.send( { "##{settings["channel"]}", "Must provide provide a value of true or false as the third argument." } ) - elsif ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) - request["sourceName"] = match[2] - request["message-id"] = "GetSourceFilters" - obs4ipc.send( request.to_json ) - else - ircipc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument, and optionally one filter name and a value of true or false." } ) - end - elsif ( cmd == "create" && ( mod || own || vip ) ) - if ( match[2]? ) && ( match[2] =~ /^([a-zA-Z0-9-_]+)/ ) - obs4ipc.send( "\x10newsource-temporary #{match[2]}" ) - else - ircipc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument." } ) - end - elsif ( cmd =~ /^(metaminute|youdied)$/ ) - obs4ipc.send( "\x10source-notransition media-#{cmd}" ) - elsif ( cmd =~ /^explosion$/ ) - if explosions - explosion = explosions.sample( 1 )[0][1] - puts explosion - obs4ipc.send( "\x10newsource-temporary #{explosion}" ) - else - puts "explosions undefined" - end - # FIXME: This is only half-implemented - elsif ( ( cmd =~ /^(user)$/ ) && ( mod || own ) ) - if match[2]? && match[2] =~ /[0-9]+/ - twitchipc.send( { "get_user", match[2].to_u64 } ) - else - twitchipc.send( { "get_user", settings["channel_id"].to_u64 } ) - end - elsif ( ( cmd =~ /^(status|title)$/ ) && ( mod || own ) ) - begin + # FIXME: This is only half-implemented + elsif ( ( cmd =~ /^(user)$/ ) && ( mod || own ) ) + if match[2]? && match[2] =~ /[a-z][a-z0-9_]+/ + twitchipc.send( { "get_user", match[2] } ) + elsif match[2]? && match[2] =~ /[0-9]+/ + twitchipc.send( { "get_user", match[2].to_u64 } ) + else + twitchipc.send( { "get_user", settings["channel_id"].to_u64 } ) + end + elsif ( ( cmd =~ /^(status|title)$/ ) && ( mod || own ) ) 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 ) ) @@ -1188,11 +742,7 @@ loop do json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) ) ircipc.send( { "##{settings["channel"]}", "| Title is currently \"#{ json["data"][0]["title"] }\""} ) end - rescue ex - ircipc.send( { "##{settings["channel"]}", "| An error occurred! " + ex.message.to_s } ) - end - elsif ( ( cmd =~ /^(game|category)$/ ) && ( mod || own ) ) - begin + elsif ( ( cmd =~ /^(game|category)$/ ) && ( mod || own ) ) if match[2]? puts "2 matches" client.put_channel!( settings["channel_id"].to_u64, game: match[2] ) @@ -1203,140 +753,138 @@ loop do json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) ) ircipc.send( { "##{settings["channel"]}", "| Game is currently \"#{ json["data"][0]["game_name"] }\""} ) end - rescue ex - ircipc.send( { "##{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] ) - ircipc.send( { "##{settings["channel"]}", definition[0,400] } ) - else - ircipc.send( { "##{settings["channel"]}", "| Urban Dictionary search term should consist of letters, numbers, spaces, and/or hyphens." } ) - end - elsif ( cmd == "matrix" ) - effectsmsg( settings, "overlay glmatrix" ) - elsif ( cmd =~ /juggle|juggler/ ) - effectsmsg( settings, "overlay juggler3d" ) - elsif ( cmd =~ /fireworks|firework/ ) - effectsmsg( settings, "overlay fireworkx" ) - elsif ( cmd == "pipes" ) - if match[2]? && match[2] =~ /^(fast|faster)$/ - effectsmsg( settings, "overlay pipes " + match[2] ) - else - effectsmsg( settings, "overlay pipes" ) - end - elsif ( cmd == "jellyfish" ) - effectsmsg( settings, "overlay hydrostat" ) - elsif ( cmd == "gluten" ) - effectsmsg( settings, "overlay flyingtoasters" ) - elsif ( cmd =~ /^(glsnake|glmatrix|gibson|xmatrix|flyingtoasters|moebiusgears|fireworkx|hydrostat|hypertorus|jigsaw|juggler3d|kaleidocycle|kumppa|molecule|noof|polyhedra)$/ ) - effectsmsg( settings, "overlay " + cmd ) - elsif ( cmd =~ /gltext|cowsay|xcowsay|cowfuscious/ ) - ( cmd == "cowfuscious" ) && ( cmd = "xcowsay" ) - if ( match[2]? ) && ( gltextargs = match[2].match( /^([0-9]+) +(.+)$/ ) ) - seconds=UInt64.new( 1 ) - seconds=gltextargs[1].to_u64 - if ( own || mod || vip ) + elsif ( ( cmd == "urban" ) && ( mod || own || sub || vip ) ) + if match[2]? && match[2] =~ /^([a-zA-Z0-9 -])+$/ + definition = urbandef( match[2] ) + ircipc.send( { "##{settings["channel"]}", definition[0,400] } ) + # FIXME: maybe somehow fold this into the existing voice logic + t2s( t2sipc, settings, userdir, chatuser, definition[0,400] ) + else + ircipc.send( { "##{settings["channel"]}", "| Urban Dictionary search term should consist of letters, numbers, spaces, and/or hyphens." } ) + end + elsif ( cmd == "matrix" ) + effectsmsg( settings, "overlay glmatrix" ) + elsif ( cmd =~ /juggle|juggler/ ) + effectsmsg( settings, "overlay juggler3d" ) + elsif ( cmd =~ /fireworks|firework/ ) + effectsmsg( settings, "overlay fireworkx" ) + elsif ( cmd == "pipes" ) + if match[2]? && match[2] =~ /^(fast|faster)$/ + effectsmsg( settings, "overlay pipes " + match[2] ) + else + effectsmsg( settings, "overlay pipes" ) + end + elsif ( cmd == "jellyfish" ) + effectsmsg( settings, "overlay hydrostat" ) + elsif ( cmd == "gluten" ) + effectsmsg( settings, "overlay flyingtoasters" ) + elsif ( cmd =~ /^(glsnake|glmatrix|gibson|xmatrix|flyingtoasters|moebiusgears|fireworkx|hydrostat|hypertorus|jigsaw|juggler3d|kaleidocycle|kumppa|molecule|noof|polyhedra)$/ ) + effectsmsg( settings, "overlay " + cmd ) + elsif ( cmd =~ /gltext|cowsay|xcowsay|cowfuscious/ ) + ( cmd == "cowfuscious" ) && ( cmd = "xcowsay" ) + if ( match[2]? ) && ( gltextargs = match[2].match( /^([0-9]+) +(.+)$/ ) ) + seconds=UInt64.new( 1 ) + seconds=gltextargs[1].to_u64 + if ( own || mod || vip ) + effectsmsg( settings, "overlay #{cmd} #{match[2]}" ) + puts "matched #{cmd} #{match[2]}" + elsif ( sub ) + if ( seconds > 20 ) + seconds=20 + end + effectsmsg( settings, "overlay #{cmd} #{seconds} #{gltextargs[2]}" ) + puts "matched #{cmd} #{seconds} #{gltextargs[2]}" + else + if ( seconds > 5 ) + seconds=5 + end + effectsmsg( settings, "overlay #{cmd} #{seconds} #{gltextargs[2]}" ) + puts "matched #{cmd} #{seconds} #{gltextargs[2]}" + end + elsif match[2]? && match[2] =~ /^.+$/ effectsmsg( settings, "overlay #{cmd} #{match[2]}" ) puts "matched #{cmd} #{match[2]}" - elsif ( sub ) - if ( seconds > 20 ) - seconds=20 - end - effectsmsg( settings, "overlay #{cmd} #{seconds} #{gltextargs[2]}" ) - puts "matched #{cmd} #{seconds} #{gltextargs[2]}" else - if ( seconds > 5 ) - seconds=5 + effectsmsg( settings, "overlay #{cmd}" ) + puts "failed to match gltext" + end + elsif ( cmd =~ /^(cow(s|)|cowabunga|holycow$)/ ) + if match[2]? && match[2] =~ /^([0-9])+$/ + effectsmsg( settings, "overlay bouncingcow #{match[2]}" ) + else + effectsmsg( settings, "overlay bouncingcow" ) + end + elsif ( cmd == "overlay" ) + if match[2]? && match[2] =~ /^[a-z]+$/ + ircipc.send( { "##{settings["channel"]}", "| overlay requires an argument consisting wholly of lower case characters."} ) + effectsmsg( settings, "overlay #{match[2]}" ) + end + elsif ( cmd =~ /^(shout|shoutout)$/ ) + if match[2]? && match[2] =~ /^[a-z]+$/ + ircipc.send( { "##{settings["channel"]}", "| Go check out twitch.tv/#{match[2]}"} ) + effectsmsg( settings, "overlay gltext 5 Go follow #{match[2]}" ) + else + ircipc.send( { "##{settings["channel"]}", "| Missing argument."} ) + end + elsif ( cmd =~ /^(system|dxdiag|computer)$/ ) && ( Crystal::DESCRIPTION =~ /linux/ ) + ircipc.send( { "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/tmp/DxDiag.txt" } ) + elsif ( cmd == "hackerman" ) + ircipc.send( { "##{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=/ ) ) + ircipc.send( { "##{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 - effectsmsg( settings, "overlay #{cmd} #{seconds} #{gltextargs[2]}" ) - puts "matched #{cmd} #{seconds} #{gltextargs[2]}" + if ( status = m.status ) && ( playlistinfo = m.playlistinfo ) + ircipc.send( { "##{settings["channel"]}", "| " + playlistinfo[ status["playlistlength"].to_i - 1 ]["file"].to_s + " added in position " + status["playlistlength"].to_s } ) + else + ircipc.send( { "##{settings["channel"]}", "| A failure occured." } ) + end + m.disconnect + else end - elsif match[2]? && match[2] =~ /^.+$/ - effectsmsg( settings, "overlay #{cmd} #{match[2]}" ) - puts "matched #{cmd} #{match[2]}" - else - effectsmsg( settings, "overlay #{cmd}" ) - puts "failed to match gltext" - end - elsif ( cmd =~ /^(cow(s|)|cowabunga|holycow$)/ ) - if match[2]? && match[2] =~ /^([0-9])+$/ - effectsmsg( settings, "overlay bouncingcow #{match[2]}" ) - else - effectsmsg( settings, "overlay bouncingcow" ) - end - elsif ( cmd == "overlay" ) - if match[2]? && match[2] =~ /^[a-z]+$/ - ircipc.send( { "##{settings["channel"]}", "| overlay requires an argument consisting wholly of lower case characters."} ) - effectsmsg( settings, "overlay #{match[2]}" ) - end - elsif ( cmd =~ /^(shout|shoutout)$/ ) - if match[2]? && match[2] =~ /^[a-z]+$/ - ircipc.send( { "##{settings["channel"]}", "| Go check out twitch.tv/#{match[2]}"} ) - effectsmsg( settings, "overlay gltext 5 Go follow #{match[2]}" ) - else - ircipc.send( { "##{settings["channel"]}", "| Missing argument."} ) - end - elsif ( cmd =~ /^(system|dxdiag|computer)$/ ) && ( Crystal::DESCRIPTION =~ /linux/ ) - ircipc.send( { "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/tmp/DxDiag.txt" } ) - elsif ( cmd == "hackerman" ) - ircipc.send( { "##{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=/ ) ) - ircipc.send( { "##{settings["channel"]}", "| Lists are not accepted.\n" } ) - elsif Process.run( "sraddsong.sh", {match[2]} ) + elsif ( cmd =~ /^current(|song)$/ ) 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 ) - ircipc.send( { "##{settings["channel"]}", "| " + playlistinfo[ status["playlistlength"].to_i - 1 ]["file"].to_s + " added in position " + status["playlistlength"].to_s } ) + if ( currentsong = m.currentsong ) + ircipc.send( { "##{settings["channel"]}", "| Currently playing: " + currentsong["file"].to_s } ) else ircipc.send( { "##{settings["channel"]}", "| A failure occured." } ) end m.disconnect - else - end - elsif ( cmd =~ /^current(|song)$/ ) - m = MPD::Client.new - if ( currentsong = m.currentsong ) - ircipc.send( { "##{settings["channel"]}", "| Currently playing: " + currentsong["file"].to_s } ) - else - ircipc.send( { "##{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 - ircipc.send( { "##{settings["channel"]}", "| " + sprintf( "2%d:2%d", min, sec ) } ) + 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 + ircipc.send( { "##{settings["channel"]}", "| " + sprintf( "2%d:2%d", min, sec ) } ) + else + ircipc.send( { "##{settings["channel"]}", "| An error occurred. " } ) + end + m.disconnect else - ircipc.send( { "##{settings["channel"]}", "| An error occurred. " } ) + ircipc.send( { "##{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 - m.disconnect - else - ircipc.send( { "##{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 ) - ircipc.send( { "##{settings["channel"]}", "| Currently playing: " + currentsong["file"].to_s } ) + elsif ( cmd =~ /^next(|song)$/ ) + m = MPD::Client.new + m.next + if ( status = m.status ) && ( status["playlistlength"].to_i > 0 ) + if ( currentsong = m.nextsong ) + ircipc.send( { "##{settings["channel"]}", "| Currently playing: " + currentsong["file"].to_s } ) + else + ircipc.send( { "##{settings["channel"]}", "| A failure occured." } ) + end else - ircipc.send( { "##{settings["channel"]}", "| A failure occured." } ) + ircipc.send( { "##{settings["channel"]}", "| Playlist is now empty." } ) end - else - ircipc.send( { "##{settings["channel"]}", "| Playlist is now empty." } ) - end - m.disconnect - elsif ( cmd == "followage" ) - begin + m.disconnect + elsif ( cmd == "followage" ) if match[2]? args = match[2].split(/\s/) if args[1]? @@ -1356,10 +904,10 @@ loop do puts json ircipc.send( { "##{settings["channel"]}", "| " + json["data"][0]["followed_at"].to_s } ) end - rescue ex - ircipc.send( { "##{settings["channel"]}", "An error occurred! " + ex.message.to_s } ) end - end + rescue ex + puts ex + ircipc.send( { "##{settings["channel"]}", "An error occurred! " + ex.message.to_s } ) end end -- cgit v1.2.3