From f51cf03b65c84b67b1a5d6927899c30e7c563e1b Mon Sep 17 00:00:00 2001 From: Joe Rayhawk Date: Thu, 7 Jul 2022 20:18:55 -0700 Subject: crystal/irc: rename to bungmobott.cr --- crystal/bungmobott.cr | 950 ++++++++++++++++++++++++++++++++++++++++++++++++++ crystal/irc.cr | 950 -------------------------------------------------- 2 files changed, 950 insertions(+), 950 deletions(-) create mode 100755 crystal/bungmobott.cr delete mode 100755 crystal/irc.cr diff --git a/crystal/bungmobott.cr b/crystal/bungmobott.cr new file mode 100755 index 0000000..3b592e9 --- /dev/null +++ b/crystal/bungmobott.cr @@ -0,0 +1,950 @@ +require "twitch/irc" +require "http" +require "uri" +require "twitcr" +require "json" +require "crystal_mpd" + +STDOUT.sync = true + +struct Nil + def as_s? + self + end +end + +settings = Hash(String, String).new + +settings["configdir"] = Path.home./("/.config/bungmobott/").to_s +settings["statedir"] = Path.home./("/.local/state/bungmobott/").to_s + +if ENV["LOCALAPPDATA"]? + settings["configdir"] = Path.windows( ENV["LOCALAPPDATA"] + "\\bungmobott\\" ).to_s + settings["statedir"] = Path.windows( ENV["LOCALAPPDATA"] + "\\bungmobott\\state\\" ).to_s +end + +settings["tempdir"] = "/tmp/bungmobott/" +ENV["TEMP"]? && ( settings["tempdir"] = "#{ENV["TEMP"]}\\bungmobott\\" ) + +ENV["XDG_CONFIG_HOME"]? && ( settings["configdir"] = ENV["XDG_CONFIG_HOME"] + "/bungmobott/" ) +#ENV["XDG_DATA_HOME"]? && ( settings["datadir"] = ENV["XDG_DATA_HOME"] ) # unused? +ENV["XDG_STATE_HOME"]? && ( settings["statedir"] = ENV["XDG_STATE_HOME"] + "/bungmobott/" ) + +Dir.mkdir_p( settings["configdir"] ) +Dir.mkdir_p( settings["statedir"] ) +Dir.mkdir_p( settings["tempdir"] ) + +settings["home"] = Path.home.to_s +regextwitchuser = /^[0-9a-zA-Z_]+$/ + +error = false +[ "access_token", "client_id" ].each do |key| + begin + settings[key] = File.read( settings["configdir"] + key ).chomp + rescue IO::Error + STDERR.puts "ERROR: Missing " + settings["configdir"] + key + error = true + end +end + +# enable gcloud? +if File.exists?( settings["configdir"] + "gcloud_token" ) && ( settings["gcloud_token"] = File.read( settings["configdir"] + "gcloud_token" ).chomp ) + gcloud = true +else + gcloud = false + STDERR.puts "Warning: #{settings["configdir"]}gcloud_token is missing; GCS voices disabled." +end + +# enable aws? +if ! File.exists?( ENV["USERPROFILE"] + "\\.aws\\credentials" ) + STDERR.puts "Warning: #{ENV["USERPROFILE"]}\\.aws\\credentials is missing; AWS voices disabled." + aws = false +elsif ! Process.find_executable( "aws.exe" ) + STDERR.puts "Warning: aws.exe is missing; AWS voices disabled." + aws = false +else + aws = true +end + +client = Twitcr::Client.new( settings ) + +# derive channel_id from channel or vice versa +if ( + File.exists?( settings["configdir"] + "channel" ) && + ( settings["channel"] = File.read( settings["configdir"] + "channel" ).chomp ) + ) || ( + File.exists?( settings["configdir"] + "channel_id" ) && + ( settings["channel_id"] = File.read( settings["configdir"] + "channel_id" ).chomp ) + ) + if ! ( settings["channel"]? =~ regextwitchuser ) && settings["channel_id"]? =~ /^[0-9]+$/ + settings["channel"] = client.user( settings["channel_id"].to_u64 ).login + elsif ( settings["channel_id"]? =~ regextwitchuser ) && ! settings["channel_id"]? =~ /^[0-9]+$/ + settings["channel_id"] = client.user( settings["channel"] ).id.to_s + end +else + STDERR.puts "ERROR: Missing #{settings["configdir"]}channel and channel_id configuration keys." + error = true + # exit 2 +end +if error == true + {% if flag?(:windows) %} + puts "press enter to end program" + gets + {% end %} + exit 1 +end + +if File.exists?( settings["configdir"] + "chatuser" ) + File.read( settings["configdir"] + "chat_user" ).chomp +else + STDERR.puts "Missing " + settings["configdir"] + "chat_user; using configured channel instead." + settings["chat_user"] = settings["channel"] +end + +obsipc = Channel( String ).new +ircipc = Channel( Tuple( String, String ) ).new +t2sipc = Channel( Tuple( String, String ) ).new + +snifflast = Int64.new(0) + +def urbandef( term : String ) + #http://api.urbandictionary.com/v0/define?term=waifu + client = HTTP::Client.new( "api.urbandictionary.com" ) + response = client.exec( "GET", "/v0/define?term=" + URI.encode_www_form( term ) ) + puts response.status_code + json = JSON.parse( response.body ) + return json["list"][0]["definition"].to_s.gsub( /[\[\]]/, "" ).gsub( /\n/, "" ) +end + +# Currently only used in flags?(:unix) +def t2smsg( settings : Hash(String, String), msg : String) + if File.exists?( settings["statedir"] + "/.t2s.sock" ) + sock = Socket.unix + sock.connect Socket::UNIXAddress.new( settings["statedir"] + "/.t2s.sock" ) + sock.puts( msg ) + sock.close + end +rescue ex + puts ex +end + +def effectsmsg( settings : Hash(String, String), msg : String ) + if File.exists?( settings["statedir"] + "/.effects.sock" ) + sock = Socket.unix + sock.connect Socket::UNIXAddress.new( settings["statedir"] + "/.effects.sock" ) + sock.puts( msg ) + sock.close + end +rescue ex + puts ex +end + +def userlog( ircipc : Channel, settings : Hash(String, String), message : FastIRC::Message ) + unless ( ( prefix = message.prefix ) && ( chatuser = prefix.source ) && ( uid = message.tags["user-id"]? ) ) + return nil + end + basedir = settings["statedir"] + userdir = basedir + "/uids/" + uid + Dir.mkdir_p( basedir + "/names" ) + Dir.mkdir_p( userdir + "/names" ) + unless testrefuser2uid( userdir + "/names/" + chatuser ) + namelatest = "" + datelatest = Time::UNIX_EPOCH + Dir.each_child( userdir + "/names/" ) do |name| + namedate = File.info( userdir + "/names/" + name ).modification_time + if namedate > datelatest + namelatest = name + datelatest = namedate + end + end + unless namelatest.empty? + ircipc.send( { "##{settings["channel"]}", "Rename detected: #{uid}: #{namelatest} -> #{chatuser}" } ) + end + genrefuser2uid( userdir + "/names/" + chatuser, uid, 3 ) + end + unless testrefuser2uid( basedir + "/names/" + chatuser ) + genrefuser2uid( basedir + "/names/" + chatuser, uid, 1 ) + end + unless ( message.params[0] == "##{settings["channel"]}" ) + return nil + end + return ( [ chatuser, uid, userdir ] ) +rescue ex + puts ex +end + +def getvoice( settings : Hash(String, String), userdir : String, chatuser : String ) + if File.exists?( userdir + "/voice" ) + voice_output = File.read( userdir + "/voice" ).chomp + voice_setting = voice_output + else + voice_output = File.read( settings["configdir"] + "/voicelist.txt" ).chomp.split( "\n" ).sample( 1 )[0].chomp + voice_setting = "random" + end + if File.exists?( userdir + "/voicesub" ) + namesub = File.read( userdir + "/voicesub" ).chomp + else + namesub = chatuser + end + return( [namesub, voice_setting, voice_output ] ) +end + +# text2speech +def t2s( t2sipc : Channel, settings : Hash(String, String), userdir : String, chatuser : String, text : String ) + if ( text !~ /^ *(!|\|)/ ) + namesub, voice_setting, voice = getvoice( settings, userdir, chatuser ) + subs = Array( Tuple( Regex, String ) ){ + { /http(s|):\/\/([a-z0-9.-]+)\/[a-zA-Z0-9\/&=%-_]+/, "link to \\2" }, + { /([^a-zA-Z0-9])-/, "\\1 dash "}, + { /\|/, " vertical bar "}, + { /\`/, " grave accent "}, + { /\+/, " plus "}, + { /×/, " multiplied by "}, + { /=/, " equals "}, + { /\//, " slash "}, + { /\\/, " backslash "}, + { /@/, " at "}, + { /&/, " and "}, + { />/, " greater than "}, + { / { +# "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["type"] == "scene" + scenes[source.to_s].each do | metasource, metasourcedata | + sourcemap[metasource.to_s] = source.to_s + end + end + end + return sourcemap +end + +def playmp3file( filepath : String ) + p = Process.new( + "powershell.exe", + [ "-Command", "#Set-PSDebug -Trace 1; + Add-Type -AssemblyName presentationCore; + $player = New-Object system.windows.media.mediaplayer; + $player.open(\"#{filepath}\"); + $player.volume = .99; + $player.play(); + Start-Sleep -Milliseconds 1000; + $duration = $player.NaturalDuration.TimeSpan.TotalMilliseconds; + Start-Sleep -Milliseconds ($duration - 1000 ); + "], + output: STDOUT, error: STDERR + ) + # https://geekeefy.wordpress.com/2016/07/19/powershellmediaplayer/ has some ideas + p.wait +end + +macro testrefuser2uid( path ) + {% if flag?(:windows) %} + File.exists?( {{path}} ) && ( File.read( {{ path }} ) =~ /^[0-9]+$/ ) + {% elsif flag?(:unix) %} + File.symlink?( {{path}} ) + {% end %} +end + +macro genrefuser2uid( path, uid, depth ) + {% if flag?(:windows) %} + File.write( {{path}}, {{uid}}.to_s ) + {% elsif flag?(:unix) %} + pp "unix" + File.symlink( "../"*{{depth}} + "uids/#{{{uid}}}", {{path}} ) + {% end %} +end + +macro temporaryduplicate( item ) + request = Hash( String, String | Bool ){ + "request-type" => "SetSceneItemProperties", + "message-id" => "SetSceneItemProperties", + "scene-name" => "meta", + "item" => {{item}}, + "visible" => true, + } + obsipc.send( request.to_json ) + request = Hash( String, String | Hash( String, String ) ){ + "request-type" => "DuplicateSceneItem", + "message-id" => "DuplicateSceneItem", + "item" => { "name" => {{item}} }, + "fromScene" => "meta" + } + ircipc.send( { "##{settings["channel"]}", "Duplicating source #{match[2]}" } ) + obsipc.send( request.to_json ) +end + +voices = Hash(String, String).new +File.each_line( settings["configdir"] + "/voicelist.txt" ) do |line| + voices[ line.downcase ] = line +end + +lastvoice = Array(String).new + +# Put tts stuff into the same thread so each playback blocks the next +spawn do + loop do + begin + while t2stuple = t2sipc.receive + voice, text = [ *t2stuple ] + if ( match = voice.match( /^Microsoft-([A-Za-z]+)/ ) ) + if ( match[1] =~ /Lili|Mary|Mike|Sam|Anna/ ) + msttsvoice="Microsoft #{match[1]}" + else + msttsvoice="Microsoft #{match[1]} Desktop" + end + p = Process.new( + "powershell.exe", + [ "-Command", " + Add-Type -AssemblyName System.Speech; + $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; + $speak.SelectVoice(\"#{msttsvoice}\"); + $speak.Speak($Input); + $speak.Finalize; + "], + input: Process::Redirect::Pipe, output: STDOUT + ) + p.input.puts text + p.input.close + p.wait + elsif gcloud && ( match = voice.match( /^([a-zA-Z]{2,3}-[a-zA-Z]{2})/ ) ) # Google cloud voice + request = Hash( String, Hash( String, String ) ){ + "input" => { "text" => text }, + "audioConfig" => { "audioEncoding" => "MP3" }, + "voice" => { + "name" => voice, + "languageCode" => match[1], + }, + } + body = request.to_json + ssl_context = OpenSSL::SSL::Context::Client.new + ssl_context.verify_mode = OpenSSL::SSL::VerifyMode::NONE + + headers = HTTP::Headers.new + headers["Content-Type"] = "application/json; charset=utf-8" + + response = HTTP::Client.exec( "POST", "https://texttospeech.googleapis.com/v1/text:synthesize?key=#{settings["gcloud_token"]}", headers, body, tls: ssl_context ) + + response.body + + filepath="#{settings["tempdir"]}#{Time.utc.to_unix}.mp3" + json=JSON.parse(response.body) + File.write( filepath, Base64.decode_string( json["audioContent"].as_s ) ) + playmp3file( filepath ) + File.delete( filepath ) + elsif aws # AWS polly voices + filepath="#{settings["tempdir"]}#{Time.utc.to_unix}.mp3" + p = Process.new( + "aws.exe", [ + "polly", "synthesize-speech", + "--output-format", "mp3", + "--voice-id", voice, + "--text", text, + filepath + ], output: STDOUT, error: STDERR + ) + p.wait + playmp3file( filepath ) + File.delete( filepath ) + else # unknown + STDERR.puts "Voice not recognized or available." + end + end + rescue ex + puts ex + end + end +end + +# obs-websocket thread +spawn do + loop do + begin + + obs_pubsub = HTTP::WebSocket.new( URI.parse( "ws://127.0.0.1:4444/" ), HTTP::Headers{"Cookie" => "SESSIONID=1234"} ) + # Outgoing + # state + currentscene = "unknown" + # scenename => { sourcename => { property => value } + scenes = Hash( String, Hash( String, Hash( String, Int64 | Float64 | Bool | String ) ) ).new + spawn do + while msg = obsipc.receive + pp msg + if ( match = msg.match( /^\x10source-toggle ([a-z0-9-_]+)/ ) ) + source = match[1] + sources = reversesource( scenes, currentscene ) + pp sources + request = Hash( String, String | Bool ).new + request["request-type"] = "SetSceneItemProperties" + request["message-id"] = "SetSceneItemProperties" + #request["request-type"] = "GetSceneItemProperties" + request["message-id"] = "toggle-source" + scene = sources[source] + request["scene-name"] = sources[source] + request["item"] = source + if scenes[scene][source]["render"] == true + request["visible"] = false + else + request["visible"] = true + end + obs_pubsub.send( request.to_json ) + elsif ( match = msg.match( /^\x10source ([a-z0-9-_]+)/ ) ) + # FIXME + else + obs_pubsub.send( msg ) + end + 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" ) } ) + currentscene = json["scene-name"].as_s + puts "currentscene is " + currentscene + 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 ) + #"sourceName": "media-youdied" + puts "currentscene is " + currentscene + if ( ( json["sourceName"].as_s =~ /^media-/ ) && ( currentscene != "meta" ) ) + request = Hash( String, String | Hash( String, String ) ){ + "request-type" => "DeleteSceneItem", + "message-id" => "DeleteSceneItem", + "item" => { "name" => json["sourceName"].as_s }, + } + ircipc.send( { "##{settings["channel"]}", "Deleting #{json["sourceName"].as_s}" } ) + obsipc.send( request.to_json ) + end + when "SourceFilterVisibilityChanged" + ircipc.send( { "##{settings["channel"]}", "| obs: source #{json["sourceName"]} filter #{json["filterName"]} visibility is now #{json["filterEnabled"]}" } ) + when "SceneItemVisibilityChanged" + ircipc.send( { "##{settings["channel"]}", "| obs: source #{json["item-name"]} visibility is now #{json["item-visible"].as_bool}" } ) + end + print( "RECEIVED: ") + print( json.to_pretty_json ) + print( "\n") + case json["message-id"]? + when "GetCurrentScene" + currentscene = json["name"].as_s + 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(", ") } ) + 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["cx"].as_i64? && ( scenes[scene["name"].as_s][source["name"].as_s]["cx"] = source["cx"].as_i64 ) + source["cy"].as_i64? && ( scenes[scene["name"].as_s][source["name"].as_s]["cy"] = source["cy"].as_i64 ) + 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 ) + } + } + pp scenes + 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 + request = Hash( String, String ){ + "request-type" => "GetCurrentScene", + "message-id" => "GetCurrentScene", + } + obsipc.send( request.to_json ) + + obs_pubsub.run + rescue ex : Socket::ConnectError + # these are a bit noisy + #pp ex + obs_pubsub && obs_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/, ' ') } ) + obs_pubsub && obs_pubsub.close + end + sleep 10 + next + end +end + +# main thread: IRC +loop do + begin + + bot = Twitch::IRC::Client.new( nick: settings["chat_user"], token: "oauth:" + settings["access_token"], log_mode: true ) + bot.tags = [ "membership", "tags", "commands" ] + + # Outgoing IRC message thread + # "Most IRC servers limit messages to 512 bytes in length, including the trailing CR-LF characters." + # PRIVMSG #channel message\r\n + spawn do + while msgtuple = ircipc.receive # why does this need to be a tuple? + sizelimit=( 512 - ( msgtuple[0].size + 11 ) ) + bot.message( msgtuple[0], msgtuple[1][0..sizelimit] ) # limit size + end + end + + # Create a handler to process incoming messages + bot.on_message do |message| + spawn do + next unless ( userlogreturn = userlog( ircipc, settings, message ) ) + chatuser, uid, userdir = userlogreturn + if ( chatuser == "pipne" ) && ( ( Time.utc.to_unix - snifflast ) >= 14400 ) + snifflast = Time.utc.to_unix + t2smsg( settings, chatuser + " sniff null" ) + end + if ( t2sreturn = t2s( t2sipc, 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" ) + 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 + 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 =~ /^(voices|voicelist)$/ ) && ( 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" )}." ) + # TODO: make separate script to print streamelements URLs to client machine + else + pp ( match ) + bot.message( "##{settings["channel"]}", "| 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( "\x10source #{match[2]}" ) + elsif ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) + #sources = reversesource( scenes, currentscene ) + #request["request-type"] = "SetSceneItemProperties" + #request["message-id"] = "SetSceneItemProperties" + #request["request-type"] = "GetSceneItemProperties" + #request["message-id"] = "toggle-source" + #request["item"] = match[2] + #obsipc.send( request.to_json ) + obsipc.send( "\x10source-toggle #{match[2]}" ) + else +# request["request-type"] = "GetCurrentScene" +# request["message-id"] = "GetCurrentScene" +# obsipc.send( request.to_json ) + obsipc.send( "\x10source #{match[2]}" ) + 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 == "duplicate" && ( mod || own || vip ) ) + if ( match[2]? ) && ( filterargs = match[2].match( /^([a-zA-Z0-9-_]+)/ ) ) + temporaryduplicate( match[2] ) + else + ircipc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument." } ) + 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 ) ) + ircipc.send( { "##{settings["channel"]}", "Title is now \"#{ json["data"][0]["title"] }\""} ) + else + 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 + 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 ) ) + ircipc.send( { "##{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 ) ) + 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 ) + 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]}" + 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]}" ) + else + end + {% if flag?(:unix) %} + elsif ( cmd =~ /^(system|dxdiag|computer)$/ ) + ircipc.send( { "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/tmp/DxDiag.txt" } ) + {% end %} + 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 + 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 ( 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 ) } ) + else + ircipc.send( { "##{settings["channel"]}", "| An error occurred. " } ) + 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 } ) + else + ircipc.send( { "##{settings["channel"]}", "| A failure occured." } ) + end + else + ircipc.send( { "##{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 + ircipc.send( { "##{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 + ircipc.send( { "##{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 + 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 + end + end + + rooms = Array( String ).new + rooms = [ "##{settings["channel"]}" ] + + # 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 + {% if flag?(:windows) %} + puts "press enter to end program" + gets + {% end %} + exit 1 + end +end + + +# FastIRC::Message.to_s +# @badge-info=;badges=;color=;display-name=BungMonkey;emotes=;flags=;id=20fcc358-4fc3-4919-8229-f1034743d18f;mod=0;room-id=46694819;subscriber=0;tmi-sent-ts=1587876383907;turbo=0;user-id=59895482;user-type= :bungmonkey!bungmonkey@bungmonkey.tmi.twitch.tv PRIVMSG #kr3wzz test + +# FastIRC::Message.tags +# {"badge-info" => "subscriber/34", +# "badges" => "broadcaster/1,subscriber/12", +# "color" => "", +# "display-name" => "BungMonkey", +# "emote-only" => "1", +# "emotes" => "300780134:0-9", +# "flags" => "", +# "id" => "c5c08c05-6e39-483f-b426-488dfc477a6c", +# "mod" => "0", +# "room-id" => "59895482", +# "subscriber" => "1", +# "tmi-sent-ts" => "1587871386678", +# "turbo" => "0", +# "user-id" => "59895482", +# "user-type" => ""} + +#command = "PRIVMSG" +#prefix = Prefix(@source="bungmonkey", @user="bungmonkey", @host="bungmonkey.tmi.twitch.tv") +#params = ["#bungmonkey", "test test test test"] diff --git a/crystal/irc.cr b/crystal/irc.cr deleted file mode 100755 index 3b592e9..0000000 --- a/crystal/irc.cr +++ /dev/null @@ -1,950 +0,0 @@ -require "twitch/irc" -require "http" -require "uri" -require "twitcr" -require "json" -require "crystal_mpd" - -STDOUT.sync = true - -struct Nil - def as_s? - self - end -end - -settings = Hash(String, String).new - -settings["configdir"] = Path.home./("/.config/bungmobott/").to_s -settings["statedir"] = Path.home./("/.local/state/bungmobott/").to_s - -if ENV["LOCALAPPDATA"]? - settings["configdir"] = Path.windows( ENV["LOCALAPPDATA"] + "\\bungmobott\\" ).to_s - settings["statedir"] = Path.windows( ENV["LOCALAPPDATA"] + "\\bungmobott\\state\\" ).to_s -end - -settings["tempdir"] = "/tmp/bungmobott/" -ENV["TEMP"]? && ( settings["tempdir"] = "#{ENV["TEMP"]}\\bungmobott\\" ) - -ENV["XDG_CONFIG_HOME"]? && ( settings["configdir"] = ENV["XDG_CONFIG_HOME"] + "/bungmobott/" ) -#ENV["XDG_DATA_HOME"]? && ( settings["datadir"] = ENV["XDG_DATA_HOME"] ) # unused? -ENV["XDG_STATE_HOME"]? && ( settings["statedir"] = ENV["XDG_STATE_HOME"] + "/bungmobott/" ) - -Dir.mkdir_p( settings["configdir"] ) -Dir.mkdir_p( settings["statedir"] ) -Dir.mkdir_p( settings["tempdir"] ) - -settings["home"] = Path.home.to_s -regextwitchuser = /^[0-9a-zA-Z_]+$/ - -error = false -[ "access_token", "client_id" ].each do |key| - begin - settings[key] = File.read( settings["configdir"] + key ).chomp - rescue IO::Error - STDERR.puts "ERROR: Missing " + settings["configdir"] + key - error = true - end -end - -# enable gcloud? -if File.exists?( settings["configdir"] + "gcloud_token" ) && ( settings["gcloud_token"] = File.read( settings["configdir"] + "gcloud_token" ).chomp ) - gcloud = true -else - gcloud = false - STDERR.puts "Warning: #{settings["configdir"]}gcloud_token is missing; GCS voices disabled." -end - -# enable aws? -if ! File.exists?( ENV["USERPROFILE"] + "\\.aws\\credentials" ) - STDERR.puts "Warning: #{ENV["USERPROFILE"]}\\.aws\\credentials is missing; AWS voices disabled." - aws = false -elsif ! Process.find_executable( "aws.exe" ) - STDERR.puts "Warning: aws.exe is missing; AWS voices disabled." - aws = false -else - aws = true -end - -client = Twitcr::Client.new( settings ) - -# derive channel_id from channel or vice versa -if ( - File.exists?( settings["configdir"] + "channel" ) && - ( settings["channel"] = File.read( settings["configdir"] + "channel" ).chomp ) - ) || ( - File.exists?( settings["configdir"] + "channel_id" ) && - ( settings["channel_id"] = File.read( settings["configdir"] + "channel_id" ).chomp ) - ) - if ! ( settings["channel"]? =~ regextwitchuser ) && settings["channel_id"]? =~ /^[0-9]+$/ - settings["channel"] = client.user( settings["channel_id"].to_u64 ).login - elsif ( settings["channel_id"]? =~ regextwitchuser ) && ! settings["channel_id"]? =~ /^[0-9]+$/ - settings["channel_id"] = client.user( settings["channel"] ).id.to_s - end -else - STDERR.puts "ERROR: Missing #{settings["configdir"]}channel and channel_id configuration keys." - error = true - # exit 2 -end -if error == true - {% if flag?(:windows) %} - puts "press enter to end program" - gets - {% end %} - exit 1 -end - -if File.exists?( settings["configdir"] + "chatuser" ) - File.read( settings["configdir"] + "chat_user" ).chomp -else - STDERR.puts "Missing " + settings["configdir"] + "chat_user; using configured channel instead." - settings["chat_user"] = settings["channel"] -end - -obsipc = Channel( String ).new -ircipc = Channel( Tuple( String, String ) ).new -t2sipc = Channel( Tuple( String, String ) ).new - -snifflast = Int64.new(0) - -def urbandef( term : String ) - #http://api.urbandictionary.com/v0/define?term=waifu - client = HTTP::Client.new( "api.urbandictionary.com" ) - response = client.exec( "GET", "/v0/define?term=" + URI.encode_www_form( term ) ) - puts response.status_code - json = JSON.parse( response.body ) - return json["list"][0]["definition"].to_s.gsub( /[\[\]]/, "" ).gsub( /\n/, "" ) -end - -# Currently only used in flags?(:unix) -def t2smsg( settings : Hash(String, String), msg : String) - if File.exists?( settings["statedir"] + "/.t2s.sock" ) - sock = Socket.unix - sock.connect Socket::UNIXAddress.new( settings["statedir"] + "/.t2s.sock" ) - sock.puts( msg ) - sock.close - end -rescue ex - puts ex -end - -def effectsmsg( settings : Hash(String, String), msg : String ) - if File.exists?( settings["statedir"] + "/.effects.sock" ) - sock = Socket.unix - sock.connect Socket::UNIXAddress.new( settings["statedir"] + "/.effects.sock" ) - sock.puts( msg ) - sock.close - end -rescue ex - puts ex -end - -def userlog( ircipc : Channel, settings : Hash(String, String), message : FastIRC::Message ) - unless ( ( prefix = message.prefix ) && ( chatuser = prefix.source ) && ( uid = message.tags["user-id"]? ) ) - return nil - end - basedir = settings["statedir"] - userdir = basedir + "/uids/" + uid - Dir.mkdir_p( basedir + "/names" ) - Dir.mkdir_p( userdir + "/names" ) - unless testrefuser2uid( userdir + "/names/" + chatuser ) - namelatest = "" - datelatest = Time::UNIX_EPOCH - Dir.each_child( userdir + "/names/" ) do |name| - namedate = File.info( userdir + "/names/" + name ).modification_time - if namedate > datelatest - namelatest = name - datelatest = namedate - end - end - unless namelatest.empty? - ircipc.send( { "##{settings["channel"]}", "Rename detected: #{uid}: #{namelatest} -> #{chatuser}" } ) - end - genrefuser2uid( userdir + "/names/" + chatuser, uid, 3 ) - end - unless testrefuser2uid( basedir + "/names/" + chatuser ) - genrefuser2uid( basedir + "/names/" + chatuser, uid, 1 ) - end - unless ( message.params[0] == "##{settings["channel"]}" ) - return nil - end - return ( [ chatuser, uid, userdir ] ) -rescue ex - puts ex -end - -def getvoice( settings : Hash(String, String), userdir : String, chatuser : String ) - if File.exists?( userdir + "/voice" ) - voice_output = File.read( userdir + "/voice" ).chomp - voice_setting = voice_output - else - voice_output = File.read( settings["configdir"] + "/voicelist.txt" ).chomp.split( "\n" ).sample( 1 )[0].chomp - voice_setting = "random" - end - if File.exists?( userdir + "/voicesub" ) - namesub = File.read( userdir + "/voicesub" ).chomp - else - namesub = chatuser - end - return( [namesub, voice_setting, voice_output ] ) -end - -# text2speech -def t2s( t2sipc : Channel, settings : Hash(String, String), userdir : String, chatuser : String, text : String ) - if ( text !~ /^ *(!|\|)/ ) - namesub, voice_setting, voice = getvoice( settings, userdir, chatuser ) - subs = Array( Tuple( Regex, String ) ){ - { /http(s|):\/\/([a-z0-9.-]+)\/[a-zA-Z0-9\/&=%-_]+/, "link to \\2" }, - { /([^a-zA-Z0-9])-/, "\\1 dash "}, - { /\|/, " vertical bar "}, - { /\`/, " grave accent "}, - { /\+/, " plus "}, - { /×/, " multiplied by "}, - { /=/, " equals "}, - { /\//, " slash "}, - { /\\/, " backslash "}, - { /@/, " at "}, - { /&/, " and "}, - { />/, " greater than "}, - { / { -# "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["type"] == "scene" - scenes[source.to_s].each do | metasource, metasourcedata | - sourcemap[metasource.to_s] = source.to_s - end - end - end - return sourcemap -end - -def playmp3file( filepath : String ) - p = Process.new( - "powershell.exe", - [ "-Command", "#Set-PSDebug -Trace 1; - Add-Type -AssemblyName presentationCore; - $player = New-Object system.windows.media.mediaplayer; - $player.open(\"#{filepath}\"); - $player.volume = .99; - $player.play(); - Start-Sleep -Milliseconds 1000; - $duration = $player.NaturalDuration.TimeSpan.TotalMilliseconds; - Start-Sleep -Milliseconds ($duration - 1000 ); - "], - output: STDOUT, error: STDERR - ) - # https://geekeefy.wordpress.com/2016/07/19/powershellmediaplayer/ has some ideas - p.wait -end - -macro testrefuser2uid( path ) - {% if flag?(:windows) %} - File.exists?( {{path}} ) && ( File.read( {{ path }} ) =~ /^[0-9]+$/ ) - {% elsif flag?(:unix) %} - File.symlink?( {{path}} ) - {% end %} -end - -macro genrefuser2uid( path, uid, depth ) - {% if flag?(:windows) %} - File.write( {{path}}, {{uid}}.to_s ) - {% elsif flag?(:unix) %} - pp "unix" - File.symlink( "../"*{{depth}} + "uids/#{{{uid}}}", {{path}} ) - {% end %} -end - -macro temporaryduplicate( item ) - request = Hash( String, String | Bool ){ - "request-type" => "SetSceneItemProperties", - "message-id" => "SetSceneItemProperties", - "scene-name" => "meta", - "item" => {{item}}, - "visible" => true, - } - obsipc.send( request.to_json ) - request = Hash( String, String | Hash( String, String ) ){ - "request-type" => "DuplicateSceneItem", - "message-id" => "DuplicateSceneItem", - "item" => { "name" => {{item}} }, - "fromScene" => "meta" - } - ircipc.send( { "##{settings["channel"]}", "Duplicating source #{match[2]}" } ) - obsipc.send( request.to_json ) -end - -voices = Hash(String, String).new -File.each_line( settings["configdir"] + "/voicelist.txt" ) do |line| - voices[ line.downcase ] = line -end - -lastvoice = Array(String).new - -# Put tts stuff into the same thread so each playback blocks the next -spawn do - loop do - begin - while t2stuple = t2sipc.receive - voice, text = [ *t2stuple ] - if ( match = voice.match( /^Microsoft-([A-Za-z]+)/ ) ) - if ( match[1] =~ /Lili|Mary|Mike|Sam|Anna/ ) - msttsvoice="Microsoft #{match[1]}" - else - msttsvoice="Microsoft #{match[1]} Desktop" - end - p = Process.new( - "powershell.exe", - [ "-Command", " - Add-Type -AssemblyName System.Speech; - $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; - $speak.SelectVoice(\"#{msttsvoice}\"); - $speak.Speak($Input); - $speak.Finalize; - "], - input: Process::Redirect::Pipe, output: STDOUT - ) - p.input.puts text - p.input.close - p.wait - elsif gcloud && ( match = voice.match( /^([a-zA-Z]{2,3}-[a-zA-Z]{2})/ ) ) # Google cloud voice - request = Hash( String, Hash( String, String ) ){ - "input" => { "text" => text }, - "audioConfig" => { "audioEncoding" => "MP3" }, - "voice" => { - "name" => voice, - "languageCode" => match[1], - }, - } - body = request.to_json - ssl_context = OpenSSL::SSL::Context::Client.new - ssl_context.verify_mode = OpenSSL::SSL::VerifyMode::NONE - - headers = HTTP::Headers.new - headers["Content-Type"] = "application/json; charset=utf-8" - - response = HTTP::Client.exec( "POST", "https://texttospeech.googleapis.com/v1/text:synthesize?key=#{settings["gcloud_token"]}", headers, body, tls: ssl_context ) - - response.body - - filepath="#{settings["tempdir"]}#{Time.utc.to_unix}.mp3" - json=JSON.parse(response.body) - File.write( filepath, Base64.decode_string( json["audioContent"].as_s ) ) - playmp3file( filepath ) - File.delete( filepath ) - elsif aws # AWS polly voices - filepath="#{settings["tempdir"]}#{Time.utc.to_unix}.mp3" - p = Process.new( - "aws.exe", [ - "polly", "synthesize-speech", - "--output-format", "mp3", - "--voice-id", voice, - "--text", text, - filepath - ], output: STDOUT, error: STDERR - ) - p.wait - playmp3file( filepath ) - File.delete( filepath ) - else # unknown - STDERR.puts "Voice not recognized or available." - end - end - rescue ex - puts ex - end - end -end - -# obs-websocket thread -spawn do - loop do - begin - - obs_pubsub = HTTP::WebSocket.new( URI.parse( "ws://127.0.0.1:4444/" ), HTTP::Headers{"Cookie" => "SESSIONID=1234"} ) - # Outgoing - # state - currentscene = "unknown" - # scenename => { sourcename => { property => value } - scenes = Hash( String, Hash( String, Hash( String, Int64 | Float64 | Bool | String ) ) ).new - spawn do - while msg = obsipc.receive - pp msg - if ( match = msg.match( /^\x10source-toggle ([a-z0-9-_]+)/ ) ) - source = match[1] - sources = reversesource( scenes, currentscene ) - pp sources - request = Hash( String, String | Bool ).new - request["request-type"] = "SetSceneItemProperties" - request["message-id"] = "SetSceneItemProperties" - #request["request-type"] = "GetSceneItemProperties" - request["message-id"] = "toggle-source" - scene = sources[source] - request["scene-name"] = sources[source] - request["item"] = source - if scenes[scene][source]["render"] == true - request["visible"] = false - else - request["visible"] = true - end - obs_pubsub.send( request.to_json ) - elsif ( match = msg.match( /^\x10source ([a-z0-9-_]+)/ ) ) - # FIXME - else - obs_pubsub.send( msg ) - end - 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" ) } ) - currentscene = json["scene-name"].as_s - puts "currentscene is " + currentscene - 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 ) - #"sourceName": "media-youdied" - puts "currentscene is " + currentscene - if ( ( json["sourceName"].as_s =~ /^media-/ ) && ( currentscene != "meta" ) ) - request = Hash( String, String | Hash( String, String ) ){ - "request-type" => "DeleteSceneItem", - "message-id" => "DeleteSceneItem", - "item" => { "name" => json["sourceName"].as_s }, - } - ircipc.send( { "##{settings["channel"]}", "Deleting #{json["sourceName"].as_s}" } ) - obsipc.send( request.to_json ) - end - when "SourceFilterVisibilityChanged" - ircipc.send( { "##{settings["channel"]}", "| obs: source #{json["sourceName"]} filter #{json["filterName"]} visibility is now #{json["filterEnabled"]}" } ) - when "SceneItemVisibilityChanged" - ircipc.send( { "##{settings["channel"]}", "| obs: source #{json["item-name"]} visibility is now #{json["item-visible"].as_bool}" } ) - end - print( "RECEIVED: ") - print( json.to_pretty_json ) - print( "\n") - case json["message-id"]? - when "GetCurrentScene" - currentscene = json["name"].as_s - 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(", ") } ) - 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["cx"].as_i64? && ( scenes[scene["name"].as_s][source["name"].as_s]["cx"] = source["cx"].as_i64 ) - source["cy"].as_i64? && ( scenes[scene["name"].as_s][source["name"].as_s]["cy"] = source["cy"].as_i64 ) - 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 ) - } - } - pp scenes - 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 - request = Hash( String, String ){ - "request-type" => "GetCurrentScene", - "message-id" => "GetCurrentScene", - } - obsipc.send( request.to_json ) - - obs_pubsub.run - rescue ex : Socket::ConnectError - # these are a bit noisy - #pp ex - obs_pubsub && obs_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/, ' ') } ) - obs_pubsub && obs_pubsub.close - end - sleep 10 - next - end -end - -# main thread: IRC -loop do - begin - - bot = Twitch::IRC::Client.new( nick: settings["chat_user"], token: "oauth:" + settings["access_token"], log_mode: true ) - bot.tags = [ "membership", "tags", "commands" ] - - # Outgoing IRC message thread - # "Most IRC servers limit messages to 512 bytes in length, including the trailing CR-LF characters." - # PRIVMSG #channel message\r\n - spawn do - while msgtuple = ircipc.receive # why does this need to be a tuple? - sizelimit=( 512 - ( msgtuple[0].size + 11 ) ) - bot.message( msgtuple[0], msgtuple[1][0..sizelimit] ) # limit size - end - end - - # Create a handler to process incoming messages - bot.on_message do |message| - spawn do - next unless ( userlogreturn = userlog( ircipc, settings, message ) ) - chatuser, uid, userdir = userlogreturn - if ( chatuser == "pipne" ) && ( ( Time.utc.to_unix - snifflast ) >= 14400 ) - snifflast = Time.utc.to_unix - t2smsg( settings, chatuser + " sniff null" ) - end - if ( t2sreturn = t2s( t2sipc, 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" ) - 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 - 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 =~ /^(voices|voicelist)$/ ) && ( 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" )}." ) - # TODO: make separate script to print streamelements URLs to client machine - else - pp ( match ) - bot.message( "##{settings["channel"]}", "| 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( "\x10source #{match[2]}" ) - elsif ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) - #sources = reversesource( scenes, currentscene ) - #request["request-type"] = "SetSceneItemProperties" - #request["message-id"] = "SetSceneItemProperties" - #request["request-type"] = "GetSceneItemProperties" - #request["message-id"] = "toggle-source" - #request["item"] = match[2] - #obsipc.send( request.to_json ) - obsipc.send( "\x10source-toggle #{match[2]}" ) - else -# request["request-type"] = "GetCurrentScene" -# request["message-id"] = "GetCurrentScene" -# obsipc.send( request.to_json ) - obsipc.send( "\x10source #{match[2]}" ) - 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 == "duplicate" && ( mod || own || vip ) ) - if ( match[2]? ) && ( filterargs = match[2].match( /^([a-zA-Z0-9-_]+)/ ) ) - temporaryduplicate( match[2] ) - else - ircipc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument." } ) - 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 ) ) - ircipc.send( { "##{settings["channel"]}", "Title is now \"#{ json["data"][0]["title"] }\""} ) - else - 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 - 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 ) ) - ircipc.send( { "##{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 ) ) - 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 ) - 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]}" - 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]}" ) - else - end - {% if flag?(:unix) %} - elsif ( cmd =~ /^(system|dxdiag|computer)$/ ) - ircipc.send( { "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/tmp/DxDiag.txt" } ) - {% end %} - 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 - 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 ( 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 ) } ) - else - ircipc.send( { "##{settings["channel"]}", "| An error occurred. " } ) - 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 } ) - else - ircipc.send( { "##{settings["channel"]}", "| A failure occured." } ) - end - else - ircipc.send( { "##{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 - ircipc.send( { "##{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 - ircipc.send( { "##{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 - 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 - end - end - - rooms = Array( String ).new - rooms = [ "##{settings["channel"]}" ] - - # 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 - {% if flag?(:windows) %} - puts "press enter to end program" - gets - {% end %} - exit 1 - end -end - - -# FastIRC::Message.to_s -# @badge-info=;badges=;color=;display-name=BungMonkey;emotes=;flags=;id=20fcc358-4fc3-4919-8229-f1034743d18f;mod=0;room-id=46694819;subscriber=0;tmi-sent-ts=1587876383907;turbo=0;user-id=59895482;user-type= :bungmonkey!bungmonkey@bungmonkey.tmi.twitch.tv PRIVMSG #kr3wzz test - -# FastIRC::Message.tags -# {"badge-info" => "subscriber/34", -# "badges" => "broadcaster/1,subscriber/12", -# "color" => "", -# "display-name" => "BungMonkey", -# "emote-only" => "1", -# "emotes" => "300780134:0-9", -# "flags" => "", -# "id" => "c5c08c05-6e39-483f-b426-488dfc477a6c", -# "mod" => "0", -# "room-id" => "59895482", -# "subscriber" => "1", -# "tmi-sent-ts" => "1587871386678", -# "turbo" => "0", -# "user-id" => "59895482", -# "user-type" => ""} - -#command = "PRIVMSG" -#prefix = Prefix(@source="bungmonkey", @user="bungmonkey", @host="bungmonkey.tmi.twitch.tv") -#params = ["#bungmonkey", "test test test test"] -- cgit v1.2.3