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