summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ircobsbridge.rb200
1 files changed, 200 insertions, 0 deletions
diff --git a/ircobsbridge.rb b/ircobsbridge.rb
new file mode 100644
index 0000000..ace7de7
--- /dev/null
+++ b/ircobsbridge.rb
@@ -0,0 +1,200 @@
+#!/usr/bin/env ruby
+
+# IRC/OBS-websocket scene management bridge
+
+require 'json'
+require 'socket'
+require 'openssl'
+require 'websocket'
+require 'net/http'
+require 'pp'
+
+confdir = Dir.home + '/.config/twitch/'
+ENV['APPDATA'] && confdir = ENV['APPDATA'] + "\\twitch\\"
+ENV['XDG_CONFIG_HOME'] && confdir = ENV['XDG_CONFIG_HOME'] + '/twitch/'
+
+access_token = IO.read( confdir + 'access_token' ).chomp
+channel = IO.read( confdir + 'channel' ).chomp
+client_id = IO.read( confdir + 'client_id' ).chomp
+unix_socket_path = confdir + 'chat_socket'
+obs_password = IO.read( confdir + 'obs_password' ).chomp
+
+def ircsocketwrite( unix_socket_path, text )
+ unix_socket = UNIXSocket.new( unix_socket_path )
+ unix_socket.print( text )
+rescue
+ensure
+ unix_socket.close
+end
+
+def websocketwrite( json )
+ $tcp_socket.write( WebSocket::Frame::Outgoing::Client.new( data: json, type: :text ).to_s )
+rescue IOError, EOFError
+ $tcp_socket.close
+ websocket_create()
+ retry
+end
+
+# the obsreader thread specially interprets unusual message-id-keyed responses; '1' will be a safe default.
+
+def SetSourceRender( source, render = 'true', id = '1' )
+ websocketwrite( '{ "request-type": "SetSourceRender", "message-id" : "' + id.to_s + '", "source" : "' + source + '", "render" : ' + render.to_s + ' }' )
+end
+
+def GetCurrentScene( id = '1' )
+ websocketwrite( '{ "request-type": "GetCurrentScene", "message-id" : "' + id.to_s + '" }' )
+end
+
+def GetSceneList( id = '1' )
+ websocketwrite( '{ "request-type": "GetSceneList", "message-id" : "' + id.to_s + '" }' )
+end
+
+def SetCurrentScene( scene, id = '1' )
+ websocketwrite( '{ "request-type": "SetCurrentScene", "message-id" : "' + id.to_s + '", "scene-name": "' + scene + '" }' )
+end
+
+def GetStreamingStatus( id = '1' )
+ websocketwrite( '{ "request-type": "GetStreamingStatus", "message-id" : "' + id.to_s + '" }' )
+end
+
+def transientsource( source )
+ Thread.new {
+ SetSourceRender( 'media-' + source , false )
+ sleep 0.1
+ SetSourceRender( 'media-' + source , true )
+ sleep 60
+ SetSourceRender( 'media-' + source , false )
+ }
+end
+
+#json["subscriptions"][0]["user"]["name"]
+
+def websocket_create()
+ begin
+ $tcp_socket = TCPSocket.new( '127.0.0.1', 4444 )
+ $tcp_socket.write( WebSocket::Handshake::Client.new( url: 'ws://localhost/' ).to_s )
+ IO.select( [ $tcp_socket ] )
+ print $tcp_socket.read_nonblock( 4096 )
+ websocketwrite( '{ "request-type": "GetAuthRequired", "message-id" : "1" }' )
+ # Let the obsreader thread do the actual authentication.
+ rescue IOError, EOFError
+ $tcp_socket.close
+ sleep 1
+ retry
+ end
+end
+
+websocket_create()
+frame = WebSocket::Frame::Incoming::Client.new
+frame_decoded = String.new
+status = Hash.new
+$lastused = Hash.new
+$globallastused = 0
+
+def command_dispatch( mode, user, command, arg1 )
+ # arg1 may be empty String
+ safe = /^[-0-9a-zA-Z]+$/
+ if mode == '@' && command == 'scene'
+ if arg1 =~ safe
+ SetCurrentScene( arg1 )
+ return
+ else
+ GetSceneList( )
+ return
+ end
+ elsif mode == '@' && command == 'source'
+ if arg1 =~ safe
+ # GetCurrentScene for current visibility state
+ GetCurrentScene( arg1 )
+ # SetSourceRender to toggle
+ return
+ else
+ GetCurrentScene( )
+ return
+ end
+ elsif mode == '@' && command == 'commands'
+ ircsocketwrite( unix_socket_path, "!source !scene !metaminute !nopgl1 !nopgl2\n" )
+ return
+ end
+ # ratelimited commands after +o commands
+ if ( ! $lastused[ command ] || $lastused[ command ] <= ( Time.now.to_i - 120 ) ) && $globallastused <= ( Time.now.to_i - 60 ) # || mode == '@'
+ $globallastused = Time.now.to_i
+ $lastused[ command ] = Time.now.to_i
+ if command =~ /^(metaminute|nopgl1|nopgl2)$/
+ transientsource( command )
+ return
+ elsif command == 'commands'
+ ircsocketwrite( unix_socket_path, "!metaminute !nopgl1 !nopgl2\n" )
+ return
+ end
+ end
+end
+
+# irclog read loop event thread
+ircreader = Thread.new {
+begin
+ irclog = IO.popen('/usr/bin/tail -n0 -F ' + ENV['HOME'] + '/irclogs/twitch/#' + channel + '.log' )
+ irclog.each_line do |line|
+ if ( match = line.match(/^\d\d:\d\d <([@ +])(.+)?> *!([-0-9a-zA-Z]+) *([-0-9a-zA-Z]*)/) )
+ command_dispatch( *match.to_a[1..4] )
+ end
+ end
+rescue
+ensure
+ irclog.close
+end
+}
+
+# websocket read loop event thread
+obsreader = Thread.new {
+ begin
+ while true
+ frame << $tcp_socket.read_nonblock( 4096 )
+ while ( frame_decoded = frame.next.to_s ) != ""
+ frame_parsed = JSON.parse(frame_decoded)
+ if frame_parsed["update-type"] == "StreamStatus"
+ status = frame_parsed
+ elsif frame_parsed['current-scene'] && frame_parsed['scenes'] # GetSceneList
+ text = '| Current scene: ' + frame_parsed['current-scene'] + ' | Available: ' + frame_parsed['scenes'].map{|v| v['name'] }.sort.join(' ') + "\n"
+ print text
+ ircsocketwrite( unix_socket_path, text )
+ elsif frame_parsed['name'] && frame_parsed['sources'] # GetCurrentScene
+ pp frame_parsed
+ text = '| Sources: ' + frame_parsed['sources'].map{|v| v['name'] }.sort.join(' ') + "\n"
+ print text
+ if frame_parsed['message-id'] == '1'
+ ircsocketwrite( unix_socket_path, text )
+ else # other message-ids are a source we want to toggle
+ frame_parsed['sources'].each do |v|
+ if v['name'] == frame_parsed['message-id']
+ if v['render'] == true
+ SetSourceRender( v['name'], 'false', '2' )
+ ircsocketwrite( unix_socket_path, '| Source ' + v['name'] + " is now unrendered\n" )
+ elsif v['render'] == false
+ SetSourceRender( v['name'], 'true', '2' )
+ ircsocketwrite( unix_socket_path, '| Source ' + v['name'] + " is now visible\n" )
+ end
+ end
+ end
+ end
+ elsif frame_parsed['update-type'] == 'SwitchScenes'
+ ircsocketwrite( unix_socket_path, '| Scene is now ' + frame_parsed['scene-name'] + "\n" )
+ #elsif frame_parsed['update-type'] == 'SceneItemVisibilityChanged'
+ # ircsocketwrite( unix_socket_path, '| Source ' + frame_parsed['item-name'] + ' visibility is now ' + frame_parsed['item-visible'].to_s + "\n" )
+ elsif frame_parsed['authRequired'] == true # GetAuthRequired
+ websocketwrite( '{ "request-type": "Authenticate", "message-id" : "2", "auth" : "' + Digest::SHA256.base64digest(Digest::SHA256.base64digest(obs_password + frame_parsed['salt'])+frame_parsed['challenge']) + '" }' )
+ else
+ print frame_decoded
+ end
+ end
+ #pp JSON.parse(frame_decoded)
+ end
+ rescue IO::WaitReadable, IO::EAGAINWaitReadable, JSON::ParserError
+ IO.select([$tcp_socket])
+ retry
+ rescue IOError, EOFError
+ $tcp_socket.close
+ websocket_create()
+ retry
+ end
+}