using System; using System.Collections.Generic; using System.Data.Common; using System.IO; using System.Linq; using System.Threading.Tasks; using Dapper; using Microsoft.Data.Sqlite; using Serilog; using SharpRss.Core.Cache; using SharpRss.Models; namespace SharpRss { public class ItemFetchState { public string[]? SyndicationUrls { get; internal set; } public int TakeAmount { get; internal set; } = 20; public int TakenCount { get; internal set; } public HashSet Items { get; internal set; } = new HashSet(); } internal static class DbAccess { private static readonly string ConnectionString = $"Data Source={Path.Combine(Environment.CurrentDirectory, "sharp_rss.sqlite")};"; private static bool _isInitialized; public static async Task SetSyndicationAsync(SyndicationContainer synContainer) { if (synContainer.Category != null) { CategoryModel? catModel = await SetCategoryAsync(synContainer.Category); if (catModel != null) synContainer.SyndicationModel.CategoryId = catModel.Id; } if (synContainer.SyndicationModel != null) await SetSyndicationAsync(synContainer.SyndicationModel); if (synContainer.SyndicationItems != null && synContainer.SyndicationItems.Any()) await SetSyndicationItemsAsync(synContainer.SyndicationItems); } public static async Task> GetCategoriesAsync() { HashSet categories = new HashSet(); await using SqliteConnection dbc = new SqliteConnection(ConnectionString); dbc.Open(); await using DbDataReader reader = await dbc.ExecuteReaderAsync("SELECT * FROM category"); while (await reader.ReadAsync()) { CategoryModel categoryModel = new CategoryModel() { Id = reader["id"].ToString(), Name = reader["name"].ToString(), HexColor = reader["hex_color"].ToString(), Icon = reader["icon"].ToString(), LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())) }; categories.Add(categoryModel); } return categories; } public static async Task SetCategoryAsync(CategoryModel category) { CategoryModel? modelReturn = null; if (category == null) return modelReturn; await using SqliteConnection dbc = new SqliteConnection(ConnectionString); dbc.Open(); int affected = await dbc.ExecuteAsync("INSERT OR REPLACE INTO category (id, name, hex_color, icon, last_updated) VALUES (IFNULL((SELECT id FROM category WHERE name=@Name), @Id), @Name, @HexColor, @Icon, @LastUpdated)", new { Id = category.Id, Name = category.Name, HexColor = category.HexColor, Icon = category.Icon, LastUpdated = category.LastUpdated.ToUnixTimeMilliseconds() }); if (affected <= 0) return modelReturn; var catModel = await GetCategoriesAsync(); modelReturn = catModel.Where(x => x.Name == category.Name).ToHashSet().FirstOrDefault() ?? null; return modelReturn; } public static async Task DeleteCategory(CategoryModel category) { if (category == null) return false; await using SqliteConnection dbc = new SqliteConnection(ConnectionString); dbc.Open(); int affected = await dbc.ExecuteAsync("DELETE FROM category WHERE id=@Id; UPDATE syndication SET category_id=NULL WHERE category_id=@Id",new { category.Id }); return affected > 0; } public static async Task SetSyndicationAsync(SyndicationModel syndication) { await using SqliteConnection dbc = new SqliteConnection(ConnectionString); dbc.Open(); int affected = await dbc.ExecuteAsync(@"INSERT OR REPLACE INTO syndication (encoded_url, title, category_id, syndication_type, version, description, language, copyright, last_updated, publication_date, syn_updated_date, categories, image_url) VALUES ( @EncodedUrl, @Title, @CategoryId, @SyndicationType, @Version, @Description, @Language, @Copyright, @LastUpdated, @PublicationDate, @SynUpdatedDate, @Categories, @ImageUrl)", new { EncodedUrl = syndication.EncodedUrl, Title = syndication.Title ?? string.Empty, CategoryId = syndication.CategoryId ?? string.Empty, SyndicationType = syndication.SyndicationType ?? string.Empty, Version = syndication.Version ?? string.Empty, Description = syndication.Description ?? string.Empty, Language = syndication.Language ?? string.Empty, Copyright = syndication.Copyright ?? string.Empty, LastUpdated = syndication.LastUpdated.ToUnixTimeMilliseconds(), PublicationDate = syndication.PublicationDate?.ToUnixTimeMilliseconds() ?? 0, SynUpdatedDate = syndication.SynUpdatedDate?.ToUnixTimeMilliseconds() ?? 0, Categories = syndication.Categories != null && syndication.Categories.Any() ? SyndicationManager.FormatStringArrayToString(syndication.Categories) : string.Empty, ImageUrl = syndication.ImageUrl ?? string.Empty }); if (affected == 0) Log.Warning("Failed to add feed: {FeedUrl}", syndication.EncodedUrl); } public static async Task> GetSyndicationsAsync(string[]? categoryIds = null) { await using SqliteConnection dbc = new SqliteConnection(ConnectionString); dbc.Open(); await TempCache.UpdateCache(CacheFetch.Category); HashSet feeds = new HashSet(); await using DbDataReader reader = await dbc.ExecuteReaderAsync(categoryIds == null ? "SELECT * FROM syndication" : "SELECT * FROM syndication WHERE category_id IN(@CatIds)", new { CatIds = categoryIds }); while (await reader.ReadAsync()) { SyndicationModel syndicationModel = new SyndicationModel() { EncodedUrl = reader["encoded_url"].ToString(), Title = reader["title"].ToString(), CategoryId = reader["category_id"].ToString(), SyndicationType = reader["syndication_type"].ToString(), Version = reader["version"].ToString(), Description = reader["description"].ToString(), Language = reader["language"].ToString(), Copyright = reader["copyright"].ToString(), LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())), PublicationDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["publication_date"].ToString())), SynUpdatedDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["syn_updated_date"].ToString())), Categories = SyndicationManager.StringToStringArray(reader["categories"].ToString()), ImageUrl = reader["image_url"].ToString() }; syndicationModel.Category = TempCache.GetCategory(syndicationModel.CategoryId); syndicationModel.ItemCount = await dbc.ExecuteScalarAsync("SELECT COUNT(*) FROM syndication_item WHERE encoded_syndication_url=@EncodedFeedUrl", new { EncodedFeedUrl = syndicationModel.EncodedUrl }); feeds.Add(syndicationModel); } return feeds; } public static async Task DeleteSyndicationAsync(SyndicationModel syndication, bool deleteItems = false) { if (syndication == null) return false; Log.Information("Removing syndication..."); await using SqliteConnection dbc = new SqliteConnection(ConnectionString); int affected = await dbc.ExecuteAsync("DELETE FROM syndication WHERE encoded_url=@EncodedUrl", new { EncodedUrl = syndication.EncodedUrl }); if (affected > 0 && deleteItems) await dbc.ExecuteAsync("DELETE FROM syndication_item WHERE encoded_syndication_url=@EncodedUrl", new { EncodedUrl = syndication.EncodedUrl }); return affected > 0; } public static async Task SetSyndicationItemsAsync(HashSet items) { Log.Information("Inserting syndication items..."); await using SqliteConnection dbc = new SqliteConnection(ConnectionString); dbc.Open(); int totalAffected = 0; await using SqliteTransaction dbTransaction = dbc.BeginTransaction(); foreach (SyndicationItemModel item in items) { int affected = await dbc.ExecuteAsync(@"INSERT OR REPLACE INTO syndication_item (link, encoded_syndication_url, read, type, title, description, last_updated, item_updated_date, publishing_date, authors, categories, content, comments_url) VALUES (@Link, @EncodedSyndicationUrl, @Read, @Type, @Title, @Description, @LastUpdated, @ItemUpdatedDate, @PublishingDate, @Authors, @Categories, @Content, @CommentsUrl)", transaction: dbTransaction, param: new { Link = item.Link ?? string.Empty, EncodedSyndicationUrl = item.EncodedSyndicationUrl ?? string.Empty, Read = item.Read.ToString(), Type = item.Type ?? string.Empty, Title = item.Title ?? string.Empty, Description = item.Description ?? string.Empty, LastUpdated = item.LastUpdated.ToUnixTimeMilliseconds(), ItemUpdatedDate = item.ItemUpdatedDate?.ToUnixTimeMilliseconds() ?? 0, PublishingDate = item.PublishingDate?.ToUnixTimeMilliseconds() ?? 0, Authors = item.Authors != null && item.Authors.Any() ? SyndicationManager.FormatStringArrayToString(item.Authors) : string.Empty, Categories = item.Categories != null && item.Categories.Any() ? SyndicationManager.FormatStringArrayToString(item.Categories) : string.Empty, Content = item.Content ?? string.Empty, CommentsUrl = item.CommentsUrl ?? string.Empty }); totalAffected += affected; } dbTransaction.Commit(); } public static async Task GetSyndicationItemsAsync(ItemFetchState state) { await using SqliteConnection dbc = new SqliteConnection(ConnectionString); dbc.Open(); HashSet items = new HashSet(); await TempCache.UpdateCache(CacheFetch.Syndication); await using DbDataReader reader = await dbc.ExecuteReaderAsync(state.SyndicationUrls == null ? "SELECT * FROM syndication_item" : $"SELECT * FROM syndication_item WHERE encoded_syndication_url IN({FormatParametersFromArray(state.SyndicationUrls)})"); //NOTE(dbg): While debugging this part of the function the code below (Reader.ReadAsync) will return 0, because the debugger already reads the items from the reader. while (await reader.ReadAsync()) { SyndicationItemModel syndicationItemModel = new SyndicationItemModel() { Link = reader["link"].ToString(), EncodedSyndicationUrl = reader["encoded_syndication_url"].ToString(), Read = bool.Parse(reader["read"].ToString()), Type = reader["type"].ToString(), Title = reader["title"].ToString(), Description = reader["description"].ToString(), LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())), ItemUpdatedDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["item_updated_date"].ToString())), PublishingDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["publishing_date"].ToString())), Authors = SyndicationManager.StringToStringArray(reader["authors"].ToString()), Categories = SyndicationManager.StringToStringArray(reader["categories"].ToString()), Content = reader["content"].ToString(), CommentsUrl = reader["comments_url"].ToString() }; syndicationItemModel.SyndicationParent = TempCache.GetSyndication(syndicationItemModel.EncodedSyndicationUrl) ?? new SyndicationModel(); // The new syndication should never be initialized, if this hits then the date is not valid for some reason!!! items.Add(syndicationItemModel); } Log.Information("Fetching feed items resulted: {ItemCount} item(s)", items.Count); state.Items = items; return true; } public static async void Initialize() { if (_isInitialized) return; Log.Verbose("Checking database..."); await using SqliteConnection dbc = new SqliteConnection(ConnectionString); dbc.Open(); Log.Information("Checking table: {Table}", "category"); await dbc.ExecuteAsync("CREATE TABLE IF NOT EXISTS category (id STRING PRIMARY KEY, name STRING NOT NULL, hex_color STRING NOT NULL, icon STRING, last_updated INT)"); Log.Information("Checking table: {Table}", "syndication"); await dbc.ExecuteAsync("CREATE TABLE IF NOT EXISTS syndication (encoded_url STRING PRIMARY KEY, title STRING, category_id STRING, syndication_type STRING, version STRING, description STRING, language STRING, copyright STRING, last_updated INT, publication_date INT, syn_updated_date INT, categories STRING, image_url STRING)"); Log.Information("Checking table: {Table}", "syndication_item"); await dbc.ExecuteAsync("CREATE TABLE IF NOT EXISTS syndication_item (link STRING PRIMARY KEY, encoded_syndication_url STRING, read STRING, type STRING, title STRING, description STRING, last_updated INT, item_updated_date INT, publishing_date INT, authors STRING, categories STRING, content STRING, comments_url STRING)"); Log.Information("Checking database done!"); _isInitialized = true; } private static string FormatParametersFromArray(string[] dbParams) { string[] formatted = dbParams.Select(s => $"'{s}'").ToArray(); return string.Join(", ", formatted); } /*public static async Task GetSyndicationCountAsync(string[] encodedSyndicationUrls) { await using SqliteConnection dbc = new SqliteConnection(ConnectionString); dbc.Open(); return await dbc.ExecuteScalarAsync("SELECT COUNT(*) FROM syndication WHERE encoded_url IN(@Urls)"); }*/ } }