From c2bc6e0178cdd14278f7b749647e15d6e5c08a87 Mon Sep 17 00:00:00 2001 From: Ronny Gunawan <3048897+ronnygunawan@users.noreply.github.com> Date: Sat, 17 Feb 2024 16:01:11 +0700 Subject: [PATCH] Add vps table --- .../BotUpdate/Message/MessageUpdateHandler.cs | 6 +- .../SQL/SQLCommandHandler.cs | 103 +++++++++------- .../GoogleSheets/GoogleSheetsClient.cs | 112 +++++++++++++++++ .../Options/GoogleSheetsOptions.cs | 5 + .../ServiceCollectionExtensions.cs | 20 +++ .../ServiceCollectionExtensions.cs | 11 ++ .../VPSBenchmark.cs | 7 +- .../VPSBenchmarkDataSource.cs | 114 ++++++++++++++++++ BotNet.Services/VPS/VPSBenchmarkDataSource.cs | 14 --- BotNet/Program.cs | 8 +- 10 files changed, 335 insertions(+), 65 deletions(-) create mode 100644 BotNet.Services/GoogleSheets/GoogleSheetsClient.cs create mode 100644 BotNet.Services/GoogleSheets/Options/GoogleSheetsOptions.cs create mode 100644 BotNet.Services/GoogleSheets/ServiceCollectionExtensions.cs create mode 100644 BotNet.Services/KokizzuVPSBenchmark/ServiceCollectionExtensions.cs rename BotNet.Services/{VPS => KokizzuVPSBenchmark}/VPSBenchmark.cs (85%) create mode 100644 BotNet.Services/KokizzuVPSBenchmark/VPSBenchmarkDataSource.cs delete mode 100644 BotNet.Services/VPS/VPSBenchmarkDataSource.cs diff --git a/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs b/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs index 55f5a45..495cd29 100644 --- a/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs @@ -176,7 +176,8 @@ out AIFollowUpMessage? aiFollowUpMessage try { await _telegramBotClient.SendTextMessageAsync( chatId: update.Message.Chat.Id, - text: $"Your SQL contains more than one statement.", + text: $"Your SQL contains more than one statement.", + parseMode: ParseMode.Html, replyToMessageId: update.Message.MessageId, cancellationToken: cancellationToken ); @@ -192,7 +193,8 @@ await _telegramBotClient.SendTextMessageAsync( try { await _telegramBotClient.SendTextMessageAsync( chatId: update.Message.Chat.Id, - text: $"Your SQL is not a SELECT statement.", + text: $"Your SQL is not a SELECT statement.", + parseMode: ParseMode.Html, replyToMessageId: update.Message.MessageId, cancellationToken: cancellationToken ); diff --git a/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs b/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs index f80a5ee..828570b 100644 --- a/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs +++ b/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs @@ -2,6 +2,7 @@ using BotNet.Commands.SQL; using BotNet.Services.SQL; using BotNet.Services.Sqlite; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.DependencyInjection; using SqlParser.Ast; using Telegram.Bot; @@ -20,7 +21,8 @@ public async Task Handle(SQLCommand command, CancellationToken cancellationToken || froms.Count == 0) { await _telegramBotClient.SendTextMessageAsync( chatId: command.Chat.Id, - text: "No FROM clause found.", + text: "No FROM clause found.", + parseMode: ParseMode.Html, replyToMessageId: command.SQLMessageId, cancellationToken: cancellationToken ); @@ -53,9 +55,12 @@ await _telegramBotClient.SendTextMessageAsync( await _telegramBotClient.SendTextMessageAsync( chatId: command.Chat.Id, text: $$""" - Table '{{table}}' not found. Available tables are: + Table '{{table}}' not found. Available tables are: - pilpres + - vps + """, + parseMode: ParseMode.Html, replyToMessageId: command.SQLMessageId, cancellationToken: cancellationToken ); @@ -68,52 +73,64 @@ await _telegramBotClient.SendTextMessageAsync( // Execute query using ScopedDatabase scopedDatabase = serviceScope.ServiceProvider.GetRequiredService(); StringBuilder resultBuilder = new(); - scopedDatabase.ExecuteReader( - commandText: command.RawStatement, - readAction: (reader) => { - string[] values = new string[reader.FieldCount]; - - // Get column names - for (int i = 0; i < reader.FieldCount; i++) { - values[i] = '"' + reader.GetName(i).Replace("\"", "\"\"") + '"'; - } - resultBuilder.AppendLine(string.Join(',', values)); - // Get rows - while (reader.Read()) { + try { + scopedDatabase.ExecuteReader( + commandText: command.RawStatement, + readAction: (reader) => { + string[] values = new string[reader.FieldCount]; + + // Get column names for (int i = 0; i < reader.FieldCount; i++) { - if (reader.IsDBNull(i)) { - values[i] = ""; - continue; - } + values[i] = '"' + reader.GetName(i).Replace("\"", "\"\"") + '"'; + } + resultBuilder.AppendLine(string.Join(',', values)); - Type fieldType = reader.GetFieldType(i); - if (fieldType == typeof(string)) { - values[i] = '"' + reader.GetString(i).Replace("\"", "\"\"") + '"'; - } else if (fieldType == typeof(int)) { - values[i] = reader.GetInt32(i).ToString(); - } else if (fieldType == typeof(long)) { - values[i] = reader.GetInt64(i).ToString(); - } else if (fieldType == typeof(float)) { - values[i] = reader.GetFloat(i).ToString(); - } else if (fieldType == typeof(double)) { - values[i] = reader.GetDouble(i).ToString(); - } else if (fieldType == typeof(decimal)) { - values[i] = reader.GetDecimal(i).ToString(); - } else if (fieldType == typeof(bool)) { - values[i] = reader.GetBoolean(i).ToString(); - } else if (fieldType == typeof(DateTime)) { - values[i] = reader.GetDateTime(i).ToString(); - } else if (fieldType == typeof(byte[])) { - values[i] = BitConverter.ToString(reader.GetFieldValue(i)).Replace("-", ""); - } else { - values[i] = reader[i].ToString(); + // Get rows + while (reader.Read()) { + for (int i = 0; i < reader.FieldCount; i++) { + if (reader.IsDBNull(i)) { + values[i] = ""; + continue; + } + + Type fieldType = reader.GetFieldType(i); + if (fieldType == typeof(string)) { + values[i] = '"' + reader.GetString(i).Replace("\"", "\"\"") + '"'; + } else if (fieldType == typeof(int)) { + values[i] = reader.GetInt32(i).ToString(); + } else if (fieldType == typeof(long)) { + values[i] = reader.GetInt64(i).ToString(); + } else if (fieldType == typeof(float)) { + values[i] = reader.GetFloat(i).ToString(); + } else if (fieldType == typeof(double)) { + values[i] = reader.GetDouble(i).ToString(); + } else if (fieldType == typeof(decimal)) { + values[i] = reader.GetDecimal(i).ToString(); + } else if (fieldType == typeof(bool)) { + values[i] = reader.GetBoolean(i).ToString(); + } else if (fieldType == typeof(DateTime)) { + values[i] = reader.GetDateTime(i).ToString(); + } else if (fieldType == typeof(byte[])) { + values[i] = BitConverter.ToString(reader.GetFieldValue(i)).Replace("-", ""); + } else { + values[i] = reader[i].ToString(); + } } + resultBuilder.AppendLine(string.Join(',', values)); } - resultBuilder.AppendLine(string.Join(',', values)); } - } - ); + ); + } catch (SqliteException exc) { + await _telegramBotClient.SendTextMessageAsync( + chatId: command.Chat.Id, + text: "" + exc.Message.Replace("SQLite Error", "Error") + "", + parseMode: ParseMode.Html, + replyToMessageId: command.SQLMessageId, + cancellationToken: cancellationToken + ); + return; + } // Send result await _telegramBotClient.SendTextMessageAsync( @@ -123,8 +140,6 @@ await _telegramBotClient.SendTextMessageAsync( replyToMessageId: command.SQLMessageId, cancellationToken: cancellationToken ); - - return; } private static void CollectTableNames(ref HashSet tables, TableFactor tableFactor) { diff --git a/BotNet.Services/GoogleSheets/GoogleSheetsClient.cs b/BotNet.Services/GoogleSheets/GoogleSheetsClient.cs new file mode 100644 index 0000000..c627840 --- /dev/null +++ b/BotNet.Services/GoogleSheets/GoogleSheetsClient.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Google.Apis.Sheets.v4; +using Google.Apis.Sheets.v4.Data; + +namespace BotNet.Services.GoogleSheets { + public sealed class GoogleSheetsClient( + SheetsService sheetsService + ) { + private readonly SheetsService _sheetsService = sheetsService; + + public async Task> GetDataAsync(string spreadsheetId, string range, string firstColumn, CancellationToken cancellationToken) { + int firstColumnIndex = GetColumnIndex(firstColumn); + + // Fetch data + SpreadsheetsResource.ValuesResource.GetRequest getRequest = _sheetsService.Spreadsheets.Values.Get( + spreadsheetId: spreadsheetId, + range: range + ); + ValueRange response = await getRequest.ExecuteAsync(cancellationToken); + + // Get type info + ConstructorInfo constructor = typeof(T).GetConstructors().Single(); + PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + // Map data + ImmutableList.Builder builder = ImmutableList.CreateBuilder(); + foreach (IList row in response.Values) { + if (row.Count < properties.Length) continue; + + object?[] parameters = new object?[properties.Length]; + for (int i = 0; i < properties.Length; i++) { + PropertyInfo property = properties[i]; + + FromColumnAttribute fromColumn = property.GetCustomAttribute() + ?? throw new InvalidProgramException("Property not decorated with [FromColumn]"); + + int columnIndex = GetColumnIndex(fromColumn.Column) - firstColumnIndex; + if (columnIndex >= row.Count) { + parameters[i] = null; + continue; + } + + if (row[columnIndex] is not string value) { + parameters[i] = null; + continue; + } + + if (property.PropertyType == typeof(string)) { + parameters[i] = value; + } else if (property.PropertyType == typeof(decimal)) { + if (decimal.TryParse(value, out decimal decimalValue)) { + parameters[i] = decimalValue; + } else { + parameters[i] = 0m; + } + } else if (property.PropertyType == typeof(decimal?)) { + if (decimal.TryParse(value, out decimal decimalValue)) { + parameters[i] = decimalValue; + } else { + parameters[i] = (decimal?)null; + } + } else if (property.PropertyType == typeof(int)) { + if (int.TryParse(value, out int intValue)) { + parameters[i] = intValue; + } else { + parameters[i] = 0; + } + } else if (property.PropertyType == typeof(int?)) { + if (int.TryParse(value, out int intValue)) { + parameters[i] = intValue; + } else { + parameters[i] = (int?)null; + } + } else if (property.PropertyType == typeof(double)) { + if (double.TryParse(value, out double doubleValue)) { + parameters[i] = doubleValue; + } else { + parameters[i] = 0.0; + } + } else if (property.PropertyType == typeof(double?)) { + if (double.TryParse(value, out double doubleValue)) { + parameters[i] = doubleValue; + } else { + parameters[i] = (double?)null; + } + } else { + parameters[i] = Convert.ChangeType(value, property.PropertyType); + } + } + + builder.Add((T)constructor.Invoke(parameters)); + } + + return builder.ToImmutable(); + } + + public static int GetColumnIndex(string columnName) { + int index = 0; + for (int i = 0; i < columnName.Length; i++) { + index *= 26; + index += (columnName[i] - 'A' + 1); + } + return index - 1; + } + } +} diff --git a/BotNet.Services/GoogleSheets/Options/GoogleSheetsOptions.cs b/BotNet.Services/GoogleSheets/Options/GoogleSheetsOptions.cs new file mode 100644 index 0000000..79aa191 --- /dev/null +++ b/BotNet.Services/GoogleSheets/Options/GoogleSheetsOptions.cs @@ -0,0 +1,5 @@ +namespace BotNet.Services.GoogleSheets.Options { + public class GoogleSheetsOptions { + public string? ApiKey { get; set; } + } +} diff --git a/BotNet.Services/GoogleSheets/ServiceCollectionExtensions.cs b/BotNet.Services/GoogleSheets/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..9632027 --- /dev/null +++ b/BotNet.Services/GoogleSheets/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using BotNet.Services.GoogleSheets.Options; +using Google.Apis.Services; +using Google.Apis.Sheets.v4; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace BotNet.Services.GoogleSheets { + public static class ServiceCollectionExtensions { + public static IServiceCollection AddGoogleSheets(this IServiceCollection services) { + services.AddSingleton(serviceProvider => { + GoogleSheetsOptions options = serviceProvider.GetRequiredService>().Value; + return new SheetsService(new BaseClientService.Initializer { + ApiKey = options.ApiKey + }); + }); + services.AddTransient(); + return services; + } + } +} diff --git a/BotNet.Services/KokizzuVPSBenchmark/ServiceCollectionExtensions.cs b/BotNet.Services/KokizzuVPSBenchmark/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..1fcf397 --- /dev/null +++ b/BotNet.Services/KokizzuVPSBenchmark/ServiceCollectionExtensions.cs @@ -0,0 +1,11 @@ +using BotNet.Services.SQL; +using Microsoft.Extensions.DependencyInjection; + +namespace BotNet.Services.KokizzuVPSBenchmark { + public static class ServiceCollectionExtensions { + public static IServiceCollection AddKokizzuVPSBenchmarkDataSource(this IServiceCollection services) { + services.AddKeyedTransient("vps"); + return services; + } + } +} diff --git a/BotNet.Services/VPS/VPSBenchmark.cs b/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmark.cs similarity index 85% rename from BotNet.Services/VPS/VPSBenchmark.cs rename to BotNet.Services/KokizzuVPSBenchmark/VPSBenchmark.cs index f700109..00b85cf 100644 --- a/BotNet.Services/VPS/VPSBenchmark.cs +++ b/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmark.cs @@ -1,11 +1,10 @@ -using System; -using BotNet.Services.GoogleSheets; +using BotNet.Services.GoogleSheets; -namespace BotNet.Services.VPS { +namespace BotNet.Services.KokizzuVPSBenchmark { public sealed record VPSBenchmark( [property: FromColumn("A")] string Provider, [property: FromColumn("B")] string Location, - [property: FromColumn("C")] DateOnly BenchmarkDate, + [property: FromColumn("C")] string BenchmarkDate, [property: FromColumn("E")] string? VerdictCons, [property: FromColumn("F")] decimal IdrMo, [property: FromColumn("G")] int Core, diff --git a/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmarkDataSource.cs b/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmarkDataSource.cs new file mode 100644 index 0000000..820ff4b --- /dev/null +++ b/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmarkDataSource.cs @@ -0,0 +1,114 @@ +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using BotNet.Services.GoogleSheets; +using BotNet.Services.SQL; +using BotNet.Services.Sqlite; + +namespace BotNet.Services.KokizzuVPSBenchmark { + public sealed class VPSBenchmarkDataSource( + GoogleSheetsClient googleSheetsClient, + ScopedDatabase scopedDatabase + ) : IScopedDataSource { + private readonly GoogleSheetsClient _googleSheetsClient = googleSheetsClient; + private readonly ScopedDatabase _scopedDatabase = scopedDatabase; + + public async Task LoadTableAsync(CancellationToken cancellationToken) { + _scopedDatabase.ExecuteNonQuery(""" + CREATE TABLE vps ( + Provider TEXT, + Location VARCHAR(2), + BenchmarkDate VARCHAR(10), + VerdictCons TEXT, + IdrMo REAL, + Core INTEGER, + SsdGb INTEGER, + RamMb INTEGER, + IoMbs INTEGER, + ToCacheFlyMbs REAL, + ToHkCnMbs REAL, + ToLinodeJpMbs REAL, + ToLinodeSgMbs REAL, + ToLinodeUkMbs REAL, + ToLinodeCaMbs REAL, + BzipSec REAL, + DlMbs REAL, + AvgMbs REAL + ) + """); + + ImmutableList data = await _googleSheetsClient.GetDataAsync( + // Source: https://docs.google.com/spreadsheets/d/14nAIFzIzkQuSxiayhc5tSFWFCWFncrV-GCA3Q5BbS4g/edit#gid=0 + spreadsheetId: "14nAIFzIzkQuSxiayhc5tSFWFCWFncrV-GCA3Q5BbS4g", + range: "'Result'!A3:T", + firstColumn: "A", + cancellationToken: cancellationToken + ); + + foreach (VPSBenchmark vpsBenchmark in data) { + _scopedDatabase.ExecuteNonQuery($""" + INSERT INTO vps ( + Provider, + Location, + BenchmarkDate, + VerdictCons, + IdrMo, + Core, + SsdGb, + RamMb, + IoMbs, + ToCacheFlyMbs, + ToHkCnMbs, + ToLinodeJpMbs, + ToLinodeSgMbs, + ToLinodeUkMbs, + ToLinodeCaMbs, + BzipSec, + DlMbs, + AvgMbs + ) VALUES ( + @Provider, + @Location, + @BenchmarkDate, + @VerdictCons, + @IdrMo, + @Core, + @SsdGb, + @RamMb, + @IoMbs, + @ToCacheFlyMbs, + @ToHkCnMbs, + @ToLinodeJpMbs, + @ToLinodeSgMbs, + @ToLinodeUkMbs, + @ToLinodeCaMbs, + @BzipSec, + @DlMbs, + @AvgMbs + ) + """, + [ + ("@Provider", vpsBenchmark.Provider), + ("@Location", vpsBenchmark.Location), + ("@BenchmarkDate", vpsBenchmark.BenchmarkDate), + ("@VerdictCons", vpsBenchmark.VerdictCons), + ("@IdrMo", vpsBenchmark.IdrMo), + ("@Core", vpsBenchmark.Core), + ("@SsdGb", vpsBenchmark.SsdGb), + ("@RamMb", vpsBenchmark.RamMb), + ("@IoMbs", vpsBenchmark.IoMbs), + ("@ToCacheFlyMbs", vpsBenchmark.ToCacheFlyMbs), + ("@ToHkCnMbs", vpsBenchmark.ToHkCnMbs), + ("@ToLinodeJpMbs", vpsBenchmark.ToLinodeJpMbs), + ("@ToLinodeSgMbs", vpsBenchmark.ToLinodeSgMbs), + ("@ToLinodeUkMbs", vpsBenchmark.ToLinodeUkMbs), + ("@ToLinodeCaMbs", vpsBenchmark.ToLinodeCaMbs), + ("@BzipSec", vpsBenchmark.BzipSec), + ("@DlMbs", vpsBenchmark.DlMbs), + ("@AvgMbs", vpsBenchmark.AvgMbs) + ] + ); + } + } + } +} diff --git a/BotNet.Services/VPS/VPSBenchmarkDataSource.cs b/BotNet.Services/VPS/VPSBenchmarkDataSource.cs deleted file mode 100644 index 8fab742..0000000 --- a/BotNet.Services/VPS/VPSBenchmarkDataSource.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace BotNet.Services.VPS { - public sealed class VPSBenchmarkDataSource { - // Source: https://docs.google.com/spreadsheets/d/14nAIFzIzkQuSxiayhc5tSFWFCWFncrV-GCA3Q5BbS4g/edit#gid=0 - private const string SPREADSHEET_ID = "14nAIFzIzkQuSxiayhc5tSFWFCWFncrV-GCA3Q5BbS4g"; - - - } -} diff --git a/BotNet/Program.cs b/BotNet/Program.cs index e21251a..8914412 100644 --- a/BotNet/Program.cs +++ b/BotNet/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using BotNet.Bot; using BotNet.CommandHandlers; @@ -15,8 +15,11 @@ using BotNet.Services.DynamicExpresso; using BotNet.Services.Gemini; using BotNet.Services.GoogleMap; +using BotNet.Services.GoogleSheets; +using BotNet.Services.GoogleSheets.Options; using BotNet.Services.Hosting; using BotNet.Services.ImageConverter; +using BotNet.Services.KokizzuVPSBenchmark; using BotNet.Services.Meme; using BotNet.Services.OpenAI; using BotNet.Services.Pemilu2024; @@ -61,6 +64,7 @@ builder.Services.Configure(builder.Configuration.GetSection("WeatherOptions")); builder.Services.Configure(builder.Configuration.GetSection("CommandPrioritizationOptions")); builder.Services.Configure(builder.Configuration.GetSection("GeminiOptions")); +builder.Services.Configure(builder.Configuration.GetSection("GoogleSheetsOptions")); builder.Services.AddHttpClient(); builder.Services.AddFontService(); builder.Services.AddColorCardRenderer(); @@ -91,6 +95,8 @@ builder.Services.AddGeminiClient(); builder.Services.AddSqliteDatabases(); builder.Services.AddPemilu2024(); +builder.Services.AddGoogleSheets(); +builder.Services.AddKokizzuVPSBenchmarkDataSource(); // MediatR builder.Services.AddMediatR(config => {