From e0e8928fb82d811047d31a08515453e296ce087b Mon Sep 17 00:00:00 2001 From: Joe Rayhawk Date: Tue, 18 Jul 2017 20:39:14 -0700 Subject: ircobsbridge.rb: new IRC/OBS-websocket C&C bridge --- ircobsbridge.rb | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 ircobsbridge.rb 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 +} -- cgit v1.2.3