diff options
author | Joe Rayhawk <jrayhawk@fairlystable.org> | 2024-02-27 03:23:30 -0800 |
---|---|---|
committer | Joe Rayhawk <jrayhawk@fairlystable.org> | 2024-02-27 03:23:30 -0800 |
commit | 77b74b745aaa84fe342d56f396bc0491cea0859b (patch) | |
tree | 80faee6a10d6e3b5118464380edb0564afc283ec /crystal/tcpsocket.cr | |
parent | 061b5e956e422d585f8733e8079c60a625c6d27f (diff) | |
download | twitchtools-77b74b745aaa84fe342d56f396bc0491cea0859b.tar.gz twitchtools-77b74b745aaa84fe342d56f396bc0491cea0859b.zip |
crystal/tcpsocket: promote new codebase to bungmobott
Diffstat (limited to 'crystal/tcpsocket.cr')
-rw-r--r-- | crystal/tcpsocket.cr | 1832 |
1 files changed, 0 insertions, 1832 deletions
diff --git a/crystal/tcpsocket.cr b/crystal/tcpsocket.cr deleted file mode 100644 index 8194eb6..0000000 --- a/crystal/tcpsocket.cr +++ /dev/null @@ -1,1832 +0,0 @@ -require "socket" -require "openssl" -require "gamesurge/irc" -require "twitch/irc" -require "http" -require "uri" -require "twitcr" -require "json" -require "crystal_mpd" -require "obswebsocket" -require "yaml" -require "bungmobott" -require "file_utils" -require "option_parser" - -STDOUT.sync = true -STDOUT.flush_on_newline = true - -# Convenience mixins -struct Nil - def as_s? - self - end - def []?( v : String | Int64 | Int32 | Range( Int32, Nil ) ) - self - end -end - -class OpenSSL::SSL::Socket::Client - def fill_read( slice : Bytes ) - datasize = slice.size - datarcvdtotal = UInt32.new( 0 ) - data = Bytes.new( 0 ) - while datarcvdtotal < datasize - # OpenSSL only unbuffered_read's TLS records of max size 16384, so we may have to reassemble - data_buffer = Bytes.new( datasize ) - datarcvd = self.unbuffered_read( data_buffer ) - datarcvdtotal = ( datarcvdtotal + datarcvd ) - data = data + data_buffer[0..datarcvd-1] - data_buffer = Bytes.new( datasize - datarcvdtotal ) - end - slice.copy_from( data ) - end -end - -macro send_and_log( ifc, value ) - puts( "#{Fiber.current.name} tx {{ifc}}: #{{{value}}}" ) - {{ifc}}.send( {{value}} ) -end - -# IRC say -macro say( service, channel, text ) - case {{service}} - when "twitch" - send_and_log( twitchircifc, { "#" + {{channel}}, {{text}} } ) - when "gamesurge" - send_and_log( gamesurgeircifc, { "#" + {{channel}}, {{text}} } ) - when "twitch_remote" - send_and_log( bbscliifc, "say twitch " + {{text}} ) - when "gamesurge_remote" - send_and_log( bbscliifc, "say gamesurge " + {{text}} ) - end -end - -# Say() in all available primary channels -macro say_all_self_chan( text ) - if fibers["Twitch::IRC"]? - say( "twitch", config.chat_user.not_nil!.twitch.not_nil!, {{text}} ) - elsif fibers["BungmoBott::Socket client"]? && - say( "twitch_remote", config.chat_user.not_nil!.twitch.not_nil!, {{text}} ) - end - # FIXME: Maybe use config.join_channels.gamesurge[0]? think about this later - if fibers["GameSurge::IRC"]? - say( "gamesurge", config.chat_user.not_nil!.gamesurge.not_nil!, {{text}} ) - elsif fibers["BungmoBott::Socket client"]? - say( "gamesurge_remote", config.chat_user.not_nil!.gamesurge.not_nil!, {{text}} ) - end -end - -macro testrefuser2uid( path ) - {% if flag?(:windows) %} - File.exists?( {{path}} ) && ( File.read( {{ path }} ) =~ /^[0-9]+$/ ) - {% else %} - File.symlink?( {{path}} ) - {% end %} -end - -macro genrefuser2uid( path, uid, depth ) - {% if flag?(:windows) %} - File.write( {{path}}, {{uid}}.to_s ) - {% else %} - File.symlink( "../"*{{depth}} + "uids/#{{{uid}}}", {{path}} ) - {% end %} -end - -def ppe(object) - PrettyPrint.format( object, STDERR, 79 ) - STDERR.puts - object -end - -EXE = "bungmobott" - -regexservice = /(twitch|gamesurge)/ -regexuser = /[0-9a-zA-Z_]+/ -regexb64 = /[0-9a-fA-F]+/ -regexvoice = /[0-9a-zA-Z-]+/ - -configdir = Path.home./("/.config/#{EXE}/").to_s -ENV["XDG_CONFIG_HOME"]? && ( configdir = ENV["XDG_CONFIG_HOME"] + "/#{EXE}/" ) -ENV["LOCALAPPDATA"]? && ( configdir = Path.windows( ENV["LOCALAPPDATA"] + "\\#{EXE}\\" ).to_s ) -# cygwin will have both, but should probably use LOCALAPPDATA - -def writeconfig( filepath : Path, contents : String ) - Dir.mkdir_p( filepath.parent ) - File.write( filepath, contents ) -rescue exio : IO::Error - puts "ERROR: Unable to write #{ filepath }: #{exio.message}" - exit 7 -end - -configfile = Path[configdir + "config.txt"].normalize -if File.exists?( File.expand_path( "config.txt" ) ) - configfile = Path[File.expand_path( "config.txt" )].normalize -end - -secretsfile = Path[ configdir + "secrets.txt" ].normalize -if File.exists?( File.expand_path( "secrets.txt" ) ) - secretsfile = Path[File.expand_path( "secrets.txt" )].normalize -end - -OptionParser.parse do |parser| - parser.banner = "Usage: #{PROGRAM_NAME} (arguments...)" - parser.on("-c FILE", "--config=FILE", "YAML configuration file") { |file| configfile = Path[file].normalize } - parser.on("-s FILE", "--secrets=FILE", "YAML secrets file") { |file| secretsfile = Path[file].normalize } - {% if flag?(:windows) %} - parser.on("--install-mss-voices", "Download and install Microsoft Speech Services voices") do - t2s_mss_voice_install() - end - {% end %} - parser.on("-h", "--help", "Show help") do - puts( parser ) - exit - end - parser.invalid_option do |flag| - STDERR.puts "ERROR: #{flag} is not a valid option." - STDERR.puts parser - end -end - -puts configfile - -if File.exists?( configfile ) - config = BungmoBott::Config.from_yaml( File.read( configfile ) ) -else - config = BungmoBott::Config.from_yaml("---") - STDERR.puts "WARNING: #{configfile} not found. Writing new one." - writeconfig( configfile, config.to_yaml ) -end - -puts config.to_yaml - -# FIXME: maybe do this in the after_initialize method? -unless config.yaml_unmapped.empty? - STDERR.puts "WARNING: #{configfile} has unknown properties:" - ppe config.yaml_unmapped -end - -if ( config.chat_user && ! config.chat_user.not_nil!.yaml_unmapped.empty? ) - STDERR.puts "WARNING: #{configfile} chat_user has unknown properties:" - ppe config.chat_user.not_nil!.yaml_unmapped -end - -if ( config.join_channels && ! config.join_channels.not_nil!.yaml_unmapped.empty? ) - STDERR.puts "WARNING: #{configfile} join_channels has unknown properties:" - ppe config.join_channels.not_nil!.yaml_unmapped -end - - -if File.exists?( secretsfile ) - secrets = BungmoBott::Secrets.from_yaml( File.read( secretsfile ) ) -else - secrets = BungmoBott::Secrets.from_yaml( "---" ) - STDERR.puts "WARNING: #{secretsfile} not found. Writing new one." - writeconfig( secretsfile, secrets.to_yaml ) -end - -unless secrets.yaml_unmapped.empty? - STDERR.puts "WARNING: #{secretsfile} has unknown properties:" - 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! - else - randsiname = obs.scenes.current.metascene.keys.select( /^#{siname}/ ).sample( 1 )[0] - obs.scenes.current.metascene[randsiname][0].enable! - end -end - -def obstemporarymediacreate( obs : OBS::WebSocket, sname : String, iname, path : String ) - iname = "media-temporary-effect-#{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 - # FIXME: start with ?sceneItemEnabled false to give us time to SetSceneItemTransform - if ( rdata = response["responseData"]? ) && ( goodtransform = obs.sources.last_known_real_transform?( iname ) ) - siid = rdata["sceneItemId"].as_i64 - obs.send( OBS.req( "SetSceneItemTransform", JSON.parse( { "sceneName" => sname, "sceneItemId" => siid, "sceneItemTransform" => { "positionX" => goodtransform.to_h["positionX"], "positionY" => goodtransform.to_h["positionY"] } }.to_json ) ) ) - end -end - - -regextwitchuser = /^[0-9a-zA-Z_]+$/ - -# enable direct twitch api? - -if ( secrets.twitch_access_token && secrets.twitch_client_id ) - twitchapi = true - twitchclient = Twitcr::Client.new( Hash( String, String ){ - "client_id" => secrets.twitch_client_id.not_nil!, - "access_token" => secrets.twitch_access_token.not_nil! - - } ) - # derive twitch_channel_id from channel or vice versa - unless config.twitch_user_id - config.twitch_user_id = twitchclient.user( config.chat_user.not_nil!.twitch.not_nil! ).id.to_u32 - end -else - twitchapi = false - secrets.twitch_access_token || STDERR.puts "Warning: #{secretsfile} 'twitch_access_token' is missing; direct Twitch API access disabled." - secrets.twitch_client_id || STDERR.puts "Warning: #{secretsfile} 'twitch_client_id' is missing; direct Twitch API access disabled." - unless chat_user = ( config.chat_user && config.chat_user.not_nil!.twitch ) - if chat_user = ( config.join_channels && config.join_channels.not_nil!.twitch[0]? ) - STDERR.puts "Warning: #{configfile} 'chat_user: {twitch}' value is missing; using first configured 'join_channels: {twitch}' array value instead: #{chat_user}" - if ( config.chat_user ) - config.chat_user.not_nil!.twitch = chat_user - end - else - STDERR.puts "ERROR: 'chat_user: {twitch}' string value and 'join_channels: {twitch}' array value missing." - exit 3 - end - end -end - -# enable direct gcloud api? -if secrets.gcloud_token - gcloud = true -else - gcloud = false - STDERR.puts "Warning: #{secretsfile} gcloud_token is missing; direct GCS voices disabled." -end - -# enable aws? -if ! File.exists?( Path.home./("/.aws/credentials") ) - # FIXME: work out where this is on Windows - STDERR.puts "Warning: #{Path.home}/.aws/credentials is missing; AWS voices disabled." - aws = false -elsif ! Process.find_executable( "aws.exe" ) && ! Process.find_executable( "aws" ) - STDERR.puts "Warning: aws CLI executable is missing; AWS voices disabled." - aws = false -else - aws = true -end - -# enable microsoft speech services? -# TODO: download and msiexec /i https://www.microsoft.com/en-us/download/details.aspx?id=27224 -mss : Bool -{% if flag?(:windows) %} - mss = true -{% else %} - mss = false -{% end %} - -voices = Hash( String, String ).new -if ( mss || gcloud || aws ) && config.voice_list - text2speech = true - if File.exists?( config.voice_list.not_nil! ) - File.read( config.voice_list.not_nil! ).strip.split( "\n" ).each do |voice| - voices[voice.strip.downcase] = voice.strip - end - #else - #regeneratevoicelist() - end -else - text2speech = false -end - -lastvoice = Array(String).new - -# Inter-Fiber Communication Channels -# BungmoBott::Socket IRC channel subscriptions: { { service, chan } => [ client, client ] } -channelsubs = Hash( Tuple( String, String ), Array( OpenSSL::SSL::Socket::Server) ).new -# Unencrypted -#connections = Hash(TCPSocket, Hash(String, String)).new -# Encrypted -connections = Hash(OpenSSL::SSL::Socket::Server, Hash(String, String)).new -gamesurgeircifc = Channel( Tuple( String, String ) ).new -# commandifc: serv, chan, user, msg -commandifc = Channel( Tuple( String, FastIRC::Message ) ).new -# t2sifc: voice, text -t2sifc = Channel( Tuple( String, String ) ).new -twitchircifc = Channel( Tuple( String, String ) ).new -twitchapiifc = Channel( Tuple( String, String | UInt64 ) ).new -bbscliifc = Channel( String ).new -# currently unused -#bbssrvifc = Channel( String ).new -waitgroup = Channel( String ).new -fiberifc = Channel( Fiber ).new -fibers = Hash( String, Fiber ).new - -evchan = Channel( JSON::Any ).new -obs : Nil | OBS::WebSocket = nil -if config.obs_connect - obs = OBS::WebSocket.new( "ws://#{config.obs_connect}/", secrets.obs_password ) - # OBS event fiber - spawn name: "OBS::WebSocket" do - fiberifc.send( Fiber.current ) - obs.scenes["meta-foreground"].to_h.each_key do | key | - if key =~ /^media-temporary-/ - obs.inputs[key].delete! - end - end - obs.eventsub_add( evchan ) - while json = evchan.receive - # A Fiber.yield occurs after this to make sure "json" doesn't get overwritten before we can use it. - spawn name: "OBS::WebSocket event" do - d = json # Copy *immediately* - case d["eventType"].as_s - when "CurrentProgramSceneChanged" - say_all_self_chan( "| obs: switched scene to " + ( d["eventData"]["sceneName"]?.as_s? || "unknown" ) ) - when "MediaInputPlaybackEnded" - if d["eventData"]["inputName"].as_s =~ /^media-temporary-/ - obs.send( OBS.req( "RemoveInput", JSON.parse({ "inputName" => d["eventData"]["inputName"].as_s }.to_json) ) ) - elsif d["eventData"]["inputName"].as_s =~ /^media-/ - obs.scenes.current.metascene[d["eventData"]["inputName"].as_s][0].disable! - end - when "SceneItemEnableStateChanged" - edata = d["eventData"] - name = obs.scenes[ edata["sceneName"].as_s ][ edata["sceneItemId"].as_i64 ].name - if name !~ /media-temporary/ - say_all_self_chan( "| 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"] - say_all_self_chan( "| obs: source #{edata["sourceName"].as_s} filter #{edata["filterName"].as_s} visibility is currently #{edata["filterEnabled"].as_bool}" ) - end - end - Fiber.yield - end - rescue ex - pp ex - ensure - waitgroup.send( Fiber.current.name.not_nil! ) - puts( "#{Fiber.current.name} tx waitgroup: #{Fiber.current.name}" ) - end - fiber = fiberifc.receive - fibers[fiber.name.not_nil!] = fiber -end - -# enable effects? -#effects = Hash( String, String ).new -#if File.exists?( config.configdir + "/effects.txt" ) -# File.each_line( config.configdir + "/effects.txt" ) do |line| -# effects[ line.downcase ] = line -# end -#end - - -def urbandef( term : String ) -#{ -# "list": [ -# { -# "definition": "An overactive, small-proportioned homosexual [gentleman] who will [launch] at anything in [sight].", -# "permalink": "http://bungmonkey.urbanup.com/83517", -# "thumbs_up": 6, -# "author": "fishbear", -# "word": "bungmonkey", -# "defid": 83517, -# "current_vote": "", -# "written_on": "2003-04-04T11:55:12.000Z", -# "example": "\"That [chap] [in the corner] is a [proper] little bungmonkey. Look at him go!\"", -# "thumbs_down": 1 -# } -# ] -#} - ssl_context = OpenSSL::SSL::Context::Client.new - #{% if flag?(:windows) %} - #ssl_context.verify_mode = OpenSSL::SSL::VerifyMode::NONE - #{% end %} - #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 ) - return json["list"][0]["definition"].to_s.gsub( /[\[\]]/, "" ).gsub( /(\r|\n)/, " " ) -end - -# FIXME: maybe break this out into separate functions later -def getvoice( voicelist : String, userdir : Path, chatuser : String ) - voicefile = Path.new( userdir, "voice" ).normalize - voicenamesubfile = Path.new( userdir, "voicesub" ).normalize - if File.exists?( voicefile ) - voice_output = File.read( voicefile ).strip - voice_setting = voice_output - else - voice_output = File.read( voicelist ).strip.split( "\n" ).sample( 1 )[0].strip - voice_setting = "random" - end - if File.exists?( voicenamesubfile ) - namesub = File.read( voicenamesubfile ).strip - else - namesub = chatuser - end - return( [ namesub, voice_setting, voice_output ] ) -end - -def generatevoicelistaws( ) - voices = Array(String).new - JSON.parse( `aws polly describe-voices` )["Voices"].as_a.each do | v | - voices.push( v["Id"].as_s.strip ) - end - return voices -end - -def generatevoicelistgcs( gcloud_token : String ) - voices = Array(String).new - ssl_context = OpenSSL::SSL::Context::Client.new - headers = HTTP::Headers.new - headers["Content-Type"] = "application/json; charset=utf-8" - response = HTTP::Client.exec( "GET", "https://texttospeech.googleapis.com/v1/voices?key=#{gcloud_token}", headers, nil, tls: ssl_context ) - JSON.parse( response.body )["voices"].as_a.each do | v | - STDERR.puts( "#{v["naturalSampleRateHertz"]} #{v["languageCodes"]} #{v["name"]}" ) - voices.push( v["name"].as_s.strip ) - end - return voices -end - -{% if flag?(:windows) %} -def generatevoicelistwin() - voices = Array(String).new - p = Process.new( - "powershell.exe", - [ "-Command", " - Add-Type -AssemblyName System.Speech; - $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; - $speak.GetInstalledVoices().VoiceInfo | Select-Object Name - "], - output: Process::Redirect::Pipe - ) - p.output.each_line do | v | - v = v.gsub(/ +$/, "") - v = v.gsub(/ Desktop$/, "") - v = v.gsub( " ", "-" ) - if v =~ /[A-Za-z0-9]-[A-Za-z0-9]/ - voices.push( v.strip ) - end - end - if ( voices.size < 10 ) - puts( "WARNING: Microsoft Speech Service voice count is suspiciously low." ) - puts( "You may want to visit https://www.microsoft.com/en-us/download/details.aspx?id=27224" ) - end - return voices -end -{% end %} - -macro writevoices() - File.write( config.voice_list.not_nil!, voices.values.sort.join("\r\n") ) - say_all_self_chan( "| Wrote voice_list file." ) -end - -macro regeneratevoicelist() - if aws - generatevoicelistaws().each do | voice | - voices[ voice.downcase ] = voice - end - elsif fibers["BungmoBott::Socket client"]? - bbscliifc.send( "awsvoicelist" ) - puts( "#{Fiber.current.name} tx bbscliifc: awsvoicelist" ) - end - if secrets.gcloud_token - generatevoicelistgcs( secrets.gcloud_token.not_nil! ).each do | voice | - voices[ voice.downcase ] = voice - end - elsif fibers["BungmoBott::Socket client"]? - bbscliifc.send( "gcsvoicelist" ) - puts( "#{Fiber.current.name} tx bbscliifc: gcsvoicelist" ) - end - {% if flag?(:windows) %} - generatevoicelistwin().each do | voice | - voices[ voice.downcase ] = voice - end - {% end %} - writevoices() -end - -def file_list( path : String ) : Array( String ) - if File.file?( path ) - return Array( String ){ File.realpath( path ) } - elsif File.directory?( path ) - dir = Dir.new( path ) - return dir.children.map{ | child | dir.path + child } - else - raise Exception.new("InvalidPath") - end -end - -# TODO: add piping into mpv on POSIX -def playaudiodata( tempdir : Path | String, data : Bytes ) - filepath=Path.new( tempdir, "#{Time.utc.to_unix_ms}.mp3" ).normalize - File.write( filepath, data ) - playaudiofile( filepath ) - File.delete( filepath ) -end - -def playaudiofile( filepath : Path ) - {% 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 - - # user, uid, userdir,lastseen, oldname -def userlog( config : BungmoBott::Config, service : String, message : FastIRC::Message ) : Tuple( String, UInt64 | Nil, Path, Int32 | Int64, String | Nil ) | Nil - unless ( ( prefix = message.prefix ) && ( chatuser = prefix.source ) ) - return nil - end - if service =~ /^twitch/ - if ( uid = message.tags["user-id"]? ) - basedir = Path.new( config.statedir, "twitch" ).normalize - userdir = Path.new( basedir, "uids", uid ).normalize - if File.directory?( userdir ) - lastseen = File.info( userdir ).modification_time.to_unix - else - lastseen = 0 - end - Dir.mkdir_p( Path.new( basedir, "names" ).normalize ) - Dir.mkdir_p( Path.new( userdir, "names" ).normalize ) - File.touch( userdir ) - unless testrefuser2uid( Path.new( userdir, "names", chatuser ).normalize ) - oldname = nil - datelatest = Time::UNIX_EPOCH - Dir.each_child( Path.new( userdir, "names" ).normalize ) do |name| - namedate = File.info( Path.new( userdir, "names", name ).normalize ).modification_time - if namedate > datelatest - oldname = name - datelatest = namedate - end - end - genrefuser2uid( Path.new( userdir, "names", chatuser ).normalize, uid, 3 ) - end - unless testrefuser2uid( Path.new( basedir, "names", chatuser ).normalize ) - genrefuser2uid( Path.new( basedir, "names", chatuser ).normalize, 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 - elsif( service =~ /^gamesurge/ ) - basedir = Path.new( config.statedir, "gamesurge" ).normalize - userdir = Path.new( basedir, "names", chatuser ).normalize - 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, userdir, lastseen, oldname } ) -rescue ex - pp ex - return nil -end - -{% if flag?(:windows) %} -def t2s_mss_voice_install() - puts "Downloading Microsoft Speech Services voice index" - puts "https://www.microsoft.com/en-us/download/details.aspx?id=27224" - ENV["TEMP"]? || ( STDERR.puts "TEMP environment variable undefined" && exit 2 ) - ssl_context = OpenSSL::SSL::Context::Client.new - response = HTTP::Client.exec( "GET", "https://www.microsoft.com/en-us/download/details.aspx?id=27224", tls: ssl_context ) - if ( response.status_code == 200 ) - response.body.split("\"").select(/^https.+MSSpeech.+\.msi$/).each do | msi_url | - puts "Downloading: #{msi_url}" - ssl_context = OpenSSL::SSL::Context::Client.new - response = HTTP::Client.exec( "GET", msi_url, tls: ssl_context ) - if ( ( response.status_code == 200 ) && ( response.content_type == "application/octet-stream" ) && ( match = /^.+\/(.+msi)$/.match(msi_url) ) ) - msi_path = Path.new( Path[ENV["LOCALAPPDATA"]].normalize, "Temp", match[1] ) - puts "Writing : #{msi_path}" - File.write( msi_path, response.body ) - puts "Installing : #{msi_path}" - p = Process.new( - "msiexec", [ "/i", msi_path.normalize.to_s ], output: STDOUT, error: STDERR, - ) - p.wait - puts "Deleting : #{msi_path}" - File.delete( msi_path ) - else - end - end - else - puts response.status_code - pp response.headers - puts response.body - exit 1 - end - exit -end -{% end %} - -# Currently only used in flag?(:unix) -def t2smsg( config : BungmoBott::Config, msg : String) - if File.exists?( config.rundir + "/.t2s.sock" ) - sock = Socket.unix - sock.connect Socket::UNIXAddress.new( config.rundir + "/.t2s.sock" ) - sock.puts( msg ) - sock.close - end -rescue ex - pp ex -end - -def t2s( t2sifc : Channel, config : BungmoBott::Config, userdir : Path, chatuser : String, text : String ) - if ( text !~ /^ *(!|\|)/ ) - namesub, voice_setting, voice = getvoice( config.voice_list.not_nil!, 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 "}, - { /</, " less than "}, - { /_/, " underscore "}, - { /\.\.\./, " dot dot dot "}, - { /\^/, " circumflex accent "}, - { /\#/, " octothorpe "}, - { /:([^ ])/, " colon \\1"}, - { /;([^ ])/, " semicolon \\1"}, - { /\.([^ ])/, " dot \\1"}, - { /\%([^ ])/, " percent sign \\1"}, - { /!([^ ])/, " tchik \\1"}, - { /([^ ])\$/, "\\1 dollar sign "}, - { /\(/, " open paren "}, - { /\)/, " close paren "}, - { /\{/, " open curly bracket "}, - { /\}/, " close curly bracket "}, - { /\[/, " open square bracket "}, - { /\]/, " close square bracket "}, - { /0/, " zero " }, - { /1/, " one " }, - { /2/, " two " }, - { /3/, " three " }, - { /4/, " four " }, - { /5/, " five " }, - { /6/, " six " }, - { /7/, " seven " }, - { /8/, " eight " }, - { /9/, " nine " }, - { /rrr.+/, "rr" }, - }.each do | subtuple | - text = text.gsub( subtuple[0], subtuple[1] ) - end - #{% if flag?(:windows) %} - t2sifc.send( { voice, "#{namesub} #{text}" } ) - puts( "#{Fiber.current.name} tx t2sifc: #{voice}, #{namesub} #{text}" ) - #{% else %} - # t2smsg( config, "#{voice} #{namesub} #{text}" ) - #{% end %} - return( voice ) - else - return( nil ) - end -end - -spawn name: "command_dispatch" do - fiberifc.send( Fiber.current ) - loop do - while commandmsg : Tuple( String, FastIRC::Message ) = commandifc.receive - spawn name: "command_dispatch ifc rx" do - service = String.new - local_commandmsg = commandmsg - puts( "#{Fiber.current.name}: #{local_commandmsg}" ) - 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] - ircchannel = message.params[0] - unless service.empty? - if config.bungmobott_listen && ! ircchannel.empty? - # Do we send to the bungmobott_listen fiber? Probably no need. - #bbssrvifc.send( "#{message.to_s}" ) - #puts( "#{Fiber.current.name} tx bbssrvifc: #{message.to_s}" ) - if channelsubs[ { service, ircchannel } ]? - channelsubs[ { service, ircchannel } ].each do |channelsub| - if channelsub.is_a? OpenSSL::SSL::Socket::Server - channelsub.puts( "msg #{service} #{message.to_s}" ) - end - end - end - end - - next unless ( userlogreturn = userlog( config, service, message ) ) - chatuser, uid, userdir, lastseen, oldname = userlogreturn - pp userlogreturn - # Have we seen this user lately? - if ( ( Time.utc.to_unix - lastseen ) >= 14400 ) - if service =~ /^twitch/ && uid.is_a?( UInt64 ) - if twitchapi - puts( "#{Fiber.current.name} tx twitchapiifc: get_user, #{uid}" ) - twitchapiifc.send( { "get_user", uid.to_s } ) - puts( "#{Fiber.current.name} tx twitchapiifc: get_followers, #{uid}" ) - twitchapiifc.send( { "get_followers", uid.to_s } ) - end - prevnames = Array( String ).new - if ( prevnames = Dir.children( Path.new( userdir, "names" ).normalize ) ) && ( 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. - # 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?( Path.new( userdir, "fanfare" ).normalize ) - playaudiofile( Path.new( userdir, "fanfare", Dir.children( Path.new( userdir, "fanfare" ).normalize ).sample ).normalize ) - end - end - # FIXME: Generalize this across Twitch/IRC? Mods are +o? vips are +v? - # FIXME: Add configuration interface for this - # 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 | - # emoteid = fx[0] - # fxname = fx[1] - # if ( message.tags["emotes"]? ) && - # ( message.tags["emotes"] ) && - # ( fxemotes = message.tags["emotes"].not_nil!.split("/").select( /^#{emoteid}[:_]/ ).join(",").split(",") ) && - # ( ! fxemotes[0].empty? ) && - # ( effects.values.select(/^#{fxname}/).size > 0 ) - # effects.values.select(/^#{fxname}/).sample( fxemotes.size ).each do | filepath | - # obstemporarymediacreate( obs, "meta-foreground", filepath.gsub(/[\/ ]/, '_').downcase, "C:/cygwin64/home/user/effects/#{filepath}" ) - # end - # end - # end - config.match && config.match.not_nil!.each do | networkregex, channelhash | - Regex.new( networkregex ).match( service ) || next - channelhash.each do | channelregex, texthash | - Regex.new( channelregex ).match( ircchannel ) || next - texthash.each do | textregex, command | # Hash( String, Array ) - Regex.new( textregex ).match( message.params[1] ) || next - command.each do |exec| # Array - if ( - ( botowner || ( exec.perm && exec.perm.not_nil!.includes?("any") ) ) || - ( chanowner && ( 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") ) ) - ) - # As a matter of policy: - # BungmoBott::Socket clients can only say things in their own authenticated channel - # Direct IRC clients can say things whereever. - if ( service =~ /^twitch/ && exec.func == "detect_rename" && uid.is_a?( UInt64 ) ) - if oldname.is_a?( String ) - say( service, config.chat_user.not_nil!.twitch.not_nil!, "Rename detected: #{uid}: #{oldname} -> #{chatuser}" ) - end - next - end - if obs - # FIXME: better validate args - ( exec.func == "obs_random_source_enable" ) && obsrandommediaenable( obs, exec.arg.not_nil![0].not_nil! ) && next - if ( exec.func == "obs_stats_get" ) - puts "Exec-ing obs_stats_get" - stats = obs.stats.to_h - ostatus = obs.outputs["adv_stream"].status.to_h - say( service, ircchannel, "| #{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)}" ) - next - end - if ( exec.func == "obs_scene_list_get" ) - puts "Exec-ing obs_scene_list_get" - say( service, ircchannel, "| scenes: #{obs.scenes.to_h.keys.join(" ")}" ) - next - end - if ( exec.func == "obs_scene_get" ) - puts "Exec-ing obs_scene_get" - say( service, ircchannel, "| Current scene: #{obs.scenes.program.name}" ) - next - end - if ( exec.func == "obs_scene_set" ) - puts "Exec-ing obs_scene_set" - if ( match = / ([a-zA-Z0-9-_ ]+)/.match( message.params[1] ) ) - obs.scenes[match[1]].program! - else - say( service, ircchannel, "| Could not find scene name." ) - end - next - end - if ( exec.func == "obs_input_list_get" ) - puts "Exec-ing obs_input_list_get" - say( service, ircchannel, "| inputs: #{obs.inputs.to_h.keys.join(" ")}" ) - next - end - if ( exec.func == "obs_source_list_get" ) - puts "Exec-ing obs_source_list_get" - say( service, ircchannel, "| current sources: #{obs.scenes.current.metascene.keys.join(" ")}" ) - next - end - if ( exec.func == "obs_source_toggle" ) - puts "Exec-ing obs_source_toggle" - if ( match = / ([a-zA-Z0-9-_ ]+)/.match( message.params[1] ) ) - obs.scenes.current.metascene[match[1]][0].toggle! - # in studio mode, direct Scene->SceneItem toggles require a transition - obs.scenes.current.preview! - obs.transition! - else - say( service, ircchannel, "| Could not find source name." ) - end - next - end - if ( exec.func == "obs_source_filters_list_get" ) - puts "Exec-ing obs_source_filters_list_get" - if ( match = / ([a-zA-Z0-9-_]+)/.match( message.params[1] ) ) - say( service, ircchannel, "| #{match[1]} filters: #{obs.sources.to_h[match[1]].filters.to_h.keys.join(" ")}" ) - else - say( service, ircchannel, "| Could not match source name." ) - end - next - end - if ( exec.func == "obs_source_filter_toggle" ) - puts "Exec-ing obs_source_filter_toggle" - if ( match = / ([a-zA-Z0-9-_]+) ([a-zA-Z0-9-_]+)/.match( message.params[1] ) ) - obs.sources.to_h[match[1]].filters[match[2]].toggle! - else - say( service, ircchannel, "| Could not match source and filter name to toggle." ) - end - next - end - if ( exec.func == "obs_create_ephemeral_media_sources_from_path" ) - puts "Exec-ing obs_create_ephemeral_media_sources_from_path" - count = 1 - sources = file_list( exec.arg.not_nil![1].not_nil! ) - if ( match = / ([0-9]+)/.match( message.params[1] ) ) - count = match[1].to_i - if ( ( count < 1 ) || ( count > sources.size ) ) - say( service, ircchannel, "| Ephemeral OBS source count must be between 1 and #{sources.size}" ) - next - end - end - sources.sample( count ).each do |path| - obstemporarymediacreate( obs, exec.arg.not_nil![0].not_nil!, path.gsub(/[\/ ]/, '_').downcase, path ) - end - next - end - end - if text2speech - voicenamesubfile = Path.new( userdir, "voicesub" ).normalize - voicefile = Path.new( userdir, "voice" ).normalize - if ( exec.func == "text_to_speech" ) - puts "Exec-ing text_to_speech" - if ( t2sreturn = t2s( t2sifc, config, userdir, chatuser, message.params[1] ) ) - lastvoice.insert( 0, t2sreturn.strip ) - lastvoice = lastvoice[0..4] - end - next - end - if ( exec.func == "tts_voice_list_generate" ) - puts "Exec-ing " + exec.func - regeneratevoicelist() - say( service, ircchannel, "| Regenerated voicelist." ) - next - end - if ( exec.func == "tts_last_voice" ) - unless lastvoice.empty? - say( service, ircchannel, "| Last voices were " + lastvoice.join( ", " ) ) - else - say( service, ircchannel, "| No voices used so far." ) - end - next - end - if ( exec.func == "tts_voice_get" ) - puts "Exec-ing tts_voice_get" - namesub, voice_setting, voice_output = getvoice( config.voice_list.not_nil!, userdir, chatuser ) - say( service, config.chat_user.not_nil!.twitch.not_nil!, "| Current voice is #{voice_setting}" ) - next - end - if ( exec.func == "tts_voice_set" ) - puts "Exec-ing tts_voice_set" - if ( match = / ([a-zA-Z0-9-_]+)/.match( message.params[1] ) ) - voice = match[1].downcase - if voice =~ /disabled|null|disable|none|random/ - if File.exists?( voicefile ) - File.delete( voicefile ) - say( service, ircchannel, "| Voice for #{chatuser} is now random." ) - else - say( service, ircchannel, "| Voice for #{chatuser} is already random." ) - end - elsif voices.has_key?( voice.downcase ) - csvoice = voices[voice] - Dir.mkdir_p( userdir ) - File.write( voicefile, csvoice ) - pp userdir - say( service, ircchannel, "| Voice for #{chatuser} is now #{File.read( voicefile ).strip}." ) - else - pp ( match ) - say( service, ircchannel, "| Invalid voice. To see list, use !voices" ) - end - else - say( service, ircchannel, "| tts_voice_set failed generic string match ") - end - next - end - if ( exec.func == "tts_name_get" ) - puts "Exec-ing " + exec.func - if File.exists?( voicenamesubfile ) - say( service, ircchannel, "| Current name substitution is \"#{File.read( voicenamesubfile ).strip}\"." ) - else - say( service, ircchannel, "| Current name substitution is disabled." ) - end - end - if ( exec.func == "tts_name_set" ) - puts "Exec-ing tts_name_set" - if ( match = / ([\sa-zA-Z0-9-]+)$/.match( message.params[1] ) ) - pp match[1] - voicesub = match[1].downcase - if voicesub =~ /^(disabled|null|disable|none)$/ - if File.exists?( voicenamesubfile ) - File.delete( voicenamesubfile ) - say( service, ircchannel, "| Name substitution for #{chatuser} is now disabled." ) - else - say( service, ircchannel, "| Name substitution for #{chatuser} is already disabled." ) - end - else - Dir.mkdir_p( userdir ) - File.write( voicenamesubfile, voicesub ) - say( service, ircchannel, "| Name substitution for #{chatuser} is now \"#{File.read( voicenamesubfile ).strip}\"." ) - end - end - next - end - end - if ( exec.func == "say" ) - say( service, config.chat_user.not_nil!.twitch.not_nil!, exec.arg.not_nil![0].not_nil! ) - next - end - if ( exec.func == "run" ) - if ( cmd = exec.arg[0]? ) && File.executable?( cmd ) - p = Process.new( - cmd, exec.arg[1..]?, output: STDOUT, error: STDERR, - env: { - "TEXT" => message.params[1], - "CHATUSER" => chatuser - } - ) - else - STDERR.puts( "WARNING: exec.func \"run\" called without argument" ) - end - end - if ( exec.func == "run_shell" ) - if ( cmd = exec.arg[0]? ) - p = Process.new( - cmd, exec.arg[1..]?, output: STDOUT, error: STDERR, shell: true, - env: { - "TEXT" => message.params[1], - "CHATUSER" => chatuser - } - ) - else - STDERR.puts( "WARNING: exec.func \"run_shell\" called without argument" ) - end - next - end - STDERR.puts "WARNING: unhandled function for /#{textregex}/: #{exec.func}" - else - STDOUT.print "DENIED: " - ppe command - end - end - end - end - end - end - else # String - case local_commandmsg - when "testchannelsubs" - pp channelsubs - 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] -# # 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-_]+$/ ) -# gamesurgeircifc.send( { "##{settings["channel"]}", "| #{obs.inputs[match[2]].settings.to_h.to_s} " } ) -# 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/#{match[2]}.webm" ) -# else -# gamesurgeircifc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument." } ) -# end -# # FIXME: This is only half-implemented -# elsif ( ( cmd =~ /^(user)$/ ) && ( mod || own ) ) -# if match[2]? && match[2] =~ /[a-z][a-z0-9_]+/ -# twitchapiifc.send( { "get_user", match[2] } ) -# elsif match[2]? && match[2] =~ /[0-9]+/ -# twitchapiifc.send( { "get_user", match[2].to_u64 } ) -# else -# twitchapiifc.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 ) ) -# gamesurgeircifc.send( { "##{settings["channel"]}", "Title is now \"#{ json["data"][0]["title"] }\""} ) -# else -# json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) ) -# gamesurgeircifc.send( { "##{settings["channel"]}", "| Title is currently \"#{ json["data"][0]["title"] }\""} ) -# end -# elsif ( ( cmd =~ /^(game|category)$/ ) && ( mod || own ) ) -# 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 ) ) -# gamesurgeircifc.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 ) ) -# gamesurgeircifc.send( { "##{settings["channel"]}", "| Game is currently \"#{ json["data"][0]["game_name"] }\""} ) -# end -# elsif ( ( cmd == "urban" ) && ( mod || own || sub || vip ) ) -# if match[2]? && match[2] =~ /^([a-zA-Z0-9 -])+$/ -# definition = urbandef( match[2] ) -# gamesurgeircifc.send( { "##{settings["channel"]}", definition[0,400] } ) -# t2s( t2sifc, settings, userdir, chatuser, definition[0,400] ) -# else -# gamesurgeircifc.send( { "##{settings["channel"]}", "| Urban Dictionary search term should consist of letters, numbers, spaces, and/or hyphens." } ) -# end -# elsif ( cmd =~ /^(shout|shoutout)$/ ) -# if match[2]? && match[2] =~ /^[a-z]+$/ -# gamesurgeircifc.send( { "##{settings["channel"]}", "| Go check out twitch.tv/#{match[2]}"} ) -# effectsmsg( settings, "overlay gltext 5 Go follow #{match[2]}" ) -# else -# gamesurgeircifc.send( { "##{settings["channel"]}", "| Missing argument."} ) -# end -# elsif ( cmd =~ /^(songrequest|sr)$/ ) && match[2]? -# puts ("song detected: #{match[2]}") -# if ( ( match[2] =~ /list=/ ) && ( match[2] !~ /v=/ ) ) -# gamesurgeircifc.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 ) -# gamesurgeircifc.send( { "##{settings["channel"]}", "| " + playlistinfo[ status["playlistlength"].to_i - 1 ]["file"].to_s + " added in position " + status["playlistlength"].to_s } ) -# else -# gamesurgeircifc.send( { "##{settings["channel"]}", "| A failure occured." } ) -# end -# m.disconnect -# else -# end -# elsif ( cmd =~ /^current(|song)$/ ) -# m = MPD::Client.new -# if ( currentsong = m.currentsong ) -# gamesurgeircifc.send( { "##{settings["channel"]}", "| Currently playing: " + currentsong["file"].to_s } ) -# else -# gamesurgeircifc.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 -# gamesurgeircifc.send( { "##{settings["channel"]}", "| " + sprintf( "2%d:2%d", min, sec ) } ) -# else -# gamesurgeircifc.send( { "##{settings["channel"]}", "| An error occurred. " } ) -# end -# m.disconnect -# else -# gamesurgeircifc.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 ) -# gamesurgeircifc.send( { "##{settings["channel"]}", "| Currently playing: " + currentsong["file"].to_s } ) -# else -# gamesurgeircifc.send( { "##{settings["channel"]}", "| A failure occured." } ) -# end -# else -# gamesurgeircifc.send( { "##{settings["channel"]}", "| Playlist is now empty." } ) -# end -# m.disconnect -# elsif ( cmd == "followage" ) -# 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 -# gamesurgeircifc.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 -# gamesurgeircifc.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 -# gamesurgeircifc.send( { "##{settings["channel"]}", "| " + json["data"][0]["followed_at"].to_s } ) -# end -# end - rescue ex - pp ex - puts ex.backtrace - say_all_self_chan( "An error occurred! " + ex.message.to_s ) - end - end - end -ensure - waitgroup.send( Fiber.current.name.not_nil! ) -end -fiber = fiberifc.receive -fibers[fiber.name.not_nil!] = fiber - -def ttsgcs( languagecode : String, voice : String, text : String, gcskey : String ) : Bytes - request = Hash( String, Hash( String, String ) ){ - "input" => { "text" => text }, - "audioConfig" => { "audioEncoding" => "MP3" }, - "voice" => { - "name" => voice, - "languageCode" => languagecode, - }, - } - 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=#{gcskey}", headers, request.to_json, tls: ssl_context ) - - json=JSON.parse(response.body) - return Base64.decode( json["audioContent"].as_s ) -end - -def ttsaws( filepath : Path, voice : String, text : String ) - p = Process.new( - "aws", [ - "polly", "synthesize-speech", - "--output-format", "mp3", - "--voice-id", voice, - "--text", text, - filepath.to_s - ], output: STDOUT, error: STDERR - ) - p.wait -end - -# Put tts stuff into the same fiber so each playback blocks the next -spawn name: "text2speech" do - fiberifc.send( Fiber.current ) - loop do - begin - while t2stuple = t2sifc.receive - puts( "#{Fiber.current.name}: #{t2stuple}" ) - voice, text = [ *t2stuple ] - if ( match = voice.match( /^Microsoft-([A-Za-z]+)/ ) ) - {% 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 ) - playaudiodata( config.tempdir, mp3data ) - elsif fibers["BungmoBott::Socket client"]? - bbscliifc.send( "gcst2s #{voice} #{text}" ) - # The rest of this is dealt with in the BungmoBott::Socket client - else - STDERR.puts( "ERROR: google cloud voice requested, but no gcloud_token or BungmoBott::Socket client is available" ) - end - else - if aws # AWS polly voices - filepath=Path.new( config.tempdir, "#{Time.utc.to_unix_ms}.mp3" ).normalize - ttsaws( filepath, voice, text ) - playaudiofile( filepath ) - File.delete( filepath ) - elsif fibers["BungmoBott::Socket client"]? - bbscliifc.send( "awst2s #{voice} #{text}" ) - # The rest of this is dealt with in the BungmoBott::Socket client - else - STDERR.puts( "ERROR: aws polly voice requested, but no aws CLI executable or BungmoBott::Socket client is available" ) - end - #else # unknown - # STDERR.puts "Voice not recognized or available." - end - end - rescue ex - pp ex - end - end -ensure - waitgroup.send( Fiber.current.name.not_nil! ) -end -fiber = fiberifc.receive -fibers[fiber.name.not_nil!] = fiber - - - -# Twitch API request handling fiber -# FIXME: Implement ratelimiting here. -if twitchclient.is_a?( Twitcr::Client ) - spawn name: "Twitcr::Client" do - fiberifc.send( Fiber.current ) - loop do - begin - while twitchtuple = twitchapiifc.receive - puts( "#{Fiber.current.name}: #{twitchtuple}" ) - cmd, arg = [ *twitchtuple ] - case cmd - when "get_user" - userinfo = JSON.parse( twitchclient.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" - end - userage = ( Time.utc.to_unix - Time::Format::RFC_3339.parse( userinfo["created_at"].as_s ).to_unix ) - if ( userage - 172800 ) < 0 - puts "\033[38;5;1m#{userinfo["login"]}'s account is #{((172800 - userage)/60/60).to_i64} hours old.\033[0m" - end - when "get_followers" - followers = JSON.parse( twitchclient.get_channel_followers( to: arg.to_u64 ) )["total"].as_i64 - if followers > 500 - puts "\033[38;5;2m#{followers} followers\033[0m" - end - end - end - rescue ex - pp ex - end - end - ensure - waitgroup.send( Fiber.current.name.not_nil! ) - end - fiber = fiberifc.receive - fibers[fiber.name.not_nil!] = fiber -end - -if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.join_channels.not_nil!.twitch ) - # Twitch::IRC fiber - spawn name: "Twitch::IRC" do - fiberifc.send( Fiber.current ) - loop do - begin - bot = Twitch::IRC::Client.new( nick: config.chat_user.not_nil!.twitch.not_nil!, token: "oauth:" + secrets.twitch_access_token.not_nil!, log_mode: true ) - bot.tags = [ "membership", "tags", "commands" ] - - # Outgoing IRC message fiber - # "Most IRC servers limit messages to 512 bytes in length, including the trailing CR-LF characters." - # PRIVMSG #channel message\r\n - spawn name: "Twitch::IRC ifc rx" do - while tuple = twitchircifc.receive # why does this need to be a tuple? - puts( "#{Fiber.current.name}: #{tuple}" ) - sizelimit=( 512 - ( tuple[0].size + 12 ) ) - if ( tuple[0] == "JOIN" ) - if ( tuple[0] =~ regextwitchuser ) - bot.join_channel( tuple[1] ) - else - STDERR.puts "Invalid channel name #{ tuple[1] }" - end - 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 name: "Twitch::IRC irc rx" do - #FastIRC::Message( - # @tags={ - # "badge-info" => "", - # "badges" => "moderator/1,bits/100", - # "color" => "", - # "display-name" => "BungMonkey", - # "emotes" => "", - # "first-msg" => "0", - # "flags" => "", - # "id" => "3170330a-66dd-4163-a5cb-5a380abef366", - # "mod" => "1", - # "returning-chatter" => "0", - # "room-id" => "22579666", - # "subscriber" => "0", - # "tmi-sent-ts" => "1692002718705", - # "turbo" => "0", - # "user-id" => "59895482", - # "user-type" => "mod" - # }, - # @prefix=Prefix( - # @source="bungmonkey", - # @user="bungmonkey", - # @host="bungmonkey.tmi.twitch.tv" - # ), - # @command="PRIVMSG", - # @params=[ - # "#tenichi", - # "test" - # ] - #) - -# channelsubs[{ "twitch", message.params[0] }].each do | client | -# if ( ( prefix = message.prefix ) && ( chatuser = prefix.source ) ) # && ( uid = message.tags["user-id"]? ) -# if client.is_a? OpenSSL::SSL::Socket::Server -# client.puts( "msg twitch #{message.params[0]} #{chatuser} #{message.params[1]}" ) -# else # client.is_a? Channel( Tuple( String, FastIRC::Message ) ) -# client.send( { "twitch", message } ) -# end -# end -# end - - commandifc.send( { "twitch", message } ) - pp message - pp message.params - rescue ex - pp ex - #twitchircifc.send( { "##{config.channel}", "An error occurred! " + ex.message.to_s } ) - # Maybe send all error messages out through the API? Have to do channel->client mappings, though. - end - end - - rooms = Array( String ).new - #rooms = config.join_channels.not_nil!.twitch.not_nil! - - # 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 - ensure - waitgroup.send( Fiber.current.name.not_nil! ) - end - fiber = fiberifc.receive - 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 fiber - spawn name: "GameSurge::IRC" do - fiberifc.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 fiber - # "Most IRC servers limit messages to 512 bytes in length, including the trailing CR-LF characters." - # PRIVMSG #channel message\r\n - spawn name: "GameSurge::IRC ifc rx" do - while tuple = gamesurgeircifc.receive # why does this need to be a tuple? - puts( "#{Fiber.current.name}: #{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 name: "GameSurge::IRC irc rx" do - commandifc.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 -# gamesurgeircifc.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 -# gamesurgeircifc.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 -# gamesurgeircifc.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 -# gamesurgeircifc.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 -# gamesurgeircifc.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/ ) -# gamesurgeircifc.send( { "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/tmp/DxDiag.txt" } ) -# end - rescue ex - pp ex - gamesurgeircifc.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 = fiberifc.receive - fibers[fiber.name.not_nil!] = fiber -end - -# BungmoBott::Socket client fiber -if config.bungmobott_connect - bbscli_host, bbscli_port = config.bungmobott_connect.not_nil!.split(":") - spawn name: "BungmoBott::Socket client" do - fiberifc.send( Fiber.current ) - loop do - puts "#{Fiber.current.name} connecting #{config.bungmobott_connect}" - user = config.chat_user.not_nil!.twitch.not_nil! - bungmobott_key = secrets.bungmobott_key.not_nil! - ssl_socket = OpenSSL::SSL::Socket::Client.new( TCPSocket.new( bbscli_host, bbscli_port.to_u16 ), OpenSSL::SSL::Context::Client.new ) - ssl_socket.sync = true - negotiated = false - spawn name: "BungmoBott::Socket client ssl rx" do - while message = ssl_socket.gets - puts "#{Fiber.current.name}: " + message.gsub( bungmobott_key, "CENSORED" ) - if message =~ /^error/i - raise Exception.new("BungmoBott::Socket Error: #{message}") - elsif message =~ /^authed/ - negotiated = true - #ssl_socket.puts( "say twitch #{user} test" ) - elsif ( match = message.match( /^msg (twitch|gamesurge)/ ) ) - commandifc.send( { "#{match[1]}_remote", FastIRC.parse_line( message.split(" ")[2..].join(" ") ) } ) - elsif ( match = message.match( /^awst2s ([0-9]+)/ ) ) - datasize = match[1].to_u32 - audiodata = Bytes.new( datasize ) - ssl_socket.fill_read( audiodata ) - playaudiodata( config.tempdir, audiodata ) - elsif ( match = message.match( /^gcst2s ([0-9]+)/ ) ) - datasize = match[1].to_u32 - audiodata = Bytes.new( datasize ) - ssl_socket.fill_read( audiodata ) - playaudiodata( config.tempdir, audiodata ) - elsif ( match = message.match( /^awsvoicelist (.+)$/ ) ) - match[1].split(" ").each do | voice | - voices[voice.downcase] = voice - end - writevoices() - elsif ( match = message.match( /^gcsvoicelist (.+)$/ ) ) - match[1].split(" ").each do | voice | - voices[voice.downcase] = voice - end - writevoices() - end - end - end - ssl_socket.puts( "auth #{user} #{bungmobott_key}" ) - while input = bbscliifc.receive - puts( "#{Fiber.current.name} ssl tx: #{input}" ) - # ssl_socket gets redefined in the event of I/O errors, so we deal with it here. - ssl_socket.puts( input ) - end - end - rescue ex - pp ex - ensure - waitgroup.send( Fiber.current.name.not_nil! ) - sleep 2 - end - fiber = fiberifc.receive - fibers[fiber.name.not_nil!] = fiber -end - -# BungmoBott::Socket fiber -if config.bungmobott_listen - spawn name: "BungmoBott::Socket server" do - fiberifc.send( Fiber.current ) - ip, port = config.bungmobott_listen.not_nil!.split(":") - tcp_server = TCPServer.new( ip, port.to_i ) - ssl_context = OpenSSL::SSL::Context::Server.new - ssl_context.private_key=(configdir + "/privkey.pem" ) - ssl_context.certificate_chain=(configdir + "/fullchain.pem" ) - # unauthenticated config; seems to be broken in crystal v1.9.2 - # ssl_context = OpenSSL::SSL::Context::Server.insecure() - # ssl_context.add_options(OpenSSL::SSL::Options::ALL) - # ssl_context.security_level=0 - # ssl_context.ciphers=("ADH@SECLEVEL=0") - # ssl_context.ciphers=("ADH-AES256-GCM-SHA384:@SECLEVEL=0") - - # bungmobott protocol server - #while tcp_server.accept? do | clientsocket | - - while clientsocket = tcp_server.accept? - spawn name: "BungmoBott::Socket server tcp rx" do - client = OpenSSL::SSL::Socket::Server.new(clientsocket, ssl_context) - client.flush_on_newline=true - - puts "Connected: #{clientsocket.remote_address}" - connections[client] = Hash(String, String).new - connections[client]["remote_address"] = clientsocket.remote_address.to_s - connections[client]["authed"] = "false" - - while message = client.gets - if ( match = message.match( /^auth (#{regexuser}) (#{regexb64})$/i ) ) - puts "auth #{match[1]} CENSORED" - else - puts message - end - client.puts "RECEIVED " + message - if ( connections[client]["authed"] == "true" ) - # irc - if ( match = message.match( /^irc (#{regexservice}) JOIN \#(#{regexuser}) *$/i ) ) - ircservice = match[2] # regexservice has some parens, too - ircchannel = match[3] - if ircservice == "twitch" - client.puts "joining #{ircservice} \##{ircchannel}" - twitchircifc.send( { "JOIN", ircchannel } ) - unless channelsubs[ { ircservice, "#" + ircchannel } ]? - channelsubs[ { ircservice, "#" + ircchannel } ] = Array( OpenSSL::SSL::Socket::Server ).new - end - channelsubs[ { ircservice, "#" + ircchannel } ].push( client ) - pp channelsubs - elsif ircservice == "gamesurge" - client.puts "joining #{ircservice} \##{ircchannel}" - gamesurgeircifc.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 - if ( match = message.match( /^awsvoicelist$/i ) ) - client.puts "awsvoicelist " + generatevoicelistaws().join(" ") - elsif ( match = message.match( /^awst2s ([a-zA-Z-]+) (.+)/i ) ) - filepath=Path.new( config.tempdir, "#{Time.utc.to_unix_ms}.mp3" ).normalize - ttsaws( filepath, match[1], match[2] ) - mp3datasize = File.size( filepath ) - mp3data = Bytes.new( mp3datasize ) - content = File.open( filepath ) do |file| - file.read( mp3data ) - end - client.puts "awst2s #{mp3data.size}" - STDOUT.puts "SENT: awst2s #{mp3data.size}" - client.unbuffered_write( mp3data ) # Normal writes create TLS records of 4608 bytes. Unbuffered_writes create maximum 16384 byte records that need to be reassembled on the read end. - STDOUT.puts "SENT: mp3data" - end - end - elsif ( message =~ /^gcs/i) - if ( gcloud_token = secrets.gcloud_token ).is_a?( String ) - if ( match = message.match( /^gcsvoicelist$/i ) ) - client.puts "gcsvoicelist " + generatevoicelistgcs( ( gcloud_token ) ).join(" ") - elsif ( match = message.match( /^gcst2s (([a-zA-Z]{2,3}-[a-zA-Z]{2})[a-zA-Z0-9-]+) (.+)/i ) ) - mp3data = ttsgcs( match[2], match[1], match[3], gcloud_token ) - client.puts "gcst2s #{mp3data.size}" - STDOUT.puts "SENT: gcst2s #{mp3data.size}" - client.unbuffered_write( mp3data ) - STDOUT.puts "SENT: mp3data" - end - else - client.puts "ERROR: gcloud_token missing, gcs commands disabled." - end - elsif ( match = message.match( /^say (twitch|gamesurge) (.+)/i ) ) - say( match[1], connections[client]["user"], match[2] ) - elsif ( message =~ /testchannelsubs/ ) - #commandifc.send( "testchannelsubs" ) - end - - else - # auth - if ( match = message.match( /^auth (#{regexuser}) (#{regexb64})$/i ) ) - remoteuser = match[1] - remotekey = match[2] - if File.exists?( config.statedir + "apikeys/" + remoteuser ) - File.each_line( config.statedir + "apikeys/" + remoteuser ) do |localkey| - if ( localkey == remotekey ) - connections[client]["authed"] = "true" - puts "authed #{ remoteuser }" - client.puts "authed #{ remoteuser }" - connections[client]["user"] = remoteuser - connections[client]["key"] = remotekey - break - else - puts "WARNING: auth failure: #{localkey} did not match #{remotekey}" - # maybe quiet this down once users start using multiple keys - end - end - end - if ( connections[client]["authed"] == "false" ) - client.puts "error: auth failure" - end - else - client.puts "must auth [user] [key]" - end - end - end - rescue ex : IO::Error - pp ex - next - rescue ex - pp ex - next - ensure - connections.delete( client ) - channelsubs.each_key do |key| - channelsubs[key].delete( client ) - end - puts "Disconnected: #{clientsocket.remote_address}" - end - end - rescue ex - pp ex - ensure - waitgroup.send( Fiber.current.name.not_nil! ) - end - fiber = fiberifc.receive - fibers[fiber.name.not_nil!] = fiber -end - -if config.join_channels - spawn name: "join_channels" do - fiberifc.send( Fiber.current ) - if fibers["GameSurge::IRC"]? && config.join_channels.not_nil!.gamesurge - ircservice = "gamesurge" - config.join_channels.not_nil!.gamesurge.not_nil!.each do |ircchannel| - gamesurgeircifc.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"]? && config.join_channels.not_nil!.gamesurge - ircservice = "gamesurge" - config.join_channels.not_nil!.gamesurge.not_nil!.each do |ircchannel| - bbscliifc.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| - twitchircifc.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"]? && config.join_channels.not_nil!.twitch - ircservice = "twitch" - config.join_channels.not_nil!.twitch.not_nil!.each do |ircchannel| - bbscliifc.send( "irc twitch JOIN \#" + ircchannel ) - end - end - ensure - waitgroup.send( Fiber.current.name.not_nil! ) - end - fiber = fiberifc.receive - fibers[fiber.name.not_nil!] = fiber -end - -puts "Spawned fibers:" -pp fibers.keys - -fibers.size.times do - fiber = waitgroup.receive - fibers.delete( fiber ) - puts "Fiber ended: " + fiber -end - |