Merge branch 'feature/storage' into dev

This commit is contained in:
2020-03-17 19:37:58 +04:00
16 changed files with 553 additions and 41 deletions
+1
View File
@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AleXr64.BotFramework" Version="0.0.17" /> <PackageReference Include="AleXr64.BotFramework" Version="0.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="3.1.2" /> <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="3.1.2" />
+25
View File
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Kruzya.TelegramBot.Core.Data
{
public class BotUser
{
/// <summary>
/// The unique bot user identifier.
/// </summary>
[Key]
public Guid BotUserId { get; set; }
/// <summary>
/// The chat identifier where this user is related.
/// </summary>
public long ChatId { get; set; }
/// <summary>
/// The Telegram User identifier.
/// </summary>
public int UserId { get; set; }
}
}
+42
View File
@@ -0,0 +1,42 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace Kruzya.TelegramBot.Core.Data
{
public class BotUserValue
{
[Key]
public Guid BotUserValueId { get; set; }
public BotUser BotUser { get; set; }
public string Name { get; set; }
public byte[] Value
{
get
{
var fmt = new BinaryFormatter();
using var memStream = new MemoryStream();
fmt.Serialize(memStream, data);
return memStream.ToArray();
}
set
{
var fmt = new BinaryFormatter();
using var memStream = new MemoryStream();
memStream.Write(value);
memStream.Position = 0;
data = fmt.Deserialize(memStream);
}
}
[NonSerialized] public object data;
public T GetValue<T>() => (T) data;
public void SetValue<T>(T value) => data = value;
}
}
+32
View File
@@ -0,0 +1,32 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace Kruzya.TelegramBot.Core.Data
{
public class CoreContext : DbContext
{
/// <summary>
/// The all known users.
/// </summary>
public DbSet<BotUser> Users { get; set; }
public DbSet<BotUserValue> UserValues { get; set; }
public override void Dispose()
{
SaveChanges();
base.Dispose();
}
public override ValueTask DisposeAsync()
{
SaveChanges();
return base.DisposeAsync();
}
public CoreContext(DbContextOptions<CoreContext> ctx) : base(ctx)
{
Database.Migrate();
}
}
}
+84 -1
View File
@@ -1,4 +1,7 @@
using Microsoft.EntityFrameworkCore; using System.Threading.Tasks;
using Kruzya.TelegramBot.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Kruzya.TelegramBot.Core.Extensions namespace Kruzya.TelegramBot.Core.Extensions
{ {
@@ -43,5 +46,85 @@ namespace Kruzya.TelegramBot.Core.Extensions
} }
#endregion #endregion
#region Core-context repository extensions
#region BotUser
public static async Task<BotUser> FindOrCreate(this DbSet<BotUser> repository, long chatId, int userId)
{
var user = await repository.SingleOrDefaultAsync(e => e.ChatId == chatId && e.UserId == userId);
if (user == null)
{
user = repository.Create();
user.ChatId = chatId;
user.UserId = userId;
}
return user;
}
#endregion
#region BotUserValue
#region FindOption
public static async Task<BotUserValue> FindOption(this DbSet<BotUserValue> repository, long chatId,
string option) => await repository.FindOption(chatId, 0, option);
public static async Task<BotUserValue> FindOption(this DbSet<BotUserValue> repository, BotUser user,
string option) => await repository.FindOption(user.ChatId, user.UserId, option);
public static async Task<BotUserValue> FindOption(this DbSet<BotUserValue> repository, long chatId, int userId,
string option) => await repository.SingleOrDefaultAsync(e =>
e.BotUser.ChatId == chatId && e.BotUser.UserId == userId && e.Name == option);
#endregion
#region FindOrCreateOption
public static async Task<BotUserValue> FindOrCreateOption(this DbSet<BotUserValue> repository, long chatId,
string option) => await repository.FindOrCreateOption(chatId, 0, option);
public static async Task<BotUserValue> FindOrCreateOption(this DbSet<BotUserValue> repository, BotUser user,
string option) => await repository.FindOrCreateOption(user.ChatId, user.UserId, option);
public static async Task<BotUserValue> FindOrCreateOption(this DbSet<BotUserValue> repository, long chatId,
int userId, string option)
{
var opt = await repository.FindOption(chatId, userId, option);
if (opt == null)
{
opt = repository.Create();
opt.BotUser = await repository.GetService<CoreContext>().Users.FindOrCreate(chatId, userId);
}
return opt;
}
#endregion
#region SetOptionValue
public static async Task SetOptionValue<T>(this DbSet<BotUserValue> repository, long chatId, string option,
T val) => await repository.SetOptionValue(chatId, 0, option, val);
public static async Task SetOptionValue<T>(this DbSet<BotUserValue> repository, BotUser user, string option,
T val) => await repository.SetOptionValue(user.ChatId, user.UserId, option, val);
public static async Task SetOptionValue<T>(this DbSet<BotUserValue> repository, long chatId, int userId,
string option, T val)
{
var opt = await repository.FindOrCreateOption(chatId, userId, option);
opt.SetValue(val);
repository.Update(opt);
}
#endregion
#endregion
#endregion
} }
} }
+77
View File
@@ -0,0 +1,77 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BotFramework;
using BotFramework.Attributes;
using Kruzya.TelegramBot.Core.Data;
using Microsoft.EntityFrameworkCore;
using Telegram.Bot.Types;
namespace Kruzya.TelegramBot.Core
{
public class GeneralHandler : BotEventHandler
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class HandleEverythingAttribute : HandlerAttribute
{
protected override bool CanHandle(HandlerParams param)
{
return true;
}
}
protected readonly CoreContext databaseContext;
public GeneralHandler(CoreContext dbCtx)
{
databaseContext = dbCtx;
}
[HandleEverything]
public async Task Listener()
{
foreach (var message in new Message[] {RawUpdate.Message, RawUpdate.EditedMessage, RawUpdate.ChannelPost, RawUpdate.EditedChannelPost})
{
if (message == null)
{
continue;
}
await VerifyChatPair(message);
}
throw new ArgumentException();
}
public async Task VerifyChatPair(Message message)
{
await VerifyChatPair(message.Chat, message.From);
}
public async Task VerifyChatPair(Chat chat, User user)
{
await VerifyChatPair(chat.Id, user.Id);
}
public async Task VerifyChatPair(long chat, int user)
{
var hasEntry = await databaseContext.Users.AnyAsync(e => e.ChatId == chat && e.UserId == user);
if (hasEntry)
{
return;
}
await MakeChatPair(chat, user);
}
public async Task MakeChatPair(long chat, int user)
{
await databaseContext.Users.AddAsync(new BotUser()
{
ChatId = chat,
UserId = user,
BotUserId = new Guid()
});
}
}
}
+70
View File
@@ -0,0 +1,70 @@
// <auto-generated />
using System;
using Kruzya.TelegramBot.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Kruzya.TelegramBot.Core.Migrations
{
[DbContext(typeof(CoreContext))]
[Migration("20200307215450_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.2")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUser", b =>
{
b.Property<Guid>("BotUserId")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<long>("ChatId")
.HasColumnType("bigint");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("BotUserId");
b.ToTable("Users");
});
modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUserValue", b =>
{
b.Property<Guid>("BotUserValueId")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid?>("BotUserId")
.HasColumnType("char(36)");
b.Property<string>("Name")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<byte[]>("Value")
.HasColumnType("longblob");
b.HasKey("BotUserValueId");
b.HasIndex("BotUserId");
b.ToTable("UserValues");
});
modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUserValue", b =>
{
b.HasOne("Kruzya.TelegramBot.Core.Data.BotUser", "BotUser")
.WithMany()
.HasForeignKey("BotUserId");
});
#pragma warning restore 612, 618
}
}
}
+58
View File
@@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Kruzya.TelegramBot.Core.Migrations
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
BotUserId = table.Column<Guid>(nullable: false),
ChatId = table.Column<long>(nullable: false),
UserId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.BotUserId);
});
migrationBuilder.CreateTable(
name: "UserValues",
columns: table => new
{
BotUserValueId = table.Column<Guid>(nullable: false),
BotUserId = table.Column<Guid>(nullable: true),
Name = table.Column<string>(nullable: true),
Value = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserValues", x => x.BotUserValueId);
table.ForeignKey(
name: "FK_UserValues_Users_BotUserId",
column: x => x.BotUserId,
principalTable: "Users",
principalColumn: "BotUserId",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_UserValues_BotUserId",
table: "UserValues",
column: "BotUserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserValues");
migrationBuilder.DropTable(
name: "Users");
}
}
}
@@ -0,0 +1,68 @@
// <auto-generated />
using System;
using Kruzya.TelegramBot.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Kruzya.TelegramBot.Core.Migrations
{
[DbContext(typeof(CoreContext))]
partial class CoreContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.2")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUser", b =>
{
b.Property<Guid>("BotUserId")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<long>("ChatId")
.HasColumnType("bigint");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("BotUserId");
b.ToTable("Users");
});
modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUserValue", b =>
{
b.Property<Guid>("BotUserValueId")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid?>("BotUserId")
.HasColumnType("char(36)");
b.Property<string>("Name")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<byte[]>("Value")
.HasColumnType("longblob");
b.HasKey("BotUserValueId");
b.HasIndex("BotUserId");
b.ToTable("UserValues");
});
modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUserValue", b =>
{
b.HasOne("Kruzya.TelegramBot.Core.Data.BotUser", "BotUser")
.WithMany()
.HasForeignKey("BotUserId");
});
#pragma warning restore 612, 618
}
}
}
+6
View File
@@ -1,7 +1,9 @@
using BotFramework; using BotFramework;
using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions; using Kruzya.TelegramBot.Core.Extensions;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -29,6 +31,10 @@ namespace Kruzya.TelegramBot.Core
services.AddSingleton<Core>(_core); services.AddSingleton<Core>(_core);
foreach (var module in _core.Modules) services.AddSingleton(module.GetType(), module); foreach (var module in _core.Modules) services.AddSingleton(module.GetType(), module);
// Add database context to DI.
services.AddDbContext<CoreContext>(ctx =>
ctx.UseMySql(_core.Configuration.GetConnectionString("DefaultConnection")));
// Trigger module handlers. // Trigger module handlers.
_core.Modules.ConfigureServices(services); _core.Modules.ConfigureServices(services);
} }
+4
View File
@@ -5,5 +5,9 @@
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
},
"ConnectionStrings": {
"DefaultConnection": "server=127.0.0.1;UserId=rider;Password=rider;database=rider;port=8889"
} }
} }
-19
View File
@@ -1,19 +0,0 @@
using Telegram.Bot.Types;
namespace Kruzya.TelegramBot.UrlLimitations
{
public struct ChatMember
{
public long Chat;
public long User;
public static ChatMember From(long chatId, long userId)
{
return new ChatMember()
{
Chat = chatId,
User = userId
};
}
}
}
@@ -0,0 +1,64 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BotFramework;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
namespace Kruzya.TelegramBot.UrlLimitations
{
public static class MessageExtension
{
public static async Task<bool> HasUnknownMention(this Message message, ITelegramBotClient bot)
{
foreach (var mention in message.Entities.Where(e =>
e.Type == MessageEntityType.Mention || e.Type == MessageEntityType.TextMention))
{
var userId = 0;
if (mention.Type == MessageEntityType.TextMention)
{
userId = mention.User.Id;
}
else
{
try
{
var chat = await bot.GetChatAsync(
new ChatId(message.Text.Substring(mention.Offset, mention.Length)));
if (chat != null)
{
if (chat.Type != ChatType.Private)
{
return true;
}
userId = Convert.ToInt32(chat.Id);
}
}
catch (Exception e)
{
return true;
}
}
var status = (await bot.GetChatMemberAsync(message.Chat, userId)).Status;
if ((new ChatMemberStatus[]
{
ChatMemberStatus.Administrator, ChatMemberStatus.Creator, ChatMemberStatus.Member,
ChatMemberStatus.Restricted
// i'm not sure about "Restricted"
// maybe leaved members with some restrictions also can be "Restricted".
}).Any(e => e == status) == false)
{
return true;
}
}
return false;
}
}
}
+11 -11
View File
@@ -5,6 +5,7 @@ using BotFramework;
using BotFramework.Attributes; using BotFramework.Attributes;
using BotFramework.Setup; using BotFramework.Setup;
using Kruzya.TelegramBot.Core.Cache; using Kruzya.TelegramBot.Core.Cache;
using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions; using Kruzya.TelegramBot.Core.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Telegram.Bot.Types; using Telegram.Bot.Types;
@@ -15,35 +16,34 @@ namespace Kruzya.TelegramBot.UrlLimitations
public class MessageHandler : BotEventHandler public class MessageHandler : BotEventHandler
{ {
protected ILogger Logger; protected ILogger Logger;
protected MemoryCache<ChatMember, UInt16> ChatMembersCache; protected readonly CoreContext dbCtx;
public MessageHandler(ILogger<MessageHandler> logger, MemoryCache<ChatMember, UInt16> chatMembersCache) public MessageHandler(ILogger<MessageHandler> logger, CoreContext coreContext)
{ {
Logger = logger; Logger = logger;
ChatMembersCache = chatMembersCache; dbCtx = coreContext;
} }
[TextMessage(InChat.Public)] [TextMessage(InChat.Public)]
public void OnMessageReceived() public async Task OnMessageReceived()
{ {
UInt16 messageCount; var option = await dbCtx.UserValues.FindOption(Chat.Id, From.Id, "urlLimitations.messagesAfterJoin");
var chatMember = ChatMember.From(Chat.Id, From.Id); if (option == null)
if (!ChatMembersCache.TryGetValue(chatMember, out messageCount))
{ {
return; return;
} }
var messageCount = option.GetValue<UInt16>();
if (messageCount > 10) if (messageCount > 10)
{ {
ChatMembersCache.SetValue(chatMember, 0, 0); dbCtx.UserValues.Remove(option);
return; return;
} }
messageCount++; option.SetValue(messageCount + 1);
ChatMembersCache.SetValue(chatMember, messageCount);
var message = RawUpdate.Message; var message = RawUpdate.Message;
if (message.Entities.Count(e => e.Type == MessageEntityType.Url || e.Type == MessageEntityType.TextLink) > if (message.Entities.Count(e => e.Type == MessageEntityType.Url || e.Type == MessageEntityType.TextLink) >
0) 0 || await message.HasUnknownMention(Bot))
{ {
var targetChat = new ChatId(Chat.Id); var targetChat = new ChatId(Chat.Id);
Task.WaitAll(new Task[] { Task.WaitAll(new Task[] {
+1 -5
View File
@@ -1,5 +1,6 @@
using System; using System;
using Kruzya.TelegramBot.Core; using Kruzya.TelegramBot.Core;
using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions; using Kruzya.TelegramBot.Core.Extensions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -10,10 +11,5 @@ namespace Kruzya.TelegramBot.UrlLimitations
public UrlLimitations(Core.Core core) : base(core) public UrlLimitations(Core.Core core) : base(core)
{ {
} }
public override void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache<ChatMember, UInt16>();
}
} }
} }
+10 -5
View File
@@ -1,25 +1,30 @@
using System; using System;
using System.Threading.Tasks;
using BotFramework; using BotFramework;
using BotFramework.Attributes; using BotFramework.Attributes;
using BotFramework.Setup; using BotFramework.Setup;
using Kruzya.TelegramBot.Core.Cache; using Kruzya.TelegramBot.Core.Cache;
using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
namespace Kruzya.TelegramBot.UrlLimitations namespace Kruzya.TelegramBot.UrlLimitations
{ {
public class UserJoinHandler : BotEventHandler public class UserJoinHandler : BotEventHandler
{ {
protected MemoryCache<ChatMember, UInt16> ChatMembersCache; protected readonly CoreContext dbCtx;
public UserJoinHandler(MemoryCache<ChatMember, UInt16> chatMembersCache) public UserJoinHandler(CoreContext coreContext)
{ {
ChatMembersCache = chatMembersCache; dbCtx = coreContext;
} }
[Message(MessageType.ChatMembersAdded, InChat.Public)] [Message(MessageType.ChatMembersAdded, InChat.Public)]
public void OnChatMembersAdded() public async Task OnChatMembersAdded()
{ {
ChatMembersCache.SetValue(ChatMember.From(Chat.Id, From.Id), 0); foreach (var member in RawUpdate.Message.NewChatMembers)
await dbCtx.UserValues.SetOptionValue<UInt16>(Chat.Id, member.Id, "urlLimitations.messagesAfterJoin",
0);
} }
} }
} }