summaryrefslogtreecommitdiff
path: root/crystal
diff options
context:
space:
mode:
authorJoe Rayhawk <jrayhawk+git@omgwallhack.org>2023-12-27 12:21:38 -0800
committerJoe Rayhawk <jrayhawk+git@omgwallhack.org>2023-12-27 12:33:49 -0800
commit11b8b62fd544479c07f34c57aedd807de53308c4 (patch)
tree3a20b3d0a1e659bd8f192cd143c1a15bad4b3896 /crystal
parent0438391b4ec94404582b71b0fb4e12bc999c0021 (diff)
downloadtwitchtools-11b8b62fd544479c07f34c57aedd807de53308c4.tar.gz
twitchtools-11b8b62fd544479c07f34c57aedd807de53308c4.zip
crystal bungmobott: add YAML::Serializable configuration management system as module
Diffstat (limited to 'crystal')
-rw-r--r--crystal/lib/bungmobott/src/bungmobott.cr71
-rw-r--r--crystal/tcpsocket.cr214
2 files changed, 148 insertions, 137 deletions
diff --git a/crystal/lib/bungmobott/src/bungmobott.cr b/crystal/lib/bungmobott/src/bungmobott.cr
new file mode 100644
index 0000000..d54eaad
--- /dev/null
+++ b/crystal/lib/bungmobott/src/bungmobott.cr
@@ -0,0 +1,71 @@
+#---
+#statedir: /home/jrayhawk/.local/state/bungmobott/
+#tempdir: /tmp/bungmobott/
+#listen: 0.0.0.0:5555
+#join_channels:
+# twitch:
+# - bungmonkey
+# - dopefish
+# # - yackamov
+# gamesurge:
+# - bungmonkey
+#twitch_user_id: 59895482 # saves a lookup
+#chat_user:
+# twitch: bungmonkey
+# gamesurge: bungmoBott
+
+module BungmoBott
+
+ EXE = "bungmobott"
+
+ class ChatUser
+ include YAML::Serializable
+ include YAML::Serializable::Unmapped
+ property twitch : String?
+ property gamesurge : String?
+ end
+
+ class JoinChannels
+ include YAML::Serializable
+ include YAML::Serializable::Unmapped
+ property twitch : Array(String)?
+ property gamesurge : Array(String)?
+ end
+
+ class Config
+ include YAML::Serializable
+ include YAML::Serializable::Unmapped
+ property statedir : String = (
+ ENV["LOCALAPPDATA"]? && Path.windows( ENV["LOCALAPPDATA"] + "\\#{EXE}\\state\\" ).to_s ||
+ ENV["XDG_STATE_HOME"]? && ENV["XDG_STATE_HOME"] + "/#{EXE}/" ||
+ Path.home./(".local/state/#{EXE}").to_s
+ )
+ property tempdir : String = (
+ ENV["TEMP"]? && ENV["TEMP"] + "\\#{EXE}\\" ||
+ "/tmp/#{EXE}/"
+ )
+ property rundir : String = (
+ ENV["XDG_RUNTIME_DIR"]? && ENV["XDG_RUNTIME_DIR"] + "/#{EXE}/" ||
+ "/tmp/#{EXE}/"
+ ) # 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 chat_user : ChatUser? = ChatUser.from_yaml("---")
+ property join_channels : JoinChannels? = JoinChannels.from_yaml("---")
+ property twitch_user_id : UInt32? = nil
+ end
+ class Secrets
+ include YAML::Serializable
+ include YAML::Serializable::Unmapped
+ @[YAML::Field(emit_null: true)]
+ property twitch_access_token : String?
+ @[YAML::Field(emit_null: true)]
+ property twitch_client_id : String?
+ @[YAML::Field(emit_null: true)]
+ property gcloud_token : String?
+ @[YAML::Field(emit_null: true)]
+ property gamesurge_password : String?
+ end
+end
+
diff --git a/crystal/tcpsocket.cr b/crystal/tcpsocket.cr
index 8d26a68..b4ee6a8 100644
--- a/crystal/tcpsocket.cr
+++ b/crystal/tcpsocket.cr
@@ -8,6 +8,7 @@ require "json"
require "crystal_mpd"
require "obswebsocket"
require "yaml"
+require "bungmobott"
STDOUT.sync = true
STDOUT.flush_on_newline = true
@@ -21,150 +22,88 @@ struct Nil
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-]+/
-module BungmoBott
- class Config
- # YAML:
- #---
- #statedir: /home/jrayhawk/.local/state/bungmobott/
- #tempdir: /tmp/bungmobott/
- #listen: 0.0.0.0:5555
- #join_channels:
- # twitch:
- # - bungmonkey
- # - dopefish
- # # - yackamov
- # gamesurge:
- # - bungmonkey
- #twitch_channel_id: 59895482 # saves a lookup
- #chat_user:
- # twitch: bungmonkey
- # gamesurge: bungmoBott
-
- getter configdir : String
- getter statedir : String
- getter tempdir : String
- getter listen : String | Nil
- getter join_channels : Hash( String, Array( String ) ) | Nil
- getter twitch_channel_id : UInt64 | Nil
- getter twitch_client_id : String | Nil
- getter twitch_access_token : String | Nil
- getter gcloud_token : String | Nil
- getter chat_user : Hash( String, String ) | Nil
- getter metaconfig : Hash( String, Hash( String, String | UInt64 | Nil | Hash( String, String | Array( String ) ) ) )
-
- def initialize( )
-
- @metaconfig = Hash( String, Hash( String, String | UInt64 | Nil | Hash( String, String | Array( String ) ) ) ).new
- @metaconfig["default"] = Hash( String, String | UInt64 | Nil | Hash( String, String | Array( String ) ) ).new
- @metaconfig["default"]["home"] = Path.home.to_s
- @configdir = Path.home./("/.config/bungmobott/").to_s
- @metaconfig["default"]["statedir"] = Path.home./("/.local/state/bungmobott/").to_s
-
- if ENV["LOCALAPPDATA"]?
- @configdir = Path.windows( ENV["LOCALAPPDATA"] + "\\bungmobott\\" ).to_s
- @metaconfig["default"]["statedir"] = Path.windows( ENV["LOCALAPPDATA"] + "\\bungmobott\\state\\" ).to_s
- end
+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( configdir : String, file : String, contents : String )
+ Dir.mkdir_p( configdir )
+ File.write( configdir + file, contents )
+rescue exio : IO::Error
+ puts "ERROR: Unable to write #{ configdir + file }.txt: #{exio.message}"
+ exit 7
+end
- @metaconfig["default"]["tempdir"] = "/tmp/bungmobott/"
-
- ENV["TEMP"]? && ( @metaconfig["default"]["tempdir"] = "#{ENV["TEMP"]}\\bungmobott\\" )
-
- ENV["XDG_CONFIG_HOME"]? && ( @configdir = ENV["XDG_CONFIG_HOME"] + "/bungmobott/" )
- #ENV["XDG_DATA_HOME"]? && ( @metaconfig["default"]["datadir"] = ENV["XDG_DATA_HOME"] ) # unused?
- ENV["XDG_STATE_HOME"]? && ( @metaconfig["default"]["statedir"] = ENV["XDG_STATE_HOME"] + "/bungmobott/" )
-
- populate( "config" )
- populate( "secret" )
- @metaconfig["default"].merge!(
- {
- "chat_user" => nil,
- "twitch_channel_id" => nil,
- "join_channels" => nil,
- "listen" => nil,
- "twitch_client_id" => nil,
- "twitch_access_token" => nil,
- "gcloud_token" => nil,
- }
- )
-
- # FIXME: Maybe add more explicit validation on these...?
- # FIXME: Might need to do explicit maps on these to get them to typecheck
- @chat_user = @metaconfig["config"]["chat_user"]?.as( Hash( String, String ) | Nil )
- @twitch_channel_id = @metaconfig["config"]["twitch_channel_id"]?.as( UInt64 )
- @join_channels = @metaconfig["config"]["join_channels"]?.as( Hash( String, Array( String ) ) | Nil )
- @twitch_client_id = @metaconfig["secret"]["twitch_client_id"]?.as( String )
- @twitch_access_token = @metaconfig["secret"]["twitch_access_token"]?.as( String )
- @gcloud_token = @metaconfig["secret"]["gcloud_token"]?.as( String )
-
- @listen = ( @metaconfig["config"]["listen"]?.as( String | Nil ) )
- @statedir = (
- if @metaconfig["config"]["statedir"]?
- @metaconfig["config"]["statedir"].as( String )
- else
- @metaconfig["default"]["statedir"].as( String )
- end
- )
- @tempdir = (
- if @metaconfig["config"]["tempdir"]?
- @metaconfig["config"]["tempdir"].as( String )
- else
- @metaconfig["default"]["tempdir"].as( String )
- end
- )
+configfile = configdir + "config.txt"
+if File.exists?( configfile )
+ config = BungmoBott::Config.from_yaml( File.read( configdir + "config.txt" ) )
+else
+ config = BungmoBott::Config.from_yaml("---")
+ STDERR.puts "WARNING: #{configfile} not found. Writing new one."
+ writeconfig( configdir, "config.txt", config.to_yaml )
+end
- Dir.mkdir_p( @statedir )
- Dir.mkdir_p( @tempdir )
+puts config.to_yaml
- end
- def populate( configname : String )
- begin
- @metaconfig[configname] = Hash( String, String | UInt64 | Nil | Hash( String, String | Array( String ) ) ).from_yaml( File.read( configdir + configname + ".txt" ) )
- rescue exyaml : YAML::ParseException
- puts "Failed to parse #{configname}.txt: #{exyaml.message}"
- exit 4
- rescue exio : IO::Error
- puts "#{configname}.txt missing, writing new #{configname}.txt"
- if configname == "secrets"
- write( configname, @metaconfig["default"].select{ |k, v| k =~ /twitch_(client_id|access_token)|gcloud_token/ }.to_yaml )
- elsif configname == "config"
- write( configname, @metaconfig["default"].reject{ |k, v| k =~ /configdir|home|twitch_(client_id|access_token)|gcloud_token/ }.to_yaml )
- end
- # verify we can read it back
- @metaconfig[configname] = Hash( String, String | UInt64 | Nil | Hash( String, String | Array( String ) ) ).from_yaml( File.read( configdir + configname + ".txt" ) )
- end
- end
- def write( configname : String, content : String )
- File.write( configdir + "#{configname}.txt", content )
- Dir.mkdir_p( @configdir )
- rescue exio : IO::Error
- puts "ERROR: Unable to write #{configname}.txt: #{exio.message}"
- exit 7
- end
- end
+unless config.yaml_unmapped.empty?
+ STDERR.puts "WARNING: #{configfile} has unknown properties:"
+ ppe config.yaml_unmapped
end
-regextwitchuser = /^[0-9a-zA-Z_]+$/
+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
+
+secretsfile = configdir + "secrets.txt"
-config = BungmoBott::Config.new
+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 )
+end
+
+unless secrets.yaml_unmapped.empty?
+ STDERR.puts "WARNING: #{secretsfile} has unknown properties:"
+ ppe secrets.yaml_unmapped.keys
+end
+
+regextwitchuser = /^[0-9a-zA-Z_]+$/
# enable direct twitch api?
-if ( config.twitch_access_token && config.twitch_client_id )
+if ( secrets.twitch_access_token && secrets.twitch_client_id )
twitchapi = true
else
twitchapi = false
- config.twitch_access_token || STDERR.puts "Warning: #{config.configdir}secret.txt 'twitch_access_token' is missing; direct Twitch API access disabled."
- config.twitch_client_id || STDERR.puts "Warning: #{config.configdir}secret.txt '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: #{config.configdir}config.txt 'chat_user: {twitch}' value is missing; using first configured 'join_channels: {twitch}' array value instead: #{chat_user}"
+ 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
+ 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."
@@ -174,15 +113,16 @@ else
end
# enable direct gcloud api?
-if config.gcloud_token
+if secrets.gcloud_token
gcloud = true
else
gcloud = false
- STDERR.puts "Warning: #{config.configdir}secret.txt gcloud_token is missing; direct GCS voices disabled."
+ 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" )
@@ -278,7 +218,7 @@ def regeneratevoicelist( defaultsettings : Hash( String, String ), aws : Bool, g
end
end
if gcloud
- generatevoicelistgcs( config.gcloud_token ).each do | voice |
+ generatevoicelistgcs( secrets.gcloud_token ).each do | voice |
voices[ voice.downcase ] = voice
end
end
@@ -297,8 +237,8 @@ channelsubs = Hash( Tuple( String, String ), Array( OpenSSL::SSL::Socket::Server
# Encrypted
connections = Hash(OpenSSL::SSL::Socket::Server, Hash(String, String)).new
-ircipc = Channel( Tuple( String, String ) ).new
-t2sipc = Channel( Tuple( String, String ) ).new
+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
@@ -330,12 +270,12 @@ twitchipc = Channel( Tuple( String, Union( String, UInt64 ) ) ).new
#end
#
-if ( config.twitch_access_token && config.chat_user.not_nil!["twitch"]? && config.join_channels.not_nil!["twitch"]? )
+if ( secrets.twitch_access_token && config.chat_user.not_nil!.twitch && config.join_channels.not_nil!.twitch )
# IRC thread
spawn do
loop do
begin
- bot = Twitch::IRC::Client.new( nick: config.chat_user.not_nil!["twitch"], token: "oauth:" + config.twitch_access_token.not_nil!, log_mode: true )
+ 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 thread
@@ -409,7 +349,7 @@ if ( config.twitch_access_token && config.chat_user.not_nil!["twitch"]? && confi
end
rooms = Array( String ).new
- rooms = config.join_channels.not_nil!["twitch"]
+ rooms = config.join_channels.not_nil!.twitch.not_nil!
# Connect to Twitch
bot.run( rooms.map{ | room | room.sub( /^#/, "") } )
@@ -434,8 +374,8 @@ if config.listen
ip, port = config.listen.not_nil!.split(":")
tcp_server = TCPServer.new( ip, port.to_i )
ssl_context = OpenSSL::SSL::Context::Server.new
- ssl_context.private_key=(config.configdir + "/privkey.pem" )
- ssl_context.certificate_chain=(config.configdir + "/fullchain.pem" )
+ 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)
@@ -477,7 +417,7 @@ if config.listen
elsif ( match = message.match( /^awsvoicelist$/i ) )
client.puts "awsvoicelist " + generatevoicelistaws().join(" ")
elsif ( match = message.match( /^gcsvoicelist$/i ) )
- client.puts "gcsvoicelist " + generatevoicelistgcs( config.gcloud_token ).join(" ")
+ client.puts "gcsvoicelist " + generatevoicelistgcs( secrets.gcloud_token ).join(" ")
end
else