Initial commit
This commit is contained in:
commit
90a70b60c2
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@ -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/
|
125
Bot.cs
Normal file
125
Bot.cs
Normal file
@ -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<RssBotFeed> RssBotFeeds = new List<RssBotFeed>();
|
||||||
|
public static Timer JobQueue;
|
||||||
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||||
|
private static HashSet<RegexHandler> 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<RegexHandler> {
|
||||||
|
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<long> subs = new HashSet<long>();
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
284
Commands.cs
Normal file
284
Commands.cs
Normal file
@ -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,
|
||||||
|
"<b>Willkommen beim RSS-Bot!</b>\nSende /help, um zu starten.",
|
||||||
|
ParseMode.Html
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async void Help(Message message, GroupCollection matches) {
|
||||||
|
await Bot.BotClient.SendTextMessageAsync(
|
||||||
|
message.Chat,
|
||||||
|
"<b>/rss</b> <i>[Chat]</i>: Abonnierte Feeds anzeigen\n" +
|
||||||
|
"<b>/sub</b> <i>Feed-URL</i> <i>[Chat]</i>: Feed abonnieren\n" +
|
||||||
|
"<b>/del</b> <i>Feed-URL</i> <i>[Chat]</i>: Feed löschen\n" +
|
||||||
|
"<b>/show</b> <i>Feed-URL</i> <i>[Chat]</i>: Feeds auf dieser Seite anzeigen\n" +
|
||||||
|
"<i>[Chat]</i> 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<RssBotFeed> feeds = Bot.RssBotFeeds.Where(x => x.Subs.Contains(chatId)).ToList();
|
||||||
|
|
||||||
|
if (feeds.Count < 1) {
|
||||||
|
text = "❌ Keine Feeds abonniert.";
|
||||||
|
} else {
|
||||||
|
text = $"<strong>{HttpUtility.HtmlEncode(chatTitle)}</strong> hat abonniert:\n";
|
||||||
|
for (int i = 0; i < feeds.Count; i++) {
|
||||||
|
text += $"<strong>{i + 1}</strong>) {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 = $"<b>{postTitle}</b>\n<i>{feed.Title}</i>\n{content}";
|
||||||
|
text += $"\n<a href=\"{postLink}\">Weiterlesen auf {linkName}</a>";
|
||||||
|
|
||||||
|
// 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<HtmlFeedLink> feeds;
|
||||||
|
try {
|
||||||
|
feeds = await FeedReader.GetFeedUrlsFromUrlAsync(url);
|
||||||
|
} catch {
|
||||||
|
await Bot.BotClient.SendTextMessageAsync(message.Chat, "❌ Sete konnte nicht erreicht werden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HtmlFeedLink> 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 + $"* <a href=\"{feedLink.Url}\">{Utils.StripHtml(feedLink.Title)}</a>\n");
|
||||||
|
|
||||||
|
await Bot.BotClient.SendTextMessageAsync(message.Chat, text, ParseMode.Html, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
Configuration.cs
Normal file
82
Configuration.cs
Normal file
@ -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<int> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
NLog.config.example
Normal file
12
NLog.config.example
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
|
||||||
|
<targets>
|
||||||
|
<target name="logconsole" xsi:type="ColoredConsole" layout="${longdate} - ${logger} - ${level:uppercase=true} - ${message}" />
|
||||||
|
</targets>
|
||||||
|
|
||||||
|
<rules>
|
||||||
|
<logger name="*" minlevel="Info" writeTo="logconsole" />
|
||||||
|
</rules>
|
||||||
|
</nlog>
|
11
README.md
Normal file
11
README.md
Normal file
@ -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
|
25
RSSBot.csproj
Normal file
25
RSSBot.csproj
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64;linux-arm</RuntimeIdentifiers>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<!-- <PublishReadyToRun>true</PublishReadyToRun>-->
|
||||||
|
<!-- <PublishSingleFile>true</PublishSingleFile>-->
|
||||||
|
<SelfContained>false</SelfContained>
|
||||||
|
<Title>RSSBot</Title>
|
||||||
|
<Authors>Brawl</Authors>
|
||||||
|
<Description>RSSBot for Telegram</Description>
|
||||||
|
<Copyright>2020 Brawl</Copyright>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.1" />
|
||||||
|
<PackageReference Include="ini-parser" Version="2.5.2" />
|
||||||
|
<PackageReference Include="NLog" Version="4.6.8" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" Version="2.0.601" />
|
||||||
|
<PackageReference Include="Telegram.Bot" Version="15.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
25
RSSBot.sln
Normal file
25
RSSBot.sln
Normal file
@ -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
|
46
RegexHandler.cs
Normal file
46
RegexHandler.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
|
namespace RSSBot {
|
||||||
|
/// <summary>
|
||||||
|
/// RegexHandler for Telegram Bots.
|
||||||
|
/// </summary>
|
||||||
|
public class RegexHandler {
|
||||||
|
private string Pattern;
|
||||||
|
private Action<Message, GroupCollection> CallbackFunction;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor for the RegexHandler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pattern">Regex pattern</param>
|
||||||
|
/// <param name="callback">Callback function to call when the update should be processed</param>
|
||||||
|
public RegexHandler(string pattern, Action<Message, GroupCollection> callback) {
|
||||||
|
Pattern = pattern;
|
||||||
|
CallbackFunction = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the update should be handled by this handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Telegram Message object</param>
|
||||||
|
/// <returns>true if the update should be handled</returns>
|
||||||
|
public bool HandleUpdate(Message message) {
|
||||||
|
return Regex.IsMatch(message.Text,
|
||||||
|
Pattern,
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calls the assoicated callback function.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Telegram Message object</param>
|
||||||
|
public void ProcessUpdate(Message message) {
|
||||||
|
GroupCollection matches = Regex.Match(message.Text,
|
||||||
|
Pattern,
|
||||||
|
RegexOptions.IgnoreCase
|
||||||
|
).Groups;
|
||||||
|
CallbackFunction(message, matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
RssBotFeed.cs
Normal file
84
RssBotFeed.cs
Normal file
@ -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<long> Subs = new HashSet<long>();
|
||||||
|
public string MainLink { get; private set; }
|
||||||
|
public string Title { get; private set; }
|
||||||
|
public List<FeedItem> NewEntries { get; private set; }
|
||||||
|
|
||||||
|
public RssBotFeed(string url, string lastEntry = null, HashSet<long> 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<FeedItem> GetNewEntries(IEnumerable<FeedItem> entries) {
|
||||||
|
List<FeedItem> newEntries = new List<FeedItem>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
Utils.cs
Normal file
89
Utils.cs
Normal file
@ -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<bool> IsBotAdmin(long chatId) {
|
||||||
|
ChatMember chatMember = await Bot.BotClient.GetChatMemberAsync(chatId, Bot.BotClient.BotId);
|
||||||
|
return chatMember.Status.Equals(ChatMemberStatus.Administrator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
config.ini.example
Normal file
13
config.ini.example
Normal file
@ -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
|
7
publish.bat
Normal file
7
publish.bat
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user