using Microsoft.Extensions.Logging; using MySqlConnector; using SharpChat; using SharpChat.Configuration; using SharpChat.Flashii; using SharpChat.MariaDB; using SharpChat.SQLite; using System.Data.SQLite; using System.Reflection.PortableExecutable; using System.Text; using ZLogger; using ZLogger.Providers; const string CONFIG = "sharpchat.cfg"; Console.WriteLine(@" _____ __ ________ __ "); Console.WriteLine(@" / ___// /_ ____ __________ / ____/ /_ ____ _/ /_"); Console.WriteLine(@" \__ \/ __ \/ __ `/ ___/ __ \/ / / __ \/ __ `/ __/"); Console.WriteLine(@" ___/ / / / / /_/ / / / /_/ / /___/ / / / /_/ / /_ "); Console.WriteLine(@"/____/_/ /_/\__,_/_/ / .___/\____/_/ /_/\__,_/\__/ "); /**/Console.Write(@" /__/"); if(SharpInfo.IsDebugBuild) { Console.WriteLine(); Console.Write(@"== "); Console.Write(SharpInfo.VersionString); Console.WriteLine(@" == DBG =="); } else Console.WriteLine(SharpInfo.VersionStringShort.PadLeft(28, ' ')); using ILoggerFactory logFactory = LoggerFactory.Create(logging => { logging.ClearProviders(); #if DEBUG logging.SetMinimumLevel(LogLevel.Trace); #else logging.SetMinimumLevel(LogLevel.Information); #endif logging.AddZLoggerConsole(opts => { opts.OutputEncodingToUtf8 = true; opts.UsePlainTextFormatter(formatter => { formatter.SetPrefixFormatter($"{0} [{1} {2}] ", (in MessageTemplate template, in LogInfo info) => template.Format(info.Timestamp, info.Category, info.LogLevel)); }); }); logging.AddZLoggerRollingFile(opts => { opts.FilePathSelector = (ts, seqNo) => $"logs/{ts.ToLocalTime():yyyy-MM-dd_HH-mm-ss}_{seqNo:000}.json"; opts.RollingInterval = RollingInterval.Day; opts.RollingSizeKB = 1024; opts.FileShared = true; opts.UseJsonFormatter(formatter => { formatter.UseUtcTimestamp = true; }); }); }); ILogger logger = logFactory.CreateLogger("main"); using CancellationTokenSource cts = new(); void exitHandler() { if(cts.IsCancellationRequested) return; try { cts.Cancel(); logger.ZLogInformation($"Shutdown requested through console..."); } catch(ObjectDisposedException) { } } AppDomain.CurrentDomain.ProcessExit += (sender, ev) => { exitHandler(); }; Console.CancelKeyPress += (sender, ev) => { ev.Cancel = true; exitHandler(); }; if(cts.IsCancellationRequested) return; string configFile = CONFIG; // If the config file doesn't exist and we're using the default path, run the converter if(!File.Exists(configFile) && configFile == CONFIG) { logger.ZLogInformation($"Creating example configuration file..."); using Stream s = new FileStream(CONFIG, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); s.SetLength(0); s.Flush(); using StreamWriter sw = new(s, new UTF8Encoding(false)); sw.WriteLine("# and ; can be used at the start of a line for comments."); sw.WriteLine(); sw.WriteLine("# General Configuration"); sw.WriteLine($"#chat:msgMaxLength {Context.DEFAULT_MSG_LENGTH_MAX}"); sw.WriteLine($"#chat:connMaxCount {Context.DEFAULT_MAX_CONNECTIONS}"); sw.WriteLine($"#chat:floodKickLength {Context.DEFAULT_FLOOD_KICK_LENGTH}"); sw.WriteLine("# Sock Chat Configuration"); sw.WriteLine($"#sockchat:port {SockChatServer.DEFAULT_PORT}"); sw.WriteLine(); sw.WriteLine("# Channels"); sw.WriteLine("chat:channels lounge staff"); sw.WriteLine(); sw.WriteLine("# Lounge channel settings"); sw.WriteLine("chat:channels:lounge:name Lounge"); sw.WriteLine("chat:channels:lounge:autoJoin true"); sw.WriteLine(); sw.WriteLine("# Staff channel settings"); sw.WriteLine("chat:channels:staff:name Staff"); sw.WriteLine("chat:channels:staff:minRank 5"); const string msz_secret = "login_key.txt"; const string msz_url = "msz_url.txt"; sw.WriteLine(); sw.WriteLine("# Misuzu integration settings"); if(File.Exists(msz_secret)) sw.WriteLine(string.Format("msz:secret {0}", File.ReadAllText(msz_secret).Trim())); else sw.WriteLine("#msz:secret woomy"); if(File.Exists(msz_url)) sw.WriteLine(string.Format("msz:url {0}/_sockchat", File.ReadAllText(msz_url).Trim())); else sw.WriteLine("#msz:url https://flashii.net/_sockchat"); const string mdb_config = @"mariadb.txt"; string[] mdbCfg = File.Exists(mdb_config) ? File.ReadAllLines(mdb_config) : []; sw.WriteLine(); sw.WriteLine("# MariaDB configuration"); if(mdbCfg.Length > 0) sw.WriteLine($"mariadb:host {mdbCfg[0]}"); else sw.WriteLine($"#mariadb:host <username>"); if(mdbCfg.Length > 1) sw.WriteLine($"mariadb:user {mdbCfg[1]}"); else sw.WriteLine($"#mariadb:user <username>"); if(mdbCfg.Length > 2) sw.WriteLine($"mariadb:pass {mdbCfg[2]}"); else sw.WriteLine($"#mariadb:pass <password>"); if(mdbCfg.Length > 3) sw.WriteLine($"mariadb:db {mdbCfg[3]}"); else sw.WriteLine($"#mariadb:db <database>"); sw.Flush(); } logger.ZLogInformation($"Initialising configuration..."); using StreamConfig config = StreamConfig.FromPath(configFile); if(cts.IsCancellationRequested) return; if(args.Contains("--convert-db")) { logger.ZLogInformation($"Converting MariaDB storage to SQLite"); MariaDBStorage mariadbStorage = new(logFactory.CreateLogger("mariadb"), MariaDBStorage.BuildConnectionString(config.ScopeTo("mariadb"))); await mariadbStorage.UpgradeStorage(); using SQLiteStorage sqlite = new(logFactory.CreateLogger("sqlite"), SQLiteStorage.BuildConnectionString(config.ScopeTo("sqlite"), false)); await sqlite.UpgradeStorage(); using MariaDBConnection mariadb = await mariadbStorage.CreateConnection(); long rows = await mariadb.RunQueryValue<long>("SELECT COUNT(*) FROM sqc_events"); using MySqlCommand export = mariadb.Connection.CreateCommand(); export.CommandText = "SELECT event_id, event_type, UNIX_TIMESTAMP(event_created) AS event_created, UNIX_TIMESTAMP(event_deleted) AS event_deleted, event_channel, event_sender, event_sender_name, event_sender_colour, event_sender_rank, event_sender_nick, event_sender_perms, event_data FROM sqc_events"; export.CommandTimeout = int.MaxValue; using MySqlDataReader reader = await export.ExecuteReaderAsync(); using SQLiteCommand import = sqlite.Connection.Connection.CreateCommand(); import.CommandText = "INSERT OR IGNORE INTO messages (msg_id, msg_type, msg_created, msg_deleted, msg_channel, msg_sender, msg_sender_name, msg_sender_colour, msg_sender_rank, msg_sender_nick, msg_sender_perms, msg_data) VALUES (@id, @type, @created, @deleted, @channel, @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms, @data)"; long completed = 0; DateTimeOffset lastReport = DateTimeOffset.UtcNow; logger.ZLogInformation($"Beginning conversion of {rows} rows..."); while(reader.Read()) { if(cts.IsCancellationRequested) return; import.Parameters.Clear(); import.Parameters.Add(new SQLiteParameter("id", reader.GetInt64("event_id").ToString())); import.Parameters.Add(new SQLiteParameter("type", reader.GetString("event_type"))); import.Parameters.Add(new SQLiteParameter("created", DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")).ToString("s") + "Z")); import.Parameters.Add(new SQLiteParameter("deleted", reader.IsDBNull(reader.GetOrdinal("event_deleted")) ? null : DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_deleted")).ToString("s") + "Z")); import.Parameters.Add(new SQLiteParameter("channel", reader.IsDBNull(reader.GetOrdinal("event_channel")) ? null : reader.GetString("event_channel"))); import.Parameters.Add(new SQLiteParameter("sender", reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : reader.GetString("event_sender"))); import.Parameters.Add(new SQLiteParameter("sender_name", reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"))); ColourInheritable colour = ColourInheritable.FromMisuzu(reader.GetInt32("event_sender_colour")); import.Parameters.Add(new SQLiteParameter("sender_colour", colour.Rgb.HasValue ? colour.Rgb.Value : null)); import.Parameters.Add(new SQLiteParameter("sender_rank", reader.IsDBNull(reader.GetOrdinal("event_sender_rank")) ? null : reader.GetInt32("event_sender_rank"))); import.Parameters.Add(new SQLiteParameter("sender_nick", reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? string.Empty : reader.GetString("event_sender_nick"))); import.Parameters.Add(new SQLiteParameter("sender_perms", SQLiteUserPermissionsConverter.To(MariaDBUserPermissionsConverter.From((MariaDBUserPermissions)reader.GetInt32("event_sender_perms"))))); import.Parameters.Add(new SQLiteParameter("data", reader.GetString("event_data"))); await import.PrepareAsync(); await import.ExecuteNonQueryAsync(); ++completed; if(DateTimeOffset.UtcNow - lastReport > TimeSpan.FromMinutes(1)) { lastReport = DateTimeOffset.UtcNow; double completion = (double)completed / rows; logger.ZLogInformation($"{completed} of {rows} converted ({completion:P2})..."); } } logger.ZLogInformation($"Converted all {completed} rows!"); return; } logger.ZLogInformation($"Initialising HTTP client..."); using HttpClient httpClient = new(new HttpClientHandler() { UseProxy = false, }); httpClient.DefaultRequestHeaders.Add("User-Agent", SharpInfo.ProgramName); if(cts.IsCancellationRequested) return; logger.ZLogInformation($"Initialising Flashii client..."); FlashiiClient flashii = new(logFactory.CreateLogger("flashii"), httpClient, config.ScopeTo("msz")); if(cts.IsCancellationRequested) return; logger.ZLogInformation($"Initialising storage..."); Storage storage = string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty)) ? new SQLiteStorage(logFactory.CreateLogger("sqlite"), SQLiteStorage.BuildConnectionString(config.ScopeTo("sqlite"))) : new MariaDBStorage(logFactory.CreateLogger("mariadb"), MariaDBStorage.BuildConnectionString(config.ScopeTo("mariadb"))); try { if(cts.IsCancellationRequested) return; await storage.UpgradeStorage(); if(cts.IsCancellationRequested) return; logger.ZLogDebug($"Creating context..."); Config chatConfig = config.ScopeTo("chat"); Context ctx = new(logFactory, chatConfig, storage, flashii, flashii); if(cts.IsCancellationRequested) return; logger.ZLogInformation($"Preparing server..."); await new SockChatServer( cts, ctx, config.ScopeTo("sockchat") ).Listen(cts.Token); } finally { if(storage is IDisposable disp) { logger.ZLogInformation($"Cleaning storage..."); disp.Dispose(); } } logger.ZLogInformation($"Exiting...");