using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Dapper; using Microsoft.Data.Sqlite; using Serilog; using SharpRss.Models; namespace SharpRss.Services { internal class DatabaseService : IDisposable { internal DatabaseService() { _sqlConn = new SqliteConnection(_connectionString); InitializeDb(); } private readonly SqliteConnection _sqlConn; private readonly string _connectionString = $"Data Source={Path.Combine(Environment.CurrentDirectory, "sharp_rss.sqlite")};"; private readonly string _groupTable = "group_data"; private readonly string _feedTable = "feed_data"; private readonly string _feedItemTable = "feed_item_data"; // Groups public async Task> GetGroupsAsync(string? groupName = null) { _sqlConn.Open(); using SqliteCommand cmd = new SqliteCommand(groupName != null ? $"SELECT * FROM {_groupTable} WHERE name=@name;" : $"SELECT * FROM {_groupTable}", _sqlConn) { Parameters = { new SqliteParameter("name", groupName) } }; await using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); HashSet groups = new HashSet(); while (reader.Read()) { groups.Add(new GroupModel() { Name = reader["name"].ToString(), HexColor = reader["hex_color"].ToString(), Icon = reader["icon"].ToString(), Id = reader["id"].ToString() }); } _sqlConn.Close(); return groups; } /// /// Creates a group if not exists then update the group. /// /// /// public async Task SetGroupAsync(GroupModel groupModel) { bool result = false; _sqlConn.Open(); using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_groupTable} (id, hex_color, icon, name) VALUES (IFNULL((SELECT id FROM {_groupTable} WHERE name=@name), @id), @hexColor, @icon, @name)", _sqlConn) { Parameters = { new SqliteParameter("id", groupModel.Id), new SqliteParameter("hexColor", groupModel.HexColor), new SqliteParameter("icon", groupModel.Icon), new SqliteParameter("name", groupModel.Name) } }; int affected = await cmd.ExecuteNonQueryAsync(); if (affected != 0) result = true; _sqlConn.Close(); return result; } public async Task RemoveGroupAsync(GroupModel groupModel) { bool result = false; _sqlConn.Open(); // Remove the group and remove the feeds that were part of the group. using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_groupTable} WHERE id=@id; UPDATE {_feedTable} SET group_id=NULL WHERE group_id=@id", _sqlConn) { Parameters = { new SqliteParameter("id", groupModel.Id) } }; int affected = await cmd.ExecuteNonQueryAsync(); if (affected != 0) result = true; _sqlConn.Close(); return result; } // Feeds public async Task> GetFeedsAsync(GroupModel? group = null) { HashSet feeds = new HashSet(); _sqlConn.Open(); using SqliteCommand cmd = new SqliteCommand(group != null ? $"SELECT * FROM {_feedTable} WHERE group_id=@groupid" : $"SELECT * FROM {_feedTable}", _sqlConn) { Parameters = { new SqliteParameter("groupId", group?.Id == string.Empty ? null : group?.Id) } }; await using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); while (reader.Read()) { feeds.Add(new FeedModel() { Id = reader["id"].ToString(), Url = reader["url"].ToString(), GroupId = reader["group_id"].ToString(), FeedType = reader["feed_type"].ToString(), Description = reader["description"].ToString(), Language = reader["language"].ToString(), Copyright = reader["copyright"].ToString(), DateAdded = DateTimeOffset.FromUnixTimeMilliseconds(long.TryParse(reader["date_added"].ToString(), out long parsedVal) ? parsedVal : 0), LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.TryParse(reader["last_updated"].ToString(), out long lastUpdated) ? lastUpdated : 0), ImageUrl = reader["image_url"].ToString() }); } _sqlConn.Close(); return feeds; } public async Task SetFeedAsync(FeedModel feedModel) { bool result = false; _sqlConn.Open(); using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_feedTable} (id, url, group_id, feed_type, description, language, copyright, date_added, last_updated, image_url) VALUES (IFNULL((SELECT id FROM {_feedTable} WHERE url=@url), @id), @url, @groupId, @feedType, @description, @language, @copyright, @dateAdded, @lastUpdated, @imageUrl)", _sqlConn) { Parameters = { new SqliteParameter("id", feedModel.Id), new SqliteParameter("url", feedModel.Url), new SqliteParameter("groupId", feedModel.GroupId), new SqliteParameter("feedType", feedModel.FeedType), new SqliteParameter("description", feedModel.Description), new SqliteParameter("language", feedModel.Language), new SqliteParameter("copyright", "EMPTY"), new SqliteParameter("dateAdded", feedModel.DateAdded.ToUnixTimeMilliseconds()), new SqliteParameter("lastUpdated", feedModel.LastUpdated.ToUnixTimeMilliseconds()), new SqliteParameter("imageUrl", "EMPTY") } }; int affected = await cmd.ExecuteNonQueryAsync(); if (affected != 0) result = true; _sqlConn.Close(); return result; } public async Task RemoveFeedAsync(FeedModel feedModel) { bool result = false; _sqlConn.Open(); // After removing the feed unset the feed id from the feed items using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_feedTable} WHERE id=@id; UPDATE {_feedItemTable} SET feed_id=NULL WHERE feed_id=@id", _sqlConn) { Parameters = { new SqliteParameter("id", feedModel.Id) } }; int affected = await cmd.ExecuteNonQueryAsync(); if (affected != 0) result = true; _sqlConn.Close(); return result; } // Feed items public async Task> GetFeedItemsAsync() { HashSet feeditems = new HashSet (); _sqlConn.Open(); using SqliteCommand cmd = new SqliteCommand($"SELECT * FROM {_feedItemTable}", _sqlConn); await using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); while (reader.Read()) { feeditems.Add(new FeedItemModel() { Id = reader["id"].ToString(), FeedId = reader["feed_id"].ToString(), Read = int.TryParse(reader["read"].ToString(), out int parsedValue)? parsedValue == 0 ? false : true : false, Type = reader["type"].ToString(), Title = reader["title"].ToString(), Description = reader["description"].ToString(), Link = reader["link"].ToString(), LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())), PublishingDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["publishing_date"].ToString())), Author = reader["author"].ToString(), Categories = reader["categories"].ToString().Split(','), Content = reader["content"].ToString() }); } _sqlConn.Close(); return feeditems; } public async Task SetFeedItemsAsync(HashSet items) { int result = 0; _sqlConn.Open(); using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_feedItemTable} (id, feed_id, read, type, title, description, link, last_updated, publishing_date, author, categories, content)" + $"VALUES (IFNULL((SELECT id FROM {_feedItemTable} WHERE url=@url), @id), @feedId, @read, @type, @title, @description, @link, @lastUpdated, @publishingDate, @author, @categories, @content)", _sqlConn); foreach (FeedItemModel item in items) { cmd.Parameters.Clear(); cmd.Parameters.Add(new SqliteParameter("id", item.Id)); cmd.Parameters.Add(new SqliteParameter("feedid", item.FeedId)); cmd.Parameters.Add(new SqliteParameter("read", item.Read ? 1 : 0)); cmd.Parameters.Add(new SqliteParameter("type", item.Type)); cmd.Parameters.Add(new SqliteParameter("title", item.Title)); cmd.Parameters.Add(new SqliteParameter("description", item.Description)); cmd.Parameters.Add(new SqliteParameter("link", item.Link)); cmd.Parameters.Add(new SqliteParameter("lastUpdated", item.LastUpdated.ToUnixTimeMilliseconds())); cmd.Parameters.Add(new SqliteParameter("publishingDate", item.PublishingDate?.ToUnixTimeMilliseconds() ?? 0)); cmd.Parameters.Add(new SqliteParameter("author", item.Author)); cmd.Parameters.Add(new SqliteParameter("categories", string.Join(',', item.Categories))); cmd.Parameters.Add(new SqliteParameter("content", item.Content)); int affected = await cmd.ExecuteNonQueryAsync(); if (affected == 0) Log.Verbose($"Could not set feed item: {item.Link}"); else result += affected; } _sqlConn.Close(); return result; // Return the amount affected rows. } public async Task RemoveFeedItemAsync(FeedItemModel itemModel) { bool result = false; _sqlConn.Open(); using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_feedItemTable} WHERE id=@id", _sqlConn) { Parameters = { new SqliteParameter("id", itemModel.Id) } }; int affected = await cmd.ExecuteNonQueryAsync(); if (affected != 0) result = true; _sqlConn.Close(); return result; } private async void InitializeDb() { Log.Verbose("Checking database..."); HashSet failed = new HashSet(); _sqlConn.Open(); Log.Verbose("Checking table: {Table}", _groupTable); var queryResponse = await _sqlConn.QueryAsync($"CREATE TABLE IF NOT EXISTS {_groupTable} (name STRING NOT NULL, hex_color STRING NOT NULL, icon STRING, id STRING PRIMARY KEY)"); if (queryResponse.Any()) failed.Add("category_data"); Log.Verbose("Checking table: {Table}", _feedTable); queryResponse = await _sqlConn.QueryAsync($"CREATE TABLE IF NOT EXISTS {_feedTable} (id STRING PRIMARY KEY, url STRING NOT NULL, group_id STRING DEFAULT NULL, feed_type STRING, description STRING, language STRING, copyright STRING, date_added INT, last_updated INT, image_url STRING)"); if (queryResponse.Any()) failed.Add("feed_data"); Log.Verbose("Checking table: {Table}", _feedItemTable); queryResponse = await _sqlConn.QueryAsync($"CREATE TABLE IF NOT EXISTS {_feedItemTable} (id STRING PRIMARY KEY, feed_id STRING DEFAULT NULL, read INT, type STRING, title STRING, description STRING, link STRING, last_updated INT, publishing_date INT, author STRING, categories STRING, content STRING)"); if (queryResponse.Any()) failed.Add("feed_item_data"); _sqlConn.Close(); if (failed.Any()) { var joined = string.Join(',', failed); Log.Error("Failed to initialize table(s): {TableNames}", joined); } else Log.Verbose("Checking database done!"); } public void Dispose() { _sqlConn.Dispose(); } } }