summaryrefslogtreecommitdiff
path: root/crystal
diff options
context:
space:
mode:
authorJoe Rayhawk <jrayhawk+git@omgwallhack.org>2024-01-11 17:37:20 -0800
committerJoe Rayhawk <jrayhawk+git@omgwallhack.org>2024-01-11 17:37:20 -0800
commit24197ba7c6709498e930b6e25ec4d7d30a4e33e9 (patch)
treeec59798af8114a6ea5b845dde38ded3de6eb4426 /crystal
parent11b8b62fd544479c07f34c57aedd807de53308c4 (diff)
downloadtwitchtools-24197ba7c6709498e930b6e25ec4d7d30a4e33e9.tar.gz
twitchtools-24197ba7c6709498e930b6e25ec4d7d30a4e33e9.zip
crystal/tcpsocket.cr: Get irc message transport working
Diffstat (limited to 'crystal')
-rw-r--r--crystal/lib/bungmobott/src/bungmobott.cr39
-rw-r--r--crystal/tcpsocket.cr1058
2 files changed, 1052 insertions, 45 deletions
diff --git a/crystal/lib/bungmobott/src/bungmobott.cr b/crystal/lib/bungmobott/src/bungmobott.cr
index d54eaad..8c7f755 100644
--- a/crystal/lib/bungmobott/src/bungmobott.cr
+++ b/crystal/lib/bungmobott/src/bungmobott.cr
@@ -24,7 +24,25 @@ module BungmoBott
property twitch : String?
property gamesurge : String?
end
-
+
+# Do we turn this into a class? Maybe an Enum?
+# class Permissions
+# include YAML::Serializable
+# include YAML::Serializable::Unmapped
+# property any
+# property sub
+# property mod
+# property vip
+# end
+
+ class Commands
+ include YAML::Serializable
+ include YAML::Serializable::Unmapped
+ property perm : Array( String )? = nil
+ property func : String
+ property arg : Array( String? )? = nil
+ end
+
class JoinChannels
include YAML::Serializable
include YAML::Serializable::Unmapped
@@ -50,10 +68,23 @@ module BungmoBott
) # FIXME: do sockets and such even work on Windows?
# Should probably warn about that somewhere around here.
#@[YAML::Field(emit_null: true)]
- property listen : String? = nil
+ property bungmobott_listen : String? = nil
+ property bungmobott_connect : String? = nil
+ #@[YAML::Field(emit_null: true)]
+ property obs_connect : String? = nil
property chat_user : ChatUser? = ChatUser.from_yaml("---")
property join_channels : JoinChannels? = JoinChannels.from_yaml("---")
+ property commands : Hash( String, Array( Commands ) )? = nil
+ @[YAML::Field(emit_null: true)]
property twitch_user_id : UInt32? = nil
+ @[YAML::Field(emit_null: true)]
+ property voice_list : String? = nil
+
+# def after_initialize()
+# if @voice_list?
+# # @voice_list = @statedir + "/voices.txt"
+# end
+# end
end
class Secrets
include YAML::Serializable
@@ -66,6 +97,10 @@ module BungmoBott
property gcloud_token : String?
@[YAML::Field(emit_null: true)]
property gamesurge_password : String?
+ @[YAML::Field(emit_null: true)]
+ property obs_password : String?
+ @[YAML::Field(emit_null: true)]
+ property bungmobott_key : String?
end
end
diff --git a/crystal/tcpsocket.cr b/crystal/tcpsocket.cr
index b4ee6a8..f97169e 100644
--- a/crystal/tcpsocket.cr
+++ b/crystal/tcpsocket.cr
@@ -22,6 +22,22 @@ struct Nil
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
@@ -40,25 +56,31 @@ ENV["XDG_CONFIG_HOME"]? && ( configdir = ENV["XDG_CONFIG_HOME"] +
ENV["LOCALAPPDATA"]? && ( configdir = Path.windows( ENV["LOCALAPPDATA"] + "\\#{EXE}\\" ).to_s )
# cygwin will have both, but should probably use LOCALAPPDATA
-def writeconfig( configdir : String, file : String, contents : String )
- Dir.mkdir_p( configdir )
- File.write( configdir + file, contents )
+def writeconfig( filepath : Path, contents : String )
+ Dir.mkdir_p( filepath.parent )
+ File.write( filepath, contents )
rescue exio : IO::Error
- puts "ERROR: Unable to write #{ configdir + file }.txt: #{exio.message}"
+ puts "ERROR: Unable to write #{ filepath }: #{exio.message}"
exit 7
end
-configfile = configdir + "config.txt"
+configfile = Path[configdir + "config.txt"].normalize
+if ARGV.size >= 1
+ configfile = Path[ ARGV[0] ].normalize
+end
+puts configfile
+
if File.exists?( configfile )
- config = BungmoBott::Config.from_yaml( File.read( configdir + "config.txt" ) )
+ config = BungmoBott::Config.from_yaml( File.read( configfile ) )
else
config = BungmoBott::Config.from_yaml("---")
STDERR.puts "WARNING: #{configfile} not found. Writing new one."
- writeconfig( configdir, "config.txt", config.to_yaml )
+ 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
@@ -74,14 +96,17 @@ if ( config.join_channels && ! config.join_channels.not_nil!.yaml_unmapped.empty
ppe config.join_channels.not_nil!.yaml_unmapped
end
-secretsfile = configdir + "secrets.txt"
+secretsfile = Path[ configdir + "secrets.txt" ].normalize
+if ARGV.size >= 2
+ secretsfile = Path[ ARGV[1] ].normalize
+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( configdir, "secrets.txt", secrets.to_yaml )
+ writeconfig( secretsfile, secrets.to_yaml )
end
unless secrets.yaml_unmapped.empty?
@@ -89,6 +114,35 @@ unless secrets.yaml_unmapped.empty?
ppe secrets.yaml_unmapped.keys
end
+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
+ 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?
@@ -132,6 +186,110 @@ else
aws = true
end
+# enable microsoft speech services?
+mss : Bool
+{% if flag?(:windows) %}
+ mss = true
+{% else %}
+ mss = false
+{% end %}
+
+if ( mss || gcloud || aws ) && config.voice_list
+ text2speech = true
+else
+ text2speech = false
+end
+
+lastvoice = Array(String).new
+
+# channel message subscriptions: { service, chan } => [ client, client ]
+#channelsubs = Hash( Tuple( String, String ), Array( OpenSSL::SSL::Socket::Server | Channel( Tuple( String, FastIRC::Message ) ) ) ).new
+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
+ircipc = Channel( Tuple( String, String ) ).new
+# commandircipc: serv, chan, user, msg
+commandircipc = Channel( Tuple( String, FastIRC::Message ) ).new
+commandipc = Channel( String ).new
+# t2sipc: voice, text
+t2sipc = Channel( Tuple( String, String ) ).new
+twitchipc = Channel( Tuple( String, String ) ).new
+bbscliipc = Channel( String ).new
+bbssrvipc = Channel( String ).new
+waitgroup = Channel( String ).new
+
+
+fiberipc = 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}/" )
+ # OBS event thread
+ spawn name: "obs_event_thread" do
+ fiberipc.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 do
+ d = json # Copy *immediately*
+ case d["eventType"].as_s
+ when "CurrentProgramSceneChanged"
+ say( twitchipc, config.chat_user.not_nil!.twitch.not_nil!, "| 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( twitchipc, config.chat_user.not_nil!.twitch.not_nil!, "| 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( twitchipc, config.chat_user.not_nil!.twitch.not_nil!, "| 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
+ puts ex
+ ensure
+ waitgroup.send( Fiber.current.name.not_nil! )
+ end
+ fiber = fiberipc.receive
+ fibers[fiber.name.not_nil!] = fiber
+end
+
# enable effects?
#effects = Hash( String, String ).new
#if File.exists?( config.configdir + "/effects.txt" )
@@ -166,6 +324,50 @@ end
# 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
+
+def getvoice( configdir : String, userdir : String, chatuser : String )
+ if File.exists?( userdir + "/voice" )
+ voice_output = File.read( userdir + "/voice" ).chomp
+ voice_setting = voice_output
+ else
+ voice_output = File.read( configdir + "/voicelist.txt" ).chomp.split( "\n" ).sample( 1 )[0].chomp
+ voice_setting = "random"
+ end
+ if File.exists?( userdir + "/voicesub" )
+ namesub = File.read( userdir + "/voicesub" ).chomp
+ else
+ namesub = chatuser
+ end
+ return( [namesub, voice_setting, voice_output ] )
+end
+
def generatevoicelistaws( )
voices = Array(String).new
JSON.parse( `aws polly describe-voices` )["Voices"].as_a.each do | v |
@@ -230,16 +432,676 @@ def regeneratevoicelist( defaultsettings : Hash( String, String ), aws : Bool, g
return voices
end
-# channel message 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
+def playaudiofile( filepath : String )
+ p = Process.new(
+ "powershell.exe",
+ [ "-Command", "#Set-PSDebug -Trace 1;
+ Add-Type -AssemblyName presentationCore;
+ $player = New-Object system.windows.media.mediaplayer;
+ $player.open(\"#{filepath}\");
+ $player.volume = .99;
+ $player.play();
+ Start-Sleep -Milliseconds 1000;
+ $duration = $player.NaturalDuration.TimeSpan.TotalMilliseconds;
+ Start-Sleep -Milliseconds ($duration - 1000 );
+ "],
+ output: STDOUT, error: STDERR
+ )
+ # https://geekeefy.wordpress.com/2016/07/19/powershellmediaplayer/ has some ideas
+ p.wait
+end
+
+def userlog( ircipc : Channel, config : BungmoBott::Config, message : FastIRC::Message )
+ unless ( ( prefix = message.prefix ) && ( chatuser = prefix.source ) && ( uid = message.tags["user-id"]? ) )
+ return nil
+ end
+ basedir = config.statedir
+ userdir = basedir + "/uids/" + uid
+ if File.directory?( userdir )
+ lastseen = File.info( userdir ).modification_time.to_unix
+ else
+ lastseen = 0
+ end
+ Dir.mkdir_p( basedir + "/names" )
+ Dir.mkdir_p( userdir + "/names" )
+ File.touch( userdir )
+ unless testrefuser2uid( userdir + "/names/" + chatuser )
+ namelatest = ""
+ datelatest = Time::UNIX_EPOCH
+ Dir.each_child( userdir + "/names/" ) do |name|
+ namedate = File.info( userdir + "/names/" + name ).modification_time
+ if namedate > datelatest
+ namelatest = name
+ datelatest = namedate
+ end
+ end
+ unless namelatest.empty?
+ ircipc.send( { "##{config.chat_user.not_nil!.twitch}", "Rename detected: #{uid}: #{namelatest} -> #{chatuser}" } )
+ end
+ genrefuser2uid( userdir + "/names/" + chatuser, uid, 3 )
+ end
+ unless testrefuser2uid( basedir + "/names/" + chatuser )
+ genrefuser2uid( basedir + "/names/" + chatuser, uid, 1 )
+ end
+ unless ( message.params[0] == "##{config.chat_user.not_nil!.twitch}" )
+ return nil
+ end
+ return ( { chatuser, uid, userdir, lastseen } )
+rescue ex
+ puts ex
+end
+
+# IRC say
+# does this function have a reason to exist?
+def say( ipc : Channel( Tuple( String, String ) ), channel : String, text : String )
+ ipc.send( { "#" + channel, text } )
+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
+ puts ex
+end
+
+def t2s( t2sipc : Channel, config : BungmoBott::Config, userdir : String, 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) %}
+ t2sipc.send( { voice, "#{namesub} #{text}" } )
+ {% else %}
+ t2smsg( config, "#{voice} #{namesub} #{text}" )
+ {% end %}
+ return( voice )
+ else
+ return( nil )
+ end
+end
+
+spawn name: "command_dispatch" do
+ fiberipc.send( Fiber.current )
+ loop do
+ while commandmsg : Tuple( String, FastIRC::Message ) = commandircipc.receive
+ spawn do
+ service = String.new
+ local_commandmsg = 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.
+ #bbssrvipc.send( "#{message.to_s}" )
+ if channelsubs[ { service, ircchannel } ]?
+ channelsubs[ { service, ircchannel } ].each do |channelsub|
+ if channelsub.is_a? OpenSSL::SSL::Socket::Server
+ channelsub.puts( "msg twitch #{message.to_s}" )
+ # don't think this is ever useful
+ #else # client.is_a? Channel( Tuple( String, FastIRC::Message ) )
+ # client.send( { "twitch", message } )
+ end
+ end
+ end
+ end
+
+ next unless ( userlogreturn = userlog( twitchipc, config, message ) )
+ chatuser, uid, userdir, lastseen = userlogreturn
+ if config.voice_list
+ if ( t2sreturn = t2s( t2sipc, config, userdir, chatuser, message.params[1] ) )
+ lastvoice.insert( 0, t2sreturn )
+ lastvoice = lastvoice[0..4]
+ end
+ else
+ STDERR.puts "WARNING: config.voice_list empty"
+ end
+ # Have we seen this user lately?
+ if ( ( Time.utc.to_unix - lastseen ) >= 14400 )
+ twitchipc.send( { "get_user", uid.to_s } )
+ twitchipc.send( { "get_followers", uid.to_s } )
+ prevnames = Array( String ).new
+ if ( prevnames = Dir.children( config.statedir + "/uids/" + uid + "/names" ) ) && ( prevnames.size > 1 )
+ prevnames.delete( chatuser )
+ puts "\033[38;5;14m#{chatuser} previous names: #{prevnames.join(", ")}\033[0m"
+ end
+ # play random fanfare if available.
+ {% if flag?(:windows) %}
+ # This file hierarchy gets manually set up for now.
+ # Maybe someday let mods do something like:
+ # !fanfare add pipne https://pip.ne/sniff.mp3
+ if File.exists?( userdir + "/fanfare/" )
+ playaudiofile( userdir + "/fanfare/" + Dir.children( userdir + "/fanfare/" ).sample )
+ end
+ {% else %}
+ t2smsg( config, "fanfare #{uid} #{chatuser}" )
+ {% end %}
+ end
+ # FIXME: Generalize this across Twitch/IRC? Mods are +o? vips are +v?
+ # FIXME: Add configuration interface for this
+ own = ( message.tags["room-id"] == message.tags["user-id"] )
+ vip = ( message.tags["badges"].to_s.matches?( /(^|,)vip\// ) )
+ mod = ( message.tags["mod"] == "1" )
+ sub = ( message.tags["subscriber"] == "1" )
+ # 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.commands && config.commands.not_nil!.each do | commandregex, command | # Hash( String, Array )
+ if Regex.new( commandregex ).match( message.params[1] )
+ command.each do |exec| # Array
+ if (
+ ( own || ( exec.perm && exec.perm.not_nil!.includes?("any") ) ) ||
+ ( mod && ( exec.perm && exec.perm.not_nil!.includes?("mod") ) ) ||
+ ( sub && ( exec.perm && exec.perm.not_nil!.includes?("sub") ) ) ||
+ ( vip && ( exec.perm && exec.perm.not_nil!.includes?("vip") ) )
+ )
+ if obs
+ # FIXME: better validate args
+ ( exec.func == "obs_random_source_enable" ) && obsrandommediaenable( obs, exec.arg.not_nil![0].not_nil! ) && next
+ # ( exec.func == "obs_stats" ) && obs_stats( obs )
+ end
+ if text2speech
+ if ( exec.func == "text_to_speech" )
+ if ( t2sreturn = t2s( t2sipc, config, userdir, chatuser, message.params[1] ) )
+ lastvoice.insert( 0, t2sreturn )
+ lastvoice = lastvoice[0..4]
+ end
+ next
+ end
+ end
+ # FIXME: add gamesurge support
+ ( exec.func == "say" ) && say( twitchipc, config.chat_user.not_nil!.twitch.not_nil!, exec.arg.not_nil![0].not_nil! ) && next
+ STDERR.puts "WARNING: unhandled function for /#{commandregex}/: #{exec.func}"
+ else
+ STDOUT.print "DENIED: "
+ ppe command
+ 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]
+# if ( ( cmd =~ /^(substitute|voicesub)$/ ) && ( mod || sub ) )
+# case message.params[1].split( " " ).size
+# when 1
+# if File.exists?( userdir + "/voicesub" )
+# bot.message( "##{settings["channel"]}", "| Current name substitution is \"#{File.read( userdir + "/voicesub" )}\"." )
+# else
+# bot.message( "##{settings["channel"]}", "| Current name substitution is disabled." )
+# end
+# else
+# if match[2]?
+# voicesub = match[2].downcase
+# if voicesub =~ /^(disabled|null|disable|none)$/
+# if File.exists?( userdir + "/voicesub" )
+# File.delete( userdir + "/voicesub" )
+# bot.message( "##{settings["channel"]}", "| Name substitution for #{chatuser} is now disabled." )
+# else
+# bot.message( "##{settings["channel"]}", "| Name substitution for #{chatuser} is already disabled." )
+# end
+# else
+# Dir.mkdir_p( userdir )
+# File.write( userdir + "/voicesub", voicesub )
+# bot.message( "##{settings["channel"]}", "| Name substitution for #{chatuser} is now \"#{File.read( userdir + "/voicesub" )}\"." )
+# end
+# end
+# end
+# elsif ( ( cmd == "lastvoice" ) && ( mod || sub ) )
+# unless lastvoice.empty?
+# bot.message( "##{settings["channel"]}", "| Last voices were " + lastvoice.join( ", " ) )
+# else
+# bot.message( "##{settings["channel"]}", "| No voices used so far." )
+# end
+# elsif ( ( cmd == "regeneratevoicelist" ) && ( own ) )
+# voices = regeneratevoicelist( settings, aws, gcloud )
+# ircipc.send( { "##{settings["channel"]}", "| Regenerated voicelist." } )
+# elsif ( ( cmd =~ /^(setvoice|voice)$/ ) && ( mod || sub ) )
+# case message.params[1].split( " " ).size
+# when 1
+# namesub, voice_setting, voice_output = getvoice( config.voice_list_file, userdir, chatuser )
+# bot.message( "##{settings["channel"]}", "| Current voice is #{voice_setting}" )
+# when 2
+# if match[2]?
+# voice = match[2].downcase
+# if voice =~ /disabled|null|disable|none|random/
+# if File.exists?( userdir + "/voice" )
+# File.delete( userdir + "/voice" )
+# bot.message( "##{settings["channel"]}", "| Voice for #{chatuser} is now random." )
+# else
+# bot.message( "##{settings["channel"]}", "| Voice for #{chatuser} is already random." )
+# end
+# elsif voices.has_key?( voice )
+# csvoice = voices[voice]
+# Dir.mkdir_p( userdir )
+# File.write( userdir + "/voice", csvoice )
+# pp userdir
+# bot.message( "##{settings["channel"]}", "| Voice for #{chatuser} is now #{File.read( userdir + "/voice" )}." )
+# else
+# pp ( match )
+# bot.message( "##{settings["channel"]}", "| Invalid voice. To see list, use !voices" )
+# end
+# end
+# when 3
+# else
+# end
+# elsif ( cmd =~ /^uptime$/ )
+# stats = obs.stats.to_h
+# ostatus = obs.outputs["adv_stream"].status.to_h
+# ircipc.send( { "##{settings["channel"]}", "| #{ostatus["outputTimecode"].to_s[0..7]} #{stats["activeFps"].to_s[0,2]}fps Usage: #{stats["cpuUsage"].as(Float64).to_i64}% #{stats["memoryUsage"].as(Float64).to_i64}MiB Frame losses: #{stats["outputSkippedFrames"].as(Int64)} #{stats["renderSkippedFrames"].as(Int64)}" } )
+# # !newimage [name] [url] (random|top|bottom|left|right|center)
+# # download url, verify mimetype with libmagic, duplicate existing medialoop source, change "file" SourceSetting
+# elsif ( ( cmd =~ /^inputsetting(|s)$/ ) && ( mod || own || vip ) )
+# if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ )
+# ircipc.send( { "##{settings["channel"]}", "| #{obs.inputs[match[2]].settings.to_h.to_s} " } )
+# end
+# elsif ( ( cmd =~ /^input(|s)$/ ) && ( mod || own || vip ) )
+# ircipc.send( { "##{settings["channel"]}", "| inputs: #{obs.inputs.to_h.keys.join(" ")}" } )
+# elsif ( ( cmd =~ /^scene(|s)$/ ) && ( mod || own || vip ) )
+# if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ )
+# obs.scenes[match[2]].program!
+# else
+# ircipc.send( { "##{settings["channel"]}", "| scenes: #{obs.scenes.to_h.keys.join(" ")}" } )
+# end
+# elsif ( ( cmd =~ /^source(|s)$/ ) && ( mod || own || vip ) )
+# request = Hash( String, String | Bool ).new
+# if ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ )
+# obs.scenes.current.metascene[match[2]][0].toggle!
+# # in studio mode, direct Scene->SceneItem toggles require a transition
+# obs.scenes.current.preview!
+# obs.transition!
+# else
+# ircipc.send( { "##{settings["channel"]}", "| current sources: #{obs.scenes.current.metascene.keys.join(" ")}" } )
+# end
+# elsif ( ( cmd =~ /^filter(|s)$/ ) && ( mod || own ) )
+# if ( match[2]? ) && ( filterargs = match[2].match( /^([a-zA-Z0-9-_]+) +([a-zA-Z0-9-_]+)/ ) )
+# obs.sources.to_h[filterargs[1]].filters[filterargs[2]].toggle!
+# elsif ( match[2]? && match[2] =~ /^[a-zA-Z0-9-_]+$/ )
+# ircipc.send( { "##{settings["channel"]}", "| #{match[2]} filters: #{obs.sources.to_h[match[2]].filters.to_h.keys.join(" ")}" } )
+# else
+# ircipc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument, and optionally one filter name to toggle on or off." } )
+# end
+# elsif ( cmd == "create" && ( mod || own || vip ) )
+# if ( match[2]? ) && ( match[2] =~ /^([\/a-zA-Z0-9-_]+)/ )
+# obstemporarymediacreate( obs, "meta-foreground", match[2], "C:/cygwin64/home/user/effects/#{match[2]}.webm" )
+# else
+# ircipc.send( { "##{settings["channel"]}", "Must provide at least one source name as argument." } )
+# end
+# elsif ( cmd =~ /^(metaminute|youdied)$/ )
+# obsrandommediaenable( obs, "media-#{cmd}" )
+# elsif ( cmd =~ /^fart(s|)$/ ) && ( sub || mod || own || vip )
+# if effects.values.select( /^farts/ ).size > 0
+# if ( match[2]? ) && ( match[2].match( /^([0-9]+)/ ) && ( match[2].to_i > 1 ) && ( match[2].to_i <= 32 ) )
+# count = match[2].to_i
+# else
+# count = 1
+# end
+# effects.values.select( /^farts/ ).sample( count ).each do | effect |
+# obstemporarymediacreate( obs, "meta-foreground", effect.gsub(/[\/ ]/, '_').downcase, "C:/cygwin64/home/user/effects/#{effect}" )
+# end
+# else
+# puts "farts undefined"
+# end
+# elsif ( cmd =~ /^explosion(s|)$/ ) && ( sub || mod || own || vip )
+# if effects.values.select( /^explosions/ ).size > 0
+# if ( match[2]? ) && ( match[2].match( /^([0-9]+)/ ) && ( match[2].to_i > 1 ) && ( match[2].to_i <= 32 ) )
+# count = match[2].to_i
+# else
+# count = 1
+# end
+# effects.values.select( /^explosions/ ).sample( count ).each do | effect |
+# obstemporarymediacreate( obs, "meta-foreground", effect.gsub(/[\/ ]/, '_').downcase, "C:/cygwin64/home/user/effects/#{effect}" )
+# end
+# else
+# puts "explosions undefined"
+# end
+# # FIXME: This is only half-implemented
+# elsif ( ( cmd =~ /^(user)$/ ) && ( mod || own ) )
+# if match[2]? && match[2] =~ /[a-z][a-z0-9_]+/
+# twitchipc.send( { "get_user", match[2] } )
+# elsif match[2]? && match[2] =~ /[0-9]+/
+# twitchipc.send( { "get_user", match[2].to_u64 } )
+# else
+# twitchipc.send( { "get_user", settings["channel_id"].to_u64 } )
+# end
+# elsif ( ( cmd =~ /^(status|title)$/ ) && ( mod || own ) )
+# if match[2]?
+# client.put_channel!( settings["channel_id"].to_u64, title: match[2] )
+# json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
+# ircipc.send( { "##{settings["channel"]}", "Title is now \"#{ json["data"][0]["title"] }\""} )
+# else
+# json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
+# ircipc.send( { "##{settings["channel"]}", "| Title is currently \"#{ json["data"][0]["title"] }\""} )
+# end
+# 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 ) )
+# ircipc.send( { "##{settings["channel"]}", "Game is now \"#{ json["data"][0]["game_name"] }\""} )
+# else
+# puts "1 matches"
+# json = JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) )
+# ircipc.send( { "##{settings["channel"]}", "| Game is currently \"#{ json["data"][0]["game_name"] }\""} )
+# end
+# elsif ( ( cmd == "urban" ) && ( mod || own || sub || vip ) )
+# if match[2]? && match[2] =~ /^([a-zA-Z0-9 -])+$/
+# definition = urbandef( match[2] )
+# ircipc.send( { "##{settings["channel"]}", definition[0,400] } )
+# t2s( t2sipc, settings, userdir, chatuser, definition[0,400] )
+# else
+# ircipc.send( { "##{settings["channel"]}", "| Urban Dictionary search term should consist of letters, numbers, spaces, and/or hyphens." } )
+# end
+# elsif ( cmd == "matrix" )
+# effectsmsg( settings, "overlay glmatrix" )
+# elsif ( cmd =~ /juggle|juggler/ )
+# effectsmsg( settings, "overlay juggler3d" )
+# elsif ( cmd =~ /fireworks|firework/ )
+# effectsmsg( settings, "overlay fireworkx" )
+# elsif ( cmd == "pipes" )
+# if match[2]? && match[2] =~ /^(fast|faster)$/
+# effectsmsg( settings, "overlay pipes " + match[2] )
+# else
+# effectsmsg( settings, "overlay pipes" )
+# end
+# elsif ( cmd == "jellyfish" )
+# effectsmsg( settings, "overlay hydrostat" )
+# elsif ( cmd == "gluten" )
+# effectsmsg( settings, "overlay flyingtoasters" )
+# elsif ( cmd =~ /^(glsnake|glmatrix|gibson|xmatrix|flyingtoasters|moebiusgears|fireworkx|hydrostat|hypertorus|jigsaw|juggler3d|kaleidocycle|kumppa|molecule|noof|polyhedra)$/ )
+# effectsmsg( settings, "overlay " + cmd )
+# elsif ( cmd =~ /gltext|cowsay|xcowsay|cowfuscious/ )
+# ( cmd == "cowfuscious" ) && ( cmd = "xcowsay" )
+# if ( match[2]? ) && ( gltextargs = match[2].match( /^([0-9]+) +(.+)$/ ) )
+# seconds=UInt64.new( 1 )
+# seconds=gltextargs[1].to_u64
+# if ( own || mod || vip )
+# effectsmsg( settings, "overlay #{cmd} #{match[2]}" )
+# puts "matched #{cmd} #{match[2]}"
+# elsif ( sub )
+# if ( seconds > 20 )
+# seconds=20
+# end
+# effectsmsg( settings, "overlay #{cmd} #{seconds} #{gltextargs[2]}" )
+# puts "matched #{cmd} #{seconds} #{gltextargs[2]}"
+# else
+# if ( seconds > 5 )
+# seconds=5
+# end
+# effectsmsg( settings, "overlay #{cmd} #{seconds} #{gltextargs[2]}" )
+# puts "matched #{cmd} #{seconds} #{gltextargs[2]}"
+# end
+# elsif match[2]? && match[2] =~ /^.+$/
+# effectsmsg( settings, "overlay #{cmd} #{match[2]}" )
+# puts "matched #{cmd} #{match[2]}"
+# else
+# effectsmsg( settings, "overlay #{cmd}" )
+# puts "failed to match gltext"
+# end
+# elsif ( cmd =~ /^(cow(s|)|cowabunga|holycow$)/ )
+# if match[2]? && match[2] =~ /^([0-9])+$/
+# effectsmsg( settings, "overlay bouncingcow #{match[2]}" )
+# else
+# effectsmsg( settings, "overlay bouncingcow" )
+# end
+# elsif ( cmd == "overlay" )
+# if match[2]? && match[2] =~ /^[a-z]+$/
+# ircipc.send( { "##{settings["channel"]}", "| overlay requires an argument consisting wholly of lower case characters."} )
+# effectsmsg( settings, "overlay #{match[2]}" )
+# end
+# elsif ( cmd =~ /^(shout|shoutout)$/ )
+# if match[2]? && match[2] =~ /^[a-z]+$/
+# ircipc.send( { "##{settings["channel"]}", "| Go check out twitch.tv/#{match[2]}"} )
+# effectsmsg( settings, "overlay gltext 5 Go follow #{match[2]}" )
+# else
+# ircipc.send( { "##{settings["channel"]}", "| Missing argument."} )
+# end
+# elsif ( cmd =~ /^(system|dxdiag|computer)$/ ) && ( Crystal::DESCRIPTION =~ /linux/ )
+# ircipc.send( { "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/tmp/DxDiag.txt" } )
+# elsif ( cmd == "hackerman" )
+# ircipc.send( { "##{settings["channel"]}", "| https://bungmonkey.omgwallhack.org/img/hackerman.jpg" } )
+# elsif ( cmd =~ /^(songrequest|sr)$/ ) && match[2]?
+# puts ("song detected: #{match[2]}")
+# if ( ( match[2] =~ /list=/ ) && ( match[2] !~ /v=/ ) )
+# ircipc.send( { "##{settings["channel"]}", "| Lists are not accepted.\n" } )
+# elsif Process.run( "sraddsong.sh", {match[2]} )
+# m = MPD::Client.new
+# currentsong = m.currentsong
+# if ( currentsong ) && ( currentsong["file"].to_s == "http://music/music.ogg" )
+# m.next
+# end
+# if ( status = m.status ) && ( playlistinfo = m.playlistinfo )
+# ircipc.send( { "##{settings["channel"]}", "| " + playlistinfo[ status["playlistlength"].to_i - 1 ]["file"].to_s + " added in position " + status["playlistlength"].to_s } )
+# else
+# ircipc.send( { "##{settings["channel"]}", "| A failure occured." } )
+# end
+# m.disconnect
+# else
+# end
+# elsif ( cmd =~ /^current(|song)$/ )
+# m = MPD::Client.new
+# if ( currentsong = m.currentsong )
+# ircipc.send( { "##{settings["channel"]}", "| Currently playing: " + currentsong["file"].to_s } )
+# else
+# ircipc.send( { "##{settings["channel"]}", "| A failure occured." } )
+# end
+# m.disconnect
+# elsif ( cmd =~ /^seek$/ )
+# if ( ( match[2] ) && ( match[2] =~ /([+-]|)[0-9]/ ) )
+# m = MPD::Client.new
+# m.seekcur( match[2] )
+# if ( ( musicstatus = m.status ) && ( pos = musicstatus["elapsed"].to_f.to_u64 ) )
+# min=( pos / 60 ).to_u64
+# sec=( pos % 60 ).to_u64
+# ircipc.send( { "##{settings["channel"]}", "| " + sprintf( "2%d:2%d", min, sec ) } )
+# else
+# ircipc.send( { "##{settings["channel"]}", "| An error occurred. " } )
+# end
+# m.disconnect
+# else
+# ircipc.send( { "##{settings["channel"]}", "| Seek requires an argument of an absolute position in integer number of seconds, or a relative position in in signed integer number of seconds such as +60" } )
+# end
+# elsif ( cmd =~ /^next(|song)$/ )
+# m = MPD::Client.new
+# m.next
+# if ( status = m.status ) && ( status["playlistlength"].to_i > 0 )
+# if ( currentsong = m.nextsong )
+# ircipc.send( { "##{settings["channel"]}", "| Currently playing: " + currentsong["file"].to_s } )
+# else
+# ircipc.send( { "##{settings["channel"]}", "| A failure occured." } )
+# end
+# else
+# ircipc.send( { "##{settings["channel"]}", "| Playlist is now empty." } )
+# end
+# m.disconnect
+# elsif ( cmd == "followage" )
+# if match[2]?
+# args = match[2].split(/\s/)
+# if args[1]?
+# json = JSON.parse( client.get_user_follows( from: client.user_id( args[0] ).to_u64 , to: client.user_id( args[1] ).to_u64 ) )
+# puts client.user_id( args[0] ).to_s
+# puts client.user_id( args[1] ).to_s
+# puts json
+# ircipc.send( { "##{settings["channel"]}", "| " + json["data"][0]["followed_at"].to_s } )
+# elsif args[0]?
+# json = JSON.parse( client.get_user_follows( from: client.user_id( args[0] ).to_u64 , to: settings["channel_id"].to_u64 ) )
+# puts client.user_id( args[0] ).to_s
+# puts json
+# ircipc.send( { "##{settings["channel"]}", "| " + json["data"][0]["followed_at"].to_s } )
+# end
+# else
+# json = JSON.parse( client.get_user_follows( from: uid.to_u64 , to: settings["channel_id"].to_u64 ) )
+# puts json
+# ircipc.send( { "##{settings["channel"]}", "| " + json["data"][0]["followed_at"].to_s } )
+# end
+# end
+ rescue ex
+ puts ex
+ puts ex.backtrace
+ if service == "twitch"
+ say( twitchipc, config.chat_user.not_nil!.twitch.not_nil!, "An error occurred! " + ex.message.to_s )
+ elsif service == "gamesurge"
+ say( ircipc, config.chat_user.not_nil!.gamesurge.not_nil!, "An error occurred! " + ex.message.to_s )
+ else
+ puts "...also, the chat service is undefined."
+ end
+ end
+
+ end
+ end
+ensure
+ waitgroup.send( Fiber.current.name.not_nil! )
+end
+fiber = fiberipc.receive
+fibers[fiber.name.not_nil!] = fiber
+
+
+# Put tts stuff into the same thread so each playback blocks the next
+spawn name: "text2speech" do
+ fiberipc.send( Fiber.current )
+ loop do
+ begin
+ while t2stuple = t2sipc.receive
+ voice, text = [ *t2stuple ]
+ if ( match = voice.match( /^Microsoft-([A-Za-z]+)/ ) )
+ if ( match[1] =~ /Lili|Mary|Mike|Sam|Anna/ )
+ msttsvoice="Microsoft #{match[1]}"
+ else
+ msttsvoice="Microsoft #{match[1]} Desktop"
+ end
+ p = Process.new(
+ "powershell.exe",
+ [ "-Command", "
+ Add-Type -AssemblyName System.Speech;
+ $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer;
+ $speak.SelectVoice(\"#{msttsvoice}\");
+ $speak.Speak($Input);
+ $speak.Finalize;
+ "],
+ input: Process::Redirect::Pipe, output: STDOUT
+ )
+ p.input.puts text
+ p.input.close
+ p.wait
+ elsif gcloud && ( match = voice.match( /^([a-zA-Z]{2,3}-[a-zA-Z]{2})/ ) ) # Google cloud voice
+ request = Hash( String, Hash( String, String ) ){
+ "input" => { "text" => text },
+ "audioConfig" => { "audioEncoding" => "MP3" },
+ "voice" => {
+ "name" => voice,
+ "languageCode" => match[1],
+ },
+ }
+ 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=#{secrets.gcloud_token}", headers, request.to_json, tls: ssl_context )
+
+ response.body
+
+ filepath="#{config.tempdir}#{Time.utc.to_unix_ms}.mp3"
+ json=JSON.parse(response.body)
+ File.write( filepath, Base64.decode_string( json["audioContent"].as_s ) )
+ playaudiofile( filepath )
+ File.delete( filepath )
+ elsif aws # AWS polly voices
+ filepath="#{config.tempdir}#{Time.utc.to_unix_ms}.mp3"
+ p = Process.new(
+ "aws.exe", [
+ "polly", "synthesize-speech",
+ "--output-format", "mp3",
+ "--voice-id", voice,
+ "--text", text,
+ filepath
+ ], output: STDOUT, error: STDERR
+ )
+ p.wait
+ playaudiofile( filepath )
+ File.delete( filepath )
+ else # unknown
+ STDERR.puts "Voice not recognized or available."
+ end
+ end
+ rescue ex
+ puts ex
+ end
+ end
+ensure
+ waitgroup.send( Fiber.current.name.not_nil! )
+end
+fiber = fiberipc.receive
+fibers[fiber.name.not_nil!] = fiber
+
-ircipc = Channel( Tuple( String, String ) ).new
-t2sipc = Channel( Tuple( String, String ) ).new
-twitchipc = Channel( Tuple( String, Union( String, UInt64 ) ) ).new
# Twitch API request handling thread
#spawn do
@@ -271,8 +1133,9 @@ twitchipc = Channel( Tuple( String, Union( String, UInt64 ) ) ).new
#
if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.join_channels.not_nil!.twitch )
- # IRC thread
- spawn do
+ # Twitch::IRC thread
+ spawn name: "Twitch::IRC" do
+ fiberipc.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 )
@@ -282,7 +1145,7 @@ if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.j
# "Most IRC servers limit messages to 512 bytes in length, including the trailing CR-LF characters."
# PRIVMSG #channel message\r\n
spawn do
- while tuple = ircipc.receive # why does this need to be a tuple?
+ while tuple = twitchipc.receive # why does this need to be a tuple?
sizelimit=( 512 - ( tuple[0].size + 12 ) )
if ( tuple[0] == "JOIN" )
if ( tuple[0] =~ regextwitchuser )
@@ -299,7 +1162,7 @@ if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.j
# Create a handler to process incoming messages
bot.on_message do |message|
spawn do
- #Message(
+ #FastIRC::Message(
# @tags={
# "badge-info" => "",
# "badges" => "moderator/1,bits/100",
@@ -330,11 +1193,17 @@ if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.j
# ]
#)
- channelsubs[{ "twitch", message.params[0] }].each do | client |
- if ( ( prefix = message.prefix ) && ( chatuser = prefix.source ) ) # && ( uid = message.tags["user-id"]? )
- client.puts "msg twitch #{message.params[0]} #{chatuser} #{message.params[1]}"
- end
- end
+# 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
+
+ commandircipc.send( { "twitch", message } )
pp message
pp message.params
own = ( message.tags["room-id"] == message.tags["user-id"] )
@@ -343,13 +1212,13 @@ if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.j
sub = ( message.tags["subscriber"] == "1" )
rescue ex
puts ex
- #ircipc.send( { "##{config.channel}", "An error occurred! " + ex.message.to_s } )
+ #twitchipc.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!
+ #rooms = config.join_channels.not_nil!.twitch.not_nil!
# Connect to Twitch
bot.run( rooms.map{ | room | room.sub( /^#/, "") } )
@@ -366,12 +1235,65 @@ if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.j
exit 1
end
end
+ ensure
+ waitgroup.send( Fiber.current.name.not_nil! )
end
+ fiber = fiberipc.receive
+ fibers[fiber.name.not_nil!] = fiber
end
-if config.listen
- begin
- ip, port = config.listen.not_nil!.split(":")
+# BungmoBott::Socket client thread
+if config.bungmobott_connect
+ bbscli_host, bbscli_port = config.bungmobott_connect.not_nil!.split(":")
+ puts "spawning BungmoBott::Socket client"
+ spawn name: "BungmoBott::Socket client" do
+ loop do
+ puts "looping BungmoBott::Socket client"
+ user = config.chat_user.not_nil!.twitch.not_nil!
+ bungmobott_key = secrets.bungmobott_key.not_nil!
+ fiberipc.send( Fiber.current )
+ socket = TCPSocket.new( bbscli_host, bbscli_port.to_u16 )
+ context = OpenSSL::SSL::Context::Client.new
+ ssl_socket = OpenSSL::SSL::Socket::Client.new( socket, context )
+ ssl_socket.sync = true
+ negotiated = false
+ spawn do
+ while input = bbscliipc.receive
+ # ssl_socket gets redefined in the event of I/O errors, so we deal with it here.
+ #if ( climsg = input.match( /^irc +twitch +JOIN \#+([a-zA-Z0-9_]+) *$/ ) )
+ #end
+ ssl_socket.puts( input )
+ end
+ end
+ ssl_socket.puts( "auth #{user} #{bungmobott_key}" )
+ while message = ssl_socket.gets
+ puts "BungmoBott::Socket cli recv: " + message.gsub( bungmobott_key, "CENSORED" )
+ case message
+ when /^error/
+ raise Exception.new("BungmoBott::Socket Error: #{message}")
+ when /^authed/
+ negotiated = true
+ ssl_socket.puts( "say twitch #{user} test" )
+ when /^msg twitch$/
+ commandircipc.send( { "twitch", FastIRC.parse_line( message.split(" ")[2..].join(" ") ) } )
+ end
+ end
+ end
+ rescue ex
+ pp ex
+ ensure
+ waitgroup.send( Fiber.current.name.not_nil! )
+ sleep 2
+ end
+ fiber = fiberipc.receive
+ fibers[fiber.name.not_nil!] = fiber
+end
+
+# BungmoBott::Socket thread
+if config.bungmobott_listen
+ spawn name: "BungmoBott::Socket server" do
+ fiberipc.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" )
@@ -385,6 +1307,7 @@ if config.listen
# bungmobott protocol server
#while tcp_server.accept? do | clientsocket |
+
while clientsocket = tcp_server.accept?
spawn do
client = OpenSSL::SSL::Socket::Server.new(clientsocket, ssl_context)
@@ -396,20 +1319,26 @@ if config.listen
connections[client]["authed"] = "false"
while message = client.gets
- puts message
- client.puts message
+ 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}) (#{regexuser})$/i ) )
- ircservice = match[1]
+ if ( match = message.match( /^irc (#{regexservice}) JOIN \#(#{regexuser}) *$/i ) )
+ ircservice = match[2] # regexservice has some parens, too
ircchannel = match[3]
- client.puts "joining #{ircservice} #{ircchannel}"
- ircipc.send( { "JOIN", ircchannel } )
- unless channelsubs[ { ircservice, ircchannel } ]?
- channelsubs[ { ircservice, "#" + ircchannel } ] = Array( OpenSSL::SSL::Socket::Server ).new
+ if ircservice == "twitch"
+ client.puts "joining #{ircservice} \##{ircchannel}"
+ twitchipc.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
- channelsubs[ { ircservice, "#" + ircchannel } ].push( client )
- pp channelsubs
elsif ( match = message.match( /^awst2s (#{regexvoice}) (.+)/i ) )
# FIXME # powershell doesn't take bytestreams; maybe use http file hosting for this?
elsif ( match = message.match( /^gcst2s (#{regexvoice}) (.+)/i ) )
@@ -418,12 +1347,15 @@ if config.listen
client.puts "awsvoicelist " + generatevoicelistaws().join(" ")
elsif ( match = message.match( /^gcsvoicelist$/i ) )
client.puts "gcsvoicelist " + generatevoicelistgcs( secrets.gcloud_token ).join(" ")
+ elsif ( match = message.match( /^say twitch (.+)/i ) )
+ say( twitchipc, connections[client]["user"], match[1] )
+ elsif ( message =~ /testchannelsubs/ )
+ #commandircipc.send( "testchannelsubs" )
end
else
# auth
if ( match = message.match( /^auth (#{regexuser}) (#{regexb64})$/i ) )
- pp match
remoteuser = match[1]
remotekey = match[2]
if File.exists?( config.statedir + "apikeys/" + remoteuser )
@@ -431,6 +1363,7 @@ if config.listen
puts "comparing #{localkey} to #{remotekey}"
if ( localkey == remotekey )
connections[client]["authed"] = "true"
+ puts "authed #{ remoteuser }"
client.puts "authed #{ remoteuser }"
connections[client]["user"] = remoteuser
connections[client]["key"] = remotekey
@@ -463,6 +1396,45 @@ if config.listen
end
rescue ex
puts ex
+ ensure
+ waitgroup.send( Fiber.current.name.not_nil! )
end
+ fiber = fiberipc.receive
+ fibers[fiber.name.not_nil!] = fiber
+end
+
+if config.join_channels && config.join_channels.not_nil!.twitch
+ spawn name: "join_channels" do
+ fiberipc.send( Fiber.current )
+ if fibers["Twitch::IRC"]?
+ ircservice = "twitch"
+ config.join_channels.not_nil!.twitch.not_nil!.each do |ircchannel|
+ twitchipc.send( { "JOIN", ircchannel } )
+ unless channelsubs[ { ircservice, ircchannel } ]?
+ channelsubs[ { ircservice, "#" + ircchannel } ] = Array( OpenSSL::SSL::Socket::Server ).new
+ # Do we ever care about this?
+ #channelsubs[ { ircservice, "#" + ircchannel } ] = Array( OpenSSL::SSL::Socket::Server | Channel( Tuple( String, FastIRC::Message ) ) ).new
+ end
+ end
+ elsif fibers["BungmoBott::Socket client"]?
+ ircservice = "twitch"
+ config.join_channels.not_nil!.twitch.not_nil!.each do |ircchannel|
+ bbscliipc.send( "irc twitch JOIN \#" + ircchannel )
+ end
+ end
+ ensure
+ waitgroup.send( Fiber.current.name.not_nil! )
+ end
+ fiber = fiberipc.receive
+ fibers[fiber.name.not_nil!] = fiber
+end
+
+puts "Spawned fibers:"
+pp fibers
+
+fibers.size.times do
+ fiber = waitgroup.receive
+ fibers.delete( fiber )
+ puts "Fiber ended: " + fiber
end