summaryrefslogtreecommitdiff
path: root/crystal/tcpsocket.cr
diff options
context:
space:
mode:
authorJoe Rayhawk <jrayhawk@fairlystable.org>2024-02-27 03:23:30 -0800
committerJoe Rayhawk <jrayhawk@fairlystable.org>2024-02-27 03:23:30 -0800
commit77b74b745aaa84fe342d56f396bc0491cea0859b (patch)
tree80faee6a10d6e3b5118464380edb0564afc283ec /crystal/tcpsocket.cr
parent061b5e956e422d585f8733e8079c60a625c6d27f (diff)
downloadtwitchtools-77b74b745aaa84fe342d56f396bc0491cea0859b.tar.gz
twitchtools-77b74b745aaa84fe342d56f396bc0491cea0859b.zip
crystal/tcpsocket: promote new codebase to bungmobott
Diffstat (limited to 'crystal/tcpsocket.cr')
-rw-r--r--crystal/tcpsocket.cr1832
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
-