Finish migration to MongoDb and general improvements

This commit is contained in:
2024-12-15 21:22:36 +03:30
parent 29011e34f9
commit dc38ce927f
13 changed files with 370 additions and 416 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ obj/
*.log *.log
*.json *.json
.env .env
data/

View File

@@ -1,31 +1,32 @@
namespace CatsOfMastodonBot.Models namespace CatsOfMastodonBot.Models;
{
public class ConfigData
{
public class ConfigData
{
public static config fetchData() public static config fetchData()
{ {
// Load from .env file first // Load from .env file first
DotNetEnv.Env.Load(); DotNetEnv.Env.Load();
// Fetch values from .env file or environment variables (fall back) // Fetch values from .env file or environment variables (fall back)
string dbName = DotNetEnv.Env.GetString("DB_NAME") ?? Environment.GetEnvironmentVariable("DB_NAME") ?? "catsofmastodon"; var dbName = DotNetEnv.Env.GetString("DB_NAME") ??
string mongoDbConnectionString = DotNetEnv.Env.GetString("MONGODB_CONNECTION_STRING") ?? Environment.GetEnvironmentVariable("MONGODB_CONNECTION_STRING"); Environment.GetEnvironmentVariable("DB_NAME") ?? "catsofmastodon";
string botToken = DotNetEnv.Env.GetString("BOT_TOKEN") ?? Environment.GetEnvironmentVariable("BOT_TOKEN"); var mongoDbConnectionString = DotNetEnv.Env.GetString("MONGODB_CONNECTION_STRING") ??
string tag = DotNetEnv.Env.GetString("TAG") ?? Environment.GetEnvironmentVariable("TAG"); Environment.GetEnvironmentVariable("MONGODB_CONNECTION_STRING");
string channelNumId = DotNetEnv.Env.GetString("CHANNEL_NUMID") ?? Environment.GetEnvironmentVariable("CHANNEL_NUMID"); var botToken = DotNetEnv.Env.GetString("BOT_TOKEN") ?? Environment.GetEnvironmentVariable("BOT_TOKEN");
string adminNumId = DotNetEnv.Env.GetString("ADMIN_NUMID") ?? Environment.GetEnvironmentVariable("ADMIN_NUMID"); var tag = DotNetEnv.Env.GetString("TAG") ?? Environment.GetEnvironmentVariable("TAG");
string instance = DotNetEnv.Env.GetString("CUSTOM_INSTANCE") ?? Environment.GetEnvironmentVariable("CUSTOM_INSTANCE") ?? "https://haminoa.net"; var channelNumId = DotNetEnv.Env.GetString("CHANNEL_NUMID") ??
Environment.GetEnvironmentVariable("CHANNEL_NUMID");
var adminNumId = DotNetEnv.Env.GetString("ADMIN_NUMID") ?? Environment.GetEnvironmentVariable("ADMIN_NUMID");
var instance = DotNetEnv.Env.GetString("CUSTOM_INSTANCE") ??
Environment.GetEnvironmentVariable("CUSTOM_INSTANCE") ?? "https://haminoa.net";
// Check if any of the values are still null or empty // Check if any of the values are still null or empty
if (string.IsNullOrEmpty(botToken) || string.IsNullOrEmpty(tag) if (string.IsNullOrEmpty(botToken) || string.IsNullOrEmpty(tag)
|| string.IsNullOrEmpty(channelNumId) || string.IsNullOrEmpty(adminNumId)) || string.IsNullOrEmpty(channelNumId) || string.IsNullOrEmpty(adminNumId))
{
return null; // Failure if any are missing return null; // Failure if any are missing
}
// If all required variables are present, assign to the config // If all required variables are present, assign to the config
config config = new config var config = new config
{ {
DB_NAME = dbName, DB_NAME = dbName,
MONGODB_CONNECTION_STRING = mongoDbConnectionString, MONGODB_CONNECTION_STRING = mongoDbConnectionString,
@@ -49,5 +50,4 @@ namespace CatsOfMastodonBot.Models
public string INSTANCE { get; set; } public string INSTANCE { get; set; }
public string MONGODB_CONNECTION_STRING { get; set; } public string MONGODB_CONNECTION_STRING { get; set; }
} }
}
} }

View File

@@ -4,52 +4,36 @@ using MongoDB.Bson.Serialization.Attributes;
using Telegram.Bot.Types; using Telegram.Bot.Types;
namespace mstdnCats.Models namespace mstdnCats.Models;
[BsonIgnoreExtraElements]
public class Post
{ {
[BsonIgnoreExtraElements] [JsonPropertyName("id")] public required string mstdnPostId { get; set; }
public class Post [JsonPropertyName("url")] public required string Url { get; set; }
{ [JsonPropertyName("account")] public required Account Account { get; set; }
[JsonPropertyName("id")]
public required string mstdnPostId { get; set; }
[JsonPropertyName("url")]
public required string Url { get; set; }
[JsonPropertyName("account")]
public required Account Account { get; set; }
[JsonPropertyName("media_attachments")] [JsonPropertyName("media_attachments")]
public required List<MediaAttachment> MediaAttachments { get; set; } public required List<MediaAttachment> MediaAttachments { get; set; }
} }
public class Account public class Account
{ {
[JsonPropertyName("id")] [JsonPropertyName("id")] public required string AccId { get; set; }
public required string AccId { get; set; } [JsonPropertyName("username")] public required string Username { get; set; }
[JsonPropertyName("username")] [JsonPropertyName("acct")] public required string Acct { get; set; }
public required string Username { get; set; } [JsonPropertyName("display_name")] public required string DisplayName { get; set; }
[JsonPropertyName("acct")] [JsonPropertyName("bot")] public required bool IsBot { get; set; }
public required string Acct { get; set; } [JsonPropertyName("url")] public required string Url { get; set; }
[JsonPropertyName("display_name")] [JsonPropertyName("avatar_static")] public required string AvatarStatic { get; set; }
public required string DisplayName { get; set; } }
[JsonPropertyName("bot")]
public required Boolean IsBot { get; set; } public class MediaAttachment
[JsonPropertyName("url")] {
public required string Url { get; set; } [JsonPropertyName("id")] public required string MediaId { get; set; }
[JsonPropertyName("avatar_static")] [JsonPropertyName("type")] public required string Type { get; set; }
public required string AvatarStatic { get; set; } [JsonPropertyName("url")] public required string Url { get; set; }
} [JsonPropertyName("preview_url")] public required string PreviewUrl { get; set; }
[JsonPropertyName("remote_url")] public required string RemoteUrl { get; set; }
public class MediaAttachment public bool Approved { get; set; } = false;
{
[JsonPropertyName("id")]
public required string MediaId { get; set; }
[JsonPropertyName("type")]
public required string Type { get; set; }
[JsonPropertyName("url")]
public required string Url { get; set; }
[JsonPropertyName("preview_url")]
public required string PreviewUrl { get; set; }
[JsonPropertyName("remote_url")]
public required string RemoteUrl { get; set; }
public Boolean Approved { get; set; } = false;
}
} }

View File

@@ -12,30 +12,21 @@ public class MastodonBot
private static async Task Main() private static async Task Main()
{ {
// Configure logging // Configure logging
using var loggerFactory = LoggerFactory.Create(builder => using var loggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); });
{
builder.AddConsole();
});
var logger = loggerFactory.CreateLogger<MastodonBot>(); var logger = loggerFactory.CreateLogger<MastodonBot>();
// Read environment variables // Read environment variables
var config = ConfigData.fetchData(); var config = ConfigData.fetchData();
if (config==null) if (config == null)
{ {
logger.LogCritical("Error reading environment variables, either some values are missing or no .env file was found"); logger.LogCritical(
throw new Exception("Error reading environment variables, either some values are missing or no .env file was found"); "Error reading environment variables, either some values are missing or no .env file was found");
throw new Exception(
"Error reading environment variables, either some values are missing or no .env file was found");
} }
// Setup DB // Setup DB
var backupDb = await DbInitializer.SetupJsonDb(config.DB_NAME);
if (backupDb == null)
{
logger.LogCritical("Unable to setup json DB");
throw new Exception("Unable to setup json DB");
}
var db = await DbInitializer.SetupDb(config.MONGODB_CONNECTION_STRING, config.DB_NAME); var db = await DbInitializer.SetupDb(config.MONGODB_CONNECTION_STRING, config.DB_NAME);
logger.LogInformation("DB setup done"); logger.LogInformation("DB setup done");
@@ -55,40 +46,47 @@ public class MastodonBot
{ {
switch (update) switch (update)
{ {
case { CallbackQuery: { } callbackQuery }: { case { CallbackQuery: { } callbackQuery }:
if(callbackQuery.Data == "new_random"){ await HandleStartMessage.HandleStartMessageAsync(callbackQuery.Message, bot, db, logger,callbackQuery); break;} {
if (callbackQuery.Data == "new_random")
else {await HandlePostAction.HandleCallbackQuery(callbackQuery, db, bot, logger); break;} {
await HandleStartMessage.HandleStartMessageAsync(callbackQuery.Message, bot, db, logger,
callbackQuery);
break;
}
else
{
await HandlePostAction.HandleCallbackQuery(callbackQuery, db, bot, logger);
break;
}
} }
default: logger.LogInformation($"Received unhandled update {update.Type}"); break; default: logger.LogInformation($"Received unhandled update {update.Type}"); break;
}; }
;
} }
// Handle bot messages // Handle bot messages
async Task OnMessage(Message message, UpdateType type) async Task OnMessage(Message message, UpdateType type)
{ {
if (message.Text == "/start" && message.Chat.Type == ChatType.Private) if (message.Text == "/start" && message.Chat.Type == ChatType.Private)
{ await HandleStartMessage.HandleStartMessageAsync(message, bot, db, logger);
await HandleStartMessage.HandleStartMessageAsync(message,bot, db, logger);
}
else if (message.Text == "/backup") else if (message.Text == "/backup")
{ await HandleDbBackup.HandleDbBackupAsync(bot, logger, config.DB_NAME, config.ADMIN_NUMID, db);
await HandleDbBackup.HandleDbBackupAsync(bot, logger, config.DB_NAME, config.ADMIN_NUMID, backupDb, db);
}
// Send a message to prompt user to send /start and recieve their cat photo only if its from a telegram user and not a channel // Send a message to prompt user to send /start and recieve their cat photo only if its from a telegram user and not a channel
else if (message.Chat.Type == ChatType.Private) else if (message.Chat.Type == ChatType.Private)
{
await HandleStartMessage.HandleStartMessageAsync(message, bot, db, logger); await HandleStartMessage.HandleStartMessageAsync(message, bot, db, logger);
} }
}
// Set a timer to fetch and process posts every 15 minutes // Set a timer to fetch and process posts every 15 minutes
_postFetchTimer = new Timer(async _ => await RunCheck.runAsync(db, bot, config.TAG, logger, config.INSTANCE), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); _postFetchTimer = new Timer(async _ => await RunCheck.runAsync(db, bot, config.TAG, logger, config.INSTANCE),
// Another timer to automatically backup the DB every 1 hour null, TimeSpan.Zero, TimeSpan.FromMinutes(10));
_backupTimer = new Timer(async _ => await HandleDbBackup.HandleDbBackupAsync(bot, logger, config.DB_NAME, config.ADMIN_NUMID, backupDb,db), null, TimeSpan.Zero, TimeSpan.FromHours(6)); // Another timer to automatically backup the DB every 6 hour
_backupTimer =
new Timer(
async _ => await HandleDbBackup.HandleDbBackupAsync(bot, logger, config.DB_NAME, config.ADMIN_NUMID, db), null, TimeSpan.Zero, TimeSpan.FromHours(6));
// Keep the bot running // Keep the bot running
await Task.Delay(-1); await Task.Delay(-1);
} }
} }

View File

@@ -1,13 +1,16 @@
# Disclaimer: # Disclaimer:
This project involves scraping public data from Mastodon timelines. While this data is generally publicly accessible, it's important to note that scraping and archiving such data may have implications, especially if it involves sensitive information or terms of service violations. This project involves scraping public data from Mastodon timelines. While this data is generally publicly accessible,
it's important to note that scraping and archiving such data may have implications, especially if it involves sensitive
information or terms of service violations.
I am not responsible for any misuse or unauthorized use of the scraped data. It is the user's responsibility to ensure compliance with all applicable laws, regulations, and terms of service when storing, archiving, or using the data. I am not responsible for any misuse or unauthorized use of the scraped data. It is the user's responsibility to ensure
compliance with all applicable laws, regulations, and terms of service when storing, archiving, or using the data.
## Required Environment Variables ## Required Environment Variables
| Variable Name | Description | Default Value | Format | | Variable Name | Description | Default Value | Format |
|---|---|---|---| |---------------|---------------------------------------|---------------|-----------------------------------------|
| DB_NAME | Database file name | (Required) | Must not have any extension, plain text | | DB_NAME | Database file name | (Required) | Must not have any extension, plain text |
| BOT_TOKEN | Telegram bot token | (Required) | Standard Telegram bot token format | | BOT_TOKEN | Telegram bot token | (Required) | Standard Telegram bot token format |
| TAG | Mastodon timeline tag | (Required) | Text with no spaces | | TAG | Mastodon timeline tag | (Required) | Text with no spaces |
@@ -17,7 +20,7 @@ I am not responsible for any misuse or unauthorized use of the scraped data. It
## Optional Environment Variables ## Optional Environment Variables
| Variable Name | Description | Default Value | Format | | Variable Name | Description | Default Value | Format |
|---|---|---|---| |-----------------|------------------------------|--------------------------------------------|------------|
| CUSTOM_INSTANCE | Custom Mastodon instance URL | [https://haminoa.net](https://haminoa.net) | URL format | | CUSTOM_INSTANCE | Custom Mastodon instance URL | [https://haminoa.net](https://haminoa.net) | URL format |
**Note:** All environment variables except `CUSTOM_INSTANCE` are required for the project to function. **Note:** All environment variables except `CUSTOM_INSTANCE` are required for the project to function.
@@ -26,14 +29,17 @@ I am not responsible for any misuse or unauthorized use of the scraped data. It
### Published Executable ### Published Executable
1. Download the published executable for `Linux-x86` from the [pipeline artifacts](https://gitlab.com/api/v4/projects/61685511/jobs/artifacts/main/raw/publish.tar.gz?job=build) 1. Download the published executable for `Linux-x86` from
the [pipeline artifacts](https://gitlab.com/api/v4/projects/61685511/jobs/artifacts/main/raw/publish.tar.gz?job=build)
2. Navigate to the directory containing the downloaded archive in your terminal and extract the archive. 2. Navigate to the directory containing the downloaded archive in your terminal and extract the archive.
**Providing Environment Variables:** **Providing Environment Variables:**
**Using a `.env` file:** **Using a `.env` file:**
1. Create a file named .env in the root directory of your project. 1. Create a file named .env in the root directory of your project.
2. Add each environment variable on a separate line in the format KEY=VALUE. For example: 2. Add each environment variable on a separate line in the format KEY=VALUE. For example:
``` ```
DB_NAME=my_data DB_NAME=my_data
BOT_TOKEN=your_telegram_bot_token BOT_TOKEN=your_telegram_bot_token
@@ -41,10 +47,14 @@ TAG=mastodontimelinetag
CHANNEL_NUMID=1234567890 CHANNEL_NUMID=1234567890
ADMIN_NUMID=9876543210 ADMIN_NUMID=9876543210
``` ```
1. Run the following command: 1. Run the following command:
```bash ```bash
dotnet run dotnet run
or or
./mstdnCats ./mstdnCats
``` ```
**Remember to replace `your_telegram_bot_token`, `my_data.json`, `important_data`, `1234567890`, and `9876543210` with your actual values.**
**Remember to replace `your_telegram_bot_token`, `my_data.json`, `important_data`, `1234567890`, and `9876543210` with
your actual values.**

View File

@@ -1,37 +1,14 @@
using JsonFlatFileDataStore;
using MongoDB.Driver; using MongoDB.Driver;
using mstdnCats.Models; using mstdnCats.Models;
namespace mstdnCats.Services namespace mstdnCats.Services;
public class DbInitializer
{ {
public class DbInitializer
{
public static Task<IDocumentCollection<Post>> SetupJsonDb(string _dbname)
{
// Setup DB
IDocumentCollection<Post>? collection = null;
try
{
// Initialize Backup DB
var store = new DataStore($"./data/{_dbname + "_BK"}.json", minifyJson: true);
collection = store.GetCollection<Post>();
}
catch
{
return Task.FromResult<IDocumentCollection<Post>>(null);
}
// Return collection
return Task.FromResult(collection);
}
public static Task<IMongoCollection<Post>> SetupDb(string mongoDbConnectionString, string dbName) public static Task<IMongoCollection<Post>> SetupDb(string mongoDbConnectionString, string dbName)
{ {
if (mongoDbConnectionString == null) if (mongoDbConnectionString == null) throw new Exception("MongoDb connection string is null");
{
throw new Exception("MongoDb connection string is null");
}
try try
{ {
@@ -44,5 +21,4 @@ namespace mstdnCats.Services
throw new Exception("Error while connecting to MongoDB: " + ex.Message); throw new Exception("Error while connecting to MongoDB: " + ex.Message);
} }
} }
}
} }

View File

@@ -1,5 +1,6 @@
using JsonFlatFileDataStore; using System.Text;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using mstdnCats.Models; using mstdnCats.Models;
using Telegram.Bot; using Telegram.Bot;
@@ -10,25 +11,20 @@ namespace mstdnCats.Services;
public class HandleDbBackup public class HandleDbBackup
{ {
public static async Task HandleDbBackupAsync(TelegramBotClient _bot, ILogger<MastodonBot>? logger, string dbname, string adminId,IDocumentCollection<Post> _bkDb,IMongoCollection<Post> _db) public static async Task HandleDbBackupAsync(TelegramBotClient _bot, ILogger<MastodonBot>? logger, string dbname,
string adminId, IMongoCollection<Post> _db)
{ {
logger?.LogInformation("Backup requested"); logger?.LogInformation("Backup requested");
// Retrieve all posts from DB (Exclude _id field from mongoDB since it is not needed nor implemented in Post model) var json = _db.Find(new BsonDocument()).ToList().ToJson();
var posts = _db.AsQueryable().ToList();
// Retrieve all existing posts in backup DB
var existingPosts = _bkDb.AsQueryable().ToList();
// Insert new posts that are not in backup DB (First saves all the new ones in a list and then inserts them all at once)
var newPosts = posts.Where(x => !existingPosts.Any(y => y.mstdnPostId == x.mstdnPostId)).ToList();
await _bkDb.InsertManyAsync(newPosts);
var bytes = Encoding.UTF8.GetBytes(json);
var stream = new MemoryStream(bytes);
await using Stream stream = System.IO.File.OpenRead("./data/" + dbname+"_BK.json"); await _bot.SendDocument(adminId, InputFile.FromStream(stream, "backup.json"),
var message = await _bot.SendDocument(adminId, document: InputFile.FromStream(stream, dbname+"_BK.json"), "Backup of your collection\nCreated at " +
caption: "Backup of " + dbname + "\nCreated at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss" + "\nCurrent post count: " + _db.AsQueryable().Count()), parseMode: ParseMode.Html); DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss" + "\nCurrent post count: " + _db.CountDocuments(new BsonDocument())),
ParseMode.Html);
logger?.LogInformation("Backup sent"); logger?.LogInformation("Backup sent");
} }
} }

View File

@@ -1,6 +1,4 @@
using CatsOfMastodonBot.Models; using CatsOfMastodonBot.Models;
using JsonFlatFileDataStore;
using Microsoft.AspNetCore.Mvc.Diagnostics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MongoDB.Driver; using MongoDB.Driver;
using mstdnCats.Models; using mstdnCats.Models;
@@ -9,11 +7,12 @@ using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups; using Telegram.Bot.Types.ReplyMarkups;
namespace mstdnCats.Services namespace mstdnCats.Services;
public class HandlePostAction
{ {
public class HandlePostAction public static async Task HandleCallbackQuery(CallbackQuery callbackQuery, IMongoCollection<Post> _db,
{ TelegramBotClient _bot, ILogger<MastodonBot>? logger)
public static async Task HandleCallbackQuery(CallbackQuery callbackQuery, IMongoCollection<Post> _db, TelegramBotClient _bot, ILogger<MastodonBot>? logger)
{ {
var config = ConfigData.fetchData(); var config = ConfigData.fetchData();
@@ -25,8 +24,8 @@ namespace mstdnCats.Services
return; return;
} }
string action = parts[0]; var action = parts[0];
string mediaId = parts[1]; var mediaId = parts[1];
var filter = Builders<Post>.Filter.Eq("MediaAttachments.MediaId", mediaId); var filter = Builders<Post>.Filter.Eq("MediaAttachments.MediaId", mediaId);
var post = await _db.Find(filter).FirstOrDefaultAsync(); var post = await _db.Find(filter).FirstOrDefaultAsync();
@@ -47,6 +46,7 @@ namespace mstdnCats.Services
if (mediaAttachment.Approved) if (mediaAttachment.Approved)
{ {
await _bot.AnswerCallbackQuery(callbackQuery.Id, "Media attachment is already approved.", true); await _bot.AnswerCallbackQuery(callbackQuery.Id, "Media attachment is already approved.", true);
await _bot.DeleteMessage(callbackQuery.Message.Chat.Id, callbackQuery.Message.Id);
return; return;
} }
@@ -55,15 +55,14 @@ namespace mstdnCats.Services
var result = await _db.UpdateOneAsync(filter, update); var result = await _db.UpdateOneAsync(filter, update);
if (result.ModifiedCount > 0) if (result.ModifiedCount > 0)
{
try try
{ {
// Send the media attachment to the channel // Send the media attachment to the channel
var allMediaAttachments = post.MediaAttachments.ToList(); var allMediaAttachments = post.MediaAttachments.ToList();
await _bot.SendPhoto(config.CHANNEL_NUMID, await _bot.SendPhoto(config.CHANNEL_NUMID,
allMediaAttachments.First(m => m.MediaId == mediaId).Url, allMediaAttachments.First(m => m.MediaId == mediaId).Url,
caption: $"Post from " + $"<a href=\"" + post.Account.Url + "\">" + $"Post from " + $"<a href=\"" + post.Account.Url + "\">" +
post.Account.DisplayName + " </a>", parseMode: ParseMode.Html post.Account.DisplayName + " </a>", ParseMode.Html
, replyMarkup: new InlineKeyboardMarkup(InlineKeyboardButton.WithUrl("View on Mastodon", post.Url))); , replyMarkup: new InlineKeyboardMarkup(InlineKeyboardButton.WithUrl("View on Mastodon", post.Url)));
await _bot.AnswerCallbackQuery(callbackQuery.Id, await _bot.AnswerCallbackQuery(callbackQuery.Id,
@@ -76,11 +75,9 @@ namespace mstdnCats.Services
{ {
logger?.LogError($"Error while sending image to the channel:{e}"); logger?.LogError($"Error while sending image to the channel:{e}");
} }
}
else else
{ logger?.LogError(
logger?.LogError($"Failed to update the media attachment {mediaId}. Record might not exist or was not found."); $"Failed to update the media attachment {mediaId}. Record might not exist or was not found.");
}
} }
else else
{ {
@@ -94,14 +91,16 @@ namespace mstdnCats.Services
// Check if the post has only one attachment, if so, do not delete it, else delete the associated attachment // Check if the post has only one attachment, if so, do not delete it, else delete the associated attachment
if (post.MediaAttachments.Count == 1 && post.MediaAttachments.First().MediaId == mediaId) if (post.MediaAttachments.Count == 1 && post.MediaAttachments.First().MediaId == mediaId)
{ {
await _bot.AnswerCallbackQuery(callbackQuery.Id, "Post has only one attachment. No deletion performed."); await _bot.AnswerCallbackQuery(callbackQuery.Id,
"Post has only one attachment. No deletion performed.");
await _bot.DeleteMessage(callbackQuery.Message.Chat.Id, callbackQuery.Message.Id); await _bot.DeleteMessage(callbackQuery.Message.Chat.Id, callbackQuery.Message.Id);
logger?.LogTrace($"Post {post.mstdnPostId} has only one attachment. No deletion performed."); logger?.LogTrace($"Post {post.mstdnPostId} has only one attachment. No deletion performed.");
} }
else else
{ {
var update = Builders<Post>.Update.PullFilter("MediaAttachments", Builders<MediaAttachment>.Filter.Eq("MediaId", mediaId)); var update = Builders<Post>.Update.PullFilter("MediaAttachments",
Builders<MediaAttachment>.Filter.Eq("MediaId", mediaId));
var result = await _db.UpdateOneAsync(filter, update); var result = await _db.UpdateOneAsync(filter, update);
if (result.ModifiedCount > 0) if (result.ModifiedCount > 0)
@@ -122,5 +121,4 @@ namespace mstdnCats.Services
logger?.LogError("Invalid action specified."); logger?.LogError("Invalid action specified.");
} }
} }
}
} }

View File

@@ -1,42 +1,41 @@
using JsonFlatFileDataStore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MongoDB.Driver; using MongoDB.Driver;
using MongoDB.Driver.Linq;
using mstdnCats.Models; using mstdnCats.Models;
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups; using Telegram.Bot.Types.ReplyMarkups;
namespace CatsOfMastodonBot.Services namespace CatsOfMastodonBot.Services;
public class HandleStartMessage
{ {
public class HandleStartMessage public static async Task HandleStartMessageAsync(Message message, TelegramBotClient _bot,
IMongoCollection<Post> _db, ILogger<MastodonBot>? logger, CallbackQuery callbackQuery = null)
{ {
public static async Task HandleStartMessageAsync(Message message, TelegramBotClient _bot, IMongoCollection<Post> _db, ILogger<MastodonBot>? logger, CallbackQuery callbackQuery = null) logger?.LogInformation("Start message received, trigger source: " +
{ (callbackQuery != null ? "Callback" : "Start command"));
logger?.LogInformation("Start message received, trigger source: " + (callbackQuery != null ? "Callback" : "Start command"));
// choose all media attachments that are approved // choose all media attachments that are approved
var mediaAttachmentsToSelect =await _db.AsQueryable() var mediaAttachmentsToSelect = _db.AsQueryable()
.Where(post => post.MediaAttachments.Any(media => media.Approved)) .Where(post => post.MediaAttachments.Any(media => media.Approved))
.ToListAsync(); .ToList();
// select random approved media attachment // select random approved media attachment
var selectedMediaAttachment = mediaAttachmentsToSelect[new Random().Next(mediaAttachmentsToSelect.Count)]; var selectedMediaAttachment = mediaAttachmentsToSelect[new Random().Next(mediaAttachmentsToSelect.Count)];
// send media attachment // send media attachment
await _bot.SendPhoto(message.Chat.Id, selectedMediaAttachment.MediaAttachments.FirstOrDefault(m => m.Approved == true).Url, await _bot.SendPhoto(message.Chat.Id,
caption: $"Here is your cat!🐈\n" + "<a href=\"" + selectedMediaAttachment.Url + "\">" + $"View on Mastodon " + " </a>", parseMode: ParseMode.Html selectedMediaAttachment.MediaAttachments.FirstOrDefault(m => m.Approved == true).Url,
, replyMarkup: new InlineKeyboardMarkup().AddButton(InlineKeyboardButton.WithUrl("Join channel 😺", "https://t.me/catsofmastodon")) $"Here is your cat!🐈\n" + "<a href=\"" + selectedMediaAttachment.Url + "\">" +
$"View on Mastodon " + " </a>", ParseMode.Html
, replyMarkup: new InlineKeyboardMarkup()
.AddButton(InlineKeyboardButton.WithUrl("Join channel 😺", "https://t.me/catsofmastodon"))
.AddNewRow() .AddNewRow()
.AddButton(InlineKeyboardButton.WithCallbackData("Send me another one!", $"new_random"))); .AddButton(InlineKeyboardButton.WithCallbackData("Send me another one!", $"new_random")));
// answer callback query from "send me another cat" button // answer callback query from "send me another cat" button
if (callbackQuery != null) if (callbackQuery != null) await _bot.AnswerCallbackQuery(callbackQuery.Id, "Catch your cat! 😺");
{
await _bot.AnswerCallbackQuery(callbackQuery.Id, "Catch your cat! 😺");
}
logger?.LogInformation("Random cat sent!"); logger?.LogInformation("Random cat sent!");
}
} }
} }

View File

@@ -2,16 +2,16 @@ using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using mstdnCats.Models; using mstdnCats.Models;
namespace mstdnCats.Services namespace mstdnCats.Services;
public sealed class PostResolver
{ {
public sealed class PostResolver
{
public static async Task<List<Post>?> GetPostsAsync(string tag, ILogger<MastodonBot>? logger, string instance) public static async Task<List<Post>?> GetPostsAsync(string tag, ILogger<MastodonBot>? logger, string instance)
{ {
// Get posts // Get posts
HttpClient _httpClient = new HttpClient(); var _httpClient = new HttpClient();
// Get posts from mastodon api (40 latest posts) // Get posts from mastodon api (40 latest posts)
string requestUrl = $"{instance}/api/v1/timelines/tag/{tag}?limit=40"; var requestUrl = $"{instance}/api/v1/timelines/tag/{tag}?limit=40";
var response = await _httpClient.GetAsync(requestUrl); var response = await _httpClient.GetAsync(requestUrl);
// Print out ratelimit info // Print out ratelimit info
@@ -23,8 +23,8 @@ namespace mstdnCats.Services
if ( if (
response.StatusCode == System.Net.HttpStatusCode.OK || response.StatusCode == System.Net.HttpStatusCode.OK ||
response.Content.Headers.ContentType.MediaType.Contains("application/json") || response.Content.Headers.ContentType.MediaType.Contains("application/json") ||
response.Headers.TryGetValues("X-RateLimit-Remaining", out var remaining) && (response.Headers.TryGetValues("X-RateLimit-Remaining", out var remaining) &&
int.Parse(remaining.First()) != 0 int.Parse(remaining.First()) != 0)
) )
{ {
// Deserialize response based on 'Post' model // Deserialize response based on 'Post' model
@@ -37,5 +37,4 @@ namespace mstdnCats.Services
return null; return null;
} }
} }
}
} }

View File

@@ -1,5 +1,4 @@
using CatsOfMastodonBot.Models; using CatsOfMastodonBot.Models;
using JsonFlatFileDataStore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MongoDB.Driver; using MongoDB.Driver;
using mstdnCats.Models; using mstdnCats.Models;
@@ -7,55 +6,53 @@ using Telegram.Bot;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups; using Telegram.Bot.Types.ReplyMarkups;
namespace mstdnCats.Services namespace mstdnCats.Services;
public class ProcessPosts
{ {
public class ProcessPosts public static async Task<List<MediaAttachment>> checkAndInsertPostsAsync(IMongoCollection<Post> _db,
{ TelegramBotClient _bot, List<Post> fetchedPosts, ILogger<MastodonBot>? logger)
public static async Task<List<MediaAttachment>> checkAndInsertPostsAsync(IMongoCollection<Post> _db, TelegramBotClient _bot, List<Post> fetchedPosts, ILogger<MastodonBot>? logger)
{ {
var config = ConfigData.fetchData(); var config = ConfigData.fetchData();
// Get existing posts // Get existing posts
var existingPosts = _db.AsQueryable().Select(x => x.mstdnPostId).ToArray(); var existingPosts = _db.AsQueryable().Select(x => x.mstdnPostId).ToArray();
logger?.LogInformation($"Recieved posts to proccess: {fetchedPosts.Count} - total existing posts: {existingPosts.Length}"); logger?.LogInformation(
int newPosts = 0; $"Recieved posts to proccess: {fetchedPosts.Count} - total existing posts: {existingPosts.Length}");
var newPosts = 0;
// Process posts // Process posts
foreach (Post post in fetchedPosts) List<Post> validPosts = new();
{ foreach (var post in fetchedPosts)
// Check if post already exists // Check if post already exists
if (!existingPosts.Contains(post.mstdnPostId) && post.MediaAttachments.Count > 0 && post.Account.IsBot == false) if (!existingPosts.Contains(post.mstdnPostId) && post.MediaAttachments.Count > 0 &&
post.Account.IsBot == false)
{ {
// Send approve or reject message to admin // Send approve or reject message to admin
foreach (var media in post.MediaAttachments) foreach (var media in post.MediaAttachments)
{
if (media.Type == "image") if (media.Type == "image")
{
try try
{ {
await _bot.SendPhoto(config.ADMIN_NUMID, media.PreviewUrl, caption: $"<a href=\"" + post.Url + "\"> Mastodon </a>", parseMode: ParseMode.Html await _bot.SendPhoto(config.ADMIN_NUMID, media.PreviewUrl,
$"<a href=\"" + post.Url + "\"> Mastodon </a>", ParseMode.Html
, replyMarkup: new InlineKeyboardMarkup().AddButton("Approve", $"approve-{media.MediaId}").AddButton("Reject", $"reject-{media.MediaId}")); , replyMarkup: new InlineKeyboardMarkup().AddButton("Approve", $"approve-{media.MediaId}").AddButton("Reject", $"reject-{media.MediaId}"));
validPosts.Add(post);
} }
catch (System.Exception ex) catch (Exception ex)
{ {
logger?.LogError("Error while sending message to admin: " + ex.Message + " - Media URL: " + media.PreviewUrl); logger?.LogError("Error while sending message to admin: " + ex.Message + " - Media URL: " +
media.PreviewUrl);
} }
}
}
// Insert post
await _db.InsertOneAsync(post);
newPosts++; newPosts++;
}
} }
logger?.LogInformation($"Proccesing done, stats: received {fetchedPosts.Count} posts, inserted and sent {newPosts} new posts."); // Insert post
await _db.InsertManyAsync(validPosts);
logger?.LogInformation(
$"Proccesing done, stats: received {fetchedPosts.Count} posts, inserted and sent {newPosts} new posts.");
// Return list of media attachments // Return list of media attachments
var alldbpostsattachmentlist = _db.AsQueryable().SelectMany(x => x.MediaAttachments).ToList(); var alldbpostsattachmentlist = _db.AsQueryable().SelectMany(x => x.MediaAttachments).ToList();
return alldbpostsattachmentlist; return alldbpostsattachmentlist;
} }
}
} }

View File

@@ -1,14 +1,14 @@
using JsonFlatFileDataStore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MongoDB.Driver; using MongoDB.Driver;
using mstdnCats.Models; using mstdnCats.Models;
using Telegram.Bot; using Telegram.Bot;
namespace mstdnCats.Services namespace mstdnCats.Services;
public class RunCheck
{ {
public class RunCheck public static async Task<bool> runAsync(IMongoCollection<Post> _db, TelegramBotClient _bot, string _tag,
{ ILogger<MastodonBot>? logger, string _instance)
public static async Task<bool> runAsync(IMongoCollection<Post> _db, TelegramBotClient _bot, string _tag, ILogger<MastodonBot>? logger, string _instance)
{ {
// Run check // Run check
try try
@@ -16,10 +16,7 @@ namespace mstdnCats.Services
// First get posts // First get posts
var posts = await PostResolver.GetPostsAsync(_tag, logger, _instance); var posts = await PostResolver.GetPostsAsync(_tag, logger, _instance);
if (posts == null) if (posts == null) logger?.LogCritical("Unable to get posts");
{
logger?.LogCritical("Unable to get posts");
}
// Then process them // Then process them
await ProcessPosts.checkAndInsertPostsAsync(_db, _bot, posts, logger); await ProcessPosts.checkAndInsertPostsAsync(_db, _bot, posts, logger);
@@ -28,7 +25,7 @@ namespace mstdnCats.Services
{ {
logger?.LogCritical("Error while running check: " + ex.Message); logger?.LogCritical("Error while running check: " + ex.Message);
} }
return true; return true;
} }
}
} }

View File

@@ -11,7 +11,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNetEnv" Version="3.1.1" /> <PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="JsonFlatFileDataStore" Version="2.4.2" />
<PackageReference Include="MongoDB.Driver" Version="3.1.0" /> <PackageReference Include="MongoDB.Driver" Version="3.1.0" />
<PackageReference Include="Telegram.Bot" Version="22.2.0" /> <PackageReference Include="Telegram.Bot" Version="22.2.0" />
</ItemGroup> </ItemGroup>