This commit is contained in:
kislovdm
2026-03-10 14:23:42 +03:00
commit 6c8b9334c8
106 changed files with 4538 additions and 0 deletions
@@ -0,0 +1,83 @@
using Microsoft.Data.Sqlite;
using PepperBot.Application.Interfaces;
using PepperBot.Domain;
namespace PepperBot.Infrastructure.Data;
public class SqliteDealRepository : IDealRepository
{
private const string DatabaseFileName = "pepper_bot.db";
private string ConnectionString =>
new SqliteConnectionStringBuilder
{
DataSource = DatabaseFileName
}.ToString();
public async Task InitializeAsync(CancellationToken cancellationToken)
{
await using var connection = new SqliteConnection(ConnectionString);
await connection.OpenAsync(cancellationToken);
var command = connection.CreateCommand();
command.CommandText =
"""
CREATE TABLE IF NOT EXISTS Deals (
Id INTEGER PRIMARY KEY,
Title TEXT NOT NULL,
CurrentPrice REAL NULL,
RetailPrice REAL NULL,
PercentOff INTEGER NULL,
CreatedAt TEXT NOT NULL,
CreatedAtInMillis INTEGER NOT NULL,
DealUrl TEXT NOT NULL,
StoreName TEXT NOT NULL,
StorePermalink TEXT NOT NULL
);
""";
await command.ExecuteNonQueryAsync(cancellationToken);
}
public async Task<bool> ExistsAsync(long id, CancellationToken cancellationToken)
{
await using var connection = new SqliteConnection(ConnectionString);
await connection.OpenAsync(cancellationToken);
var command = connection.CreateCommand();
command.CommandText = "SELECT 1 FROM Deals WHERE Id = $id LIMIT 1;";
command.Parameters.AddWithValue("$id", id);
var result = await command.ExecuteScalarAsync(cancellationToken);
return result is not null;
}
public async Task AddAsync(Deal deal, CancellationToken cancellationToken)
{
await using var connection = new SqliteConnection(ConnectionString);
await connection.OpenAsync(cancellationToken);
var command = connection.CreateCommand();
command.CommandText =
"""
INSERT INTO Deals
(Id, Title, CurrentPrice, RetailPrice, PercentOff, CreatedAt, CreatedAtInMillis, DealUrl, StoreName, StorePermalink)
VALUES
($id, $title, $currentPrice, $retailPrice, $percentOff, $createdAt, $createdAtInMillis, $dealUrl, $storeName, $storePermalink);
""";
command.Parameters.AddWithValue("$id", deal.Id);
command.Parameters.AddWithValue("$title", deal.Title);
command.Parameters.AddWithValue("$currentPrice", (object?)deal.CurrentPrice ?? DBNull.Value);
command.Parameters.AddWithValue("$retailPrice", (object?)deal.RetailPrice ?? DBNull.Value);
command.Parameters.AddWithValue("$percentOff", (object?)deal.PercentOff ?? DBNull.Value);
command.Parameters.AddWithValue("$createdAt", deal.CreatedAt.ToString("O"));
command.Parameters.AddWithValue("$createdAtInMillis", deal.CreatedAtInMillis);
command.Parameters.AddWithValue("$dealUrl", deal.DealUrl);
command.Parameters.AddWithValue("$storeName", deal.StoreName);
command.Parameters.AddWithValue("$storePermalink", deal.StorePermalink);
await command.ExecuteNonQueryAsync(cancellationToken);
}
}
@@ -0,0 +1,126 @@
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using PepperBot.Application.Interfaces;
using PepperBot.Domain;
namespace PepperBot.Infrastructure.Pepper;
public class PepperClient : IPepperClient
{
private const string Url =
"https://auth.pepper.ru/v4/home/new?per_page=10&page=1&type=new&fields=id%2Ctitle%2Ccurrent_price%2Cshipping_and_handling%2Cretail_price%2Cpercent_off%2Ctop_deal%2Cimage_medium%2Ccreated_at%2Ccreated_at_in_millis%2Ccomments_count%2Clife_time_hotness%2Cuser%7Bid%2Clogin%2Cimage_medium%2Ccurrent_title%2Cdisplay_title%2Cdisplay_title_icon%7D%2Cstore%7Bpermalink%2Cname%2Cimage%7D%2Cpermalink%2Cposts_count%2Cvote_value%2Cdeal_types%2Cview_count%2Cforum%7Bforum_type%2Cname%2Cpermalink%7D%2Ctag_color_code%2C+dark_theme_tag_color_code%2Cworkflow_state%2Cdisplay_hotness_icon%2Creferral_state%2Ccoupon_code%2Cdeal_url%2Ccomment_preview%2Cis_nsfw%2Cdescription";
private const string ClientHeaderName = "x-desidime-client";
private const string DefaultClientHeaderValue =
"6c266d5dc070c811e61f611ddd970a3defb493a23c50ee01f32eb9b0cfe46cd8";
private readonly HttpClient _httpClient;
private readonly ILogger<PepperClient> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public PepperClient(ILogger<PepperClient> logger)
{
_logger = logger;
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://auth.pepper.ru")
};
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
var clientHeader = Environment.GetEnvironmentVariable("PEPPER_CLIENT_HEADER");
_httpClient.DefaultRequestHeaders.Add(ClientHeaderName,
string.IsNullOrWhiteSpace(clientHeader) ? DefaultClientHeaderValue : clientHeader);
_httpClient.DefaultRequestHeaders.Host = "auth.pepper.ru";
}
public async Task<IReadOnlyList<Deal>> GetLatestDealsAsync(CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, Url);
using var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
var dto = await JsonSerializer.DeserializeAsync<PepperResponse>(stream, JsonOptions, cancellationToken);
if (dto?.Deals is null)
{
_logger.LogWarning("Не удалось распарсить ответ Pepper");
return Array.Empty<Deal>();
}
return dto.Deals.Select(MapToDeal).ToArray();
}
private static Deal MapToDeal(PepperDeal dto)
{
return new Deal
{
Id = dto.Id,
Title = dto.Title ?? string.Empty,
CurrentPrice = dto.CurrentPrice,
RetailPrice = dto.RetailPrice,
PercentOff = dto.PercentOff,
CreatedAt = dto.CreatedAt ?? DateTimeOffset.FromUnixTimeMilliseconds(dto.CreatedAtInMillis).UtcDateTime,
CreatedAtInMillis = dto.CreatedAtInMillis,
DealUrl = dto.DealUrl ?? string.Empty,
StoreName = dto.Store?.Name ?? string.Empty,
StorePermalink = dto.Store?.Permalink ?? string.Empty
};
}
private sealed class PepperResponse
{
[JsonPropertyName("deals")]
public List<PepperDeal>? Deals { get; set; }
}
private sealed class PepperDeal
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("current_price")]
public decimal? CurrentPrice { get; set; }
[JsonPropertyName("retail_price")]
public decimal? RetailPrice { get; set; }
[JsonPropertyName("percent_off")]
public int? PercentOff { get; set; }
[JsonPropertyName("created_at")]
public DateTime? CreatedAt { get; set; }
[JsonPropertyName("created_at_in_millis")]
public long CreatedAtInMillis { get; set; }
[JsonPropertyName("deal_url")]
public string? DealUrl { get; set; }
[JsonPropertyName("store")]
public PepperStore? Store { get; set; }
}
private sealed class PepperStore
{
[JsonPropertyName("permalink")]
public string? Permalink { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
}
}
@@ -0,0 +1,87 @@
using System.Net.Http.Json;
using System.Text;
using Microsoft.Extensions.Logging;
using PepperBot.Application.Interfaces;
using PepperBot.Domain;
namespace PepperBot.Infrastructure.Telegram;
public class TelegramNotifier : ITelegramNotifier
{
private readonly HttpClient _httpClient;
private readonly ILogger<TelegramNotifier> _logger;
private readonly string _botToken;
private readonly string _chatId;
public TelegramNotifier(ILogger<TelegramNotifier> logger)
{
_logger = logger;
_botToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN") ?? string.Empty;
_chatId = Environment.GetEnvironmentVariable("TELEGRAM_CHAT_ID") ?? string.Empty;
if (string.IsNullOrWhiteSpace(_botToken) || string.IsNullOrWhiteSpace(_chatId))
{
_logger.LogWarning("TELEGRAM_BOT_TOKEN или TELEGRAM_CHAT_ID не заданы. Отправка сообщений отключена.");
}
_httpClient = new HttpClient();
}
public async Task NotifyNewDealAsync(Deal deal, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(_botToken) || string.IsNullOrWhiteSpace(_chatId))
{
return;
}
var url = $"https://api.telegram.org/bot{_botToken}/sendMessage";
var textBuilder = new StringBuilder();
textBuilder.AppendLine($"🔥 *{Escape(deal.Title)}*");
if (deal.CurrentPrice is not null)
{
textBuilder.AppendLine($"Цена: {deal.CurrentPrice:0.##} ₽");
}
if (deal.RetailPrice is not null && deal.RetailPrice > 0 && deal.CurrentPrice is not null)
{
textBuilder.AppendLine($"Старая цена: {deal.RetailPrice:0.##} ₽");
}
if (deal.PercentOff is not null)
{
textBuilder.AppendLine($"Скидка: {deal.PercentOff}%");
}
textBuilder.AppendLine();
textBuilder.AppendLine($"Магазин: {Escape(deal.StoreName)}");
textBuilder.AppendLine();
textBuilder.AppendLine($"[Открыть на Pepper.ru]({Escape(deal.DealUrl)})");
var payload = new
{
chat_id = _chatId,
text = textBuilder.ToString(),
parse_mode = "Markdown"
};
var response = await _httpClient.PostAsJsonAsync(url, payload, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogWarning("Не удалось отправить сообщение в Telegram: {Status} {Body}", response.StatusCode, body);
}
}
private static string Escape(string value)
{
return value
.Replace("_", "\\_")
.Replace("*", "\\*")
.Replace("[", "\\[")
.Replace("`", "\\`");
}
}