< Summary

Information
Class: GistBackend.Handlers.MariaDbHandler.MariaDbHandler
Assembly: GistBackend
File(s): /home/runner/work/the-gist-of-it-sec/the-gist-of-it-sec/backend/GistBackend/Handlers/MariaDbHandler/MariaDbHandler.cs
Line coverage
81%
Covered lines: 403
Uncovered lines: 91
Coverable lines: 494
Total lines: 939
Line coverage: 81.5%
Branch coverage
47%
Covered branches: 64
Total branches: 134
Branch coverage: 47.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
OpenTransactionAsync()0%3250%
CommitTransactionAsync()25%8437.5%
GetFeedInfoByRssUrlAsync()0%3250%
InsertFeedInfoAsync()50%22100%
UpdateFeedInfoAsync()75%44100%
GetGistByReferenceAsync()0%3250%
GetConstructedGistByReference()0%2286.66%
InsertGistAsync()50%22100%
InsertGistAsync()50%22100%
InsertDisabledGistAsync()0%3250%
InsertSummaryAsync()50%22100%
InsertSummaryAsync()50%22100%
UpdateGistAsync()50%22100%
UpdateGistAsync()75%44100%
UpdateDisabledGistAsync()25%5466.66%
UpdateSummaryAsync()75%44100%
DailyRecapExistsAsync(...)100%11100%
WeeklyRecapExistsAsync(...)100%11100%
RecapExistsAsync()83.33%6694.73%
GetConstructedGistsOfLastDayAsync(...)100%11100%
GetConstructedGistsOfLastWeekAsync(...)100%11100%
GetGistsWithFeedOfLastDaysAsync()0%2288.23%
InsertDailyRecapAsync(...)100%11100%
InsertWeeklyRecapAsync(...)100%11100%
InsertRecapAsync()0%2277.77%
GetAllGistsAsync()0%3250%
EnsureCorrectDisabledStateForGistAsync()60%111076.92%
GetDisabledStateForGistAsync()0%3250%
GetPreviousConstructedGistsAsync()0%2290.24%
GetLanguageModeConstraint(...)70%101083.33%
AddLastGistIdConstraint(...)100%11100%
AddSearchQueryConstraint(...)100%22100%
ParseSearchQuery(...)100%22100%
AddTagsConstraint(...)100%22100%
ParseTags(...)100%11100%
AddDisabledFeedsConstraint(...)100%11100%
AddSponsoredContentConstraint(...)100%44100%
GetConstructedGistByIdAsync()0%2286.66%
GetAllFeedInfosAsync()0%3250%
GetLatestRecapAsync()50%7666.66%
IsChatRegisteredAsync()25%6454.54%
RegisterChatAsync()75%88100%
GetMostRecentGistWithFeedAsync()75%44100%
DeregisterChatAsync()50%11863.63%
GetAllChatsAsync()0%3250%
GetNextFiveConstructedGistsAsync()0%2284.84%
SetGistIdLastSentForChatAsync()50%4469.23%
GetOpenConnectionAsync()0%5455.55%

File(s)

/home/runner/work/the-gist-of-it-sec/the-gist-of-it-sec/backend/GistBackend/Handlers/MariaDbHandler/MariaDbHandler.cs

#LineLine coverage
 1using System.Data.Common;
 2using System.Text.Json;
 3using Dapper;
 4using GistBackend.Exceptions;
 5using GistBackend.Types;
 6using GistBackend.Utils;
 7using Microsoft.Extensions.Logging;
 8using Microsoft.Extensions.Options;
 9using MySqlConnector;
 10using static GistBackend.Utils.LogEvents;
 11
 12namespace GistBackend.Handlers.MariaDbHandler;
 13
 14public interface IMariaDbHandler {
 15    Task< TransactionHandle> OpenTransactionAsync(CancellationToken ct);
 16    Task<RssFeedInfo?> GetFeedInfoByRssUrlAsync(Uri rssUrl, CancellationToken ct);
 17    Task<int> InsertFeedInfoAsync(RssFeedInfo feedInfo, CancellationToken ct);
 18    Task UpdateFeedInfoAsync(RssFeedInfo feedInfo, CancellationToken ct);
 19    Task<Gist?> GetGistByReferenceAsync(string reference, CancellationToken ct);
 20    Task<ConstructedGist?> GetConstructedGistByReference(string reference, LanguageMode? languageMode, CancellationToken
 21    Task<int> InsertGistAsync(Gist gist, CancellationToken ct);
 22    Task<int> InsertGistAsync(Gist gist,  TransactionHandle handle, CancellationToken ct);
 23    Task<int> InsertDisabledGistAsync(DisabledGist gist, CancellationToken ct);
 24    Task UpdateDisabledGistAsync(DisabledGist gist, CancellationToken ct);
 25    Task InsertSummaryAsync(Summary summary, CancellationToken ct);
 26    Task InsertSummaryAsync(Summary summary,  TransactionHandle handle, CancellationToken ct);
 27    Task UpdateGistAsync(Gist gist,  TransactionHandle handle, CancellationToken ct);
 28    Task UpdateSummaryAsync(Summary summary,  TransactionHandle handle, CancellationToken ct);
 29    Task<bool> DailyRecapExistsAsync(CancellationToken ct);
 30    Task<bool> WeeklyRecapExistsAsync(CancellationToken ct);
 31    Task<List<ConstructedGist>> GetConstructedGistsOfLastDayAsync(CancellationToken ct);
 32    Task<List<ConstructedGist>> GetConstructedGistsOfLastWeekAsync(CancellationToken ct);
 33    Task<int> InsertDailyRecapAsync(RecapAIResponse recapAIResponse, CancellationToken ct);
 34    Task<int> InsertWeeklyRecapAsync(RecapAIResponse recapAIResponse, CancellationToken ct);
 35    Task<List<Gist>> GetAllGistsAsync(CancellationToken ct);
 36    Task<bool> EnsureCorrectDisabledStateForGistAsync(int gistId, bool disabled, CancellationToken ct);
 37    Task<List<ConstructedGist>> GetPreviousConstructedGistsAsync(int take, int? lastGistId, IEnumerable<string> tags,
 38        string? searchQuery, IEnumerable<int> disabledFeeds, LanguageMode? languageMode, bool? includeSponsoredContent,
 39        CancellationToken ct);
 40    Task<ConstructedGist?> GetConstructedGistByIdAsync(int id, LanguageMode? languageMode, CancellationToken ct);
 41    Task<List<RssFeedInfo>> GetAllFeedInfosAsync(CancellationToken ct);
 42    Task<SerializedRecap?> GetLatestRecapAsync(RecapType recapType, CancellationToken ct);
 43    Task<bool> IsChatRegisteredAsync(long chatId, CancellationToken ct);
 44    Task RegisterChatAsync(long chatId, CancellationToken ct);
 45    Task DeregisterChatAsync(long chatId, CancellationToken ct);
 46    Task<List<Chat>> GetAllChatsAsync(CancellationToken ct);
 47    Task<List<ConstructedGist>> GetNextFiveConstructedGistsAsync(int lastGistId, LanguageMode languageMode,
 48        CancellationToken ct);
 49    Task SetGistIdLastSentForChatAsync(long chatId, int gistId, CancellationToken ct);
 50}
 51
 52public class MariaDbHandler : IMariaDbHandler
 53{
 54    private readonly string _connectionString;
 55    private readonly IDateTimeHandler _dateTimeHandler;
 56    private readonly ILogger<MariaDbHandler>? _logger;
 57
 12858    public MariaDbHandler(IOptions<MariaDbHandlerOptions> options,
 12859        IDateTimeHandler dateTimeHandler,
 12860        ILogger<MariaDbHandler>? logger)
 61    {
 12862        _dateTimeHandler = dateTimeHandler;
 12863        _logger = logger;
 12864        _connectionString = options.Value.GetConnectionString();
 12865        SqlMapper.AddTypeHandler(new UriTypeHandler());
 12866    }
 67
 68    public async Task< TransactionHandle> OpenTransactionAsync(CancellationToken ct)
 69    {
 70        try
 71        {
 62772            var connection = await GetOpenConnectionAsync(ct);
 62773            return new TransactionHandle(connection, await connection.BeginTransactionAsync(ct));
 74        }
 075        catch (MySqlException e)
 76        {
 077            _logger?.LogError(OpeningTransactionFailed, e, "Opening transaction failed");
 078            throw;
 79        }
 62780    }
 81
 82    public async Task CommitTransactionAsync(DbTransaction transaction, CancellationToken ct)
 83    {
 84        try
 85        {
 62386            await transaction.CommitAsync(ct);
 124687            if (transaction.Connection is null) return;
 088            await transaction.Connection.CloseAsync();
 089        }
 090        catch (MySqlException e)
 91        {
 092            _logger?.LogError(CommittingTransactionFailed, e, "Committing transaction failed");
 093            throw;
 94        }
 62395    }
 96
 97    public async Task<RssFeedInfo?> GetFeedInfoByRssUrlAsync(Uri rssUrl, CancellationToken ct)
 98    {
 99        const string query = "SELECT Title, RssUrl, Language, Type, Id FROM Feeds WHERE RssUrl = @RssUrl";
 3100        var command = new CommandDefinition(query, new { RssUrl = rssUrl }, cancellationToken: ct);
 101
 102        try
 103        {
 3104            await using var connection = await GetOpenConnectionAsync(ct);
 3105            return await connection.QueryFirstOrDefaultAsync<RssFeedInfo>(command).WithDeadlockRetry(_logger);
 0106        }
 0107        catch (MySqlException e)
 108        {
 0109            _logger?.LogError(GettingFeedInfoByUrlFailed, e, "Getting feedInfo by rssUrl failed");
 0110            throw;
 111        }
 3112    }
 113
 114    public async Task<int> InsertFeedInfoAsync(RssFeedInfo feedInfo, CancellationToken ct)
 115    {
 116        const string query = """
 117            INSERT INTO Feeds (Title, RssUrl, Language, Type)
 118                VALUES (@Title, @RssUrl, @Language, @Type);
 119            SELECT LAST_INSERT_ID();
 120        """;
 87121        var command = new CommandDefinition(query, feedInfo, cancellationToken: ct);
 122
 87123        await using var connection = await GetOpenConnectionAsync(ct);
 124        try
 125        {
 87126            return await connection.ExecuteScalarAsync<int>(command).WithDeadlockRetry(_logger);
 127        }
 1128        catch (MySqlException e)
 129        {
 1130            _logger?.LogError(InsertingFeedInfoFailed, e, "Inserting FeedInfo failed");
 1131            throw;
 132        }
 86133    }
 134
 135    public async Task UpdateFeedInfoAsync(RssFeedInfo feedInfo, CancellationToken ct)
 136    {
 137        const string query =
 138            "UPDATE Feeds SET Title = @Title, Language = @Language, Type = @Type WHERE RssUrl = @RssUrl";
 3139        var command = new CommandDefinition(query, feedInfo, cancellationToken: ct);
 140
 3141        await using var connection = await GetOpenConnectionAsync(ct);
 142        try
 143        {
 3144            var rowsAffected = await connection.ExecuteAsync(command).WithDeadlockRetry(_logger);
 4145            if (rowsAffected != 1) throw new DatabaseOperationException("Did not successfully update feed info");
 2146        }
 1147        catch (Exception e) when (e is MySqlException or DatabaseOperationException)
 148        {
 1149            _logger?.LogError(UpdatingFeedInfoFailed, e, "Updating FeedInfo failed");
 1150            throw;
 151        }
 2152    }
 153
 154    public async Task<Gist?> GetGistByReferenceAsync(string reference, CancellationToken ct)
 155    {
 156        const string query = """
 157            SELECT Reference, FeedId, Author, IsSponsoredContent, Published, Updated, Url, Tags, Id
 158                FROM Gists WHERE Reference = @Reference
 159        """;
 3160        var command = new CommandDefinition(query, new { Reference = reference }, cancellationToken: ct);
 161
 162        try
 163        {
 3164            await using var connection = await GetOpenConnectionAsync(ct);
 3165            return await connection.QueryFirstOrDefaultAsync<Gist>(command).WithDeadlockRetry(_logger);
 0166        }
 0167        catch (MySqlException e)
 168        {
 0169            _logger?.LogError(GettingGistByReferenceFailed, e, "Getting gist by reference failed");
 0170            throw;
 171        }
 3172    }
 173
 174    public async Task<ConstructedGist?> GetConstructedGistByReference(string reference, LanguageMode? languageMode,
 175        CancellationToken ct)
 176    {
 3177        var parameters = new DynamicParameters();
 3178        var query = $"""
 3179            SELECT
 3180                g.Id as Id,
 3181                g.Reference as Reference,
 3182                f.Title as FeedTitle,
 3183                f.RssUrl as FeedUrl,
 3184                f.Type as FeedType,
 3185                s.Title as Title,
 3186                g.Author as Author,
 3187                g.IsSponsoredContent as IsSponsoredContent,
 3188                g.Url as Url,
 3189                DATE_FORMAT(g.Published, '%Y-%m-%dT%H:%i:%s.%fZ') as Published,
 3190                DATE_FORMAT(g.Updated, '%Y-%m-%dT%H:%i:%s.%fZ') as Updated,
 3191                s.SummaryText as Summary,
 3192                g.Tags as Tags
 3193            FROM Gists g
 3194            INNER JOIN Feeds f ON g.FeedId = f.Id
 3195            INNER JOIN Summaries s ON s.GistId = g.Id
 3196            WHERE g.Reference = @Reference AND {GetLanguageModeConstraint(parameters, languageMode)}
 3197        """;
 3198        parameters.Add("Reference", reference);
 3199        var command = new CommandDefinition(query, parameters, cancellationToken: ct);
 200
 201        try
 202        {
 3203            await using var connection = await GetOpenConnectionAsync(ct);
 3204            return await connection.QueryFirstOrDefaultAsync<ConstructedGist>(command).WithDeadlockRetry(_logger);
 0205        }
 0206        catch (MySqlException e)
 207        {
 0208            _logger?.LogError(GettingGistByReferenceFailed, e, "Getting gist by reference failed");
 0209            throw;
 210        }
 3211    }
 212
 213    public async Task<int> InsertGistAsync(Gist gist, CancellationToken ct)
 214    {
 215        try
 216        {
 268217            await using var handle = await OpenTransactionAsync(ct);
 268218            var gistId = await InsertGistAsync(gist, handle, ct);
 267219            await CommitTransactionAsync(handle.Transaction, ct);
 267220            return gistId;
 1221        } catch (MySqlException e)
 222        {
 1223            _logger?.LogError(InsertingGistFailed, e, "Inserting Gist failed");
 1224            throw;
 225        }
 267226    }
 227
 228    public async Task<int> InsertGistAsync(Gist gist, TransactionHandle handle, CancellationToken ct)
 229    {
 230        const string query = """
 231            INSERT INTO Gists
 232                (Reference, FeedId, Author, IsSponsoredContent, Published, Updated, Url, Tags)
 233                VALUES (
 234                    @Reference, @FeedId, @Author, @IsSponsoredContent, @Published, @Updated, @Url, @Tags
 235                );
 236            SELECT LAST_INSERT_ID();
 237        """;
 268238        var command = new CommandDefinition(query, gist, handle.Transaction, cancellationToken: ct);
 239
 240        try
 241        {
 268242            return await handle.Connection.ExecuteScalarAsync<int>(command).WithDeadlockRetry(_logger);
 243        }
 1244        catch (MySqlException e)
 245        {
 1246            _logger?.LogError(InsertingGistFailed, e, "Inserting Gist failed");
 1247            throw;
 248        }
 267249    }
 250
 251    public async Task<int> InsertDisabledGistAsync(DisabledGist gist, CancellationToken ct)
 252    {
 253        const string query = """
 254            INSERT INTO Gists
 255                (Reference, FeedId, Author, IsSponsoredContent, Published, Updated, Url, Tags, Disabled)
 256                VALUES (
 257                    @Reference, @FeedId, @Author, @IsSponsoredContent, @Published, @Updated, @Url, @Tags, TRUE
 258                );
 259            SELECT LAST_INSERT_ID();
 260        """;
 1261        var command = new CommandDefinition(query, gist, cancellationToken: ct);
 262
 263        try
 264        {
 1265            await using var connection = await GetOpenConnectionAsync(ct);
 1266            return await connection.ExecuteScalarAsync<int>(command).WithDeadlockRetry(_logger);
 0267        }
 0268        catch (MySqlException e)
 269        {
 0270            _logger?.LogError(InsertingGistFailed, e, "Inserting Gist failed");
 0271            throw;
 272        }
 1273    }
 274
 275    public async Task InsertSummaryAsync(Summary summary, CancellationToken ct)
 276    {
 277        try
 278        {
 355279            await using var handle = await OpenTransactionAsync(ct);
 355280            await InsertSummaryAsync(summary, handle, ct);
 354281            await CommitTransactionAsync(handle.Transaction, ct);
 354282        }
 1283        catch (MySqlException e)
 284        {
 1285            _logger?.LogError(InsertingSummaryFailed, e, "Inserting Summary failed");
 1286            throw;
 287        }
 354288    }
 289
 290    public async Task InsertSummaryAsync(Summary summary,  TransactionHandle handle, CancellationToken ct)
 291    {
 292        const string query = """
 293            INSERT INTO Summaries (GistId, Language, IsTranslated, Title, SummaryText)
 294                VALUES (@GistId, @Language, @IsTranslated, @Title, @SummaryText);
 295        """;
 355296        var command = new CommandDefinition(query, summary, handle.Transaction, cancellationToken: ct);
 297
 298        try
 299        {
 355300            await handle.Connection.ExecuteAsync(command);
 354301        }
 1302        catch (MySqlException e)
 303        {
 1304            _logger?.LogError(InsertingSummaryFailed, e, "Inserting Summary failed");
 1305            throw;
 306        }
 354307    }
 308
 309    public async Task UpdateGistAsync(Gist gist, CancellationToken ct)
 310    {
 311        try
 312        {
 2313            await using var handle = await OpenTransactionAsync(ct);
 2314            await UpdateGistAsync(gist, handle, ct);
 1315            await CommitTransactionAsync(handle.Transaction, ct);
 1316        }
 1317        catch (Exception e) when (e is MySqlException or DatabaseOperationException)
 318        {
 1319            _logger?.LogError(UpdatingGistFailed, e, "Updating gist failed");
 1320            throw;
 321        }
 1322    }
 323
 324    public async Task UpdateGistAsync(Gist gist,  TransactionHandle handle, CancellationToken ct)
 325    {
 326        const string query = """
 327            UPDATE Gists
 328                SET FeedId = @FeedId, Author = @Author, IsSponsoredContent = @IsSponsoredContent,
 329                    Published = @Published, Updated = @Updated, Url = @Url, Tags = @Tags
 330                WHERE Reference = @Reference;
 331        """;
 2332        var command = new CommandDefinition(query, gist, handle.Transaction, cancellationToken: ct);
 333
 334        try
 335        {
 2336            var rowsAffected = await handle.Connection.ExecuteAsync(command).WithDeadlockRetry(_logger);
 3337            if (rowsAffected != 1) throw new DatabaseOperationException("Did not successfully update gist");
 1338        }
 1339        catch (Exception e) when (e is MySqlException or DatabaseOperationException)
 340        {
 1341            _logger?.LogError(UpdatingGistFailed, e, "Updating gist failed");
 1342            throw;
 343        }
 1344    }
 345
 346    public async Task UpdateDisabledGistAsync(DisabledGist gist, CancellationToken ct)
 347    {
 348        const string query = """
 349            UPDATE Gists
 350                SET FeedId = @FeedId, Author = @Author, IsSponsoredContent = @IsSponsoredContent,
 351                    Published = @Published, Updated = @Updated, Url = @Url, Tags = @Tags, Disabled = TRUE
 352                WHERE Reference = @Reference;
 353        """;
 1354        var command = new CommandDefinition(query, gist, cancellationToken: ct);
 355
 356        try
 357        {
 1358            await using var connection = await GetOpenConnectionAsync(ct);
 1359            var rowsAffected = await connection.ExecuteAsync(command).WithDeadlockRetry(_logger);
 1360            if (rowsAffected != 1) throw new DatabaseOperationException("Did not successfully update gist");
 1361        }
 0362        catch (MySqlException e)
 363        {
 0364            _logger?.LogError(UpdatingGistFailed, e, "Updating Gist failed");
 0365            throw;
 366        }
 1367    }
 368
 369    public async Task UpdateSummaryAsync(Summary summary, TransactionHandle handle, CancellationToken ct)
 370    {
 371        const string query = """
 372            UPDATE Summaries
 373                SET Title = @Title, SummaryText = @SummaryText
 374                WHERE GistId = @GistId AND Language = @Language;
 375        """;
 2376        var command = new CommandDefinition(query, summary, handle.Transaction, cancellationToken: ct);
 377
 378        try
 379        {
 2380            var rowsAffected = await handle.Connection.ExecuteAsync(command).WithDeadlockRetry(_logger);
 3381            if (rowsAffected != 1) throw new DatabaseOperationException("Did not successfully update summary");
 1382        }
 1383        catch (Exception e) when (e is MySqlException or DatabaseOperationException)
 384        {
 1385            _logger?.LogError(UpdatingSummaryFailed, e, "Updating summary failed");
 1386            throw;
 387        }
 1388    }
 389
 4390    public Task<bool> DailyRecapExistsAsync(CancellationToken ct) => RecapExistsAsync(RecapType.Daily, ct);
 391
 4392    public Task<bool> WeeklyRecapExistsAsync(CancellationToken ct) => RecapExistsAsync(RecapType.Weekly, ct);
 393
 394    private async Task<bool> RecapExistsAsync(RecapType recapType, CancellationToken ct)
 395    {
 8396        var query = $"SELECT COUNT(id) FROM Recaps{recapType.ToTypeString()}" +
 8397            " WHERE Created >= @StartOfDay AND Created < @StartOfNextDay";
 8398        var startOfDay = _dateTimeHandler.GetUtcNow().Date;
 8399        var command = new CommandDefinition(query,
 8400            new { StartOfDay = startOfDay, StartOfNextDay = startOfDay.AddDays(1) },
 8401            cancellationToken: ct);
 402
 403        try
 404        {
 8405            await using var connection = await GetOpenConnectionAsync(ct);
 8406            var recapCount = await connection.QuerySingleAsync<int>(command).WithDeadlockRetry(_logger);
 8407            return recapCount switch {
 4408                0 => false,
 2409                1 => true,
 2410                _ => throw new DatabaseOperationException("Found multiple recaps in database")
 8411            };
 0412        }
 2413        catch (Exception e) when (e is MySqlException or DatabaseOperationException)
 414        {
 2415            _logger?.LogError(CheckIfRecapExistsFailed, e, "Check if the {RecapType} recap exists failed",
 2416                recapType.ToTypeString());
 2417            throw;
 418        }
 6419    }
 420
 421    public Task<List<ConstructedGist>> GetConstructedGistsOfLastDayAsync(CancellationToken ct) =>
 3422        GetGistsWithFeedOfLastDaysAsync(1, ct);
 423
 424    public Task<List<ConstructedGist>> GetConstructedGistsOfLastWeekAsync(CancellationToken ct) =>
 3425        GetGistsWithFeedOfLastDaysAsync(7, ct);
 426
 427    private async Task<List<ConstructedGist>> GetGistsWithFeedOfLastDaysAsync(int days, CancellationToken ct)
 428    {
 6429        var parameters = new DynamicParameters();
 6430        var query = $"""
 6431            SELECT
 6432                g.Id as Id,
 6433                g.Reference as Reference,
 6434                f.Title as FeedTitle,
 6435                f.RssUrl as FeedUrl,
 6436                f.Type as FeedType,
 6437                s.Title as Title,
 6438                g.Author as Author,
 6439                g.IsSponsoredContent as IsSponsoredContent,
 6440                g.Url as Url,
 6441                DATE_FORMAT(g.Published, '%Y-%m-%dT%H:%i:%s.%fZ') as Published,
 6442                DATE_FORMAT(g.Updated, '%Y-%m-%dT%H:%i:%s.%fZ') as Updated,
 6443                s.SummaryText as Summary,
 6444                g.Tags as Tags
 6445            FROM Gists g
 6446            INNER JOIN Feeds f ON g.FeedId = f.Id
 6447            INNER JOIN Summaries s ON s.GistId = g.Id
 6448            WHERE {GetLanguageModeConstraint(parameters, LanguageMode.Original)} AND g.IsSponsoredContent IS FALSE
 6449                AND Updated >= @EarliestUpdated AND Updated <= @Now
 6450        """;
 6451        var now = _dateTimeHandler.GetUtcNow();
 6452        var earliestUpdated = now.AddDays(-days);
 6453        parameters.Add("Now", now);
 6454        parameters.Add("EarliestUpdated", earliestUpdated);
 6455        var command = new CommandDefinition(query, parameters, cancellationToken: ct);
 456
 457        try
 458        {
 6459            await using var connection = await GetOpenConnectionAsync(ct);
 6460            return (await connection.QueryAsync<ConstructedGist>(command).WithDeadlockRetry(_logger)).ToList();
 0461        }
 0462        catch (MySqlException e)
 463        {
 0464            _logger?.LogError(GettingGistsForRecapFailed, e, "Getting the gists of the last {Days} days failed", days);
 0465            throw;
 466        }
 6467    }
 468
 469    public Task<int> InsertDailyRecapAsync(RecapAIResponse recapAIResponse, CancellationToken ct) =>
 10470        InsertRecapAsync(RecapType.Daily, recapAIResponse, ct);
 471
 472    public Task<int> InsertWeeklyRecapAsync(RecapAIResponse recapAIResponse, CancellationToken ct) =>
 10473        InsertRecapAsync(RecapType.Weekly, recapAIResponse, ct);
 474
 475    private async Task<int> InsertRecapAsync(RecapType recapType, RecapAIResponse recapAIResponse, CancellationToken ct)
 476    {
 20477        var query = $"""
 20478            INSERT INTO Recaps{recapType.ToTypeString()} (Created, RecapEn, RecapDe)
 20479                VALUES (@Created, @RecapEn, @RecapDe);
 20480            SELECT LAST_INSERT_ID();
 20481        """;
 20482        var serializedRecap = new SerializedRecap(
 20483            _dateTimeHandler.GetUtcNow(),
 20484            JsonSerializer.Serialize(recapAIResponse.RecapSectionsEnglish, SerializerDefaults.JsonOptions),
 20485            JsonSerializer.Serialize(recapAIResponse.RecapSectionsGerman, SerializerDefaults.JsonOptions)
 20486        );
 20487        var command = new CommandDefinition(query, serializedRecap, cancellationToken: ct);
 488
 489        try
 490        {
 20491            await using var connection = await GetOpenConnectionAsync(ct);
 20492            return await connection.ExecuteScalarAsync<int>(command).WithDeadlockRetry(_logger);
 0493        }
 0494        catch (MySqlException e)
 495        {
 0496            _logger?.LogError(e, "Inserting {RecapType} recap failed", recapType.ToTypeString());
 0497            throw;
 498        }
 20499    }
 500
 501    public async Task<List<Gist>> GetAllGistsAsync(CancellationToken ct)
 502    {
 503        const string query =
 504            "SELECT Reference, FeedId, Author, IsSponsoredContent, Published, Updated, Url, Tags, Id FROM Gists";
 2505        var command = new CommandDefinition(query, cancellationToken: ct);
 506
 507        try
 508        {
 2509            await using var connection = await GetOpenConnectionAsync(ct);
 2510            return (await connection.QueryAsync<Gist>(command).WithDeadlockRetry(_logger)).ToList();
 0511        }
 0512        catch (MySqlException e)
 513        {
 0514            _logger?.LogError(GettingAllGistsFailed, e, "Getting all gists failed");
 0515            throw;
 516        }
 2517    }
 518
 519    public async Task<bool> EnsureCorrectDisabledStateForGistAsync(int gistId, bool disabled, CancellationToken ct)
 520    {
 18521        if (await GetDisabledStateForGistAsync(gistId, ct) == disabled) return true;
 522        const string query = "UPDATE Gists SET Disabled = @Disabled WHERE Id = @GistId";
 14523        var command = new CommandDefinition(query, new { Disabled = disabled, GistId = gistId }, cancellationToken: ct);
 524
 525        try
 526        {
 14527            await using var connection = await GetOpenConnectionAsync(ct);
 14528            var rowsAffected = await connection.ExecuteAsync(command).WithDeadlockRetry(_logger);
 14529            if (rowsAffected != 1) throw new DatabaseOperationException("Did not successfully set gist disabled state");
 14530        }
 0531        catch (Exception e) when (e is MySqlException or DatabaseOperationException)
 532        {
 0533            _logger?.LogError(EnsuringCorrectDisabledFailed, e, "Ensuring correct disabled state for gist failed");
 0534            throw;
 535        }
 536
 14537        _logger?.LogInformation(ChangedDisabledStateOfGistInDb,
 14538            "Changed disabled state of gist with ID {GistId} to {Disabled}", gistId, disabled);
 14539        return false;
 16540    }
 541
 542    private async Task<bool> GetDisabledStateForGistAsync(int gistId, CancellationToken ct)
 543    {
 544        const string query = "SELECT Disabled FROM Gists WHERE Id = @GistId";
 16545        var command = new CommandDefinition(query, new { GistId = gistId }, cancellationToken: ct);
 546
 547        try
 548        {
 16549            await using var connection = await GetOpenConnectionAsync(ct);
 16550            return await connection.QuerySingleAsync<bool>(command).WithDeadlockRetry(_logger);
 0551        }
 0552        catch (MySqlException e)
 553        {
 0554            _logger?.LogError(GettingDisabledStateFailed, e, "Getting disabled state for gist failed");
 0555            throw;
 556        }
 16557    }
 558
 559    public async Task<List<ConstructedGist>> GetPreviousConstructedGistsAsync(int take, int? lastGistId, IEnumerable<str
 560        string? searchQuery, IEnumerable<int> disabledFeeds, LanguageMode? languageMode, bool? includeSponsoredContent,
 561        CancellationToken ct)
 562    {
 52563        var parameters = new DynamicParameters();
 52564        var constraints = new List<string> {
 52565            "Disabled IS FALSE",
 52566            GetLanguageModeConstraint(parameters, languageMode)
 52567        };
 568
 52569        AddSponsoredContentConstraint(constraints, includeSponsoredContent);
 52570        AddLastGistIdConstraint(parameters, constraints, lastGistId);
 52571        AddSearchQueryConstraint(parameters, constraints, searchQuery);
 52572        AddTagsConstraint(parameters, constraints, tags);
 52573        AddDisabledFeedsConstraint(parameters, constraints, disabledFeeds);
 52574        parameters.Add("Take", take);
 575
 52576        var constraintsTemplate = string.Join(" AND ", constraints);
 577
 52578        var query = $"""
 52579            SELECT
 52580                g.Id as Id,
 52581                g.Reference as Reference,
 52582                f.Title as FeedTitle,
 52583                f.RssUrl as FeedUrl,
 52584                f.Type as FeedType,
 52585                s.Title as Title,
 52586                g.Author as Author,
 52587                g.IsSponsoredContent as IsSponsoredContent,
 52588                g.Url as Url,
 52589                DATE_FORMAT(g.Published, '%Y-%m-%dT%H:%i:%s.%fZ') as Published,
 52590                DATE_FORMAT(g.Updated, '%Y-%m-%dT%H:%i:%s.%fZ') as Updated,
 52591                s.SummaryText as Summary,
 52592                g.Tags as Tags
 52593            FROM Gists g
 52594            INNER JOIN Feeds f ON g.FeedId = f.Id
 52595            INNER JOIN Summaries s ON s.GistId = g.Id
 52596                WHERE {constraintsTemplate}
 52597            ORDER BY g.id DESC LIMIT @Take
 52598        """;
 599
 52600        var command = new CommandDefinition(query, parameters, cancellationToken: ct);
 601        try
 602        {
 52603            await using var connection = await GetOpenConnectionAsync(ct);
 52604            return (await connection.QueryAsync<ConstructedGist>(command).WithDeadlockRetry(_logger)).ToList();
 0605        }
 0606        catch (MySqlException e)
 607        {
 0608            _logger?.LogError(GettingPreviousGistsWithFeedFailed, e, "Getting previous gists with feed failed");
 0609            throw;
 610        }
 52611    }
 612
 613    private static string GetLanguageModeConstraint(DynamicParameters parameters, LanguageMode? languageMode)
 614    {
 70615        languageMode ??= LanguageMode.Original;
 616        switch (languageMode)
 617        {
 618            case LanguageMode.Original:
 69619                return "s.IsTranslated IS FALSE";
 620            case LanguageMode.En or LanguageMode.De:
 1621                var language = languageMode == LanguageMode.De ? Language.De : Language.En;
 1622                parameters.Add("Language", language);
 1623                return "s.Language = @Language";
 624            default:
 0625                throw new ArgumentOutOfRangeException(nameof(languageMode), languageMode, null);
 626        }
 627    }
 628
 629    private static void AddLastGistIdConstraint(DynamicParameters parameters, List<string> constraints, int? lastGistId)
 630    {
 52631        constraints.Add("g.Id < @LastGistId");
 52632        parameters.Add("LastGistId", lastGistId ?? int.MaxValue);
 52633    }
 634
 635    private static void AddSearchQueryConstraint(DynamicParameters parameters, List<string> constraints, string? searchQ
 636    {
 52637        var parsedSearchQuery = ParseSearchQuery(searchQuery);
 114638        for (var i = 0; i < parsedSearchQuery.Count; i++)
 639        {
 5640            parameters.Add($"SearchQuery{i}", parsedSearchQuery[i]);
 5641            constraints.Add($"(LOWER(s.Title) LIKE @SearchQuery{i} OR LOWER(s.SummaryText) LIKE @SearchQuery{i})");
 642        }
 52643    }
 644
 52645    private static List<string> ParseSearchQuery(string? searchQuery) => string.IsNullOrWhiteSpace(searchQuery)
 52646        ? []
 52647        : searchQuery
 52648            .Split(' ')
 6649            .Where(word => !string.IsNullOrWhiteSpace(word))
 5650            .Select(word => word.Trim().ToLowerInvariant())
 5651            .Select(word => $"%{word}%")
 52652            .ToList();
 653
 654    private static void AddTagsConstraint(DynamicParameters parameters, List<string> constraints, IEnumerable<string> ta
 655    {
 52656        var parsedTags = ParseTags(tags);
 114657        for (var i = 0; i < parsedTags.Count; i++)
 658        {
 5659            parameters.Add($"Tags{i}", parsedTags[i]);
 5660            constraints.Add($"g.Tags REGEXP @Tags{i}");
 661        }
 52662    }
 663
 52664    private static List<string> ParseTags(IEnumerable<string> tags) => tags
 5665        .Where(tag => !string.IsNullOrWhiteSpace(tag))
 5666        .Select(tag => $@"\b{tag}\b")
 52667        .ToList();
 668
 669    private static void AddDisabledFeedsConstraint(DynamicParameters parameters, List<string> constraints,
 670        IEnumerable<int> disabledFeeds)
 671    {
 52672        parameters.Add("DisabledFeeds", disabledFeeds);
 52673        constraints.Add("g.FeedId NOT IN @DisabledFeeds");
 52674    }
 675
 676    private static void AddSponsoredContentConstraint(List<string> constraints, bool? includeSponsoredContent)
 677    {
 103678        if (includeSponsoredContent is not true) constraints.Add("g.IsSponsoredContent IS FALSE");
 52679    }
 680
 681    public async Task<ConstructedGist?> GetConstructedGistByIdAsync(int id, LanguageMode? languageMode, CancellationToke
 682    {
 5683        var parameters = new DynamicParameters();
 5684        var query = $"""
 5685            SELECT
 5686                g.Id as Id,
 5687                g.Reference as Reference,
 5688                f.Title as FeedTitle,
 5689                f.RssUrl as FeedUrl,
 5690                f.Type as FeedType,
 5691                s.Title as Title,
 5692                g.Author as Author,
 5693                g.IsSponsoredContent as IsSponsoredContent,
 5694                g.Url as Url,
 5695                DATE_FORMAT(g.Published, '%Y-%m-%dT%H:%i:%s.%fZ') as Published,
 5696                DATE_FORMAT(g.Updated, '%Y-%m-%dT%H:%i:%s.%fZ') as Updated,
 5697                s.SummaryText as Summary,
 5698                g.Tags as Tags
 5699            FROM Gists g
 5700            INNER JOIN Feeds f ON g.FeedId = f.Id
 5701            INNER JOIN Summaries s ON s.GistId = g.Id
 5702            WHERE g.Id = @Id AND {GetLanguageModeConstraint(parameters, languageMode)}
 5703        """;
 5704        parameters.Add("Id", id);
 5705        var command = new CommandDefinition(query, parameters, cancellationToken: ct);
 706
 707        try
 708        {
 5709            await using var connection = await GetOpenConnectionAsync(ct);
 5710            return await connection.QuerySingleOrDefaultAsync<ConstructedGist>(command).WithDeadlockRetry(_logger);
 0711        }
 0712        catch (MySqlException e)
 713        {
 0714            _logger?.LogError(GettingGistByReferenceFailed, e, "Getting gist by ID failed");
 0715            throw;
 716        }
 5717    }
 718
 719    public async Task<List<RssFeedInfo>> GetAllFeedInfosAsync(CancellationToken ct)
 720    {
 721        const string query = "SELECT Title, RssUrl, Language, Type, Id FROM Feeds";
 4722        var command = new CommandDefinition(query, cancellationToken: ct);
 723
 724        try
 725        {
 4726            await using var connection = await GetOpenConnectionAsync(ct);
 4727            return (await connection.QueryAsync<RssFeedInfo>(command).WithDeadlockRetry(_logger)).ToList();
 0728        }
 0729        catch (MySqlException e)
 730        {
 0731            _logger?.LogError(GettingAllFeedInfosFailed, e, "Getting all feed infos failed");
 0732            throw;
 733        }
 4734    }
 735
 736    public async Task<SerializedRecap?> GetLatestRecapAsync(RecapType recapType, CancellationToken ct)
 737    {
 4738        var query = $"SELECT Created, RecapEn, RecapDe, Id FROM Recaps{recapType.ToTypeString()} ORDER BY Created DESC L
 4739        var command = new CommandDefinition(query, cancellationToken: ct);
 740
 741        try
 742        {
 4743            await using var connection = await GetOpenConnectionAsync(ct);
 4744            var serializedRecap = await connection.QuerySingleOrDefaultAsync<SerializedRecap>(command)
 4745                .WithDeadlockRetry(_logger);
 6746            if (serializedRecap is not null) return serializedRecap;
 2747            _logger?.LogInformation(NoRecapFound, "No {RecapType} recap found in database",
 2748                recapType.ToTypeString());
 2749            return null;
 0750        }
 0751        catch (MySqlException e)
 752        {
 0753            _logger?.LogError(GettingLatestRecapFailed, e, "Getting latest {RecapType} recap failed",
 0754                recapType.ToTypeString());
 0755            throw;
 756        }
 4757    }
 758
 759    public async Task<bool> IsChatRegisteredAsync(long chatId, CancellationToken ct)
 760    {
 761        const string query = "SELECT COUNT(Id) FROM Chats WHERE Id = @ChatId";
 3762        var command = new CommandDefinition(query, new { ChatId = chatId }, cancellationToken: ct);
 763
 764        try
 765        {
 3766            await using var connection = await GetOpenConnectionAsync(ct);
 3767            var count = await connection.ExecuteScalarAsync<int>(command).WithDeadlockRetry(_logger);
 3768            if (count > 1)
 769            {
 0770                throw new DatabaseOperationException($"Found multiple chats with the same ID {chatId} in database");
 771            }
 3772            return count > 0;
 0773        }
 0774        catch (MySqlException e)
 775        {
 0776            _logger?.LogError(ChatRegisterCheckFailed, e, "Checking if chat is registered failed");
 0777            throw;
 778        }
 3779    }
 780
 781    public async Task RegisterChatAsync(long chatId, CancellationToken ct)
 782    {
 783        const string query = "INSERT INTO Chats (Id, GistIdLastSent) VALUES (@ChatId, @GistIdLastSent)";
 32784        var mostRecentGistWithFeed = await GetMostRecentGistWithFeedAsync(ct);
 785        // Default to 0 if no gists are found, so that the first gist will be sent
 786        // otherwise set it to 5 less than the most recent gist ID to send the last 5 gists
 32787        var gistIdLastSent = mostRecentGistWithFeed?.Id - 5 ?? 0;
 32788        var command = new CommandDefinition(query, new { ChatId = chatId, GistIdLastSent = gistIdLastSent },
 32789            cancellationToken: ct);
 790
 791        try
 792        {
 32793            await using var connection = await GetOpenConnectionAsync(ct);
 32794            await connection.ExecuteAsync(command).WithDeadlockRetry(_logger);
 31795            _logger?.LogInformation(ChatRegistered,
 31796                "Chat with ID {ChatId} registered with GistIdLastSend {GistIdLastSend}", chatId, gistIdLastSent);
 31797        }
 1798        catch (MySqlException e)
 799        {
 1800            _logger?.LogError(RegisteringChatFailed, e,
 1801                "Registering chat with ID {ChatId} and GistIdLastSend {GistIdLastSend} failed", chatId, gistIdLastSent);
 1802            throw;
 803        }
 31804    }
 805
 806    private async Task<ConstructedGist?> GetMostRecentGistWithFeedAsync(CancellationToken ct)
 807    {
 32808        var gistsWithFeed = await GetPreviousConstructedGistsAsync(1, null, [], null, [], null, false, ct);
 34809        if (gistsWithFeed.Count != 0) return gistsWithFeed.Single();
 30810        _logger?.LogInformation(NoRecentGistFound, "No recent gist found in database");
 30811        return null;
 32812    }
 813
 814    public async Task DeregisterChatAsync(long chatId, CancellationToken ct)
 815    {
 816        const string query = "DELETE FROM Chats WHERE Id = @ChatId";
 3817        var command = new CommandDefinition(query, new { ChatId = chatId }, cancellationToken: ct);
 818
 819        try
 820        {
 3821            await using var connection = await GetOpenConnectionAsync(ct);
 3822            var rowsAffected = await connection.ExecuteAsync(command).WithDeadlockRetry(_logger);
 823            switch (rowsAffected)
 824            {
 825                case 0:
 1826                    throw new DatabaseOperationException($"No chat with ID {chatId} found to deregister");
 827                case > 1:
 0828                    throw new DatabaseOperationException($"Deregistered multiple chats with the same ID {chatId}");
 829                default:
 2830                    _logger?.LogInformation(ChatDeregistered, "Chat with ID {ChatId} deregistered", chatId);
 831                    break;
 832            }
 2833        }
 0834        catch (MySqlException e)
 835        {
 0836            _logger?.LogError(DeregisteringChatFailed, e, "Deregistering chat with ID {ChatId} failed", chatId);
 0837            throw;
 838        }
 2839    }
 840
 841    public async Task<List<Chat>> GetAllChatsAsync(CancellationToken ct)
 842    {
 843        const string query = "SELECT Id, GistIdLastSent FROM Chats";
 3844        var command = new CommandDefinition(query, cancellationToken: ct);
 845
 846        try
 847        {
 3848            await using var connection = await GetOpenConnectionAsync(ct);
 3849            return (await connection.QueryAsync<Chat>(command).WithDeadlockRetry(_logger)).ToList();
 0850        }
 0851        catch (MySqlException e)
 852        {
 0853            _logger?.LogError(GettingAllChatsFailed, e, "Getting all chats failed");
 0854            throw;
 855        }
 3856    }
 857
 858    public async Task<List<ConstructedGist>> GetNextFiveConstructedGistsAsync(int lastGistId, LanguageMode languageMode,
 859        CancellationToken ct)
 860    {
 4861        var parameters = new DynamicParameters();
 4862        var query = $"""
 4863            SELECT
 4864                g.Id as Id,
 4865                g.Reference as Reference,
 4866                f.Title as FeedTitle,
 4867                f.RssUrl as FeedUrl,
 4868                f.Type as FeedType,
 4869                s.Title as Title,
 4870                g.Author as Author,
 4871                g.IsSponsoredContent as IsSponsoredContent,
 4872                g.Url as Url,
 4873                DATE_FORMAT(g.Published, '%Y-%m-%dT%H:%i:%s.%fZ') as Published,
 4874                DATE_FORMAT(g.Updated, '%Y-%m-%dT%H:%i:%s.%fZ') as Updated,
 4875                s.SummaryText as Summary,
 4876                g.Tags as Tags
 4877            FROM Gists g
 4878            INNER JOIN Feeds f ON g.FeedId = f.Id
 4879            INNER JOIN Summaries s ON s.GistId = g.Id
 4880            WHERE g.Id > @LastGistId AND g.Disabled IS FALSE AND {GetLanguageModeConstraint(parameters, languageMode)}
 4881              AND g.IsSponsoredContent IS FALSE
 4882            ORDER BY g.Id ASC LIMIT 5
 4883        """;
 4884        parameters.Add("LastGistId", lastGistId);
 4885        var command = new CommandDefinition(query, parameters, cancellationToken: ct);
 886
 887        try
 888        {
 4889            await using var connection = await GetOpenConnectionAsync(ct);
 4890            return (await connection.QueryAsync<ConstructedGist>(command).WithDeadlockRetry(_logger)).ToList();
 0891        }
 0892        catch (MySqlException e)
 893        {
 0894            _logger?.LogError(GettingNextFiveGistsWithFeedFailed, e,
 0895                "Getting next gists with feed with lastGistId {LastGistId} failed", lastGistId);
 0896            throw;
 897        }
 4898    }
 899
 900    public async Task SetGistIdLastSentForChatAsync(long chatId, int gistId, CancellationToken ct)
 901    {
 902        const string query = "UPDATE Chats SET GistIdLastSent = @GistIdLastSent WHERE Id = @ChatId";
 3903        var command = new CommandDefinition(query, new { GistIdLastSent = gistId, ChatId = chatId },
 3904            cancellationToken: ct);
 905
 906        try
 907        {
 3908            await using var connection = await GetOpenConnectionAsync(ct);
 3909            var rowsAffected = await connection.ExecuteAsync(command).WithDeadlockRetry(_logger);
 3910            if (rowsAffected != 1)
 1911                throw new DatabaseOperationException(
 1912                    $"Did not successfully set GistIdLastSent for Chat {chatId} to {gistId}");
 2913        }
 0914        catch (MySqlException e)
 915        {
 0916            _logger?.LogError(SettingGistIdLastSentFailed, e,
 0917                "Setting GistIdLastSent for chat with ID {ChatId} to {GistId} failed", chatId, gistId);
 918
 0919            throw;
 920        }
 2921    }
 922
 923    private async Task<MySqlConnection> GetOpenConnectionAsync(CancellationToken ct)
 924    {
 907925        MySqlConnection? connection = null;
 926        try
 927        {
 907928            connection = new MySqlConnection(_connectionString);
 907929            await connection.OpenAsync(ct);
 907930            return connection;
 931        }
 0932        catch (Exception e)
 933        {
 0934            _logger?.LogError(DatabaseConnectionFailed, e, "Failed to connect to database");
 0935            if (connection is not null) await connection.DisposeAsync();
 0936            throw;
 937        }
 907938    }
 939}

Methods/Properties

.ctor(Microsoft.Extensions.Options.IOptions`1<GistBackend.Handlers.MariaDbHandler.MariaDbHandlerOptions>,GistBackend.Handlers.IDateTimeHandler,Microsoft.Extensions.Logging.ILogger`1<GistBackend.Handlers.MariaDbHandler.MariaDbHandler>)
OpenTransactionAsync()
CommitTransactionAsync()
GetFeedInfoByRssUrlAsync()
InsertFeedInfoAsync()
UpdateFeedInfoAsync()
GetGistByReferenceAsync()
GetConstructedGistByReference()
InsertGistAsync()
InsertGistAsync()
InsertDisabledGistAsync()
InsertSummaryAsync()
InsertSummaryAsync()
UpdateGistAsync()
UpdateGistAsync()
UpdateDisabledGistAsync()
UpdateSummaryAsync()
DailyRecapExistsAsync(System.Threading.CancellationToken)
WeeklyRecapExistsAsync(System.Threading.CancellationToken)
RecapExistsAsync()
GetConstructedGistsOfLastDayAsync(System.Threading.CancellationToken)
GetConstructedGistsOfLastWeekAsync(System.Threading.CancellationToken)
GetGistsWithFeedOfLastDaysAsync()
InsertDailyRecapAsync(GistBackend.Types.RecapAIResponse,System.Threading.CancellationToken)
InsertWeeklyRecapAsync(GistBackend.Types.RecapAIResponse,System.Threading.CancellationToken)
InsertRecapAsync()
GetAllGistsAsync()
EnsureCorrectDisabledStateForGistAsync()
GetDisabledStateForGistAsync()
GetPreviousConstructedGistsAsync()
GetLanguageModeConstraint(Dapper.DynamicParameters,System.Nullable`1<GistBackend.Types.LanguageMode>)
AddLastGistIdConstraint(Dapper.DynamicParameters,System.Collections.Generic.List`1<System.String>,System.Nullable`1<System.Int32>)
AddSearchQueryConstraint(Dapper.DynamicParameters,System.Collections.Generic.List`1<System.String>,System.String)
ParseSearchQuery(System.String)
AddTagsConstraint(Dapper.DynamicParameters,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.IEnumerable`1<System.String>)
ParseTags(System.Collections.Generic.IEnumerable`1<System.String>)
AddDisabledFeedsConstraint(Dapper.DynamicParameters,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.IEnumerable`1<System.Int32>)
AddSponsoredContentConstraint(System.Collections.Generic.List`1<System.String>,System.Nullable`1<System.Boolean>)
GetConstructedGistByIdAsync()
GetAllFeedInfosAsync()
GetLatestRecapAsync()
IsChatRegisteredAsync()
RegisterChatAsync()
GetMostRecentGistWithFeedAsync()
DeregisterChatAsync()
GetAllChatsAsync()
GetNextFiveConstructedGistsAsync()
SetGistIdLastSentForChatAsync()
GetOpenConnectionAsync()