From 90a70b60c21072a04570bbaa5d24f1e26dd9c4e2 Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Thu, 30 Jan 2020 20:57:41 +0100 Subject: [PATCH] Initial commit --- .gitignore | 19 +++ Bot.cs | 125 +++++++++++++++++++ Commands.cs | 284 ++++++++++++++++++++++++++++++++++++++++++++ Configuration.cs | 82 +++++++++++++ NLog.config.example | 12 ++ README.md | 11 ++ RSSBot.csproj | 25 ++++ RSSBot.sln | 25 ++++ RegexHandler.cs | 46 +++++++ RssBotFeed.cs | 84 +++++++++++++ Utils.cs | 89 ++++++++++++++ config.ini.example | 13 ++ publish.bat | 7 ++ 13 files changed, 822 insertions(+) create mode 100644 .gitignore create mode 100644 Bot.cs create mode 100644 Commands.cs create mode 100644 Configuration.cs create mode 100644 NLog.config.example create mode 100644 README.md create mode 100644 RSSBot.csproj create mode 100644 RSSBot.sln create mode 100644 RegexHandler.cs create mode 100644 RssBotFeed.cs create mode 100644 Utils.cs create mode 100644 config.ini.example create mode 100644 publish.bat diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4a57f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# IDE and config +.idea/ +.vs/ +config.ini +NLog.config + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ diff --git a/Bot.cs b/Bot.cs new file mode 100644 index 0000000..1168d2e --- /dev/null +++ b/Bot.cs @@ -0,0 +1,125 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using NLog; +using StackExchange.Redis; +using Telegram.Bot; +using Telegram.Bot.Args; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace RSSBot { + public static class Bot { + public static ITelegramBotClient BotClient; + public static User BotInfo; + public static readonly List RssBotFeeds = new List(); + public static Timer JobQueue; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static HashSet Handlers; + + private static void Main(string[] args) { + Configuration.Parse(); + BotClient = new TelegramBotClient(Configuration.BotToken); + try { + BotInfo = BotClient.GetMeAsync().Result; + } catch (AggregateException) { + Logger.Fatal("Login fehlgeschlagen, Bot-Token prüfen!"); + return; + } + + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + Console.CancelKeyPress += delegate { Save(); }; + + // Read subscribed feeds from Redis + ReadAllFeeds(); + + // Add handlers + Handlers = new HashSet { + new RegexHandler($"^/start(?:@{BotInfo.Username})?$", Commands.Welcome), + new RegexHandler($"^/help(?:@{BotInfo.Username})?$", Commands.Help), + new RegexHandler($"^/rss(?:@{BotInfo.Username})?$", Commands.Show), + new RegexHandler($"^/rss(?:@{BotInfo.Username})? (@?[A-z0-9_]+)$", Commands.Show), + new RegexHandler( + $"^/show(?:@{BotInfo.Username})? (http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&~+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)$", + Commands.ShowAvailableFeeds), + new RegexHandler( + $"^/sub(?:@{BotInfo.Username})? (http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&~+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)$", + Commands.Subscribe), + new RegexHandler( + $"^/sub(?:@{BotInfo.Username})? (http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&~+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+) (@?[A-z0-9_]+)$$", + Commands.Subscribe), + new RegexHandler( + $"^/del(?:@{BotInfo.Username})? (http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&~+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)$", + Commands.Unsubscribe), + new RegexHandler( + $"^/del(?:@{BotInfo.Username})? (http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&~+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+) (@?[A-z0-9_]+)$$", + Commands.Unsubscribe), + }; + + JobQueue = new Timer(e => { Commands.Sync(); }, null, TimeSpan.FromSeconds(5), + TimeSpan.FromMilliseconds(-1)); + + Logger.Info($"Bot gestartet: {BotInfo.FirstName}, AKA {BotInfo.Username} ({BotInfo.Id})."); + + BotClient.OnMessage += Bot_OnMessage; + BotClient.StartReceiving(); + Console.ReadLine(); + BotClient.StopReceiving(); + Save(); + } + + private static void ReadAllFeeds() { + RedisValue[] allFeedUrls = Configuration.Database.SetMembers($"{Configuration.RedisHash}:feeds"); + foreach (RedisValue feedUrl in allFeedUrls) { + HashSet subs = new HashSet(); + RedisValue[] allSubs = Configuration.Database.SetMembers($"{Configuration.RedisHash}:{feedUrl}:subs"); + foreach (RedisValue sub in allSubs) { + subs.Add(Convert.ToInt64(sub)); + } + + string lastEntry = Configuration.Database.HashGet($"{Configuration.RedisHash}:{feedUrl}", "last_entry"); + + RssBotFeed feed = new RssBotFeed(feedUrl, lastEntry, subs); + RssBotFeeds.Add(feed); + } + } + + private static void OnProcessExit(object? sender, EventArgs e) { + Save(); + } + + private static void Bot_OnMessage(object? sender, MessageEventArgs messageEventArgs) { + var message = messageEventArgs.Message; + if (message == null || message.Type != MessageType.Text) return; + if (!Configuration.Admins.Contains(message.From.Id)) { + return; + } + + foreach (RegexHandler handler in Handlers.Where(handler => handler.HandleUpdate(message))) { + handler.ProcessUpdate(message); + } + } + + public static async void Save() { + if (RssBotFeeds.Count > 0) { + Logger.Info("Speichere Daten..."); + } + + foreach (RssBotFeed feed in RssBotFeeds) { + string feedKey = $"{Configuration.RedisHash}:{feed.Url}"; + if (string.IsNullOrWhiteSpace(feed.LastEntry)) continue; + + await Configuration.Database.HashSetAsync(feedKey, "last_entry", feed.LastEntry); + foreach (long chatId in feed.Subs) { + await Configuration.Database.SetAddAsync($"{feedKey}:subs", chatId); + } + + await Configuration.Database.SetAddAsync($"{Configuration.RedisHash}:feeds", feed.Url); + } + + Logger.Info("Gespeichert!"); + } + } +} \ No newline at end of file diff --git a/Commands.cs b/Commands.cs new file mode 100644 index 0000000..86a41f2 --- /dev/null +++ b/Commands.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Web; +using CodeHollow.FeedReader; +using NLog; +using Telegram.Bot.Exceptions; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace RSSBot { + public static class Commands { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public static async void Welcome(Message message, GroupCollection matches) { + await Bot.BotClient.SendTextMessageAsync( + message.Chat, + "Willkommen beim RSS-Bot!\nSende /help, um zu starten.", + ParseMode.Html + ); + } + + public static async void Help(Message message, GroupCollection matches) { + await Bot.BotClient.SendTextMessageAsync( + message.Chat, + "/rss [Chat]: Abonnierte Feeds anzeigen\n" + + "/sub Feed-URL [Chat]: Feed abonnieren\n" + + "/del Feed-URL [Chat]: Feed löschen\n" + + "/show Feed-URL [Chat]: Feeds auf dieser Seite anzeigen\n" + + "[Chat] ist ein optionales Argument mit dem @Kanalnamen.", + ParseMode.Html + ); + } + + public static async void Subscribe(Message message, GroupCollection args) { + string url = args[1].Value; + long chatId = message.Chat.Id; + RssBotFeed feed = new RssBotFeed(url); + + await Bot.BotClient.SendChatActionAsync(message.Chat, ChatAction.Typing); + + if (args.Count > 2) { + string chatName = args[2].Value; + if (!chatName.StartsWith("@")) { + chatName = $"@{chatName}"; + } + Chat chatInfo; + try { + chatInfo = await Bot.BotClient.GetChatAsync(chatName); + } catch { + await Bot.BotClient.SendTextMessageAsync(message.Chat, "❌ Dieser Kanal existiert nicht."); + return; + } + + chatId = chatInfo.Id; + + if (!await Utils.IsBotAdmin(chatId)) { + await Bot.BotClient.SendTextMessageAsync(message.Chat, + "❌ Du musst den Bot als Administrator zu diesem Kanal hinzufügen."); + return; + } + } + + try { + await feed.Check(); + } catch { + await Bot.BotClient.SendTextMessageAsync( + message.Chat, + "❌ Kein gültiger RSS-Feed." + ); + return; + } + + // Check if we already have the feed + RssBotFeed existingFeed = Bot.RssBotFeeds + .FirstOrDefault(x => x.Url.ToLower().Equals(feed.Url.ToLower())); + if (existingFeed == null) { + Bot.RssBotFeeds.Add(feed); + } else { + feed = existingFeed; + } + + // Check if chat already subscribed + if (feed.Subs.Contains(chatId)) { + await Bot.BotClient.SendTextMessageAsync(message.Chat, "✅ Dieser Feed wurde bereits abonniert."); + } else { + feed.Subs.Add(chatId); + await Bot.BotClient.SendTextMessageAsync(message.Chat, "✅ Feed abonniert!"); + Bot.Save(); + } + } + + public static async void Unsubscribe(Message message, GroupCollection args) { + string url = args[1].Value; + long chatId = message.Chat.Id; + RssBotFeed feed = Bot.RssBotFeeds + .FirstOrDefault(x => x.Url.ToLower().Equals(url.ToLower())); + + if (args.Count > 2) { + string chatName = args[2].Value; + if (!chatName.StartsWith("@")) { + chatName = $"@{chatName}"; + } + Chat chatInfo; + try { + chatInfo = await Bot.BotClient.GetChatAsync(chatName); + } catch { + await Bot.BotClient.SendTextMessageAsync(message.Chat, "❌ Dieser Kanal existiert nicht."); + return; + } + + chatId = chatInfo.Id; + + if (!await Utils.IsBotAdmin(chatId)) { + await Bot.BotClient.SendTextMessageAsync(message.Chat, + "❌ Du musst den Bot als Administrator zu diesem Kanal hinzufügen."); + return; + } + } + + if (feed == null || !feed.Subs.Contains(chatId)) { + await Bot.BotClient.SendTextMessageAsync(message.Chat, "❌ Feed wurde nicht abonniert."); + return; + } + + feed.Cleanup(chatId); + if (feed.Subs.Count == 0) { + Bot.RssBotFeeds.Remove(feed); + } + + await Bot.BotClient.SendTextMessageAsync(message.Chat, "✅ Feed deabonniert!"); + Bot.Save(); + } + + public static async void Show(Message message, GroupCollection args) { + long chatId = message.Chat.Id; + string chatTitle = message.Chat.Type.Equals(ChatType.Private) ? message.Chat.FirstName : message.Chat.Title; + await Bot.BotClient.SendChatActionAsync(message.Chat, ChatAction.Typing); + + if (args.Count > 1) { + string chatName = args[1].Value; + if (!chatName.StartsWith("@")) { + chatName = $"@{chatName}"; + } + Chat chatInfo; + + try { + chatInfo = await Bot.BotClient.GetChatAsync(chatName); + } catch { + await Bot.BotClient.SendTextMessageAsync(message.Chat, "❌ Dieser Kanal existiert nicht."); + return; + } + + chatId = chatInfo.Id; + chatTitle = chatInfo.Title; + + if (!await Utils.IsBotAdmin(chatId)) { + await Bot.BotClient.SendTextMessageAsync(message.Chat, + "❌ Du musst den Bot als Administrator zu diesem Kanal hinzufügen."); + return; + } + } + + string text; + + List feeds = Bot.RssBotFeeds.Where(x => x.Subs.Contains(chatId)).ToList(); + + if (feeds.Count < 1) { + text = "❌ Keine Feeds abonniert."; + } else { + text = $"{HttpUtility.HtmlEncode(chatTitle)} hat abonniert:\n"; + for (int i = 0; i < feeds.Count; i++) { + text += $"{i + 1}) {feeds[i].Url}\n"; + } + } + await Bot.BotClient.SendTextMessageAsync(message.Chat, text, ParseMode.Html, true); + } + + public static async void Sync() { + Logger.Info(("================================")); + bool hadEntries = false; + foreach (RssBotFeed feed in Bot.RssBotFeeds.ToList()) { + Logger.Info(feed.Url); + try { + await feed.Check(); + } catch (Exception e) { + Logger.Warn($"FEHLER: {e.Message}"); + continue; + } + + if (feed.NewEntries.Count == 0) { + Logger.Info("Keine neuen Beiträge."); + continue; + } + + hadEntries = true; + Logger.Info(feed.NewEntries.Count == 1 + ? "1 neuer Beitrag" + : $"{feed.NewEntries.Count} neue Beiträge"); + + foreach (FeedItem entry in feed.NewEntries) { + string postTitle = "Kein Titel"; + if (!string.IsNullOrWhiteSpace(entry.Title)) { + postTitle = Utils.StripHtml(entry.Title); + } + + string postLink = feed.MainLink; + string linkName = postLink; + if (!string.IsNullOrWhiteSpace(entry.Link)) { + postLink = entry.Link; + // FeedProxy URLs + GroupCollection feedProxy = + Utils.ReturnMatches(postLink, "^https?://feedproxy.google.com/~r/(.+)/.*"); + linkName = feedProxy.Count > 1 ? feedProxy[1].Value : new Uri(postLink).Host; + } + + // Remove "www." + int index = linkName.IndexOf("www.", StringComparison.Ordinal); + if (index > 0) { + linkName = linkName.Remove(index, 4); + } + + string content = ""; + if (!string.IsNullOrWhiteSpace(entry.Content)) { + content = Utils.ProcessContent(entry.Content); // magic processing missing + } else if (!string.IsNullOrWhiteSpace(entry.Description)) { + content = Utils.ProcessContent(entry.Description); + } + + string text = $"{postTitle}\n{feed.Title}\n{content}"; + text += $"\nWeiterlesen auf {linkName}"; + + // Send + foreach (long chatId in feed.Subs.ToList()) { + try { + await Bot.BotClient.SendTextMessageAsync(chatId, text, ParseMode.Html, true, true); + } catch (ApiRequestException e) { + if (e.ErrorCode.Equals(403)) { + Logger.Warn(e.Message); + feed.Cleanup(chatId); + if (feed.Subs.Count == 0) { // was last subscriber + Bot.RssBotFeeds.Remove(feed); + } + } else { + Logger.Error($"{e.ErrorCode}: {e.Message}"); + } + } + } + } + } + + Logger.Info("Nächster Check in 60 Sekunden"); + + if (hadEntries) { + Bot.Save(); + } + + Bot.JobQueue.Change(TimeSpan.FromMinutes(1), TimeSpan.FromMilliseconds(-1)); + } + + public static async void ShowAvailableFeeds(Message message, GroupCollection args) { + string url = args[1].Value; + IEnumerable feeds; + try { + feeds = await FeedReader.GetFeedUrlsFromUrlAsync(url); + } catch { + await Bot.BotClient.SendTextMessageAsync(message.Chat, "❌ Sete konnte nicht erreicht werden."); + return; + } + + List htmlFeedLinks = feeds.ToList(); + if (htmlFeedLinks.Count == 0) { + await Bot.BotClient.SendTextMessageAsync(message.Chat, "❌ Keine Feeds gefunden."); + return; + } + + string text = htmlFeedLinks.Aggregate("Feeds gefunden:\n", + (current, feedLink) => current + $"* {Utils.StripHtml(feedLink.Title)}\n"); + + await Bot.BotClient.SendTextMessageAsync(message.Chat, text, ParseMode.Html, true); + } + } +} \ No newline at end of file diff --git a/Configuration.cs b/Configuration.cs new file mode 100644 index 0000000..30642d1 --- /dev/null +++ b/Configuration.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using IniParser; +using IniParser.Model; +using NLog; +using NLog.Config; +using NLog.Targets; +using StackExchange.Redis; + +namespace RSSBot { + public static class Configuration { + public static string BotToken; + private static ConnectionMultiplexer _redis; + public static IDatabase Database; + public static string RedisHash; + public static List Admins; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public static void Parse() { + if (!File.Exists("NLog.config")) { + Console.WriteLine("NLog.config nicht gefunden, setze auf INFO..."); + LoggingConfiguration config = new LoggingConfiguration(); + + ConsoleTarget logconsole = new ConsoleTarget("logconsole"); + config.AddRule(LogLevel.Info, LogLevel.Fatal, logconsole); + logconsole.Layout = "${longdate} - ${logger} - ${level:uppercase=true} - ${message}"; + LogManager.Configuration = config; + } else { + LogManager.LoadConfiguration("NLog.config"); + } + + FileIniDataParser parser = new FileIniDataParser(); + if (!File.Exists("config.ini")) { + Logger.Fatal("config.ini nicht gefunden."); + Environment.Exit(1); + } + + IniData data = parser.ReadFile("config.ini"); + + BotToken = data["DEFAULT"]["token"]; + string host = data["REDIS"]["host"] ?? "127.0.0.1"; + string port = data["REDIS"]["port"] ?? "6379"; + string configuration = data["REDIS"]["configuration"] ?? $"{host}:{port}"; + + RedisHash = data["REDIS"]["hash"] ?? "telegram:rssbot"; + int db = 0; + try { + db = int.Parse(data["REDIS"]["db"] ?? "0"); + } catch (FormatException) { + Logger.Fatal("Keine valide Datenbanknummer."); + Environment.Exit(1); + } + + string admins = data["ADMIN"]["id"]; + + if (string.IsNullOrWhiteSpace(BotToken)) { + Logger.Fatal("Bitte Bot-Token in der config.ini angeben."); + Environment.Exit(1); + } + + try { + Admins = admins.Split(",").Select(int.Parse).ToList(); + } catch (FormatException) { + Logger.Fatal("Admin-IDs sind keine Integer."); + Environment.Exit(1); + } + + Logger.Info("Verbinde mit Redis..."); + // TODO: Sockets + try { + _redis = ConnectionMultiplexer.Connect(configuration); + } catch (RedisConnectionException) { + Logger.Fatal("Redis-Verbindung fehlgeschlagen."); + Environment.Exit(1); + } + + Database = _redis.GetDatabase(db); + } + } +} \ No newline at end of file diff --git a/NLog.config.example b/NLog.config.example new file mode 100644 index 0000000..86ba154 --- /dev/null +++ b/NLog.config.example @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4db22f --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +RSS Bot for Telegram +===================== +1. **Install .NET Core Runtime >= 3.1 and Redis** +2. **Copy config:** `cp config.ini.example config.ini` +3. **Insert bot token** in `config.ini` + 1. Adjust Redis settings if needed +4. **Insert your Telegram ID as admin** (send `@Brawlbot id` inside a chat to get yours) +5. Optional: Copy the `NLog.config.example` next to the config.ini as `NLog.config` +6. Run the `RSSBot` executable + +(c) 2020 Andreas Bielawski diff --git a/RSSBot.csproj b/RSSBot.csproj new file mode 100644 index 0000000..cb2138e --- /dev/null +++ b/RSSBot.csproj @@ -0,0 +1,25 @@ + + + + Exe + netcoreapp3.1 + win-x64;osx-x64;linux-x64;linux-arm + 1.0.0 + + + false + RSSBot + Brawl + RSSBot for Telegram + 2020 Brawl + + + + + + + + + + + diff --git a/RSSBot.sln b/RSSBot.sln new file mode 100644 index 0000000..a566643 --- /dev/null +++ b/RSSBot.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29709.97 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RSSBot", "RSSBot.csproj", "{8229A450-2EB5-4E3B-B6A5-647DB7363D86}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8229A450-2EB5-4E3B-B6A5-647DB7363D86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8229A450-2EB5-4E3B-B6A5-647DB7363D86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8229A450-2EB5-4E3B-B6A5-647DB7363D86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8229A450-2EB5-4E3B-B6A5-647DB7363D86}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {365EEFBA-E5A1-419A-828F-54CA76CCE745} + EndGlobalSection +EndGlobal diff --git a/RegexHandler.cs b/RegexHandler.cs new file mode 100644 index 0000000..765e6e1 --- /dev/null +++ b/RegexHandler.cs @@ -0,0 +1,46 @@ +using System; +using System.Text.RegularExpressions; +using Telegram.Bot.Types; + +namespace RSSBot { + /// + /// RegexHandler for Telegram Bots. + /// + public class RegexHandler { + private string Pattern; + private Action CallbackFunction; + + /// + /// Constructor for the RegexHandler. + /// + /// Regex pattern + /// Callback function to call when the update should be processed + public RegexHandler(string pattern, Action callback) { + Pattern = pattern; + CallbackFunction = callback; + } + + /// + /// Checks whether the update should be handled by this handler. + /// + /// Telegram Message object + /// true if the update should be handled + public bool HandleUpdate(Message message) { + return Regex.IsMatch(message.Text, + Pattern, + RegexOptions.IgnoreCase); + } + + /// + /// Calls the assoicated callback function. + /// + /// Telegram Message object + public void ProcessUpdate(Message message) { + GroupCollection matches = Regex.Match(message.Text, + Pattern, + RegexOptions.IgnoreCase + ).Groups; + CallbackFunction(message, matches); + } + } +} \ No newline at end of file diff --git a/RssBotFeed.cs b/RssBotFeed.cs new file mode 100644 index 0000000..e8f9bb4 --- /dev/null +++ b/RssBotFeed.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CodeHollow.FeedReader; + +namespace RSSBot { + public class RssBotFeed { + public readonly string Url; + public string LastEntry; + public readonly HashSet Subs = new HashSet(); + public string MainLink { get; private set; } + public string Title { get; private set; } + public List NewEntries { get; private set; } + + public RssBotFeed(string url, string lastEntry = null, HashSet subs = null) { + Url = url; + if (!string.IsNullOrWhiteSpace(lastEntry)) { + LastEntry = lastEntry; + } + + if (subs != null) { + Subs = subs; + } + } + + public async Task Check() { + Feed feed = await FeedReader.ReadAsync(Url); + if (string.IsNullOrWhiteSpace(feed.Link)) { + throw new Exception("Kein gültiger RSS-Feed."); + } + + MainLink = feed.Link; + Title = feed.Title; + + if (feed.Items == null || feed.Items.Count <= 0) return; + NewEntries = string.IsNullOrWhiteSpace(LastEntry) + ? feed.Items.ToList() + : GetNewEntries(feed.Items); + + LastEntry = string.IsNullOrWhiteSpace(feed.Items.First().Id) + ? feed.Items.First().Link + : feed.Items.First().Id; + } + + private List GetNewEntries(IEnumerable entries) { + List newEntries = new List(); + foreach (FeedItem entry in entries) { + if (!string.IsNullOrWhiteSpace(entry.Id)) { + if (entry.Id.Equals(LastEntry)) { + break; + } + + newEntries.Add(entry); + } else { + if (entry.Link.Equals(LastEntry)) { + break; + } + + newEntries.Add(entry); + } + } + + newEntries.Reverse(); + return newEntries; + } + + public override string ToString() { + return $"RSS-Feed: '{Url}'"; + } + + public void Cleanup(long chatId) { + Subs.Remove(chatId); + string feedKey = $"{Configuration.RedisHash}:{Url}"; + Configuration.Database.SetRemove($"{feedKey}:subs", chatId); + + // No subscribers, delete all references + if (Subs.Count != 0 || !Configuration.Database.KeyExists(feedKey)) return; + Configuration.Database.KeyDelete(feedKey); + Configuration.Database.KeyDelete($"{feedKey}:subs"); + Configuration.Database.SetRemove($"{Configuration.RedisHash}:feeds", Url); + } + } +} \ No newline at end of file diff --git a/Utils.cs b/Utils.cs new file mode 100644 index 0000000..fa5ad7e --- /dev/null +++ b/Utils.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace RSSBot { + public static class Utils { + public static string StripHtml(string input) { + return Regex.Replace(input, "<.*?>", String.Empty).Trim(); + } + + private static string CleanRss(string input) { + string[] replacements = { + "[…]", + "[bilder]", + "[boerse]", + "[mehr]", + "[video]", + "...[more]", + "[more]", + "[liveticker]", + "[livestream]", + "[multimedia]", + "[sportschau]", + "[phoenix]", + "[swr]", + "[ndr]", + "[mdr]", + "[rbb]", + "[wdr]", + "[hr]", + "[br]", + "Click for full.", + "Read more »", + "Read more", + "...Read More", + "(more…)", + "View On WordPress", + "Continue reading →", + "(RSS generated with FetchRss)", + "-- Delivered by Feed43 service", + "Meldung bei www.tagesschau.de lesen" + }; + + string[] regexReplacements = { + "Der Beitrag.*erschien zuerst auf .+.", + "The post.*appeared first on .+.", + "http://www.serienjunkies.de/.*.html" + }; + + input = replacements.Aggregate(input, (current, replacement) => current.Replace(replacement, "")); + input = regexReplacements.Aggregate(input, + (current, replacement) => Regex.Replace(current, replacement, "")); + + return input; + } + + public static string ProcessContent(string input) { + string content = StripHtml(input); + content = CleanRss(content); + if (content.Length > 250) { + content = content.Substring(0, 250) + "..."; + } + + return content; + } + + public static bool IsCommand(string messageText, string command) { + return Regex.Match(messageText, + $"^/{command}(?:@{Bot.BotInfo.Username})?$", + RegexOptions.IgnoreCase + ).Success; + } + + public static GroupCollection ReturnMatches(string text, string pattern) { + return Regex.Match(text, + pattern, + RegexOptions.IgnoreCase + ).Groups; + } + + public static async Task IsBotAdmin(long chatId) { + ChatMember chatMember = await Bot.BotClient.GetChatMemberAsync(chatId, Bot.BotClient.BotId); + return chatMember.Status.Equals(ChatMemberStatus.Administrator); + } + } +} \ No newline at end of file diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..60df9f0 --- /dev/null +++ b/config.ini.example @@ -0,0 +1,13 @@ +[DEFAULT] +token = 1337:1234567890abcdefgh + +[REDIS] +#db = 0 +#host = localhost +#port = 6379 +#socket_path = /home/user/.redis/sock +#hash = telegram:rssbot + + +[ADMIN] +id = 12345678 diff --git a/publish.bat b/publish.bat new file mode 100644 index 0000000..40895ac --- /dev/null +++ b/publish.bat @@ -0,0 +1,7 @@ +@echo off +rd /s /q bin\Release\ +dotnet publish -c release -r win-x64 /p:PublishSingleFile=true /p:PublishReadyToRun=true +dotnet publish -c release -r linux-x64 /p:PublishSingleFile=true +dotnet publish -c release -r linux-arm /p:PublishSingleFile=true +pause +exit