From 298ad7ed4a6f8171e9845afa0dd842aa6ad436a2 Mon Sep 17 00:00:00 2001 From: Joe Rayhawk Date: Wed, 7 Feb 2024 06:54:53 -0800 Subject: tcpsocket.cr: add gamesurge.net support Signed-off-by: Joe Rayhawk --- crystal/tcpsocket.cr | 401 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 298 insertions(+), 103 deletions(-) (limited to 'crystal') diff --git a/crystal/tcpsocket.cr b/crystal/tcpsocket.cr index 67e036c..989233e 100644 --- a/crystal/tcpsocket.cr +++ b/crystal/tcpsocket.cr @@ -1,5 +1,6 @@ require "socket" require "openssl" +require "gamesurge/irc" require "twitch/irc" require "http" require "uri" @@ -131,6 +132,8 @@ unless secrets.yaml_unmapped.empty? ppe secrets.yaml_unmapped.keys end +Dir.mkdir_p( config.tempdir ) + def obsrandommediaenable( obs : OBS::WebSocket, siname : String ) if ( Random.rand(3) < 2 ) obs.scenes.current.metascene[siname][0].enable! @@ -242,6 +245,7 @@ commandircipc = Channel( Tuple( String, FastIRC::Message ) ).new commandipc = Channel( String ).new # t2sipc: voice, text t2sipc = Channel( Tuple( String, String ) ).new +gamesurgeircipc = Channel( Tuple( String, String ) ).new twitchircipc = Channel( Tuple( String, String ) ).new twitchapiipc = Channel( Tuple( String, String | UInt64 ) ).new bbscliipc = Channel( String ).new @@ -354,12 +358,12 @@ def urbandef( term : String ) return json["list"][0]["definition"].to_s.gsub( /[\[\]]/, "" ).gsub( /(\r|\n)/, " " ) end -def getvoice( configdir : String, userdir : String, chatuser : String ) +def getvoice( voicelist : 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( configdir + "/voicelist.txt" ).chomp.split( "\n" ).sample( 1 )[0].chomp + voice_output = File.read( voicelist ).chomp.split( "\n" ).sample( 1 )[0].chomp voice_setting = "random" end if File.exists?( userdir + "/voicesub" ) @@ -443,54 +447,86 @@ def playaudiodata( tempdir, data : Bytes ) end def playaudiofile( 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 + {% if flag?(:windows) %} + 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 + {% else %} + p = Process.new( + "ompv", # FIXME: switch this over to xdg-open at some point? + [ "#{filepath}" ], + output: STDOUT, error: STDERR + ) + p.wait + {% end %} end -def userlog( config : BungmoBott::Config, message : FastIRC::Message ) : Tuple( String, UInt64, String, Int32 | Int64, String | Nil ) | Nil - unless ( ( prefix = message.prefix ) && ( chatuser = prefix.source ) && ( uid = message.tags["user-id"]? ) ) + # user, uid, userdir,lastseen, oldname +def userlog( config : BungmoBott::Config, service : String, message : FastIRC::Message ) : Tuple( String, UInt64 | Nil, String, Int32 | Int64, String | Nil ) | Nil + unless ( ( prefix = message.prefix ) && ( chatuser = prefix.source ) ) return nil end - basedir = config.statedir - userdir = basedir + "/uids/" + uid - if File.directory?( userdir ) - lastseen = File.info( userdir ).modification_time.to_unix - else - lastseen = 0 - end - Dir.mkdir_p( basedir + "/names" ) - Dir.mkdir_p( userdir + "/names" ) - File.touch( userdir ) - unless testrefuser2uid( userdir + "/names/" + chatuser ) - oldname = nil - datelatest = Time::UNIX_EPOCH - Dir.each_child( userdir + "/names/" ) do |name| - namedate = File.info( userdir + "/names/" + name ).modification_time - if namedate > datelatest - oldname = name - datelatest = namedate + if service == "twitch" + if ( uid = message.tags["user-id"]? ) + basedir = config.statedir + "/" + service + userdir = basedir + "/uids/" + uid + if File.directory?( userdir ) + lastseen = File.info( userdir ).modification_time.to_unix + else + lastseen = 0 + end + Dir.mkdir_p( basedir + "/names" ) + Dir.mkdir_p( userdir + "/names" ) + File.touch( userdir ) + unless testrefuser2uid( userdir + "/names/" + chatuser ) + oldname = nil + datelatest = Time::UNIX_EPOCH + Dir.each_child( userdir + "/names/" ) do |name| + namedate = File.info( userdir + "/names/" + name ).modification_time + if namedate > datelatest + oldname = name + datelatest = namedate + end + end + genrefuser2uid( userdir + "/names/" + chatuser, uid, 3 ) end + unless testrefuser2uid( basedir + "/names/" + chatuser ) + genrefuser2uid( basedir + "/names/" + chatuser, uid, 1 ) + end + uid = uid.to_u64 + else # No uid? + STDERR.puts( "WARNING: userlog unexpectedly found message.prefix.source with no UID tag." ) + return( nil ) end - genrefuser2uid( userdir + "/names/" + chatuser, uid, 3 ) - end - unless testrefuser2uid( basedir + "/names/" + chatuser ) - genrefuser2uid( basedir + "/names/" + chatuser, uid, 1 ) + elsif( service == "gamesurge" ) + basedir = config.statedir + "/" + service + userdir = basedir + "/names/" + chatuser + if File.directory?( userdir ) + lastseen = File.info( userdir ).modification_time.to_unix + File.touch( userdir ) + else + Dir.mkdir_p( userdir ) + lastseen = 0 + end + oldname = nil + else + STDERR.puts( "WARNING: invalid service used with userlog()" ) + return( nil ) end - return ( { chatuser, uid.to_u64, userdir, lastseen, oldname } ) + return ( { chatuser, uid, userdir, lastseen, oldname } ) rescue ex puts ex end @@ -560,11 +596,11 @@ def t2s( t2sipc : Channel, config : BungmoBott::Config, userdir : String, chatus }.each do | subtuple | text = text.gsub( subtuple[0], subtuple[1] ) end - {% if flag?(:windows) %} + #{% if flag?(:windows) %} t2sipc.send( { voice, "#{namesub} #{text}" } ) - {% else %} - t2smsg( config, "#{voice} #{namesub} #{text}" ) - {% end %} + #{% else %} + # t2smsg( config, "#{voice} #{namesub} #{text}" ) + #{% end %} return( voice ) else return( nil ) @@ -581,6 +617,7 @@ spawn name: "command_dispatch" do if local_commandmsg.is_a?( Tuple( String, FastIRC::Message ) ) && local_commandmsg[0].is_a?( String ) service = local_commandmsg[0] message : FastIRC::Message = local_commandmsg[1] + pp message ircchannel = message.params[0] unless service.empty? if config.bungmobott_listen && ! ircchannel.empty? @@ -589,44 +626,51 @@ spawn name: "command_dispatch" do if channelsubs[ { service, ircchannel } ]? channelsubs[ { service, ircchannel } ].each do |channelsub| if channelsub.is_a? OpenSSL::SSL::Socket::Server - channelsub.puts( "msg twitch #{message.to_s}" ) - # don't think this is ever useful - #else # client.is_a? Channel( Tuple( String, FastIRC::Message ) ) - # client.send( { "twitch", message } ) + channelsub.puts( "msg #{service} #{message.to_s}" ) end end end end - next unless ( userlogreturn = userlog( config, message ) ) + next unless ( userlogreturn = userlog( config, service, message ) ) chatuser, uid, userdir, lastseen, oldname = userlogreturn # Have we seen this user lately? if ( ( Time.utc.to_unix - lastseen ) >= 14400 ) - twitchapiipc.send( { "get_user", uid.to_s } ) - twitchapiipc.send( { "get_followers", uid.to_s } ) - prevnames = Array( String ).new - if ( prevnames = Dir.children( config.statedir + "/uids/#{uid}/names" ) ) && ( prevnames.size > 1 ) - prevnames.delete( chatuser ) - puts "\033[38;5;14m#{chatuser} previous names: #{prevnames.join(", ")}\033[0m" + if service == "twitch" && uid.is_a?( UInt64 ) + twitchapiipc.send( { "get_user", uid.to_s } ) + twitchapiipc.send( { "get_followers", uid.to_s } ) + prevnames = Array( String ).new + if ( prevnames = Dir.children( "#{userdir}/names" ) ) && ( prevnames.size > 1 ) + prevnames.delete( chatuser ) + puts "\033[38;5;14m#{chatuser} previous names: #{prevnames.join(", ")}\033[0m" + end 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 ) - end - {% else %} - t2smsg( config, "fanfare #{uid} #{chatuser}" ) - {% end %} + # 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 end # FIXME: Generalize this across Twitch/IRC? Mods are +o? vips are +v? # FIXME: Add configuration interface for this - 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" ) + # FIXME: There's a distinction between channel owner and bot owner; do we care? + # channel owner + # botowner + chanowner = ( chatuser == ircchannel ) + if service == "twitch" + #chanowner = ( message.tags["room-id"] == message.tags["user-id"] ) + botowner = ( chatuser == config.chat_user.not_nil!.twitch ) + vip = ( message.tags["badges"].to_s.matches?( /(^|,)vip\// ) ) + mod = ( message.tags["mod"] == "1" ) + sub = ( message.tags["subscriber"] == "1" ) + elsif service == "gamesurge" + botowner = ( chatuser == config.chat_user.not_nil!.gamesurge ) + vip = false # FIXME: Maybe +v? FastIRC lets us see MODE changes, but does not itself track them + mod = false # FIXME: Probably +o + sub = false # No idea. Maybe check user registration? + end # Emote-triggered effects: # [ { 301501910, "farts" }, # { 322820, "explosions" } ].each do | fx | @@ -646,12 +690,17 @@ spawn name: "command_dispatch" do if Regex.new( commandregex ).match( message.params[1] ) command.each do |exec| # Array if ( - ( own || ( exec.perm && exec.perm.not_nil!.includes?("any") ) ) || + ( chanowner || ( exec.perm && exec.perm.not_nil!.includes?("any") ) ) || + ( botowner && ( exec.perm && exec.perm.not_nil!.includes?("owner") ) ) || ( mod && ( exec.perm && exec.perm.not_nil!.includes?("mod") ) ) || ( sub && ( exec.perm && exec.perm.not_nil!.includes?("sub") ) ) || ( vip && ( exec.perm && exec.perm.not_nil!.includes?("vip") ) ) ) - if ( exec.func == "detect_rename" ) + # As a matter of policy: + # BungmoBott::Socket clients can only say things in their own authenticated channel + # Direct IRC clients can say things whereever. + # FIXME: this needs to be service-specific + if ( service == "twitch" && exec.func == "detect_rename" && uid.is_a?( UInt64 ) ) unless oldname.is_a?( String ) ircipc.send( { "##{config.chat_user.not_nil!.twitch}", "Rename detected: #{uid}: #{oldname} -> #{chatuser}" } ) end @@ -673,7 +722,14 @@ spawn name: "command_dispatch" do end end # FIXME: add gamesurge support - ( exec.func == "say" ) && say( twitchircipc, config.chat_user.not_nil!.twitch.not_nil!, exec.arg.not_nil![0].not_nil! ) && next + if ( exec.func == "say" ) + if ( service == "twitch" ) + say( twitchircipc, config.chat_user.not_nil!.twitch.not_nil!, exec.arg.not_nil![0].not_nil! ) + elsif ( service == "gamesurge" ) + say( ircipc, config.chat_user.not_nil!.gamesurge.not_nil!, exec.arg.not_nil![0].not_nil! ) + end + next + end STDERR.puts "WARNING: unhandled function for /#{commandregex}/: #{exec.func}" else STDOUT.print "DENIED: " @@ -1067,25 +1123,29 @@ spawn name: "text2speech" do 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 + {% if flag?(:windows) %} + 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 + {% else %} + STDERR.puts( "WARNING: Microsoft speech services voice called on non-Windows platform." ) + {% end %} elsif ( match = voice.match( /^([a-zA-Z]{2,3}-[a-zA-Z]{2})/ ) ) if ( gcloud_token = secrets.gcloud_token ).is_a?( String ) # Google cloud voice mp3data = ttsgcs( match[1], voice, text, gcloud_token ) @@ -1236,10 +1296,6 @@ if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.j commandircipc.send( { "twitch", message } ) pp message pp message.params - 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" ) rescue ex puts ex #twitchircipc.send( { "##{config.channel}", "An error occurred! " + ex.message.to_s } ) @@ -1272,6 +1328,121 @@ if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.j fibers[fiber.name.not_nil!] = fiber end +if ( secrets.gamesurge_password && config.chat_user.not_nil!.gamesurge && config.join_channels.not_nil!.gamesurge ) + # GameSurge::IRC thread + spawn name: "GameSurge::IRC" do + fiberipc.send( Fiber.current ) + loop do + begin + bot = GameSurge::IRC::Client.new( nick: config.chat_user.not_nil!.gamesurge.not_nil!, token: secrets.gamesurge_password.not_nil!, 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 tuple = ircipc.receive # why does this need to be a tuple? + STDERR.puts( "#{Fiber.current} irc rx #{tuple}" ) + sizelimit=( 512 - ( tuple[0].size + 12 ) ) + if ( tuple[0] == "JOIN" ) + # FIXME: Do validation on this + bot.join_channel( tuple[1] ) + elsif ( tuple[0] =~ /^#/ ) + bot.message( tuple[0], tuple[1][0..sizelimit] ) # limit size + end + end + end + + # Create a handler to process incoming messages + bot.on_message do |message| + spawn do + commandircipc.send( { "gamesurge", message } ) + pp message + pp message.params +# elsif ( cmd =~ /^(create|addsource)/ ) +# if ( match[2]? ) && ( match[2] =~ /^([\/a-zA-Z0-9-_]+)$/ ) +# obstemporarymediacreate( obs, "meta-foreground", match[2], "C:/cygwin64/home/user/effects/#{match[2]}.webm" ) +# elsif ( match[2]? ) && ( match[2] =~ /http(|s):\/\// ) +# newargs = Array( String ).new +# args = match[2].split( / +/ ) +# if uri = URI.parse( args.shift ) +# newargs.push( uri.to_s ) +# else +# ircipc.send( { "##{settings["channel"]}", "Unable to parse URL." } ) +# next +# end +# colormask : UInt32 | Nil = nil +# direction : UInt16 | Nil = nil +# args.each do |arg| +# case arg +# when "black" +# colormask = 0xFF000000 +# when "red" +# colormask = 0xFF0000FF +# when "green" +# colormask = 0xFF00FF00 +# when "blue" +# colormask = 0xFFFF0000 +# when "white" +# colormask = 0xFFFFFFFF +# when /^#[0-9]{6}$/ +# colormask = "0x#{arg.sub( "#([0-9][0-9])([0-9][0-9])([0-9][0-9])", "#FF\3\2\1" )}".to_u32 +# when /^[0-9]+$/ +# direction = arg.to_u16%360 +# else +# ircipc.send( { "##{settings["channel"]}", "Unable to parse color argument." } ) +# end +# end +# colormask && newargs.push( "colormask=#{colormask}" ) +# direction && newargs.push( "direction=#{direction}" ) +# t2smsg( settings, "download #{newargs.join(" ")}" ) +# else +# ircipc.send( { "##{settings["channel"]}", "Must provide at least one URL as argument." } ) +# end +# elsif ( cmd =~ /^(delete|remove)/ ) +# if ( match[2]? ) && ( match[2] =~ /^([\/a-zA-Z0-9-_]+)$/ ) +# obs.inputs["medialoop-bullshit-#{match[2]}"].delete! +# else +# ircipc.send( { "##{settings["channel"]}", "Must provide at least one bullshit source as argument." } ) +# end +# elsif ( cmd == "bullshit" ) +# request = Hash( String, String | Bool ).new +# if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ ) +# obs.scenes["meta-meta-foreground"]["meta-bullshit"].toggle! +# else +# ircipc.send( { "##{settings["channel"]}", "| current sources: #{obs.scenes["meta-bullshit"].to_h.keys.map{ | s | s.sub( /^medialoop-bullshit-/, "") } .join(" ")}" } ) +# end +# elsif ( cmd =~ /^(system|dxdiag|computer)$/ ) && ( Crystal::DESCRIPTION =~ /linux/ ) +# ircipc.send( { "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/tmp/DxDiag.txt" } ) +# end + rescue ex + puts ex + ircipc.send( { "##{config.chat_user.not_nil!.gamesurge.not_nil!}", "An error occurred! " + ex.message.to_s } ) + end + end + + rooms = Array( String ).new + #rooms = [ "##{settings["channel"]}" ] + rooms = config.join_channels.not_nil!.gamesurge.not_nil! + + # Connect to Gamesurge + bot.run( rooms.map{ | room | room.sub( /^#/, "") } ) + rescue ex : IO::Error + pp ex + sleep 1 + # loop to reconnect + rescue ex + pp ex + exit 1 + end + end + ensure + waitgroup.send( Fiber.current.name.not_nil! ) + end + fiber = fiberipc.receive + fibers[fiber.name.not_nil!] = fiber +end + # BungmoBott::Socket client thread if config.bungmobott_connect bbscli_host, bbscli_port = config.bungmobott_connect.not_nil!.split(":") @@ -1292,9 +1463,9 @@ if config.bungmobott_connect raise Exception.new("BungmoBott::Socket Error: #{message}") elsif message =~ /^authed/ negotiated = true - ssl_socket.puts( "say twitch #{user} test" ) - elsif message =~ /^msg twitch/ - commandircipc.send( { "twitch", FastIRC.parse_line( message.split(" ")[2..].join(" ") ) } ) + #ssl_socket.puts( "say twitch #{user} test" ) + elsif ( match = message.match( /^msg (twitch|gamesurge)/ ) ) + commandircipc.send( { match[1], FastIRC.parse_line( message.split(" ")[2..].join(" ") ) } ) elsif ( match = message.match( /^awst2s ([0-9]+)/ ) ) datasize = match[1].to_u32 audiodata = Bytes.new( datasize ) @@ -1376,6 +1547,14 @@ if config.bungmobott_listen end channelsubs[ { ircservice, "#" + ircchannel } ].push( client ) pp channelsubs + elsif ircservice == "gamesurge" + client.puts "joining #{ircservice} \##{ircchannel}" + ircipc.send( { "JOIN", ircchannel } ) + unless channelsubs[ { ircservice, ircchannel } ]? + channelsubs[ { ircservice, "#" + ircchannel } ] = Array( OpenSSL::SSL::Socket::Server ).new + end + channelsubs[ { ircservice, "#" + ircchannel } ].push( client ) + pp channelsubs end elsif ( message =~ /^aws/i) if aws @@ -1464,10 +1643,26 @@ if config.bungmobott_listen fibers[fiber.name.not_nil!] = fiber end -if config.join_channels && config.join_channels.not_nil!.twitch +if config.join_channels spawn name: "join_channels" do fiberipc.send( Fiber.current ) - if fibers["Twitch::IRC"]? + if fibers["GameSurge::IRC"]? && config.join_channels.not_nil!.gamesurge + ircservice = "gamesurge" + config.join_channels.not_nil!.gamesurge.not_nil!.each do |ircchannel| + ircipc.send( { "JOIN", ircchannel } ) + unless channelsubs[ { ircservice, ircchannel } ]? + channelsubs[ { ircservice, "#" + ircchannel } ] = Array( OpenSSL::SSL::Socket::Server ).new + # Do we ever care about this? + #channelsubs[ { ircservice, "#" + ircchannel } ] = Array( OpenSSL::SSL::Socket::Server | Channel( Tuple( String, FastIRC::Message ) ) ).new + end + end + elsif fibers["BungmoBott::Socket client"]? + ircservice = "gamesurge" + config.join_channels.not_nil!.gamesurge.not_nil!.each do |ircchannel| + bbscliipc.send( "irc gamesurge JOIN \#" + ircchannel ) + end + end + if fibers["Twitch::IRC"]? && config.join_channels.not_nil!.twitch ircservice = "twitch" config.join_channels.not_nil!.twitch.not_nil!.each do |ircchannel| twitchircipc.send( { "JOIN", ircchannel } ) -- cgit v1.2.3