Merge pull request #6 from CrazyHackGUT/refactor/reputation

WIP: Move reputation to separate module.
This commit is contained in:
Kruzya
2022-02-14 22:02:25 +03:00
committed by GitHub
24 changed files with 774 additions and 299 deletions
@@ -9,9 +9,9 @@ using Telegram.Bot;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
namespace West.TelegramBot.ChatManagement.Handler namespace Kruzya.TelegramBot.Core
{ {
public abstract class ManagementHandler : BotEventHandler public abstract class CommonHandler : BotEventHandler
{ {
protected readonly CoreContext Db; protected readonly CoreContext Db;
@@ -19,7 +19,7 @@ namespace West.TelegramBot.ChatManagement.Handler
protected User? RepliedUser => Message?.ReplyToMessage?.From; protected User? RepliedUser => Message?.ReplyToMessage?.From;
protected ManagementHandler(CoreContext db) protected CommonHandler(CoreContext db)
{ {
Db = db; Db = db;
} }
+8
View File
@@ -0,0 +1,8 @@
namespace Kruzya.TelegramBot.Core.Data;
public interface IReputationEntity
{
public long UserId { get; set; }
public long ChatId { get; set; }
public double Value { get; set; }
}
+1 -1
View File
@@ -45,7 +45,7 @@ public static class DbContextExtension
dbContext.Update(entity); dbContext.Update(entity);
dbContext.SaveChanges(); dbContext.SaveChanges();
} }
catch (DbUpdateConcurrencyException e) catch (DbUpdateConcurrencyException)
{ {
dbContext.Add(entity); dbContext.Add(entity);
dbContext.SaveChanges(); dbContext.SaveChanges();
+16
View File
@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Kruzya.TelegramBot.Core.Data;
using Telegram.Bot.Types;
namespace Kruzya.TelegramBot.Core.Service
{
public interface IReputation
{
public Task<double> GetUserReputationAsync(Chat chat, User user);
public Task<double> SetUserReputationAsync(Chat chat, User user, double value);
public Task<double> GetReputationDiffAsync(Chat chat, User sender, User receiver);
public Task IncrementReputationAsync(Chat chat, User user, double diff);
public Task<IEnumerable<IReputationEntity>> GetChatRatingAsync(Chat chat, int limit = 10, int offset = 0);
}
}
+49 -2
View File
@@ -3,7 +3,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using BotFramework.Abstractions;
using Kruzya.TelegramBot.Core.Cache;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Types; using Telegram.Bot.Types;
namespace Kruzya.TelegramBot.Core.Service namespace Kruzya.TelegramBot.Core.Service
@@ -11,10 +16,17 @@ namespace Kruzya.TelegramBot.Core.Service
public class UserService public class UserService
{ {
private readonly List<long> _superUsers; private readonly List<long> _superUsers;
private readonly Dictionary<long, string> _statusMap; private readonly Dictionary<long, string> _statusMap;
private readonly ICacheStorage<long, User> _userCache;
private readonly ITelegramBotClient _bot;
private readonly ILogger<UserService> _logger;
public UserService(IConfiguration configuration)
public UserService(
IConfiguration configuration,
IBotInstance bot,
ICacheStorage<long, User> userCache,
ILogger<UserService> logger)
{ {
_superUsers = configuration.GetSection("SuperUsers") _superUsers = configuration.GetSection("SuperUsers")
.GetChildren() .GetChildren()
@@ -24,6 +36,41 @@ namespace Kruzya.TelegramBot.Core.Service
_statusMap = configuration.GetSection("CustomMemberStatus") _statusMap = configuration.GetSection("CustomMemberStatus")
.GetChildren() .GetChildren()
.ToDictionary(x => Convert.ToInt64(x.Key), x => x.Value); .ToDictionary(x => Convert.ToInt64(x.Key), x => x.Value);
_userCache = userCache;
_bot = bot.BotClient;
_logger = logger;
}
public async Task<User> GetUser(long userId, long chatId, bool bypassCache = false)
{
if (bypassCache || !_userCache.TryGetValue(userId, out var user))
{
if (bypassCache)
{
_logger.LogDebug("Bypassing cache for {UserId}.", userId);
}
else
{
_logger.LogDebug("User {UserId} is not found in cache. Fetching from Telegram API.", userId);
}
user = await GetUserByChat(chatId, userId);
CacheUser(user);
}
return user;
}
private void CacheUser(User user)
{
// TODO: config entry for TTL
_userCache.SetValue(user.Id, user, Convert.ToInt32(TimeSpan.FromHours(1).TotalSeconds));
}
private async Task<User> GetUserByChat(long chatId, long userId)
{
return (await _bot.GetChatMemberAsync(chatId, userId)).User;
} }
public bool IsUserSuper(User user) public bool IsUserSuper(User user)
+1
View File
@@ -14,6 +14,7 @@ COPY ./modules/ChatManagement/*.csproj ./modules/ChatManagement/
COPY ./modules/Entertainment/*.csproj ./modules/Entertainment/ COPY ./modules/Entertainment/*.csproj ./modules/Entertainment/
COPY ./modules/UserGroupTag/*.csproj ./modules/UserGroupTag/ COPY ./modules/UserGroupTag/*.csproj ./modules/UserGroupTag/
COPY ./modules/ChatQuotes/*.csproj ./modules/ChatQuotes/ COPY ./modules/ChatQuotes/*.csproj ./modules/ChatQuotes/
COPY ./modules/Reputation/*.csproj ./modules/Reputation/
RUN dotnet restore Kruzya.TelegramBot.sln RUN dotnet restore Kruzya.TelegramBot.sln
# Copy all project files # Copy all project files
+8 -1
View File
@@ -23,7 +23,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserGroupTag", "modules\Use
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Entertainment", "modules\Entertainment\Entertainment.csproj", "{1D62101B-C2B1-4E79-8F7A-690E44E3EE81}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Entertainment", "modules\Entertainment\Entertainment.csproj", "{1D62101B-C2B1-4E79-8F7A-690E44E3EE81}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatQuotes", "modules\ChatQuotes\ChatQuotes.csproj", "{04F16325-8F44-4250-9B2E-0F77E8F65CBF}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatQuotes", "modules\ChatQuotes\ChatQuotes.csproj", "{04F16325-8F44-4250-9B2E-0F77E8F65CBF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reputation", "modules\Reputation\Reputation.csproj", "{03F3C7D5-6FEF-4A83-9191-501091BFC1AC}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -71,6 +73,10 @@ Global
{04F16325-8F44-4250-9B2E-0F77E8F65CBF}.Debug|Any CPU.Build.0 = Debug|Any CPU {04F16325-8F44-4250-9B2E-0F77E8F65CBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04F16325-8F44-4250-9B2E-0F77E8F65CBF}.Release|Any CPU.ActiveCfg = Release|Any CPU {04F16325-8F44-4250-9B2E-0F77E8F65CBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04F16325-8F44-4250-9B2E-0F77E8F65CBF}.Release|Any CPU.Build.0 = Release|Any CPU {04F16325-8F44-4250-9B2E-0F77E8F65CBF}.Release|Any CPU.Build.0 = Release|Any CPU
{03F3C7D5-6FEF-4A83-9191-501091BFC1AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{03F3C7D5-6FEF-4A83-9191-501091BFC1AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{03F3C7D5-6FEF-4A83-9191-501091BFC1AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{03F3C7D5-6FEF-4A83-9191-501091BFC1AC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -84,6 +90,7 @@ Global
{5B89E22A-78CF-4037-BE0D-5F88EF47022B} = {C7821F15-DEDD-474F-A575-A296D4B58F10} {5B89E22A-78CF-4037-BE0D-5F88EF47022B} = {C7821F15-DEDD-474F-A575-A296D4B58F10}
{1D62101B-C2B1-4E79-8F7A-690E44E3EE81} = {C7821F15-DEDD-474F-A575-A296D4B58F10} {1D62101B-C2B1-4E79-8F7A-690E44E3EE81} = {C7821F15-DEDD-474F-A575-A296D4B58F10}
{04F16325-8F44-4250-9B2E-0F77E8F65CBF} = {C7821F15-DEDD-474F-A575-A296D4B58F10} {04F16325-8F44-4250-9B2E-0F77E8F65CBF} = {C7821F15-DEDD-474F-A575-A296D4B58F10}
{03F3C7D5-6FEF-4A83-9191-501091BFC1AC} = {C7821F15-DEDD-474F-A575-A296D4B58F10}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5BA73C3B-D5FC-4942-9091-504325CDC308} SolutionGuid = {5BA73C3B-D5FC-4942-9091-504325CDC308}
+4 -5
View File
@@ -1,11 +1,10 @@
using Kruzya.TelegramBot.Core; using Kruzya.TelegramBot.Core;
namespace West.TelegramBot.ChatManagement namespace West.TelegramBot.ChatManagement;
public class ChatManagement : Module
{ {
public class ChatManagement : Module public ChatManagement(Core core) : base(core)
{ {
public ChatManagement(Core core) : base(core)
{
}
} }
} }
+27 -27
View File
@@ -3,41 +3,41 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using BotFramework.Attributes; using BotFramework.Attributes;
using BotFramework.Enums; using BotFramework.Enums;
using Kruzya.TelegramBot.Core;
using Kruzya.TelegramBot.Core.Data; using Kruzya.TelegramBot.Core.Data;
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Types; using Telegram.Bot.Types;
namespace West.TelegramBot.ChatManagement.Handler namespace West.TelegramBot.ChatManagement.Handler;
public class Ban : CommonHandler
{ {
public class Ban : ManagementHandler public Ban(CoreContext db) : base(db) {}
[Command("ban", CommandParseMode.Both)]
public async Task HandleBan()
{ {
public Ban(CoreContext db) : base(db) {} if (!await CanPunish()) return;
[Command("ban", CommandParseMode.Both)] await BanMember(RepliedUser!);
public async Task HandleBan() }
{
if (!await CanPunish()) return;
await BanMember(RepliedUser!); [Command("unban", CommandParseMode.Both)]
} public async Task HandleUnban()
{
if (!await CanPunish()) return;
[Command("unban", CommandParseMode.Both)] await Bot.RestrictChatMemberAsync(Chat.Id, RepliedUser!.Id,
public async Task HandleUnban() new ChatPermissions
{ {
if (!await CanPunish()) return; CanSendMessages = true,
CanChangeInfo = true,
await Bot.RestrictChatMemberAsync(Chat.Id, RepliedUser!.Id, CanInviteUsers = true,
new ChatPermissions CanPinMessages = true,
{ CanSendPolls = true,
CanSendMessages = true, CanSendMediaMessages = true,
CanChangeInfo = true, CanSendOtherMessages = true,
CanInviteUsers = true, CanAddWebPagePreviews = true
CanPinMessages = true, });
CanSendPolls = true,
CanSendMediaMessages = true,
CanSendOtherMessages = true,
CanAddWebPagePreviews = true
});
}
} }
} }
@@ -1,183 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BotFramework.Attributes;
using BotFramework.Enums;
using BotFramework.Utils;
using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions;
using Kruzya.TelegramBot.Core.Service;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
namespace West.TelegramBot.ChatManagement.Handler
{
public class Reputation : ManagementHandler
{
private readonly UserService _userService;
private readonly ILogger _logger;
private static readonly List<string> RepUpChars = new()
{
"+",
"👍",
"👍🏼",
"👍🏽",
"👍🏾",
"👍🏿",
"👍🏻"
};
private static readonly List<string> RepDownChars = new()
{
"-",
"👎",
"👎🏼",
"👎🏽",
"👎🏾",
"👎🏿",
"👎🏻"
};
public Reputation(CoreContext db, UserService userService, ILogger<Reputation> logger) : base(db)
{
_userService = userService;
_logger = logger;
}
[Command("whois", CommandParseMode.Both)]
public async Task HandleWhois()
{
var user = RepliedUser ?? From;
var message = $"{user.ToHtml()}\n";
if (user.IsBot)
{
message += "<code>Бот</code>";
}
else
{
message += $"<code>{await GetChatMemberStatusString(user)}</code>\n";
message += $"<code>Репутация: {await GetUserReputation(user)}\n";
message += $"Предупреждения: {(await GetWarnOption(user)).GetValue<int>()}</code>";
}
await Reply(message);
}
[Message(InChat.Public, MessageFlag.HasText | MessageFlag.HasSticker)]
public async Task HandleReputation()
{
var text = Message?.Text;
if (string.IsNullOrEmpty(text))
{
text = Message?.Sticker?.Emoji;
}
if (text == null)
{
return;
}
var receiver = RepliedUser;
var canChangeRep = receiver is { IsBot: false } && !From.IsBot && receiver.Id != From.Id;
if (canChangeRep && (RepUpChars.Contains(text) || RepDownChars.Contains(text)))
{
var receiverRepOption = await GetReputationOption(receiver!);
var receiverRepCount = receiverRepOption.GetValue<double>();
var senderRepCount = (await GetReputationOption(From)).GetValue<double>();
if (senderRepCount < 0)
{
await Reply(new HtmlString()
.Code("Ты не можешь голосовать с отрицательной репутацией.")
.ToString()
);
return;
}
var diff = Math.Round(senderRepCount == 0 ? 1 : Math.Sqrt(senderRepCount), 2);
_logger.LogDebug("Calculated diff: {Diff}", diff);
string action;
if (RepUpChars.Contains(text))
{
receiverRepCount += diff;
action = "увеличил";
}
else
{
receiverRepCount -= diff;
action = "уменьшил";
}
_logger.LogDebug("Saving receiver rep with value: {Value}", receiverRepCount);
receiverRepOption.SetValue(receiverRepCount);
Db.AddOrUpdate(receiverRepOption);
_logger.LogDebug("Updated value: {Value}", receiverRepOption.GetValue<double>());
await Reply($"{From.ToHtml()}<code>({await GetUserReputation(From)}) " +
$"{action} репутацию </code>{receiver.ToHtml()}<code>({Math.Round(receiverRepCount, 2)})</code>");
}
}
[ParametrizedCommand(InChat.Public, "setrep", CommandParseMode.Both)]
public async Task HandleSetRep(double reputation)
{
if (!_userService.IsUserSuper(From))
{
_logger.LogInformation("{User} attempted to call /setrep in {Chat}", From, Chat.Title);
return;
}
if (RepliedUser == null || RepliedUser.IsBot)
{
return;
}
reputation = Math.Round(reputation, 2);
var opt = await GetReputationOption(RepliedUser);
opt.SetValue(reputation);
Db.AddOrUpdate(opt);
await Reply(
new HtmlString()
.Code("Пользователю ")
.UserMention(RepliedUser)
.Code($" установлена репутация: {reputation}")
.ToString()
);
}
private async Task<double> GetUserReputation(User user)
{
return Math.Round((await GetReputationOption(user)).GetValue<double>(), 2);
}
private async Task<BotUserValue> GetReputationOption(User user)
{
return await Db.UserValues.FindOrCreateOption(Chat.Id, user.Id, "reputation");
}
private async Task<string> GetChatMemberStatusString(User user)
{
var customStatus = _userService.GetUserCustomStatus(user.Id);
if (customStatus != null)
{
return customStatus;
}
var chatMember = await Bot.GetChatMemberAsync(Chat.Id, user.Id);
return chatMember.Status switch
{
ChatMemberStatus.Creator => "Владелец",
ChatMemberStatus.Restricted => "Заблокированный",
ChatMemberStatus.Administrator => "Администратор",
_ => "Пользователь"
};
}
}
}
+33 -33
View File
@@ -1,47 +1,47 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using BotFramework.Attributes; using BotFramework.Attributes;
using BotFramework.Enums; using BotFramework.Enums;
using Kruzya.TelegramBot.Core;
using Kruzya.TelegramBot.Core.Data; using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions; using Kruzya.TelegramBot.Core.Extensions;
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Exceptions; using Telegram.Bot.Exceptions;
namespace West.TelegramBot.ChatManagement.Handler namespace West.TelegramBot.ChatManagement.Handler;
class Rules : CommonHandler
{ {
class Rules : ManagementHandler [Command(InChat.Public, "setrules", CommandParseMode.Both)]
public async Task HandleSetRules()
{ {
[Command(InChat.Public, "setrules", CommandParseMode.Both)] var replyToMessage = Message?.ReplyToMessage;
public async Task HandleSetRules()
{
var replyToMessage = Message?.ReplyToMessage;
if (replyToMessage == null || !await Bot.IsUserAdminAsync(Chat, From)) return; if (replyToMessage == null || !await Bot.IsUserAdminAsync(Chat, From)) return;
var opt = await Db.UserValues.FindOrCreateOption(Chat.Id, "rulesMsgId"); var opt = await Db.UserValues.FindOrCreateOption(Chat.Id, "rulesMsgId");
opt.SetValue(replyToMessage.MessageId); opt.SetValue(replyToMessage.MessageId);
Db.AddOrUpdate(opt); Db.AddOrUpdate(opt);
}
[Command(InChat.Public, "rules", CommandParseMode.Both)]
public async Task HandleRules()
{
var opt = await Db.UserValues.FindOption(Chat.Id, "rulesMsgId");
if (opt == null)
{
return;
}
try
{
await Bot.CopyMessageAsync(Chat.Id, Chat.Id, opt.GetValue<int>());
}
catch (ApiRequestException)
{
Db.UserValues.Remove(opt);
throw;
}
}
public Rules(CoreContext db) : base(db) {}
} }
[Command(InChat.Public, "rules", CommandParseMode.Both)]
public async Task HandleRules()
{
var opt = await Db.UserValues.FindOption(Chat.Id, "rulesMsgId");
if (opt == null)
{
return;
}
try
{
await Bot.CopyMessageAsync(Chat.Id, Chat.Id, opt.GetValue<int>());
}
catch (ApiRequestException)
{
Db.UserValues.Remove(opt);
throw;
}
}
public Rules(CoreContext db) : base(db) {}
} }
+43 -43
View File
@@ -4,59 +4,59 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using BotFramework.Attributes; using BotFramework.Attributes;
using BotFramework.Enums; using BotFramework.Enums;
using Kruzya.TelegramBot.Core;
using Kruzya.TelegramBot.Core.Data; using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions; using Kruzya.TelegramBot.Core.Extensions;
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
namespace West.TelegramBot.ChatManagement.Handler namespace West.TelegramBot.ChatManagement.Handler;
public class Warn : CommonHandler
{ {
public class Warn : ManagementHandler public Warn(CoreContext db) : base(db) {}
[Command("warn", CommandParseMode.Both)]
public async Task HandleWarn()
{ {
public Warn(CoreContext db) : base(db) {} if (!await CanPunish()) return;
[Command("warn", CommandParseMode.Both)] var warnUser = RepliedUser!;
public async Task HandleWarn() var warnOption = await GetWarnOption(warnUser);
var warnCount = warnOption!.GetValue<int>() + 1;
warnOption.SetValue(warnCount);
Db.AddOrUpdate(warnOption);
await Bot.SendTextMessageAsync(Chat.Id, $"{warnUser.ToHtml()}<code>, Вам выдано предупреждение! " +
$"Текущее кол-во: {warnCount}/3</code>",
disableNotification: true,
parseMode: ParseMode.Html);
if (warnCount == 3)
{ {
if (!await CanPunish()) return; await BanMember(warnUser);
warnOption.SetValue(0);
var warnUser = RepliedUser!;
var warnOption = await GetWarnOption(warnUser);
var warnCount = warnOption!.GetValue<int>() + 1;
warnOption.SetValue(warnCount);
Db.AddOrUpdate(warnOption); Db.AddOrUpdate(warnOption);
await Bot.SendTextMessageAsync(Chat.Id, $"{warnUser.ToHtml()}<code>, Вам выдано предупреждение! " +
$"Текущее кол-во: {warnCount}/3</code>",
disableNotification: true,
parseMode: ParseMode.Html);
if (warnCount == 3)
{
await BanMember(warnUser);
warnOption.SetValue(0);
Db.AddOrUpdate(warnOption);
}
}
[Command("unwarn", CommandParseMode.Both)]
public async Task HandleUnwarn()
{
if (!await CanPunish()) return;
var warnOption = await GetWarnOption(RepliedUser!);
var warnCount = warnOption.GetValue<int>();
if (warnCount == 0) return;
warnCount = Math.Max(warnCount - 1, 0);
warnOption.SetValue(warnCount);
Db.AddOrUpdate(warnOption);
await Bot.SendTextMessageAsync(Chat.Id,
$"{RepliedUser.ToHtml()}<code>, с Вас снято предупреждение! " +
$"Текущее кол-во: {warnCount}/3</code>",
disableNotification: true,
parseMode: ParseMode.Html);
} }
} }
[Command("unwarn", CommandParseMode.Both)]
public async Task HandleUnwarn()
{
if (!await CanPunish()) return;
var warnOption = await GetWarnOption(RepliedUser!);
var warnCount = warnOption.GetValue<int>();
if (warnCount == 0) return;
warnCount = Math.Max(warnCount - 1, 0);
warnOption.SetValue(warnCount);
Db.AddOrUpdate(warnOption);
await Bot.SendTextMessageAsync(Chat.Id,
$"{RepliedUser.ToHtml()}<code>, с Вас снято предупреждение! " +
$"Текущее кол-во: {warnCount}/3</code>",
disableNotification: true,
parseMode: ParseMode.Html);
}
} }
+67
View File
@@ -0,0 +1,67 @@
#nullable enable
using System;
using System.Threading.Tasks;
using BotFramework.Attributes;
using BotFramework.Enums;
using Kruzya.TelegramBot.Core;
using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions;
using Kruzya.TelegramBot.Core.Service;
using Microsoft.Extensions.DependencyInjection;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
namespace West.TelegramBot.ChatManagement.Handler;
public class WhoIs : CommonHandler
{
private readonly IReputation? _reputation;
private readonly UserService _userService;
public WhoIs(CoreContext db, UserService userService, IServiceProvider provider) : base(db)
{
_reputation = provider.GetService<IReputation>();
_userService = userService;
}
[Command("whois", CommandParseMode.Both)]
public async Task HandleWhoIs()
{
var user = RepliedUser ?? From;
var message = $"{user.ToHtml()}\n";
if (user.IsBot)
{
message += "<code>Бот</code>";
}
else
{
message += $"<code>{await GetChatMemberStatusString(user)}</code>\n";
if (_reputation != null)
{
message += $"<code>Репутация: {await _reputation.GetUserReputationAsync(Chat, user)}\n</code>";
}
message += $"<code>Предупреждения: {(await GetWarnOption(user)).GetValue<int>()}</code>";
}
await Reply(message);
}
private async Task<string> GetChatMemberStatusString(User user)
{
var customStatus = _userService.GetUserCustomStatus(user.Id);
if (customStatus != null)
{
return customStatus;
}
var chatMember = await Bot.GetChatMemberAsync(Chat.Id, user.Id);
return chatMember.Status switch
{
ChatMemberStatus.Creator => "Владелец",
ChatMemberStatus.Restricted => "Заблокированный",
ChatMemberStatus.Administrator => "Администратор",
_ => "Пользователь"
};
}
}
@@ -0,0 +1,20 @@
using Kruzya.TelegramBot.Core.EF;
using Microsoft.EntityFrameworkCore;
namespace West.TelegramBot.Reputation.Data;
public class ReputationContext : AutoSaveDbContext
{
public DbSet<UserReputation> UserReputation { get; set; } = null!;
public ReputationContext(DbContextOptions<ReputationContext> options) : base(options)
{
Database.EnsureCreated();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserReputation>().HasKey(e => new {e.ChatId, e.UserId});
modelBuilder.Entity<UserReputation>().HasIndex(e => e.ChatId);
}
}
+10
View File
@@ -0,0 +1,10 @@
using Kruzya.TelegramBot.Core.Data;
namespace West.TelegramBot.Reputation.Data;
public class UserReputation : IReputationEntity
{
public long UserId { get; set; }
public long ChatId { get; set; }
public double Value { get; set; }
}
@@ -0,0 +1,21 @@
#nullable enable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace West.TelegramBot.Reputation.Data;
public class ReputationContextFactory : IDesignTimeDbContextFactory<ReputationContext>
{
public ReputationContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<ReputationContext>();
if (args.Length != 1)
{
throw new InvalidDataException("You should pass DSN as CLI argument in order to use this.");
}
var dsn = args[0];
optionsBuilder.UseMySql(dsn, ServerVersion.AutoDetect(dsn));
return new ReputationContext(optionsBuilder.Options);
}
}
+64
View File
@@ -0,0 +1,64 @@
using BotFramework;
using BotFramework.Attributes;
using BotFramework.Enums;
using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Service;
using Microsoft.EntityFrameworkCore;
using West.TelegramBot.Reputation.Data;
namespace West.TelegramBot.Reputation.Handler;
public class Debug : BotEventHandler
{
private readonly CoreContext _coreContext;
private readonly ReputationContext _reputationContext;
private readonly UserService _userService;
public Debug(CoreContext coreContext, ReputationContext reputationContext, UserService userService)
{
_coreContext = coreContext;
_reputationContext = reputationContext;
_userService = userService;
}
[Command(InChat.Public, "dbg_migrate", CommandParseMode.Both)]
public async Task HandleMigrate()
{
if (!_userService.IsUserSuper(From))
{
return;
}
var reputationValues = _coreContext.UserValues.AsNoTracking()
.Where(value => value.Name == "reputation").Include(v => v.BotUser);
foreach (var reputationValue in reputationValues)
{
var value = reputationValue.GetValue<double>();
if (value == 0D)
{
continue;
}
var userReputation = new UserReputation
{
ChatId = reputationValue.BotUser.ChatId,
UserId = reputationValue.BotUser.UserId,
Value = value
};
var repList = _reputationContext.UserReputation;
if (repList.Contains(userReputation))
{
repList.Update(userReputation);
}
else
{
repList.Add(userReputation);
}
}
await _reputationContext.SaveChangesAsync();
}
}
+151
View File
@@ -0,0 +1,151 @@
#nullable enable
using BotFramework.Attributes;
using BotFramework.Enums;
using BotFramework.Utils;
using Kruzya.TelegramBot.Core;
using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions;
using Kruzya.TelegramBot.Core.Service;
using Microsoft.Extensions.Logging;
using Telegram.Bot.Types;
namespace West.TelegramBot.Reputation.Handler;
public class Reputation : CommonHandler
{
private readonly UserService _userService;
private readonly ILogger _logger;
private readonly IReputation _reputationService;
private static readonly List<string> RepUpChars = new()
{
"+",
"👍",
"👍🏼",
"👍🏽",
"👍🏾",
"👍🏿",
"👍🏻"
};
private static readonly List<string> RepDownChars = new()
{
"-",
"👎",
"👎🏼",
"👎🏽",
"👎🏾",
"👎🏿",
"👎🏻"
};
public Reputation(
CoreContext db,
UserService userService,
IReputation reputationService,
ILogger<Reputation> logger) : base(db)
{
_reputationService = reputationService;
_userService = userService;
_logger = logger;
}
[Message(InChat.Public, MessageFlag.HasText | MessageFlag.HasSticker)]
public async Task HandleReputation()
{
var text = Message?.Text;
if (string.IsNullOrEmpty(text))
{
text = Message?.Sticker?.Emoji;
}
if (text == null)
{
return;
}
var receiver = RepliedUser;
var canChangeRep = receiver is { IsBot: false } && !From.IsBot && receiver.Id != From.Id;
if (canChangeRep && (RepUpChars.Contains(text) || RepDownChars.Contains(text)))
{
var senderRepCount = await GetUserReputation(From);
if (senderRepCount < 0)
{
await Reply(new HtmlString()
.Code("Ты не можешь голосовать с отрицательной репутацией.")
.ToString()
);
return;
}
var diff = await _reputationService.GetReputationDiffAsync(Chat, From, receiver);
_logger.LogDebug("Calculated diff: {Diff}", diff);
string action;
if (RepUpChars.Contains(text))
{
action = "увеличил";
}
else
{
diff *= -1;
action = "уменьшил";
}
await _reputationService.IncrementReputationAsync(Chat, receiver, diff);
var receiverRepCount = await GetUserReputation(receiver!);
_logger.LogDebug("Updated value: {Value}", receiverRepCount);
await Reply($"{From.ToHtml()}<code>({await GetUserReputation(From)}) " +
$"{action} репутацию </code>{receiver.ToHtml()}<code>({Math.Round(receiverRepCount, 2)})</code>");
}
}
[ParametrizedCommand(InChat.Public, "setrep", CommandParseMode.Both)]
public async Task HandleSetRep(double reputation)
{
if (!_userService.IsUserSuper(From))
{
_logger.LogInformation("{User} attempted to call /setrep in {Chat}", From, Chat.Title);
return;
}
if (RepliedUser == null || RepliedUser.IsBot)
{
return;
}
reputation = await _reputationService.SetUserReputationAsync(Chat, RepliedUser, reputation);
await Reply(
new HtmlString()
.Code("Пользователю ")
.UserMention(RepliedUser)
.Code($" установлена репутация: {reputation}")
.ToString()
);
}
[Command(InChat.Public, "top", CommandParseMode.Both)]
public async Task HandleTop()
{
var msg = new HtmlString();
msg.TextBr("Топ репутации чата: ");
var i = 1;
foreach (var rep in await _reputationService.GetChatRatingAsync(Chat))
{
var user = await _userService.GetUser(rep.UserId, rep.ChatId);
msg.Text($"{i}. ").UserMention(user).Text($" ({rep.Value})").Br();
i++;
}
await Bot.SendHtmlStringAsync(Chat, msg);
}
private async Task<double> GetUserReputation(User user)
{
return await _reputationService.GetUserReputationAsync(Chat, user);
}
}
@@ -0,0 +1,43 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using West.TelegramBot.Reputation.Data;
#nullable disable
namespace West.TelegramBot.Reputation.Migrations
{
[DbContext(typeof(ReputationContext))]
[Migration("20220213182703_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("West.TelegramBot.ChatManagement.Data.UserReputation", b =>
{
b.Property<long>("ChatId")
.HasColumnType("bigint");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<double>("Value")
.HasColumnType("double");
b.HasKey("ChatId", "UserId");
b.HasIndex("ChatId");
b.ToTable("UserReputation");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace West.TelegramBot.Reputation.Migrations;
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "UserReputation",
columns: table => new
{
UserId = table.Column<long>(type: "bigint", nullable: false),
ChatId = table.Column<long>(type: "bigint", nullable: false),
Value = table.Column<double>(type: "double", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserReputation", x => new { x.ChatId, x.UserId });
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_UserReputation_ChatId",
table: "UserReputation",
column: "ChatId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserReputation");
}
}
@@ -0,0 +1,39 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using West.TelegramBot.Reputation.Data;
#nullable disable
namespace West.TelegramBot.Reputation.Migrations;
[DbContext(typeof(ReputationContext))]
partial class ReputationContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("West.TelegramBot.ChatManagement.Data.UserReputation", b =>
{
b.Property<long>("ChatId")
.HasColumnType("bigint");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<double>("Value")
.HasColumnType("double");
b.HasKey("ChatId", "UserId");
b.HasIndex("ChatId");
b.ToTable("UserReputation");
});
#pragma warning restore 612, 618
}
}
+24
View File
@@ -0,0 +1,24 @@
using Kruzya.TelegramBot.Core;
using Kruzya.TelegramBot.Core.Service;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using West.TelegramBot.Reputation.Data;
using West.TelegramBot.Reputation.Service;
namespace West.TelegramBot.Reputation;
public class Reputation : Module
{
public Reputation(Core core) : base(core)
{
}
public override void ConfigureServices(IServiceCollection services)
{
var dsn = Configuration.GetConnectionString("Reputation");
services.AddDbContext<ReputationContext>(options => options.UseMySql(dsn, ServerVersion.AutoDetect(dsn)));
services.AddScoped<IReputation, ReputationService>();
}
}
+29
View File
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>TelegramBot.Reputation</AssemblyName>
<RootNamespace>West.TelegramBot.Reputation</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Service\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)\TelegramBot.dll" />
</Target>
</Project>
@@ -0,0 +1,73 @@
using Kruzya.TelegramBot.Core.Data;
using Kruzya.TelegramBot.Core.Extensions;
using Kruzya.TelegramBot.Core.Service;
using Microsoft.EntityFrameworkCore;
using Telegram.Bot.Types;
using West.TelegramBot.Reputation.Data;
namespace West.TelegramBot.Reputation.Service;
public class ReputationService : IReputation
{
private readonly ReputationContext _reputationContext;
public ReputationService(ReputationContext reputationContext)
{
_reputationContext = reputationContext;
}
public async Task<double> GetUserReputationAsync(Chat chat, User user)
{
var rep = await FindUserReputation(chat, user);
return rep?.Value ?? 0;
}
public async Task<double> SetUserReputationAsync(Chat chat, User user, double value)
{
var rep = await FindUserReputation(chat, user) ?? new UserReputation
{
ChatId = chat.Id,
UserId = user.Id
};
rep.Value = Math.Round(value, 2);
_reputationContext.AddOrUpdate(rep);
return rep.Value;
}
public async Task IncrementReputationAsync(Chat chat, User user, double diff)
{
await SetUserReputationAsync(chat, user, await GetUserReputationAsync(chat, user) + diff);
}
public async Task<double> GetReputationDiffAsync(Chat chat, User sender, User receiver)
{
var senderRep = await FindUserReputation(chat, sender);
if (senderRep == null)
{
return 1;
}
var senderValue = senderRep.Value;
return Math.Round(senderValue == 0 ? 1 : Math.Sqrt(senderValue), 2);
}
public async Task<IEnumerable<IReputationEntity>> GetChatRatingAsync(Chat chat, int limit = 10, int offset = 0)
{
return await _reputationContext.UserReputation
.Where(r => r.ChatId == chat.Id && r.Value > 0D)
.OrderByDescending(r => r.Value)
.Skip(offset)
.Take(limit)
.ToListAsync();
}
private async Task<UserReputation?> FindUserReputation(Chat chat, User user)
{
return await _reputationContext.UserReputation.SingleOrDefaultAsync(
e => e.ChatId == chat.Id && e.UserId == user.Id);
}
}