New feature: Web api and a custom webpage to show a random cat.
This commit is contained in:
@@ -29,7 +29,7 @@ public class Account
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class MediaAttachment
|
public class MediaAttachment
|
||||||
{
|
{
|
||||||
[JsonPropertyName("id")] public required string MediaId { get; set; }
|
[JsonPropertyName("id")] public required string MediaId { get; set; }
|
||||||
[JsonPropertyName("type")] public required string Type { get; set; }
|
[JsonPropertyName("type")] public required string Type { get; set; }
|
||||||
[JsonPropertyName("url")] public required string Url { get; set; }
|
[JsonPropertyName("url")] public required string Url { get; set; }
|
||||||
|
31
Program.cs
31
Program.cs
@@ -1,5 +1,7 @@
|
|||||||
using CatsOfMastodonBot.Models;
|
using CatsOfMastodonBot.Models;
|
||||||
using CatsOfMastodonBot.Services;
|
using CatsOfMastodonBot.Services;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using mstdnCats.Services;
|
using mstdnCats.Services;
|
||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
@@ -30,6 +32,21 @@ public class MastodonBot
|
|||||||
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");
|
||||||
|
|
||||||
|
// Web server setup
|
||||||
|
var host = Host.CreateDefaultBuilder()
|
||||||
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder.UseKestrel(options =>
|
||||||
|
{
|
||||||
|
options.ListenAnyIP(5005); // Listen on port 5005
|
||||||
|
});
|
||||||
|
ServerStartup.Serverstartup(db);
|
||||||
|
webBuilder.UseStartup<ServerStartup>();
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await host.RunAsync();
|
||||||
|
|
||||||
// Setup bot
|
// Setup bot
|
||||||
var bot = new TelegramBotClient(config.BOT_TOKEN);
|
var bot = new TelegramBotClient(config.BOT_TOKEN);
|
||||||
|
|
||||||
@@ -41,13 +58,14 @@ public class MastodonBot
|
|||||||
logger.LogInformation("Setup complete");
|
logger.LogInformation("Setup complete");
|
||||||
logger.LogInformation($"Bot is running as {me.FirstName} - instance: {config.INSTANCE}");
|
logger.LogInformation($"Bot is running as {me.FirstName} - instance: {config.INSTANCE}");
|
||||||
|
|
||||||
// Handle bot updates
|
// Handle bot updates - For glass buttons functionality
|
||||||
async Task OnUpdate(Update update)
|
async Task OnUpdate(Update update)
|
||||||
{
|
{
|
||||||
switch (update)
|
switch (update)
|
||||||
{
|
{
|
||||||
case { CallbackQuery: { } callbackQuery }:
|
case { CallbackQuery: { } callbackQuery }:
|
||||||
{
|
{
|
||||||
|
// Send a new cat picture
|
||||||
if (callbackQuery.Data == "new_random")
|
if (callbackQuery.Data == "new_random")
|
||||||
{
|
{
|
||||||
await HandleStartMessage.HandleStartMessageAsync(callbackQuery.Message, bot, db, logger,
|
await HandleStartMessage.HandleStartMessageAsync(callbackQuery.Message, bot, db, logger,
|
||||||
@@ -55,11 +73,14 @@ public class MastodonBot
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
// Approve or reject a post
|
||||||
|
else if (callbackQuery.Data.Contains("approve-") || callbackQuery.Data.Contains("reject-"))
|
||||||
{
|
{
|
||||||
await HandlePostAction.HandleCallbackQuery(callbackQuery, db, bot, logger);
|
await HandlePostAction.HandleCallbackQuery(callbackQuery, db, bot, logger);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
default: logger.LogInformation($"Received unhandled update {update.Type}"); break;
|
default: logger.LogInformation($"Received unhandled update {update.Type}"); break;
|
||||||
}
|
}
|
||||||
@@ -72,14 +93,16 @@ public class MastodonBot
|
|||||||
{
|
{
|
||||||
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" && message.Chat.Type == ChatType.Private)
|
||||||
await HandleDbBackup.HandleDbBackupAsync(bot, logger, config.DB_NAME, config.ADMIN_NUMID, db);
|
await HandleDbBackup.HandleDbBackupAsync(bot, logger, config.DB_NAME, config.ADMIN_NUMID, 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 10 minutes
|
||||||
_postFetchTimer = new Timer(async _ => await RunCheck.runAsync(db, bot, config.TAG, logger, config.INSTANCE),
|
_postFetchTimer = new Timer(async _ => await RunCheck.runAsync(db, bot, config.TAG, logger, config.INSTANCE),
|
||||||
null, TimeSpan.Zero, TimeSpan.FromMinutes(10));
|
null, TimeSpan.Zero, TimeSpan.FromMinutes(10));
|
||||||
// Another timer to automatically backup the DB every 6 hour
|
// Another timer to automatically backup the DB every 6 hour
|
||||||
|
63
Web/ServerStartup.cs
Executable file
63
Web/ServerStartup.cs
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using MongoDB.Driver.Linq;
|
||||||
|
using mstdnCats.Models;
|
||||||
|
|
||||||
|
namespace mstdnCats.Services;
|
||||||
|
|
||||||
|
public class ServerStartup
|
||||||
|
{
|
||||||
|
private static IMongoCollection<Post> _db;
|
||||||
|
public static void Serverstartup(IMongoCollection<Post> db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapGet("/", async context =>
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetEntryAssembly();
|
||||||
|
var resourceName = "mstdnCats.Web.wwwroot.index.html"; // Full resource name
|
||||||
|
|
||||||
|
using (var stream = assembly.GetManifestResourceStream(resourceName))
|
||||||
|
{
|
||||||
|
if (stream == null)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 500;
|
||||||
|
await context.Response.WriteAsync("Something went wrong in our side.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "text/html";
|
||||||
|
await context.Response.WriteAsync(reader.ReadToEnd());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
endpoints.MapGet("/api/gimme", async context =>
|
||||||
|
{
|
||||||
|
// Api endpoint
|
||||||
|
// Choose all media attachments that are approved
|
||||||
|
var mediaAttachmentsToSelect = await _db.AsQueryable()
|
||||||
|
.Where(post => post.MediaAttachments.Any(media => media.Approved))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Select random approved media attachment
|
||||||
|
var selectedPost = mediaAttachmentsToSelect[new Random().Next(mediaAttachmentsToSelect.Count)];
|
||||||
|
|
||||||
|
// Send as JSON
|
||||||
|
await context.Response.WriteAsJsonAsync(selectedPost);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
293
Web/wwwroot/index.html
Executable file
293
Web/wwwroot/index.html
Executable file
@@ -0,0 +1,293 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Cats of Mastodon!</title><style>
|
||||||
|
body {
|
||||||
|
font-family: 'Comic Neue', cursive;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastodon-title {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message span {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-container {
|
||||||
|
background-color: #282828;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 20px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-container:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title-inner {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
color: #ffb347;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-text {
|
||||||
|
color: #bbdefb;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
object-fit: cover;
|
||||||
|
max-height: 500px;
|
||||||
|
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image.loading {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-loading-spinner {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #6364ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image.loading + .image-loading-spinner {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastodon-button,
|
||||||
|
.neutral-button {
|
||||||
|
background-color: #404040;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background-color 0.3s ease, transform 0.2s ease-in-out;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastodon-button {
|
||||||
|
background-color: #6364ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastodon-button:hover {
|
||||||
|
background-color: #5253e0;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.neutral-button:hover {
|
||||||
|
background-color: #505050;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: none;
|
||||||
|
border: 6px solid #f3f3f3;
|
||||||
|
border-top: 6px solid #6364ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-container.loading .loading-spinner {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-container.loading .post-image,
|
||||||
|
.post-container.loading .button-container,
|
||||||
|
.post-container.loading .post-title-inner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #909090;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2 class="mastodon-title">Cats of Mastodon!</h2>
|
||||||
|
<p class="welcome-message">Welcome to Daily Catventures! Get your daily dose of purr-fectly adorable feline fun! <span>✨</span><br>Posts gathered across Mastodon 🤝</p>
|
||||||
|
|
||||||
|
<div class="post-container">
|
||||||
|
<div class="post-content" style="display: none;">
|
||||||
|
<div class="post-title-inner">
|
||||||
|
<span class="user-name"></span><span class="cat-text">'s cat!</span>
|
||||||
|
</div>
|
||||||
|
<div class="image-container">
|
||||||
|
<img class="post-image" src="" alt="Cat Photo">
|
||||||
|
<div class="image-loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
<div class="button-container">
|
||||||
|
<a class="mastodon-button" href="" target="_blank">View on Mastodon</a>
|
||||||
|
<button class="neutral-button" onclick="loadNewPost()">Show me another cat!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<span>© 2024 Mahdium</span>
|
||||||
|
<a href="https://mahdium.ir" class="footer-button">mahdium.ir</a>
|
||||||
|
<a href="https://gitlab.com/mahdium/cats-of-mastodon-telegram-bot" class="footer-button">Source Code</a>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const postContainer = document.querySelector('.post-container');
|
||||||
|
const postContent = document.querySelector('.post-content');
|
||||||
|
const userNameSpan = document.querySelector('.post-content .user-name');
|
||||||
|
const postImage = document.querySelector('.post-content .post-image');
|
||||||
|
const mastodonLink = document.querySelector('.post-content .mastodon-button');
|
||||||
|
const imageLoadingSpinner = document.querySelector('.image-container .image-loading-spinner');
|
||||||
|
|
||||||
|
function loadNewPost() {
|
||||||
|
postContainer.classList.add('loading');
|
||||||
|
postContent.style.display = 'none';
|
||||||
|
postImage.classList.add('loading');
|
||||||
|
|
||||||
|
fetch('/api/gimme')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
postContainer.classList.remove('loading');
|
||||||
|
postContent.style.display = 'block';
|
||||||
|
|
||||||
|
if (!data || !data.account || !data.media_attachments || data.media_attachments.length === 0) {
|
||||||
|
console.error("Invalid API response format:", data);
|
||||||
|
alert("Invalid data received from the server.");
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userNameSpan.textContent = data.account.display_name;
|
||||||
|
|
||||||
|
let imageUrl = data.media_attachments[0].remote_url;
|
||||||
|
if (imageUrl) {
|
||||||
|
imageUrl = imageUrl.replace('/original/', '/small/');
|
||||||
|
} else if (data.media_attachments[0].PreviewUrl) {
|
||||||
|
imageUrl = data.media_attachments[0].PreviewUrl;
|
||||||
|
} else {
|
||||||
|
console.warn("No RemoteUrl or PreviewUrl found, using placeholder");
|
||||||
|
postImage.src = "placeholder-no-image.png";
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postImage.onload = () => {
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
};
|
||||||
|
|
||||||
|
postImage.onerror = () => {
|
||||||
|
console.error("Error loading image:", imageUrl);
|
||||||
|
postImage.src = "placeholder-error.png";
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
};
|
||||||
|
|
||||||
|
postImage.src = imageUrl;
|
||||||
|
mastodonLink.href = data.Url;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
postContainer.classList.remove('loading');
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
alert("Failed to load a new post. Please try again later.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNewPost();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -15,4 +15,11 @@
|
|||||||
<PackageReference Include="Telegram.Bot" Version="22.2.0" />
|
<PackageReference Include="Telegram.Bot" Version="22.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Web\wwwroot\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Web\wwwroot\index.html" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
Reference in New Issue
Block a user