mirror of
https://github.com/Bubuni-Team/telegram-bot.git
synced 2026-07-31 00:29:24 +03:00
Use file-scoped namespaces
This commit is contained in:
@@ -8,83 +8,82 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a feed in database.
|
||||
/// </summary>
|
||||
public class Feed : RepresentableEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a feed in database.
|
||||
/// </summary>
|
||||
public class Feed : RepresentableEntity
|
||||
#region RepresentableEntity
|
||||
protected override string ViewableText
|
||||
{
|
||||
#region RepresentableEntity
|
||||
protected override string ViewableText
|
||||
{
|
||||
get => Title;
|
||||
}
|
||||
|
||||
protected override string ViewableUrl
|
||||
{
|
||||
get => HomePage;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The unique feed id.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public Guid FeedId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The feed URL.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The home page for this feed.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string HomePage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Feed title.
|
||||
/// </summary>
|
||||
[MaxLength(256)]
|
||||
[Required]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DateTime when feed fetched successfully in last time.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This flag indicates, bot should monitor changes in post date, or not.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[DefaultValue(false)]
|
||||
public bool WatchPostDate { get; set; }
|
||||
|
||||
[Required]
|
||||
[DefaultValue(false)]
|
||||
public bool IsPrivate { get; set; }
|
||||
|
||||
public Feed()
|
||||
{
|
||||
FeedId = new Guid();
|
||||
UpdatedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All posts from this feed.
|
||||
/// </summary>
|
||||
public virtual ICollection<Post> Posts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All exists subscriber for this feed.
|
||||
/// </summary>
|
||||
public virtual ICollection<Subscriber> Subscribers { get; set; }
|
||||
|
||||
public override string ToString() => Title;
|
||||
get => Title;
|
||||
}
|
||||
|
||||
protected override string ViewableUrl
|
||||
{
|
||||
get => HomePage;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The unique feed id.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public Guid FeedId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The feed URL.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The home page for this feed.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string HomePage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Feed title.
|
||||
/// </summary>
|
||||
[MaxLength(256)]
|
||||
[Required]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DateTime when feed fetched successfully in last time.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This flag indicates, bot should monitor changes in post date, or not.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[DefaultValue(false)]
|
||||
public bool WatchPostDate { get; set; }
|
||||
|
||||
[Required]
|
||||
[DefaultValue(false)]
|
||||
public bool IsPrivate { get; set; }
|
||||
|
||||
public Feed()
|
||||
{
|
||||
FeedId = new Guid();
|
||||
UpdatedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All posts from this feed.
|
||||
/// </summary>
|
||||
public virtual ICollection<Post> Posts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All exists subscriber for this feed.
|
||||
/// </summary>
|
||||
public virtual ICollection<Subscriber> Subscribers { get; set; }
|
||||
|
||||
public override string ToString() => Title;
|
||||
}
|
||||
@@ -9,99 +9,98 @@ using System.Text;
|
||||
using Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a post from feed in database.
|
||||
/// </summary>
|
||||
public class Post : RepresentableEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a post from feed in database.
|
||||
/// </summary>
|
||||
public class Post : RepresentableEntity
|
||||
#region RepresentableEntity
|
||||
protected override ParseMode DefaultRepresentation
|
||||
{
|
||||
#region RepresentableEntity
|
||||
protected override ParseMode DefaultRepresentation
|
||||
get => ParseMode.Html;
|
||||
}
|
||||
|
||||
protected override string ViewableText
|
||||
{
|
||||
get => Title;
|
||||
}
|
||||
|
||||
protected override string ViewableUrl
|
||||
{
|
||||
get => UtmUtility.ApplyParameters(new Uri(new(Feed.Url), Url), new UtmParameters()
|
||||
{
|
||||
get => ParseMode.Html;
|
||||
}
|
||||
Source = "telegram",
|
||||
Type = "rss_post",
|
||||
Campaign = "subscriber",
|
||||
Content = PostId.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
protected override string ViewableText
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The unique post id.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public Guid PostId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The feed unique identifier from where this post is fetched.
|
||||
/// </summary>
|
||||
public Guid FeedId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The feed from where this post is fetched.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public virtual Feed Feed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The post title.
|
||||
/// </summary>
|
||||
[MaxLength(256)]
|
||||
[Required]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URI where this post is located in web.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DateTime when post is created in RSS feed.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime PostedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DateTime when post is fetched/updated in database.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime ReceivedAt { get; set; }
|
||||
|
||||
public Post()
|
||||
{
|
||||
PostId = new Guid();
|
||||
ReceivedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
public string MessageText
|
||||
{
|
||||
get
|
||||
{
|
||||
get => Title;
|
||||
}
|
||||
|
||||
protected override string ViewableUrl
|
||||
{
|
||||
get => UtmUtility.ApplyParameters(new Uri(new(Feed.Url), Url), new UtmParameters()
|
||||
{
|
||||
Source = "telegram",
|
||||
Type = "rss_post",
|
||||
Campaign = "subscriber",
|
||||
Content = PostId.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The unique post id.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public Guid PostId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The feed unique identifier from where this post is fetched.
|
||||
/// </summary>
|
||||
public Guid FeedId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The feed from where this post is fetched.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public virtual Feed Feed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The post title.
|
||||
/// </summary>
|
||||
[MaxLength(256)]
|
||||
[Required]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URI where this post is located in web.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DateTime when post is created in RSS feed.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime PostedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DateTime when post is fetched/updated in database.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime ReceivedAt { get; set; }
|
||||
|
||||
public Post()
|
||||
{
|
||||
PostId = new Guid();
|
||||
ReceivedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
public string MessageText
|
||||
{
|
||||
get
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.Append($"{Representation("🗞")} New on {Feed.Representation(ParseMode.Html)}\n");
|
||||
message.Append("\n");
|
||||
message.Append(Title);
|
||||
message.Append("\n");
|
||||
message.Append(Representation("Open in browser"));
|
||||
var message = new StringBuilder();
|
||||
message.Append($"{Representation("🗞")} New on {Feed.Representation(ParseMode.Html)}\n");
|
||||
message.Append("\n");
|
||||
message.Append(Title);
|
||||
message.Append("\n");
|
||||
message.Append(Representation("Open in browser"));
|
||||
|
||||
return message.ToString();
|
||||
}
|
||||
return message.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,55 +7,54 @@ using System;
|
||||
using System.Web;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the basic logic for rendering Database Entities in messages.
|
||||
/// </summary>
|
||||
public abstract class RepresentableEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the basic logic for rendering Database Entities in messages.
|
||||
/// </summary>
|
||||
public abstract class RepresentableEntity
|
||||
protected virtual string ViewableText { get; }
|
||||
protected virtual string ViewableUrl { get; }
|
||||
protected virtual ParseMode DefaultRepresentation { get; }
|
||||
|
||||
public override string ToString() => Representation();
|
||||
|
||||
public string Representation(string text) => Representation(DefaultRepresentation, text);
|
||||
|
||||
public string Representation() => Representation(DefaultRepresentation);
|
||||
|
||||
public string Representation(ParseMode parseMode) => Representation(parseMode, ViewableText);
|
||||
|
||||
public string Representation(ParseMode parseMode, string text)
|
||||
{
|
||||
protected virtual string ViewableText { get; }
|
||||
protected virtual string ViewableUrl { get; }
|
||||
protected virtual ParseMode DefaultRepresentation { get; }
|
||||
|
||||
public override string ToString() => Representation();
|
||||
|
||||
public string Representation(string text) => Representation(DefaultRepresentation, text);
|
||||
|
||||
public string Representation() => Representation(DefaultRepresentation);
|
||||
|
||||
public string Representation(ParseMode parseMode) => Representation(parseMode, ViewableText);
|
||||
|
||||
public string Representation(ParseMode parseMode, string text)
|
||||
string content = String.Empty;
|
||||
switch (parseMode)
|
||||
{
|
||||
string content = String.Empty;
|
||||
switch (parseMode)
|
||||
{
|
||||
case ParseMode.Html:
|
||||
content = HtmlRepresentation(text);
|
||||
break;
|
||||
case ParseMode.Html:
|
||||
content = HtmlRepresentation(text);
|
||||
break;
|
||||
|
||||
case ParseMode.Markdown:
|
||||
case ParseMode.MarkdownV2:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
return content;
|
||||
case ParseMode.Markdown:
|
||||
case ParseMode.MarkdownV2:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#region Representations
|
||||
|
||||
public string RawRepresentation(string text)
|
||||
=> text;
|
||||
|
||||
public string HtmlRepresentation(string text)
|
||||
{
|
||||
var escapedText = HttpUtility.HtmlEncode(text);
|
||||
var escapedUrl = HttpUtility.HtmlAttributeEncode(ViewableUrl);
|
||||
|
||||
return $"<a href=\"{escapedUrl}\">{escapedText}</a>";
|
||||
}
|
||||
|
||||
#endregion
|
||||
return content;
|
||||
}
|
||||
|
||||
#region Representations
|
||||
|
||||
public string RawRepresentation(string text)
|
||||
=> text;
|
||||
|
||||
public string HtmlRepresentation(string text)
|
||||
{
|
||||
var escapedText = HttpUtility.HtmlEncode(text);
|
||||
var escapedUrl = HttpUtility.HtmlAttributeEncode(ViewableUrl);
|
||||
|
||||
return $"<a href=\"{escapedUrl}\">{escapedText}</a>";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,58 +1,57 @@
|
||||
using Kruzya.TelegramBot.Core.EF;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||
|
||||
public class RichSiteSummaryContext : AutoSaveDbContext
|
||||
{
|
||||
public class RichSiteSummaryContext : AutoSaveDbContext
|
||||
/// <summary>
|
||||
/// Repository with all feeds in database.
|
||||
/// </summary>
|
||||
public DbSet<Feed> Feeds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository with all posts in database.
|
||||
/// </summary>
|
||||
public DbSet<Post> Posts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository with all subscriptions in database.
|
||||
/// </summary>
|
||||
public DbSet<Subscriber> Subscriptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the database is created and calls parent constructor.
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
public RichSiteSummaryContext(DbContextOptions<RichSiteSummaryContext> options) : base(options)
|
||||
{
|
||||
/// <summary>
|
||||
/// Repository with all feeds in database.
|
||||
/// </summary>
|
||||
public DbSet<Feed> Feeds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository with all posts in database.
|
||||
/// </summary>
|
||||
public DbSet<Post> Posts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository with all subscriptions in database.
|
||||
/// </summary>
|
||||
public DbSet<Subscriber> Subscriptions { get; set; }
|
||||
Database.EnsureCreated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the database is created and calls parent constructor.
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
public RichSiteSummaryContext(DbContextOptions<RichSiteSummaryContext> options) : base(options)
|
||||
{
|
||||
Database.EnsureCreated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup the additional unique keys.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder"></param>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
// Setup unique keys for Feed entity.
|
||||
modelBuilder.Entity<Feed>().HasIndex(feed => feed.Url).IsUnique();
|
||||
modelBuilder.Entity<Feed>().HasIndex(feed => feed.HomePage).IsUnique();
|
||||
/// <summary>
|
||||
/// Setup the additional unique keys.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder"></param>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
// Setup unique keys for Feed entity.
|
||||
modelBuilder.Entity<Feed>().HasIndex(feed => feed.Url).IsUnique();
|
||||
modelBuilder.Entity<Feed>().HasIndex(feed => feed.HomePage).IsUnique();
|
||||
|
||||
// Setup unique keys for Post entity.
|
||||
modelBuilder.Entity<Post>()
|
||||
.HasOne(p => p.Feed)
|
||||
.WithMany(f => f.Posts)
|
||||
.HasForeignKey(p => p.FeedId);
|
||||
// Setup unique keys for Post entity.
|
||||
modelBuilder.Entity<Post>()
|
||||
.HasOne(p => p.Feed)
|
||||
.WithMany(f => f.Posts)
|
||||
.HasForeignKey(p => p.FeedId);
|
||||
|
||||
modelBuilder.Entity<Post>().HasIndex(post =>
|
||||
new {post.FeedId, post.Url}).IsUnique();
|
||||
modelBuilder.Entity<Post>().HasIndex(post =>
|
||||
new {post.FeedId, post.Url}).IsUnique();
|
||||
|
||||
// Setup foreign keys for Subscriber entity.
|
||||
modelBuilder.Entity<Subscriber>()
|
||||
.HasOne(s => s.Feed)
|
||||
.WithMany(f => f.Subscribers)
|
||||
.HasForeignKey(s => s.FeedId);
|
||||
}
|
||||
// Setup foreign keys for Subscriber entity.
|
||||
modelBuilder.Entity<Subscriber>()
|
||||
.HasOne(s => s.Feed)
|
||||
.WithMany(f => f.Subscribers)
|
||||
.HasForeignKey(s => s.FeedId);
|
||||
}
|
||||
}
|
||||
@@ -6,51 +6,50 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a subscription in database.
|
||||
/// </summary>
|
||||
public class Subscriber
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a subscription in database.
|
||||
/// The unique Subscription identifier.
|
||||
/// </summary>
|
||||
public class Subscriber
|
||||
[Key]
|
||||
public Guid SubscriptionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Telegram subscriber id.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Int64 SubscriberId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique feed identifier related with this Subscription. Identifies what user is read.
|
||||
/// </summary>
|
||||
public Guid FeedId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Feed related with this Subscription. Identifies what user is read.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public virtual Feed Feed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When user is subscribed on this feed.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime SubscribedAt { get; set; }
|
||||
|
||||
public Subscriber()
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique Subscription identifier.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public Guid SubscriptionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Telegram subscriber id.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Int64 SubscriberId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique feed identifier related with this Subscription. Identifies what user is read.
|
||||
/// </summary>
|
||||
public Guid FeedId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Feed related with this Subscription. Identifies what user is read.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public virtual Feed Feed { get; set; }
|
||||
SubscriptionId = new Guid();
|
||||
SubscribedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When user is subscribed on this feed.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime SubscribedAt { get; set; }
|
||||
|
||||
public Subscriber()
|
||||
{
|
||||
SubscriptionId = new Guid();
|
||||
SubscribedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
public Subscriber(Feed feed) : base()
|
||||
{
|
||||
Feed = feed;
|
||||
}
|
||||
public Subscriber(Feed feed) : base()
|
||||
{
|
||||
Feed = feed;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
using BotFramework;
|
||||
using Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Handler
|
||||
{
|
||||
public abstract class AbstractHandler : BotEventHandler
|
||||
{
|
||||
protected readonly RichSiteSummaryContext _dbContext;
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Handler;
|
||||
|
||||
protected AbstractHandler(RichSiteSummaryContext dbContext)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
public abstract class AbstractHandler : BotEventHandler
|
||||
{
|
||||
protected readonly RichSiteSummaryContext _dbContext;
|
||||
|
||||
protected AbstractHandler(RichSiteSummaryContext dbContext)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
}
|
||||
@@ -13,299 +13,298 @@ using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Handler
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Handler;
|
||||
|
||||
public class FeedList : AbstractHandler
|
||||
{
|
||||
public class FeedList : AbstractHandler
|
||||
public FeedList(RichSiteSummaryContext dbContext) : base(dbContext)
|
||||
{
|
||||
public FeedList(RichSiteSummaryContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[ParametrizedCommand("rss_show")]
|
||||
public async Task ShowFeed(Feed feed) => await ShowFeed(feed, Chat, From);
|
||||
[ParametrizedCommand("rss_show")]
|
||||
public async Task ShowFeed(Feed feed) => await ShowFeed(feed, Chat, From);
|
||||
|
||||
public async Task ShowFeed(Guid feedId, Chat chat, User from)
|
||||
public async Task ShowFeed(Guid feedId, Chat chat, User from)
|
||||
{
|
||||
var feed = await _dbContext.FindAsync<Feed>(feedId);
|
||||
if (feed == null)
|
||||
{
|
||||
var feed = await _dbContext.FindAsync<Feed>(feedId);
|
||||
if (feed == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ShowFeed(feed, chat, from);
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task ShowFeed(Feed feed, Chat chat, User from, Message message = null)
|
||||
await ShowFeed(feed, chat, from);
|
||||
}
|
||||
|
||||
public async Task ShowFeed(Feed feed, Chat chat, User from, Message message = null)
|
||||
{
|
||||
var textBuilder = new StringBuilder();
|
||||
textBuilder.Append(feed.Representation(ParseMode.Html));
|
||||
textBuilder.Append("\n");
|
||||
textBuilder.Append($"<b>Posts at this moment</b>: {feed.Posts.Count}\n");
|
||||
textBuilder.Append($"<b>RSS feed</b>: <code>{feed.Url}</code>\n");
|
||||
textBuilder.Append($"<b>Home page</b>: <code>{feed.HomePage}</code>\n");
|
||||
textBuilder.Append($"<b>Last synchronization</b>: <code>{feed.UpdatedAt}</code>");
|
||||
|
||||
var text = textBuilder.ToString();
|
||||
|
||||
var buttons = new List<InlineKeyboardButton>();
|
||||
var callbackData = $"feed|{from.Id}|do|{feed.FeedId}";
|
||||
if (await _dbContext.Subscriptions.HasSubscription(chat, feed))
|
||||
{
|
||||
var textBuilder = new StringBuilder();
|
||||
textBuilder.Append(feed.Representation(ParseMode.Html));
|
||||
textBuilder.Append("\n");
|
||||
textBuilder.Append($"<b>Posts at this moment</b>: {feed.Posts.Count}\n");
|
||||
textBuilder.Append($"<b>RSS feed</b>: <code>{feed.Url}</code>\n");
|
||||
textBuilder.Append($"<b>Home page</b>: <code>{feed.HomePage}</code>\n");
|
||||
textBuilder.Append($"<b>Last synchronization</b>: <code>{feed.UpdatedAt}</code>");
|
||||
|
||||
var text = textBuilder.ToString();
|
||||
|
||||
var buttons = new List<InlineKeyboardButton>();
|
||||
var callbackData = $"feed|{from.Id}|do|{feed.FeedId}";
|
||||
if (await _dbContext.Subscriptions.HasSubscription(chat, feed))
|
||||
buttons.Add(new InlineKeyboardButton("Unsubscribe")
|
||||
{
|
||||
buttons.Add(new InlineKeyboardButton("Unsubscribe")
|
||||
{
|
||||
CallbackData = callbackData,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
buttons.Add(new InlineKeyboardButton("Subscribe")
|
||||
{
|
||||
CallbackData = callbackData,
|
||||
});
|
||||
}
|
||||
buttons.Add(new InlineKeyboardButton("Last 10 posts")
|
||||
{
|
||||
CallbackData = $"feed|{from.Id}|last|{feed.FeedId}",
|
||||
CallbackData = callbackData,
|
||||
});
|
||||
|
||||
var inlineButtons = new InlineKeyboardMarkup(buttons);
|
||||
if (message == null)
|
||||
{
|
||||
await Bot.SendTextMessageAsync(new ChatId(chat.Id), text, 0, ParseMode.Html, null, true, true, false, 0, replyMarkup: inlineButtons);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Bot.EditMessageTextAsync(chat, message.MessageId, text, ParseMode.Html, null, true, replyMarkup: inlineButtons);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buttons.Add(new InlineKeyboardButton("Subscribe")
|
||||
{
|
||||
CallbackData = callbackData,
|
||||
});
|
||||
}
|
||||
buttons.Add(new InlineKeyboardButton("Last 10 posts")
|
||||
{
|
||||
CallbackData = $"feed|{from.Id}|last|{feed.FeedId}",
|
||||
});
|
||||
|
||||
var inlineButtons = new InlineKeyboardMarkup(buttons);
|
||||
if (message == null)
|
||||
{
|
||||
await Bot.SendTextMessageAsync(new ChatId(chat.Id), text, 0, ParseMode.Html, null, true, true, false, 0, replyMarkup: inlineButtons);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Bot.EditMessageTextAsync(chat, message.MessageId, text, ParseMode.Html, null, true, replyMarkup: inlineButtons);
|
||||
}
|
||||
}
|
||||
|
||||
[ParametrizedCommand("rss_list")]
|
||||
public async Task List() => await GenerateMenu();
|
||||
[ParametrizedCommand("rss_list")]
|
||||
public async Task List() => await GenerateMenu();
|
||||
|
||||
[ParametrizedCommand("rss_search")]
|
||||
public async Task Search(string query)
|
||||
[ParametrizedCommand("rss_search")]
|
||||
public async Task Search(string query)
|
||||
{
|
||||
if (!(await GenerateMenu(query)))
|
||||
{
|
||||
if (!(await GenerateMenu(query)))
|
||||
{
|
||||
await Bot.SendTextMessageAsync(Chat, $"No one feed match by search pattern (<code>{query}</code>).", 0, ParseMode.Html, null, true);
|
||||
}
|
||||
await Bot.SendTextMessageAsync(Chat, $"No one feed match by search pattern (<code>{query}</code>).", 0, ParseMode.Html, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("rss_subscriptions")]
|
||||
public async Task Subscriptions()
|
||||
[Command("rss_subscriptions")]
|
||||
public async Task Subscriptions()
|
||||
{
|
||||
if (!(await GenerateMenu(":sub")))
|
||||
{
|
||||
if (!(await GenerateMenu(":sub")))
|
||||
{
|
||||
await Bot.SendTextMessageAsync(Chat,
|
||||
"There are no one subscription. Add new with /feed_list or /feed_search.");
|
||||
}
|
||||
await Bot.SendTextMessageAsync(Chat,
|
||||
"There are no one subscription. Add new with /feed_list or /feed_search.");
|
||||
}
|
||||
}
|
||||
|
||||
#region Message generator
|
||||
#region Message generator
|
||||
|
||||
protected async Task<bool> GenerateMenu() =>
|
||||
await GenerateMenu(string.Empty);
|
||||
protected async Task<bool> GenerateMenu() =>
|
||||
await GenerateMenu(string.Empty);
|
||||
|
||||
protected async Task<bool> GenerateMenu(string query) =>
|
||||
await GenerateMenu(query, 0);
|
||||
protected async Task<bool> GenerateMenu(string query) =>
|
||||
await GenerateMenu(query, 0);
|
||||
|
||||
protected async Task<bool> GenerateMenu(string query, UInt16 page, Message editableMessage = null)
|
||||
protected async Task<bool> GenerateMenu(string query, UInt16 page, Message editableMessage = null)
|
||||
{
|
||||
var chat = editableMessage != null ? (editableMessage.Chat) : Chat;
|
||||
var elementsOnPage = 9;
|
||||
var startIndex = page * elementsOnPage;
|
||||
var dbQuery = _dbContext.Feeds.AsQueryable()
|
||||
.Where(f => !f.IsPrivate);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
var chat = editableMessage != null ? (editableMessage.Chat) : Chat;
|
||||
var elementsOnPage = 9;
|
||||
var startIndex = page * elementsOnPage;
|
||||
var dbQuery = _dbContext.Feeds.AsQueryable()
|
||||
.Where(f => !f.IsPrivate);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
var searchQuery = query.ToLower();
|
||||
if (query.EndsWith(":sub"))
|
||||
{
|
||||
var searchQuery = query.ToLower();
|
||||
if (query.EndsWith(":sub"))
|
||||
{
|
||||
var chatList = await _dbContext.Subscriptions.AllSubscriptions(chat).Select(chat => chat.Feed.FeedId).ToArrayAsync();
|
||||
dbQuery = dbQuery.Where(sub => chatList.Contains(sub.FeedId));
|
||||
var chatList = await _dbContext.Subscriptions.AllSubscriptions(chat).Select(chat => chat.Feed.FeedId).ToArrayAsync();
|
||||
dbQuery = dbQuery.Where(sub => chatList.Contains(sub.FeedId));
|
||||
|
||||
searchQuery = searchQuery.Remove(searchQuery.Length - 4, 4);
|
||||
}
|
||||
searchQuery = searchQuery.Remove(searchQuery.Length - 4, 4);
|
||||
}
|
||||
|
||||
dbQuery = dbQuery.Where(feed => feed.Title.ToLower().Contains(searchQuery) || feed.Url.ToLower().Contains(searchQuery) || feed.HomePage.ToLower().Contains(searchQuery));
|
||||
}
|
||||
dbQuery = dbQuery.Where(feed => feed.Title.ToLower().Contains(searchQuery) || feed.Url.ToLower().Contains(searchQuery) || feed.HomePage.ToLower().Contains(searchQuery));
|
||||
}
|
||||
|
||||
var totalCount = await dbQuery.CountAsync();
|
||||
if (totalCount == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (totalCount == 1)
|
||||
{
|
||||
await ShowFeed((await dbQuery.FirstOrDefaultAsync()), Chat, From);
|
||||
return true;
|
||||
}
|
||||
|
||||
var result = await dbQuery.Take(elementsOnPage * (page + 1)).Skip(startIndex)
|
||||
.ToArrayAsync();
|
||||
|
||||
var backButton = (page > 0);
|
||||
var nextButton = totalCount >= (startIndex + elementsOnPage);
|
||||
|
||||
var buttons = new List<List<InlineKeyboardButton>>();
|
||||
List<InlineKeyboardButton> row;
|
||||
foreach (var feed in result)
|
||||
{
|
||||
row = new List<InlineKeyboardButton>();
|
||||
row.Add(new InlineKeyboardButton(feed.Title)
|
||||
{
|
||||
CallbackData = $"feed|{From.Id}|show|{feed.FeedId.ToString()}",
|
||||
});
|
||||
|
||||
buttons.Add(row);
|
||||
}
|
||||
|
||||
if (backButton || nextButton)
|
||||
{
|
||||
row = new List<InlineKeyboardButton>();
|
||||
if (backButton)
|
||||
{
|
||||
row.Add(new InlineKeyboardButton("⬅️")
|
||||
{
|
||||
CallbackData = $"feed|{From.Id}|page|{query}|{page - 1}",
|
||||
});
|
||||
}
|
||||
|
||||
row.Add(new InlineKeyboardButton($"{page + 1}/{(totalCount / elementsOnPage) + 1}")
|
||||
{
|
||||
CallbackData = $"feed|{From.Id}|donothing",
|
||||
});
|
||||
|
||||
if (nextButton)
|
||||
{
|
||||
row.Add(new InlineKeyboardButton("➡️")
|
||||
{
|
||||
CallbackData = $"feed|{From.Id}|page|{query}|{page + 1}",
|
||||
});
|
||||
}
|
||||
|
||||
buttons.Add(row);
|
||||
}
|
||||
|
||||
if (editableMessage == null)
|
||||
{
|
||||
await Bot.SendTextMessageAsync(Chat, "Select the feed for viewing details", 0, null,
|
||||
null, true, true, false, 0, replyMarkup: new InlineKeyboardMarkup(buttons));
|
||||
}
|
||||
else
|
||||
{
|
||||
await Bot.EditMessageTextAsync(editableMessage.Chat, editableMessage.MessageId, "Select the feed for viewing details", null, null, false,
|
||||
new InlineKeyboardMarkup(buttons));
|
||||
}
|
||||
var totalCount = await dbQuery.CountAsync();
|
||||
if (totalCount == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (totalCount == 1)
|
||||
{
|
||||
await ShowFeed((await dbQuery.FirstOrDefaultAsync()), Chat, From);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Message handler
|
||||
|
||||
[Update(UpdateFlag.CallbackQuery)]
|
||||
public async Task HandleAction()
|
||||
{
|
||||
if (!RawUpdate.CallbackQuery.Data.StartsWith("feed|"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var data = RawUpdate.CallbackQuery.Data.Split('|');
|
||||
var fromId = Int64.Parse(data[1]);
|
||||
if (fromId != From.Id)
|
||||
{
|
||||
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id,
|
||||
"You can't perform this action: command called by another user.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check action.
|
||||
switch (data[2])
|
||||
{
|
||||
case "page":
|
||||
await GenerateMenu(data[3], UInt16.Parse(data[4]), RawUpdate.CallbackQuery.Message);
|
||||
break;
|
||||
|
||||
case "show":
|
||||
await ShowFeed(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From);
|
||||
break;
|
||||
|
||||
case "do":
|
||||
if (RawUpdate.CallbackQuery.From.Id != RawUpdate.CallbackQuery.Message.Chat.Id)
|
||||
{
|
||||
var userStatus = (await Bot.GetChatMemberAsync(RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From.Id)).Status;
|
||||
var allowedUserStatuses = new ChatMemberStatus[]
|
||||
{ChatMemberStatus.Administrator, ChatMemberStatus.Creator};
|
||||
|
||||
if (!allowedUserStatuses.Contains(userStatus))
|
||||
{
|
||||
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id,
|
||||
"You can't perform this action: you don't have administrator permissions.", true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await ChangeFeedSubscription(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From, RawUpdate.CallbackQuery.Message);
|
||||
break;
|
||||
|
||||
case "last":
|
||||
var feed = await _dbContext.FindAsync<Feed>(Guid.Parse(data[3]));
|
||||
var posts = feed.Posts.OrderByDescending(p => p.ReceivedAt).Take(10).ToList();
|
||||
|
||||
var messageTextBuilder = new StringBuilder();
|
||||
messageTextBuilder.Append($"Last 10 posts from {feed.Representation(ParseMode.Html)}\n");
|
||||
messageTextBuilder.Append("\n");
|
||||
|
||||
var idx = 1;
|
||||
foreach (var post in posts)
|
||||
{
|
||||
messageTextBuilder.Append($"{idx}. {post}\n");
|
||||
idx++;
|
||||
}
|
||||
|
||||
await Bot.SendTextMessageAsync(RawUpdate.CallbackQuery.Message.Chat, messageTextBuilder.ToString(),
|
||||
0, ParseMode.Html, null, true);
|
||||
break;
|
||||
}
|
||||
|
||||
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id);
|
||||
}
|
||||
var result = await dbQuery.Take(elementsOnPage * (page + 1)).Skip(startIndex)
|
||||
.ToArrayAsync();
|
||||
|
||||
var backButton = (page > 0);
|
||||
var nextButton = totalCount >= (startIndex + elementsOnPage);
|
||||
|
||||
protected async Task ChangeFeedSubscription(Guid feedId, Chat chat, User from, Message message)
|
||||
var buttons = new List<List<InlineKeyboardButton>>();
|
||||
List<InlineKeyboardButton> row;
|
||||
foreach (var feed in result)
|
||||
{
|
||||
var feed = await _dbContext.FindAsync<Feed>(feedId);
|
||||
if (feed == null)
|
||||
row = new List<InlineKeyboardButton>();
|
||||
row.Add(new InlineKeyboardButton(feed.Title)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CallbackData = $"feed|{From.Id}|show|{feed.FeedId.ToString()}",
|
||||
});
|
||||
|
||||
var subscriptionState = await _dbContext.Subscriptions.HasSubscription(chat, feed);
|
||||
if (subscriptionState)
|
||||
{
|
||||
var subId = await _dbContext.Subscriptions
|
||||
.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id)
|
||||
.Select(subscriber => subscriber.SubscriptionId).FirstAsync();
|
||||
|
||||
_dbContext.Remove(_dbContext.Find<Subscriber>(subId));
|
||||
}
|
||||
else
|
||||
{
|
||||
var subscription = _dbContext.Subscriptions.Create();
|
||||
subscription.Feed = feed;
|
||||
subscription.SubscriberId = chat.Id;
|
||||
|
||||
_dbContext.Add(subscription);
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
await ShowFeed(feed, chat, from, message);
|
||||
buttons.Add(row);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
if (backButton || nextButton)
|
||||
{
|
||||
row = new List<InlineKeyboardButton>();
|
||||
if (backButton)
|
||||
{
|
||||
row.Add(new InlineKeyboardButton("⬅️")
|
||||
{
|
||||
CallbackData = $"feed|{From.Id}|page|{query}|{page - 1}",
|
||||
});
|
||||
}
|
||||
|
||||
row.Add(new InlineKeyboardButton($"{page + 1}/{(totalCount / elementsOnPage) + 1}")
|
||||
{
|
||||
CallbackData = $"feed|{From.Id}|donothing",
|
||||
});
|
||||
|
||||
if (nextButton)
|
||||
{
|
||||
row.Add(new InlineKeyboardButton("➡️")
|
||||
{
|
||||
CallbackData = $"feed|{From.Id}|page|{query}|{page + 1}",
|
||||
});
|
||||
}
|
||||
|
||||
buttons.Add(row);
|
||||
}
|
||||
|
||||
if (editableMessage == null)
|
||||
{
|
||||
await Bot.SendTextMessageAsync(Chat, "Select the feed for viewing details", 0, null,
|
||||
null, true, true, false, 0, replyMarkup: new InlineKeyboardMarkup(buttons));
|
||||
}
|
||||
else
|
||||
{
|
||||
await Bot.EditMessageTextAsync(editableMessage.Chat, editableMessage.MessageId, "Select the feed for viewing details", null, null, false,
|
||||
new InlineKeyboardMarkup(buttons));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Message handler
|
||||
|
||||
[Update(UpdateFlag.CallbackQuery)]
|
||||
public async Task HandleAction()
|
||||
{
|
||||
if (!RawUpdate.CallbackQuery.Data.StartsWith("feed|"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var data = RawUpdate.CallbackQuery.Data.Split('|');
|
||||
var fromId = Int64.Parse(data[1]);
|
||||
if (fromId != From.Id)
|
||||
{
|
||||
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id,
|
||||
"You can't perform this action: command called by another user.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check action.
|
||||
switch (data[2])
|
||||
{
|
||||
case "page":
|
||||
await GenerateMenu(data[3], UInt16.Parse(data[4]), RawUpdate.CallbackQuery.Message);
|
||||
break;
|
||||
|
||||
case "show":
|
||||
await ShowFeed(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From);
|
||||
break;
|
||||
|
||||
case "do":
|
||||
if (RawUpdate.CallbackQuery.From.Id != RawUpdate.CallbackQuery.Message.Chat.Id)
|
||||
{
|
||||
var userStatus = (await Bot.GetChatMemberAsync(RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From.Id)).Status;
|
||||
var allowedUserStatuses = new ChatMemberStatus[]
|
||||
{ChatMemberStatus.Administrator, ChatMemberStatus.Creator};
|
||||
|
||||
if (!allowedUserStatuses.Contains(userStatus))
|
||||
{
|
||||
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id,
|
||||
"You can't perform this action: you don't have administrator permissions.", true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await ChangeFeedSubscription(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From, RawUpdate.CallbackQuery.Message);
|
||||
break;
|
||||
|
||||
case "last":
|
||||
var feed = await _dbContext.FindAsync<Feed>(Guid.Parse(data[3]));
|
||||
var posts = feed.Posts.OrderByDescending(p => p.ReceivedAt).Take(10).ToList();
|
||||
|
||||
var messageTextBuilder = new StringBuilder();
|
||||
messageTextBuilder.Append($"Last 10 posts from {feed.Representation(ParseMode.Html)}\n");
|
||||
messageTextBuilder.Append("\n");
|
||||
|
||||
var idx = 1;
|
||||
foreach (var post in posts)
|
||||
{
|
||||
messageTextBuilder.Append($"{idx}. {post}\n");
|
||||
idx++;
|
||||
}
|
||||
|
||||
await Bot.SendTextMessageAsync(RawUpdate.CallbackQuery.Message.Chat, messageTextBuilder.ToString(),
|
||||
0, ParseMode.Html, null, true);
|
||||
break;
|
||||
}
|
||||
|
||||
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id);
|
||||
}
|
||||
|
||||
protected async Task ChangeFeedSubscription(Guid feedId, Chat chat, User from, Message message)
|
||||
{
|
||||
var feed = await _dbContext.FindAsync<Feed>(feedId);
|
||||
if (feed == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var subscriptionState = await _dbContext.Subscriptions.HasSubscription(chat, feed);
|
||||
if (subscriptionState)
|
||||
{
|
||||
var subId = await _dbContext.Subscriptions
|
||||
.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id)
|
||||
.Select(subscriber => subscriber.SubscriptionId).FirstAsync();
|
||||
|
||||
_dbContext.Remove(_dbContext.Find<Subscriber>(subId));
|
||||
}
|
||||
else
|
||||
{
|
||||
var subscription = _dbContext.Subscriptions.Create();
|
||||
subscription.Feed = feed;
|
||||
subscription.SubscriberId = chat.Id;
|
||||
|
||||
_dbContext.Add(subscription);
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
await ShowFeed(feed, chat, from, message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -7,31 +7,30 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Handler
|
||||
{
|
||||
public class StatsHandler : AbstractHandler
|
||||
{
|
||||
public StatsHandler(RichSiteSummaryContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
[Command("rss_stats")]
|
||||
public async Task Execute()
|
||||
{
|
||||
var feedCount = await _dbContext.Feeds.CountAsync();
|
||||
var postsCount = await _dbContext.Posts.CountAsync();
|
||||
var subscriptionsCount = await _dbContext.Subscriptions.CountAsync();
|
||||
var uniqueSubscribers =
|
||||
await _dbContext.Subscriptions.Select(sub => sub.SubscriberId).Distinct().CountAsync();
|
||||
|
||||
var textMessage = new StringBuilder();
|
||||
textMessage.Append("ℹ️ In database on this moment:\n");
|
||||
textMessage.Append($"- registered <b>{feedCount} feeds</b>\n");
|
||||
textMessage.Append($"- saved <b>{postsCount} posts</b> for prevent re-sending\n");
|
||||
textMessage.Append(
|
||||
$"- exists <b>{subscriptionsCount} subscriptions on feeds</b> (<b>unique subscribers: {uniqueSubscribers}</b>)");
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Handler;
|
||||
|
||||
await Bot.SendTextMessageAsync(Chat, textMessage.ToString(), parseMode: ParseMode.Html);
|
||||
}
|
||||
public class StatsHandler : AbstractHandler
|
||||
{
|
||||
public StatsHandler(RichSiteSummaryContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
[Command("rss_stats")]
|
||||
public async Task Execute()
|
||||
{
|
||||
var feedCount = await _dbContext.Feeds.CountAsync();
|
||||
var postsCount = await _dbContext.Posts.CountAsync();
|
||||
var subscriptionsCount = await _dbContext.Subscriptions.CountAsync();
|
||||
var uniqueSubscribers =
|
||||
await _dbContext.Subscriptions.Select(sub => sub.SubscriberId).Distinct().CountAsync();
|
||||
|
||||
var textMessage = new StringBuilder();
|
||||
textMessage.Append("ℹ️ In database on this moment:\n");
|
||||
textMessage.Append($"- registered <b>{feedCount} feeds</b>\n");
|
||||
textMessage.Append($"- saved <b>{postsCount} posts</b> for prevent re-sending\n");
|
||||
textMessage.Append(
|
||||
$"- exists <b>{subscriptionsCount} subscriptions on feeds</b> (<b>unique subscribers: {uniqueSubscribers}</b>)");
|
||||
|
||||
await Bot.SendTextMessageAsync(Chat, textMessage.ToString(), parseMode: ParseMode.Html);
|
||||
}
|
||||
}
|
||||
@@ -7,31 +7,30 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary;
|
||||
|
||||
public class RichSiteSummary : Module
|
||||
{
|
||||
public class RichSiteSummary : Module
|
||||
public RichSiteSummary(Core.Core core) : base(core)
|
||||
{
|
||||
public RichSiteSummary(Core.Core core) : base(core)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public override void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
var connectionString = Configuration.GetConnectionString("RichSiteSummary");
|
||||
services.AddDbContext<RichSiteSummaryContext>(options =>
|
||||
options.UseLazyLoadingProxies()
|
||||
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
|
||||
public override void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
var connectionString = Configuration.GetConnectionString("RichSiteSummary");
|
||||
services.AddDbContext<RichSiteSummaryContext>(options =>
|
||||
options.UseLazyLoadingProxies()
|
||||
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
|
||||
|
||||
services.AddTelegramBotParameterParser<Feed, DbResolverParameter<Feed, RichSiteSummaryContext>>()
|
||||
.AddTelegramBotParameterParser<Post, DbResolverParameter<Post, RichSiteSummaryContext>>()
|
||||
.AddTelegramBotParameterParser<Subscriber, DbResolverParameter<Subscriber, RichSiteSummaryContext>>();
|
||||
services.AddTelegramBotParameterParser<Feed, DbResolverParameter<Feed, RichSiteSummaryContext>>()
|
||||
.AddTelegramBotParameterParser<Post, DbResolverParameter<Post, RichSiteSummaryContext>>()
|
||||
.AddTelegramBotParameterParser<Subscriber, DbResolverParameter<Subscriber, RichSiteSummaryContext>>();
|
||||
|
||||
services.AddQueue<UserMessage>()
|
||||
.AddQueue<UserUnsubscribe>();
|
||||
services.AddQueue<UserMessage>()
|
||||
.AddQueue<UserUnsubscribe>();
|
||||
|
||||
services.AddHostedService<RssFetch>()
|
||||
.AddHostedService<Unsubscriber>()
|
||||
.AddHostedService<MessageSender>();
|
||||
}
|
||||
services.AddHostedService<RssFetch>()
|
||||
.AddHostedService<Unsubscriber>()
|
||||
.AddHostedService<MessageSender>();
|
||||
}
|
||||
}
|
||||
@@ -5,68 +5,67 @@ using Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Telegram.Bot.Types;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary;
|
||||
|
||||
internal static class RichSiteSummaryRepositoryExtension
|
||||
{
|
||||
internal static class RichSiteSummaryRepositoryExtension
|
||||
#region Subscriber
|
||||
|
||||
/// <summary>
|
||||
/// Searchs the <see cref="Subscriber"/>, who reads a <see cref="Feed"/>.
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="feed"></param>
|
||||
/// <returns>The array with all subscribers.</returns>
|
||||
public static async Task<Subscriber[]> ByFeed(this DbSet<Subscriber> repository, Feed feed)
|
||||
{
|
||||
#region Subscriber
|
||||
|
||||
/// <summary>
|
||||
/// Searchs the <see cref="Subscriber"/>, who reads a <see cref="Feed"/>.
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="feed"></param>
|
||||
/// <returns>The array with all subscribers.</returns>
|
||||
public static async Task<Subscriber[]> ByFeed(this DbSet<Subscriber> repository, Feed feed)
|
||||
{
|
||||
return await repository.Where(subscriber => subscriber.Feed == feed)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
|
||||
public static IQueryable<Subscriber> AllSubscriptions(this DbSet<Subscriber> repository, Chat chat)
|
||||
{
|
||||
return repository.Where(entity => entity.SubscriberId == chat.Id);
|
||||
}
|
||||
|
||||
public static async Task<bool> HasSubscription(this DbSet<Subscriber> repository, Chat chat, Feed feed)
|
||||
{
|
||||
return (await repository.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id)
|
||||
.CountAsync()) != 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Feed
|
||||
|
||||
/// <summary>
|
||||
/// Searchs the <see cref="Feed"/> for fetching.
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="period">The period (in seconds) from last fetching.</param>
|
||||
/// <param name="count">Max results count</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<Feed[]> ForFetching(this DbSet<Feed> repository, UInt16 period, UInt16 count) =>
|
||||
await repository.Where(feed => feed.UpdatedAt < (DateTime.Now - TimeSpan.FromSeconds(period)))
|
||||
.OrderBy(feed => feed.UpdatedAt)
|
||||
.Take(count)
|
||||
.ToArrayAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Searchs the <see cref="Feed"/> for fetching.
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="period">The period (in seconds) from last fetching.</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<Feed[]> ForFetching(this DbSet<Feed> repository, UInt16 period) =>
|
||||
await repository.ForFetching(period, 25);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Post
|
||||
|
||||
public static async Task<Post> ByFeedAndUrl(this DbSet<Post> repository, string url, Feed feed = null) =>
|
||||
await repository.FirstOrDefaultAsync(post => post.Url == url && (feed == null || post.Feed == feed));
|
||||
|
||||
#endregion
|
||||
return await repository.Where(subscriber => subscriber.Feed == feed)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
|
||||
public static IQueryable<Subscriber> AllSubscriptions(this DbSet<Subscriber> repository, Chat chat)
|
||||
{
|
||||
return repository.Where(entity => entity.SubscriberId == chat.Id);
|
||||
}
|
||||
|
||||
public static async Task<bool> HasSubscription(this DbSet<Subscriber> repository, Chat chat, Feed feed)
|
||||
{
|
||||
return (await repository.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id)
|
||||
.CountAsync()) != 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Feed
|
||||
|
||||
/// <summary>
|
||||
/// Searchs the <see cref="Feed"/> for fetching.
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="period">The period (in seconds) from last fetching.</param>
|
||||
/// <param name="count">Max results count</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<Feed[]> ForFetching(this DbSet<Feed> repository, UInt16 period, UInt16 count) =>
|
||||
await repository.Where(feed => feed.UpdatedAt < (DateTime.Now - TimeSpan.FromSeconds(period)))
|
||||
.OrderBy(feed => feed.UpdatedAt)
|
||||
.Take(count)
|
||||
.ToArrayAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Searchs the <see cref="Feed"/> for fetching.
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="period">The period (in seconds) from last fetching.</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<Feed[]> ForFetching(this DbSet<Feed> repository, UInt16 period) =>
|
||||
await repository.ForFetching(period, 25);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Post
|
||||
|
||||
public static async Task<Post> ByFeedAndUrl(this DbSet<Post> repository, string url, Feed feed = null) =>
|
||||
await repository.FirstOrDefaultAsync(post => post.Url == url && (feed == null || post.Feed == feed));
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -9,81 +9,80 @@ using Telegram.Bot;
|
||||
using Telegram.Bot.Exceptions;
|
||||
using Telegram.Bot.Types;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Service
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Service;
|
||||
|
||||
public class MessageSender : AbstractTimedHostedService
|
||||
{
|
||||
public class MessageSender : AbstractTimedHostedService
|
||||
protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1);
|
||||
|
||||
protected readonly ConcurrentQueue<UserMessage> Queue;
|
||||
protected readonly ConcurrentQueue<UserUnsubscribe> UnsubscribeQueue;
|
||||
|
||||
public MessageSender(ILogger<MessageSender> logger, IBotInstance bot, ConcurrentQueue<UserMessage> userMessageQueue, ConcurrentQueue<UserUnsubscribe> userUnsubscribeQueue) : base(logger, bot)
|
||||
{
|
||||
protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1);
|
||||
|
||||
protected readonly ConcurrentQueue<UserMessage> Queue;
|
||||
protected readonly ConcurrentQueue<UserUnsubscribe> UnsubscribeQueue;
|
||||
|
||||
public MessageSender(ILogger<MessageSender> logger, IBotInstance bot, ConcurrentQueue<UserMessage> userMessageQueue, ConcurrentQueue<UserUnsubscribe> userUnsubscribeQueue) : base(logger, bot)
|
||||
UnsubscribeQueue = userUnsubscribeQueue;
|
||||
Queue = userMessageQueue;
|
||||
}
|
||||
|
||||
protected override async Task OnRun()
|
||||
{
|
||||
if (Queue.IsEmpty || !Queue.TryDequeue(out var message))
|
||||
{
|
||||
UnsubscribeQueue = userUnsubscribeQueue;
|
||||
Queue = userMessageQueue;
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task OnRun()
|
||||
_logger.LogDebug("Message for {ChatId} dequeued.", message.ChatId.Identifier);
|
||||
try
|
||||
{
|
||||
if (Queue.IsEmpty || !Queue.TryDequeue(out var message))
|
||||
if (string.IsNullOrWhiteSpace(message.ImageUrl))
|
||||
{
|
||||
await _bot.BotClient.SendTextMessageAsync(message.ChatId, message.Text, 0, message.ParseMode, null,
|
||||
message.DisableWebPagePreview);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await _bot.BotClient.SendPhotoAsync(message.ChatId, new InputFileUrl(message.ImageUrl), 0, message.Text,
|
||||
message.ParseMode, null, false, message.DisableWebPagePreview);
|
||||
}
|
||||
}
|
||||
catch (ApiRequestException e)
|
||||
{
|
||||
_logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message);
|
||||
if (!e.Message.Contains("bot was blocked by user"))
|
||||
{
|
||||
ReEnqueue(message, e, message.ChatId); // looks like a network issue
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Message for {ChatId} dequeued.", message.ChatId.Identifier);
|
||||
try
|
||||
// User added bot to blacklist.
|
||||
// Unsubscribe him.
|
||||
UnsubscribeQueue.Enqueue(new UserUnsubscribe
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message.ImageUrl))
|
||||
{
|
||||
await _bot.BotClient.SendTextMessageAsync(message.ChatId, message.Text, 0, message.ParseMode, null,
|
||||
message.DisableWebPagePreview);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await _bot.BotClient.SendPhotoAsync(message.ChatId, new InputFileUrl(message.ImageUrl), 0, message.Text,
|
||||
message.ParseMode, null, false, message.DisableWebPagePreview);
|
||||
}
|
||||
}
|
||||
catch (ApiRequestException e)
|
||||
{
|
||||
_logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message);
|
||||
if (!e.Message.Contains("bot was blocked by user"))
|
||||
{
|
||||
ReEnqueue(message, e, message.ChatId); // looks like a network issue
|
||||
return;
|
||||
}
|
||||
|
||||
// User added bot to blacklist.
|
||||
// Unsubscribe him.
|
||||
UnsubscribeQueue.Enqueue(new UserUnsubscribe
|
||||
{
|
||||
ChatId = message.ChatId
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message);
|
||||
ReEnqueue(message, e);
|
||||
}
|
||||
ChatId = message.ChatId
|
||||
});
|
||||
}
|
||||
|
||||
private void ReEnqueue(UserMessage message, Exception e = null, ChatId chatId = null)
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e != null)
|
||||
{
|
||||
var eMessage = new StringBuilder();
|
||||
eMessage.Append(e.Message);
|
||||
if (chatId != null)
|
||||
{
|
||||
eMessage.Append($" ({chatId.Identifier})");
|
||||
}
|
||||
|
||||
_logger.LogError(eMessage.ToString());
|
||||
}
|
||||
|
||||
Queue.Enqueue(message);
|
||||
_logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message);
|
||||
ReEnqueue(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReEnqueue(UserMessage message, Exception e = null, ChatId chatId = null)
|
||||
{
|
||||
if (e != null)
|
||||
{
|
||||
var eMessage = new StringBuilder();
|
||||
eMessage.Append(e.Message);
|
||||
if (chatId != null)
|
||||
{
|
||||
eMessage.Append($" ({chatId.Identifier})");
|
||||
}
|
||||
|
||||
_logger.LogError(eMessage.ToString());
|
||||
}
|
||||
|
||||
Queue.Enqueue(message);
|
||||
}
|
||||
}
|
||||
@@ -20,242 +20,241 @@ using Feed = Kruzya.TelegramBot.RichSiteSummary.Data.Feed;
|
||||
|
||||
using CodeHollow.FeedReader.Feeds;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Service
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Service;
|
||||
|
||||
/// <summary>
|
||||
/// Performs a RSS feed parsing.
|
||||
/// Grabs the all feeds from database.
|
||||
///
|
||||
/// TODO: move entity grabbing to another service.
|
||||
/// </summary>
|
||||
public class RssFetch : IHostedService, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs a RSS feed parsing.
|
||||
/// Grabs the all feeds from database.
|
||||
///
|
||||
/// TODO: move entity grabbing to another service.
|
||||
/// </summary>
|
||||
public class RssFetch : IHostedService, IDisposable
|
||||
{
|
||||
private Timer _timer;
|
||||
private Timer _timer;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ConcurrentQueue<UserMessage> _queue;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ConcurrentQueue<UserMessage> _queue;
|
||||
|
||||
private static TimeSpan TimerPeriod => TimeSpan.FromSeconds(45);
|
||||
private static TimeSpan TimerPeriod => TimeSpan.FromSeconds(45);
|
||||
|
||||
public RssFetch(ILogger<RssFetch> logger, ConcurrentQueue<UserMessage> queue, IServiceScopeFactory scopeFactory)
|
||||
public RssFetch(ILogger<RssFetch> logger, ConcurrentQueue<UserMessage> queue, IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_queue = queue;
|
||||
}
|
||||
|
||||
#region IHostedService
|
||||
/// <summary>
|
||||
/// Initializes the RSS fetcher timer.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("RSS fetcher service is starting.");
|
||||
_timer = new Timer(DoFetch, null, TimeSpan.Zero, TimerPeriod);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the RSS fetcher timer.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("RSS fetcher service is stopping.");
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
#endregion
|
||||
#region IDisposable
|
||||
/// <summary>
|
||||
/// Disposes the timer.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Performs the job of fetching RSS data.
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
private async void DoFetch(object state)
|
||||
{
|
||||
_timer.Change(Timeout.Infinite, 0);
|
||||
_logger.LogDebug("RSS fetcher service is triggered.");
|
||||
|
||||
try
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_queue = queue;
|
||||
}
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<RichSiteSummaryContext>();
|
||||
var period = scope.ServiceProvider.GetRequiredService<IConfiguration>()
|
||||
.GetValue<ushort>("rssFetchPeriod");
|
||||
|
||||
#region IHostedService
|
||||
/// <summary>
|
||||
/// Initializes the RSS fetcher timer.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("RSS fetcher service is starting.");
|
||||
_timer = new Timer(DoFetch, null, TimeSpan.Zero, TimerPeriod);
|
||||
var feeds = await dbContext.Feeds.ForFetching(period);
|
||||
_logger.LogDebug("Received {count} feeds for fetching", new {count = feeds.Length});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the RSS fetcher timer.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("RSS fetcher service is stopping.");
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
#endregion
|
||||
#region IDisposable
|
||||
/// <summary>
|
||||
/// Disposes the timer.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Performs the job of fetching RSS data.
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
private async void DoFetch(object state)
|
||||
{
|
||||
_timer.Change(Timeout.Infinite, 0);
|
||||
_logger.LogDebug("RSS fetcher service is triggered.");
|
||||
|
||||
try
|
||||
foreach (var feed in feeds)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<RichSiteSummaryContext>();
|
||||
var period = scope.ServiceProvider.GetRequiredService<IConfiguration>()
|
||||
.GetValue<ushort>("rssFetchPeriod");
|
||||
await ProcessFeed(feed, dbContext);
|
||||
|
||||
var feeds = await dbContext.Feeds.ForFetching(period);
|
||||
_logger.LogDebug("Received {count} feeds for fetching", new {count = feeds.Length});
|
||||
|
||||
foreach (var feed in feeds)
|
||||
{
|
||||
await ProcessFeed(feed, dbContext);
|
||||
|
||||
feed.UpdatedAt = DateTime.Now;
|
||||
dbContext.Update(feed);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_timer.Change(TimerPeriod, TimerPeriod);
|
||||
feed.UpdatedAt = DateTime.Now;
|
||||
dbContext.Update(feed);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#region Feeds
|
||||
|
||||
private async Task ProcessFeed(Feed feed, RichSiteSummaryContext dbContext)
|
||||
finally
|
||||
{
|
||||
var parsedFeed = await FetchFeed(feed);
|
||||
if (parsedFeed == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_timer.Change(TimerPeriod, TimerPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
#region Feeds
|
||||
|
||||
private async Task ProcessFeed(Feed feed, RichSiteSummaryContext dbContext)
|
||||
{
|
||||
var parsedFeed = await FetchFeed(feed);
|
||||
if (parsedFeed == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Post feedPost;
|
||||
var newPosts = new List<Post>();
|
||||
var postsImages = new Dictionary<Post, string>();
|
||||
foreach (var post in parsedFeed.Items)
|
||||
Post feedPost;
|
||||
var newPosts = new List<Post>();
|
||||
var postsImages = new Dictionary<Post, string>();
|
||||
foreach (var post in parsedFeed.Items)
|
||||
{
|
||||
feedPost = await dbContext.Posts.ByFeedAndUrl(post.Link, feed);
|
||||
if (feedPost != null)
|
||||
{
|
||||
feedPost = await dbContext.Posts.ByFeedAndUrl(post.Link, feed);
|
||||
if (feedPost != null)
|
||||
{
|
||||
// skip. This post already exists.
|
||||
continue;
|
||||
}
|
||||
|
||||
feedPost = dbContext.Posts.Create();
|
||||
feedPost.Feed = feed;
|
||||
feedPost.Title = post.Title;
|
||||
feedPost.Url = post.Link;
|
||||
feedPost.PostedAt = post.PublishingDate.GetValueOrDefault(DateTime.Now);
|
||||
|
||||
postsImages.Add(feedPost, await FetchImageUrl(post));
|
||||
newPosts.Add(feedPost);
|
||||
// skip. This post already exists.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newPosts.Count > 0)
|
||||
{
|
||||
var subscribers = await dbContext.Subscriptions.ByFeed(feed);
|
||||
foreach (var post in newPosts)
|
||||
{
|
||||
var text = post.MessageText;
|
||||
foreach (var subscriber in subscribers)
|
||||
{
|
||||
var message = new UserMessage
|
||||
{
|
||||
ChatId = new ChatId(subscriber.SubscriberId), DisableWebPagePreview = true,
|
||||
ParseMode = ParseMode.Html, Text = text, ImageUrl = postsImages.GetValueOrDefault(post)
|
||||
};
|
||||
feedPost = dbContext.Posts.Create();
|
||||
feedPost.Feed = feed;
|
||||
feedPost.Title = post.Title;
|
||||
feedPost.Url = post.Link;
|
||||
feedPost.PostedAt = post.PublishingDate.GetValueOrDefault(DateTime.Now);
|
||||
|
||||
_queue.Enqueue(message);
|
||||
_logger.LogDebug("Queued message for {SubscriberId}", subscriber.SubscriberId);
|
||||
}
|
||||
postsImages.Add(feedPost, await FetchImageUrl(post));
|
||||
newPosts.Add(feedPost);
|
||||
}
|
||||
|
||||
if (newPosts.Count > 0)
|
||||
{
|
||||
var subscribers = await dbContext.Subscriptions.ByFeed(feed);
|
||||
foreach (var post in newPosts)
|
||||
{
|
||||
var text = post.MessageText;
|
||||
foreach (var subscriber in subscribers)
|
||||
{
|
||||
var message = new UserMessage
|
||||
{
|
||||
ChatId = new ChatId(subscriber.SubscriberId), DisableWebPagePreview = true,
|
||||
ParseMode = ParseMode.Html, Text = text, ImageUrl = postsImages.GetValueOrDefault(post)
|
||||
};
|
||||
|
||||
_queue.Enqueue(message);
|
||||
_logger.LogDebug("Queued message for {SubscriberId}", subscriber.SubscriberId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<CodeHollow.FeedReader.Feed> FetchFeed(Feed feed)
|
||||
private async Task<CodeHollow.FeedReader.Feed> FetchFeed(Feed feed)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await FeedReader.ReadAsync(feed.Url);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("Feed {feed} can't be fetched: {error}", feed, e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Image parsing
|
||||
|
||||
private async Task<string> FetchImageUrl(FeedItem item)
|
||||
{
|
||||
var feedItem = item.SpecificItem;
|
||||
if (feedItem is Rss20FeedItem rss20FeedItem)
|
||||
{
|
||||
var enclosure = rss20FeedItem.Enclosure;
|
||||
if (enclosure != null && IsValidImage(enclosure.MediaType))
|
||||
{
|
||||
return enclosure.Url;
|
||||
}
|
||||
}
|
||||
|
||||
return await FetchImageUrl(item.Description);
|
||||
}
|
||||
|
||||
private async Task<string> FetchImageUrl(string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
using var httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true };
|
||||
using var httpClient = new HttpClient(httpClientHandler);
|
||||
|
||||
var imgRegEx = new Regex(@"<img[^>]+>");
|
||||
var srcRegEx = new Regex("src=\"([^\"]+)\"");
|
||||
var matches = imgRegEx.Matches(description);
|
||||
|
||||
var img = "";
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await FeedReader.ReadAsync(feed.Url);
|
||||
var srcData = srcRegEx.Match(match.Value);
|
||||
var content = srcData.Groups[1].Value;
|
||||
|
||||
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, content));
|
||||
if (response.StatusCode == HttpStatusCode.OK &&
|
||||
IsValidImage(response.Content.Headers.ContentType.MediaType))
|
||||
{
|
||||
img = content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("Feed {feed} can't be fetched: {error}", feed, e.Message);
|
||||
return null;
|
||||
_logger.LogError("Caused error when fetching image for RSS post: {error}", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Image parsing
|
||||
|
||||
private async Task<string> FetchImageUrl(FeedItem item)
|
||||
{
|
||||
var feedItem = item.SpecificItem;
|
||||
if (feedItem is Rss20FeedItem rss20FeedItem)
|
||||
{
|
||||
var enclosure = rss20FeedItem.Enclosure;
|
||||
if (enclosure != null && IsValidImage(enclosure.MediaType))
|
||||
{
|
||||
return enclosure.Url;
|
||||
}
|
||||
}
|
||||
|
||||
return await FetchImageUrl(item.Description);
|
||||
}
|
||||
|
||||
private async Task<string> FetchImageUrl(string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
using var httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true };
|
||||
using var httpClient = new HttpClient(httpClientHandler);
|
||||
|
||||
var imgRegEx = new Regex(@"<img[^>]+>");
|
||||
var srcRegEx = new Regex("src=\"([^\"]+)\"");
|
||||
var matches = imgRegEx.Matches(description);
|
||||
|
||||
var img = "";
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
try
|
||||
{
|
||||
var srcData = srcRegEx.Match(match.Value);
|
||||
var content = srcData.Groups[1].Value;
|
||||
|
||||
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, content));
|
||||
if (response.StatusCode == HttpStatusCode.OK &&
|
||||
IsValidImage(response.Content.Headers.ContentType.MediaType))
|
||||
{
|
||||
img = content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("Caused error when fetching image for RSS post: {error}", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if image is valid for Telegram Bot API.
|
||||
/// </summary>
|
||||
/// <param name="contentType">Content-type for received content</param>
|
||||
/// <returns>True, if Telegram maybe can "use" this image, false if not.</returns>
|
||||
private static bool IsValidImage(string contentType)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
"image/jpeg",
|
||||
"image/bmp",
|
||||
"image/png"
|
||||
}.Contains(contentType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
return img;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if image is valid for Telegram Bot API.
|
||||
/// </summary>
|
||||
/// <param name="contentType">Content-type for received content</param>
|
||||
/// <returns>True, if Telegram maybe can "use" this image, false if not.</returns>
|
||||
private static bool IsValidImage(string contentType)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
"image/jpeg",
|
||||
"image/bmp",
|
||||
"image/png"
|
||||
}.Contains(contentType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -9,41 +9,40 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot.Types;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Service
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.Service;
|
||||
|
||||
public class Unsubscriber : AbstractTimedHostedService
|
||||
{
|
||||
public class Unsubscriber : AbstractTimedHostedService
|
||||
protected readonly ConcurrentQueue<UserUnsubscribe> Queue;
|
||||
protected readonly IServiceScopeFactory ScopeFactory;
|
||||
|
||||
protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1);
|
||||
|
||||
public Unsubscriber(ILogger<Unsubscriber> logger, IBotInstance bot, ConcurrentQueue<UserUnsubscribe> queue, IServiceScopeFactory scopeFactory) : base(logger, bot)
|
||||
{
|
||||
protected readonly ConcurrentQueue<UserUnsubscribe> Queue;
|
||||
protected readonly IServiceScopeFactory ScopeFactory;
|
||||
ScopeFactory = scopeFactory;
|
||||
Queue = queue;
|
||||
}
|
||||
|
||||
protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1);
|
||||
|
||||
public Unsubscriber(ILogger<Unsubscriber> logger, IBotInstance bot, ConcurrentQueue<UserUnsubscribe> queue, IServiceScopeFactory scopeFactory) : base(logger, bot)
|
||||
protected override async Task OnRun()
|
||||
{
|
||||
if (Queue.IsEmpty || !Queue.TryDequeue(out var user))
|
||||
{
|
||||
ScopeFactory = scopeFactory;
|
||||
Queue = queue;
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task OnRun()
|
||||
{
|
||||
if (Queue.IsEmpty || !Queue.TryDequeue(out var user))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await UnsubscribeUser(user.ChatId);
|
||||
}
|
||||
|
||||
await UnsubscribeUser(user.ChatId);
|
||||
}
|
||||
private async Task UnsubscribeUser(ChatId chatId)
|
||||
{
|
||||
using var scope = ScopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<RichSiteSummaryContext>();
|
||||
|
||||
private async Task UnsubscribeUser(ChatId chatId)
|
||||
{
|
||||
using var scope = ScopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<RichSiteSummaryContext>();
|
||||
|
||||
var subscriptions = dbContext.Subscriptions;
|
||||
subscriptions.RemoveRange(
|
||||
subscriptions.Where(s => s.SubscriberId == chatId.Identifier)
|
||||
);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
var subscriptions = dbContext.Subscriptions;
|
||||
subscriptions.RemoveRange(
|
||||
subscriptions.Where(s => s.SubscriberId == chatId.Identifier)
|
||||
);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule;
|
||||
|
||||
public struct UtmParameters
|
||||
{
|
||||
public struct UtmParameters
|
||||
{
|
||||
public string Source;
|
||||
public string Type;
|
||||
public string Campaign;
|
||||
public string Term;
|
||||
public string Content;
|
||||
}
|
||||
public string Source;
|
||||
public string Type;
|
||||
public string Campaign;
|
||||
public string Term;
|
||||
public string Content;
|
||||
}
|
||||
@@ -3,49 +3,48 @@ using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Web;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule;
|
||||
|
||||
public class UtmUtility
|
||||
{
|
||||
public class UtmUtility
|
||||
public static string ApplyParameters(string url, UtmParameters parameters)
|
||||
=> ApplyParameters(new Uri(url), parameters);
|
||||
|
||||
public static string ApplyParameters(Uri uri, UtmParameters parameters)
|
||||
{
|
||||
public static string ApplyParameters(string url, UtmParameters parameters)
|
||||
=> ApplyParameters(new Uri(url), parameters);
|
||||
var query = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
public static string ApplyParameters(Uri uri, UtmParameters parameters)
|
||||
SetIfNotNull(query, "utm_source", parameters.Source);
|
||||
SetIfNotNull(query, "utm_medium", parameters.Type);
|
||||
SetIfNotNull(query, "utm_campaign", parameters.Campaign);
|
||||
SetIfNotNull(query, "utm_term", parameters.Term);
|
||||
SetIfNotNull(query, "utm_content", parameters.Content);
|
||||
|
||||
var uriBuilder = new UriBuilder(uri);
|
||||
uriBuilder.Query = BuildQueryString(query);
|
||||
return uriBuilder.ToString();
|
||||
}
|
||||
|
||||
protected static string BuildQueryString(NameValueCollection query)
|
||||
{
|
||||
var queryParameters = new List<string>();
|
||||
|
||||
foreach (var keyName in query.AllKeys)
|
||||
{
|
||||
var query = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
SetIfNotNull(query, "utm_source", parameters.Source);
|
||||
SetIfNotNull(query, "utm_medium", parameters.Type);
|
||||
SetIfNotNull(query, "utm_campaign", parameters.Campaign);
|
||||
SetIfNotNull(query, "utm_term", parameters.Term);
|
||||
SetIfNotNull(query, "utm_content", parameters.Content);
|
||||
|
||||
var uriBuilder = new UriBuilder(uri);
|
||||
uriBuilder.Query = BuildQueryString(query);
|
||||
return uriBuilder.ToString();
|
||||
var value = query[keyName];
|
||||
queryParameters.Add($"{HttpUtility.UrlEncode(keyName)}={HttpUtility.UrlEncode(value)}");
|
||||
}
|
||||
|
||||
protected static string BuildQueryString(NameValueCollection query)
|
||||
{
|
||||
var queryParameters = new List<string>();
|
||||
|
||||
foreach (var keyName in query.AllKeys)
|
||||
{
|
||||
var value = query[keyName];
|
||||
queryParameters.Add($"{HttpUtility.UrlEncode(keyName)}={HttpUtility.UrlEncode(value)}");
|
||||
}
|
||||
|
||||
return String.Join('&', queryParameters);
|
||||
}
|
||||
return String.Join('&', queryParameters);
|
||||
}
|
||||
|
||||
protected static void SetIfNotNull(NameValueCollection queryParameteters, string key, string value)
|
||||
protected static void SetIfNotNull(NameValueCollection queryParameteters, string key, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
queryParameteters.Set(key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
queryParameteters.Set(key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary;
|
||||
|
||||
public struct UserMessage
|
||||
{
|
||||
public struct UserMessage
|
||||
{
|
||||
public ChatId ChatId;
|
||||
public string Text;
|
||||
public ParseMode ParseMode;
|
||||
public bool DisableWebPagePreview;
|
||||
public string ImageUrl;
|
||||
}
|
||||
public ChatId ChatId;
|
||||
public string Text;
|
||||
public ParseMode ParseMode;
|
||||
public bool DisableWebPagePreview;
|
||||
public string ImageUrl;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
using Telegram.Bot.Types;
|
||||
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary
|
||||
namespace Kruzya.TelegramBot.RichSiteSummary;
|
||||
|
||||
public struct UserUnsubscribe
|
||||
{
|
||||
public struct UserUnsubscribe
|
||||
{
|
||||
public ChatId ChatId;
|
||||
}
|
||||
public ChatId ChatId;
|
||||
}
|
||||
Reference in New Issue
Block a user