diff --git a/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs b/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs index 828570b..0b79a25 100644 --- a/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs +++ b/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using SqlParser.Ast; using Telegram.Bot; +using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; namespace BotNet.CommandHandlers.SQL { @@ -56,6 +57,7 @@ await _telegramBotClient.SendTextMessageAsync( chatId: command.Chat.Id, text: $$""" Table '{{table}}' not found. Available tables are: + - pileg_dpr - pilpres - vps @@ -73,6 +75,7 @@ await _telegramBotClient.SendTextMessageAsync( // Execute query using ScopedDatabase scopedDatabase = serviceScope.ServiceProvider.GetRequiredService(); StringBuilder resultBuilder = new(); + int rows = 0; try { scopedDatabase.ExecuteReader( @@ -118,6 +121,7 @@ await _telegramBotClient.SendTextMessageAsync( } } resultBuilder.AppendLine(string.Join(',', values)); + rows++; } } ); @@ -133,13 +137,24 @@ await _telegramBotClient.SendTextMessageAsync( } // Send result - await _telegramBotClient.SendTextMessageAsync( - chatId: command.Chat.Id, - text: "```csv\n" + resultBuilder.ToString() + "```", - parseMode: ParseMode.MarkdownV2, - replyToMessageId: command.SQLMessageId, - cancellationToken: cancellationToken - ); + string csvResult = resultBuilder.ToString(); + if (csvResult.Length > 4000) { + await _telegramBotClient.SendDocumentAsync( + chatId: command.Chat.Id, + caption: $"{rows} rows affected", + document: new InputFileStream(new MemoryStream(Encoding.UTF8.GetBytes(csvResult)), "result.csv"), + replyToMessageId: command.SQLMessageId, + cancellationToken: cancellationToken + ); + } else { + await _telegramBotClient.SendTextMessageAsync( + chatId: command.Chat.Id, + text: "```csv\n" + resultBuilder.ToString() + $"```\n{rows} rows affected", + parseMode: ParseMode.MarkdownV2, + replyToMessageId: command.SQLMessageId, + cancellationToken: cancellationToken + ); + } } private static void CollectTableNames(ref HashSet tables, TableFactor tableFactor) { diff --git a/BotNet.Services/Pemilu2024/PilegDPRDataSource.cs b/BotNet.Services/Pemilu2024/PilegDPRDataSource.cs new file mode 100644 index 0000000..54fdbc5 --- /dev/null +++ b/BotNet.Services/Pemilu2024/PilegDPRDataSource.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BotNet.Services.SQL; +using BotNet.Services.Sqlite; + +namespace BotNet.Services.Pemilu2024 { + public sealed class PilegDPRDataSource( + ScopedDatabase scopedDatabase, + SirekapClient sirekapClient + ) : IScopedDataSource { + private const string PKB = "1"; + private const string GERINDRA = "2"; + private const string PDIP = "3"; + private const string GOLKAR = "4"; + private const string NASDEM = "5"; + private const string PARTAI_BURUH = "6"; + private const string GELORA = "7"; + private const string PKS = "8"; + private const string PKN = "9"; + private const string HANURA = "10"; + private const string GARUDA = "11"; + private const string PAN = "12"; + private const string PBB = "13"; + private const string DEMOKRAT = "14"; + private const string PSI = "15"; + private const string PERINDO = "16"; + private const string PPP = "17"; + private const string PNA = "18"; + private const string GABTHAT = "19"; + private const string PDA = "20"; + private const string PARTAI_ACEH = "21"; + private const string PAS_ACEH = "22"; + private const string PARTAI_SIRA = "23"; + private const string PARTAI_UMMAT = "24"; + private readonly ScopedDatabase _scopedDatabase = scopedDatabase; + private readonly SirekapClient _sirekapClient = sirekapClient; + + public async Task LoadTableAsync(CancellationToken cancellationToken) { + _scopedDatabase.ExecuteNonQuery(""" + CREATE TABLE pileg_dpr ( + provinsi VARCHAR(50) PRIMARY KEY, + progress REAL, + pkb INTEGER, + gerindra INTEGER, + pdip INTEGER, + golkar INTEGER, + nasdem INTEGER, + partai_buruh INTEGER, + gelora INTEGER, + pks INTEGER, + pkn INTEGER, + hanura INTEGER, + garuda INTEGER, + pan INTEGER, + pbb INTEGER, + demokrat INTEGER, + psi INTEGER, + perindo INTEGER, + ppp INTEGER, + pna INTEGER, + gabthat INTEGER, + pda INTEGER, + partai_aceh INTEGER, + pas_aceh INTEGER, + partai_sira INTEGER, + partai_ummat INTEGER + ) + """); + + IList listProvinsi = await _sirekapClient.GetPronvisiListAsync(cancellationToken); + Dictionary provinsiByKode = listProvinsi.ToDictionary( + keySelector: provinsi => provinsi.Kode + ); + + ReportPilegDPR report = await _sirekapClient.GetReportPilegDPRAsync(cancellationToken); + + foreach ((string kodeWilayah, ReportPilegDPR.Row row) in report.RowByKodeWilayah.OrderBy(pair => pair.Key)) { + _scopedDatabase.ExecuteNonQuery(""" + INSERT INTO pileg_dpr (provinsi, progress, pkb, gerindra, pdip, golkar, nasdem, partai_buruh, gelora, pks, pkn, hanura, garuda, pan, pbb, demokrat, psi, perindo, ppp, pna, gabthat, pda, partai_aceh, pas_aceh, partai_sira, partai_ummat) + VALUES (@provinsi, @progress, @pkb, @gerindra, @pdip, @golkar, @nasdem, @partai_buruh, @gelora, @pks, @pkn, @hanura, @garuda, @pan, @pbb, @demokrat, @psi, @perindo, @ppp, @pna, @gabthat, @pda, @partai_aceh, @pas_aceh, @partai_sira, @partai_ummat) + """, + [ + ( "@provinsi", provinsiByKode[kodeWilayah].Nama ), + ( "@progress", row.Persen ), + ( "@pkb", row.VotesByKodePartai!.TryGetValue(PKB, out int pkb) ? pkb : null), + ( "@gerindra", row.VotesByKodePartai!.TryGetValue(GERINDRA, out int gerindra) ? gerindra : null), + ( "@pdip", row.VotesByKodePartai!.TryGetValue(PDIP, out int pdip) ? pdip : null), + ( "@golkar", row.VotesByKodePartai!.TryGetValue(GOLKAR, out int golkar) ? golkar : null), + ( "@nasdem", row.VotesByKodePartai!.TryGetValue(NASDEM, out int nasdem) ? nasdem : null), + ( "@partai_buruh", row.VotesByKodePartai!.TryGetValue(PARTAI_BURUH, out int partai_buruh) ? partai_buruh : null), + ( "@gelora", row.VotesByKodePartai!.TryGetValue(GELORA, out int gelora) ? gelora : null), + ( "@pks", row.VotesByKodePartai!.TryGetValue(PKS, out int pks) ? pks : null), + ( "@pkn", row.VotesByKodePartai!.TryGetValue(PKN, out int pkn) ? pkn : null), + ( "@hanura", row.VotesByKodePartai!.TryGetValue(HANURA, out int hanura) ? hanura : null), + ( "@garuda", row.VotesByKodePartai!.TryGetValue(GARUDA, out int garuda) ? garuda : null), + ( "@pan", row.VotesByKodePartai!.TryGetValue(PAN, out int pan) ? pan : null), + ( "@pbb", row.VotesByKodePartai!.TryGetValue(PBB, out int pbb) ? pbb : null), + ( "@demokrat", row.VotesByKodePartai!.TryGetValue(DEMOKRAT, out int demokrat) ? demokrat : null), + ( "@psi", row.VotesByKodePartai!.TryGetValue(PSI, out int psi) ? psi : null), + ( "@perindo", row.VotesByKodePartai!.TryGetValue(PERINDO, out int perindo) ? perindo : null), + ( "@ppp", row.VotesByKodePartai!.TryGetValue(PPP, out int ppp) ? ppp : null), + ( "@pna", row.VotesByKodePartai!.TryGetValue(PNA, out int pna) ? pna : null), + ( "@gabthat", row.VotesByKodePartai!.TryGetValue(GABTHAT, out int gabthat) ? gabthat : null), + ( "@pda", row.VotesByKodePartai!.TryGetValue(PDA, out int pda) ? pda : null), + ( "@partai_aceh", row.VotesByKodePartai!.TryGetValue(PARTAI_ACEH, out int partai_aceh) ? partai_aceh : null), + ( "@pas_aceh", row.VotesByKodePartai!.TryGetValue(PAS_ACEH, out int pas_aceh) ? pas_aceh : null), + ( "@partai_sira", row.VotesByKodePartai!.TryGetValue(PARTAI_SIRA, out int partai_sira) ? partai_sira : null), + ( "@partai_ummat", row.VotesByKodePartai!.TryGetValue(PARTAI_UMMAT, out int partai_ummat) ? partai_ummat : null) + ] + ); + } + } + } +} diff --git a/BotNet.Services/Pemilu2024/ServiceCollectionExtensions.cs b/BotNet.Services/Pemilu2024/ServiceCollectionExtensions.cs index 7b7e91d..7cbb438 100644 --- a/BotNet.Services/Pemilu2024/ServiceCollectionExtensions.cs +++ b/BotNet.Services/Pemilu2024/ServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddPemilu2024(this IServiceCollection services) { services.AddTransient(); services.AddKeyedTransient("pilpres"); + services.AddKeyedTransient("pileg_dpr"); return services; } } diff --git a/BotNet.Services/Pemilu2024/SirekapClient.cs b/BotNet.Services/Pemilu2024/SirekapClient.cs index 197bd8e..cce7410 100644 --- a/BotNet.Services/Pemilu2024/SirekapClient.cs +++ b/BotNet.Services/Pemilu2024/SirekapClient.cs @@ -63,5 +63,12 @@ public async Task GetReportPilpresByWilayahAsync(string kodeWilay cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } + + public async Task GetReportPilegDPRAsync(CancellationToken cancellationToken) { + return await _httpClient.GetFromJsonAsync( + requestUri: "https://sirekap-obj-data.kpu.go.id/pemilu/hhcw/pdpr.json", + cancellationToken: cancellationToken + ) ?? throw new JsonException("Unexpected response"); + } } } diff --git a/BotNet.Services/Pemilu2024/Types.cs b/BotNet.Services/Pemilu2024/Types.cs index e88b938..c5f99e6 100644 --- a/BotNet.Services/Pemilu2024/Types.cs +++ b/BotNet.Services/Pemilu2024/Types.cs @@ -67,4 +67,45 @@ public sealed record Progress( int Progres ); } + + public sealed record ReportPilegDPR( + [property: JsonPropertyName("ts")] string Timestamp, + string Psu, + string Mode, + IDictionary Chart, + [property: JsonPropertyName("table")] IDictionary RowByKodeWilayah, + ReportPilegDPR.Progress Progres + ) { + public sealed record Row { + public string? Psu { get; set; } + public decimal Persen { get; set; } + public bool StatusProgress { get; set; } + + [JsonExtensionData] + public IDictionary? VotesByKodePartaiJson { get; set; } + + [JsonIgnore] + public IDictionary? VotesByKodePartai { + get { + if (VotesByKodePartaiJson is null) { + return null; + } + + Dictionary votesByKodeCalon = []; + foreach (KeyValuePair kvp in VotesByKodePartaiJson) { + if (kvp.Value.ValueKind == JsonValueKind.Number + && kvp.Value.TryGetInt32(out int votes)) { + votesByKodeCalon[kvp.Key] = votes; + } + } + return votesByKodeCalon; + } + } + } + + public sealed record Progress( + int Total, + int Progres + ); + } }