From 58dfbbedab19a5a2cf27e6384b9b1d065a926462 Mon Sep 17 00:00:00 2001 From: ZenonEl <165126589+ZenonEl@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:31:24 +0400 Subject: [PATCH] feat(contacts): add ability to rename contacts locally Add DisplayName column to Contacts table allowing users to set custom display names for their contacts. Includes FluentMigrator migration, repository methods, state machine flow, callback handler, and localized resource strings (EN/RU). --- Database/BaseDBMigration.cs | 1 + Database/Interfaces/IContactRepository.cs | 2 + Database/Migrations/AddContactDisplayName.cs | 32 +++++ Database/Repositories/SqLite/Contacts.cs | 48 ++++++- Resources/texts.resx | 17 +++ Resources/texts.ru-RU.resx | 17 +++ .../Handlers/ICallBackQuery/CallbackNames.cs | 1 + .../ICallBackQuery/IContactsCallbackQuery.cs | 25 ++++ TelegramBot/Menu/Contacts.cs | 12 ++ TelegramBot/States/RenameContact.cs | 121 ++++++++++++++++++ TelegramBot/States/States.cs | 7 + 11 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 Database/Migrations/AddContactDisplayName.cs create mode 100644 TelegramBot/States/RenameContact.cs diff --git a/Database/BaseDBMigration.cs b/Database/BaseDBMigration.cs index 74ebfeb..15d8a44 100644 --- a/Database/BaseDBMigration.cs +++ b/Database/BaseDBMigration.cs @@ -47,6 +47,7 @@ CREATE TABLE Contacts ( ContactId INTEGER NOT NULL, status TEXT, MutedUntil TEXT, + DisplayName TEXT, PRIMARY KEY (UserId, ContactId) )"); } diff --git a/Database/Interfaces/IContactRepository.cs b/Database/Interfaces/IContactRepository.cs index 692e6b9..1857dff 100644 --- a/Database/Interfaces/IContactRepository.cs +++ b/Database/Interfaces/IContactRepository.cs @@ -30,6 +30,7 @@ public interface IContactRemover public interface IContactSetter { void SetContactStatus(long SenderTelegramID, long AccepterTelegramID, string status); + bool SetContactDisplayName(int userId, int contactId, string? displayName); } public interface IContactGetter @@ -39,4 +40,5 @@ public interface IContactGetter DateTime? GetMutedUntil(int userId, int contactId); int GetContactIDByLink(string link); int GetContactByTelegramID(long telegramID); + string? GetContactDisplayName(int userId, int contactId); } diff --git a/Database/Migrations/AddContactDisplayName.cs b/Database/Migrations/AddContactDisplayName.cs new file mode 100644 index 0000000..228f688 --- /dev/null +++ b/Database/Migrations/AddContactDisplayName.cs @@ -0,0 +1,32 @@ +// Copyright (C) 2024-2025 ZenonEl +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Эта программа является свободным программным обеспечением: вы можете распространять и/или изменять +// её на условиях Стандартной общественной лицензии GNU Affero, опубликованной +// Фондом свободного программного обеспечения, либо версии 3 лицензии, либо +// (по вашему выбору) любой более поздней версии. + +using FluentMigrator; + +namespace TelegramMediaRelayBot.Database.Migrations; + +[Migration(20260329)] +public class AddContactDisplayName : Migration +{ + public override void Up() + { + if (!Schema.Table("Contacts").Column("DisplayName").Exists()) + { + Alter.Table("Contacts") + .AddColumn("DisplayName").AsString(255).Nullable(); + } + } + + public override void Down() + { + Delete.Column("DisplayName").FromTable("Contacts"); + } +} diff --git a/Database/Repositories/SqLite/Contacts.cs b/Database/Repositories/SqLite/Contacts.cs index fe25155..d591586 100644 --- a/Database/Repositories/SqLite/Contacts.cs +++ b/Database/Repositories/SqLite/Contacts.cs @@ -292,10 +292,10 @@ public class SqliteContactSetter(string connectionString) : IContactSetter public void SetContactStatus(long SenderTelegramID, long AccepterTelegramID, string status) { const string query = @" - UPDATE Contacts - SET Status = @Status + UPDATE Contacts + SET Status = @Status WHERE UserId = @UserId AND ContactId = @ContactId"; - + SqliteContactGetter contactGetter = new(_connectionString); SqliteUserGetter userGetter = new(_connectionString); @@ -303,7 +303,7 @@ UPDATE Contacts { try { - connection.Execute(query, new + connection.Execute(query, new { Status = status, UserId = userGetter.GetUserIDbyTelegramID(SenderTelegramID), @@ -316,6 +316,28 @@ UPDATE Contacts } } } + + public bool SetContactDisplayName(int userId, int contactId, string? displayName) + { + const string query = @" + UPDATE Contacts + SET DisplayName = @displayName + WHERE UserId = @userId AND ContactId = @contactId"; + + using (var connection = new SqliteConnection(_connectionString)) + { + try + { + int affected = connection.Execute(query, new { userId, contactId, displayName }); + return affected > 0; + } + catch (Exception ex) + { + Log.Error("Error editing database: " + ex.Message); + return false; + } + } + } } public class SqliteContactGetter(string connectionString) : IContactGetter @@ -434,4 +456,22 @@ public int GetContactByTelegramID(long telegramID) return -1; } } + + public string? GetContactDisplayName(int userId, int contactId) + { + const string query = @" + SELECT DisplayName + FROM Contacts + WHERE UserId = @userId AND ContactId = @contactId"; + try + { + using var connection = new SqliteConnection(_connectionString); + return connection.QueryFirstOrDefault(query, new { userId, contactId }); + } + catch (Exception ex) + { + Log.Error(ex, "An error occurred in the method {MethodName}", nameof(GetContactDisplayName)); + return null; + } + } } \ No newline at end of file diff --git a/Resources/texts.resx b/Resources/texts.resx index cf9c56a..b744dc5 100644 --- a/Resources/texts.resx +++ b/Resources/texts.resx @@ -173,6 +173,23 @@ Please specify the group ID for work: Specify the contact ID to delete: + + + To rename a contact, provide their ID or link. + + + Enter a new display name for this contact: + + + Reset to original + + + Display name changed to: {0} + + + Display name has been reset to the original. + + To mute a person (you won't receive videos from them), you need to provide either their ID or their link. diff --git a/Resources/texts.ru-RU.resx b/Resources/texts.ru-RU.resx index c3e4810..e130a92 100644 --- a/Resources/texts.ru-RU.resx +++ b/Resources/texts.ru-RU.resx @@ -172,6 +172,23 @@ ID: {0} Укажите ID контактов для удаления: + + + Чтобы переименовать контакт, укажите его ID или ссылку. + + + Введите новое отображаемое имя для этого контакта: + + + Сбросить к оригиналу + + + Отображаемое имя изменено на: {0} + + + Отображаемое имя сброшено к оригиналу. + + Чтобы замутить человека (вы не будете получать от него медиа) вам нужно указать либо его ID либо его ссылку diff --git a/TelegramBot/Handlers/ICallBackQuery/CallbackNames.cs b/TelegramBot/Handlers/ICallBackQuery/CallbackNames.cs index c7b2243..862afdc 100644 --- a/TelegramBot/Handlers/ICallBackQuery/CallbackNames.cs +++ b/TelegramBot/Handlers/ICallBackQuery/CallbackNames.cs @@ -32,6 +32,7 @@ public static class CallbackNames public const string ViewContacts = "view_contacts"; public const string MuteContact = "mute_contact"; public const string UnmuteContact = "unmute_contact"; + public const string RenameContact = "edit_contact_name"; public const string DeleteContact = "delete_contact"; // Settings diff --git a/TelegramBot/Handlers/ICallBackQuery/IContactsCallbackQuery.cs b/TelegramBot/Handlers/ICallBackQuery/IContactsCallbackQuery.cs index bf65789..144f2da 100644 --- a/TelegramBot/Handlers/ICallBackQuery/IContactsCallbackQuery.cs +++ b/TelegramBot/Handlers/ICallBackQuery/IContactsCallbackQuery.cs @@ -220,6 +220,31 @@ public async Task ExecuteAsync(Update update, ITelegramBotClient botClient, Canc } } +public class RenameContactCommand : IBotCallbackQueryHandlers +{ + private readonly IContactSetter _contactSetterRepository; + private readonly IContactGetter _contactGetterRepository; + private readonly IUserGetter _userGetter; + + public RenameContactCommand( + IContactSetter contactSetterRepository, + IContactGetter contactGetterRepository, + IUserGetter userGetter) + { + _contactSetterRepository = contactSetterRepository; + _contactGetterRepository = contactGetterRepository; + _userGetter = userGetter; + } + + public string Name => "edit_contact_name"; + + public async Task ExecuteAsync(Update update, ITelegramBotClient botClient, CancellationToken ct) + { + long chatId = update.CallbackQuery!.Message!.Chat.Id; + await Contacts.RenameContact(botClient, update, chatId, _contactSetterRepository, _contactGetterRepository, _userGetter); + } +} + public class DeleteContactCommand : IBotCallbackQueryHandlers { private readonly IContactRemover _contactRemoverRepository; diff --git a/TelegramBot/Menu/Contacts.cs b/TelegramBot/Menu/Contacts.cs index bfa855f..42f7a5e 100644 --- a/TelegramBot/Menu/Contacts.cs +++ b/TelegramBot/Menu/Contacts.cs @@ -73,6 +73,18 @@ public static async Task ViewContacts(ITelegramBotClient botClient, Update updat await CommonUtilities.SendMessage(botClient, update, KeyboardUtils.GetViewContactsKeyboardMarkup(), cancellationToken, $"{Config.GetResourceString("YourContacts")}\n{string.Join("\n", contactUsersInfo)}"); } + public static async Task RenameContact( + ITelegramBotClient botClient, + Update update, + long chatId, + IContactSetter contactSetterRepository, + IContactGetter contactGetterRepository, + IUserGetter userGetter) + { + await botClient.SendMessage(update.CallbackQuery!.Message!.Chat.Id, Config.GetResourceString("RenameContactInstructions"), cancellationToken: cancellationToken); + UserSessionManager.Set(chatId, new ProcessRenameContactState(contactSetterRepository, contactGetterRepository, userGetter)); + } + public static async Task EditContactGroup( ITelegramBotClient botClient, Update update, diff --git a/TelegramBot/States/RenameContact.cs b/TelegramBot/States/RenameContact.cs new file mode 100644 index 0000000..b5c6eab --- /dev/null +++ b/TelegramBot/States/RenameContact.cs @@ -0,0 +1,121 @@ +// Copyright (C) 2024-2025 ZenonEl +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Эта программа является свободным программным обеспечением: вы можете распространять и/или изменять +// её на условиях Стандартной общественной лицензии GNU Affero, опубликованной +// Фондом свободного программного обеспечения, либо версии 3 лицензии, либо +// (по вашему выбору) любой более поздней версии. + +using TelegramMediaRelayBot.Database.Interfaces; +using TelegramMediaRelayBot.TelegramBot.Utils; + + +namespace TelegramMediaRelayBot; + +public class ProcessRenameContactState : IUserState +{ + public UserRenameContactState currentState; + + private int userId { get; set; } + private int targetContactId { get; set; } + private readonly IContactSetter _contactSetter; + private readonly IContactGetter _contactGetter; + private readonly IUserGetter _userGetter; + + public ProcessRenameContactState( + IContactSetter contactSetter, + IContactGetter contactGetter, + IUserGetter userGetter + ) + { + currentState = UserRenameContactState.WaitingForLinkOrID; + _contactSetter = contactSetter; + _contactGetter = contactGetter; + _userGetter = userGetter; + } + + public string GetCurrentState() + { + return currentState.ToString(); + } + + public async Task ProcessState(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) + { + long chatId = CommonUtilities.GetIDfromUpdate(update); + if (CommonUtilities.CheckNonZeroID(chatId)) return; + + if (!UserSessionManager.TryGetValue(chatId, out IUserState? value)) + { + return; + } + + var userState = (ProcessRenameContactState)value; + + switch (userState.currentState) + { + case UserRenameContactState.WaitingForLinkOrID: + int contactId; + if (int.TryParse(update.Message!.Text, out contactId)) + { + List allowedIds = await _contactGetter.GetAllContactUserTGIds(_userGetter.GetUserIDbyTelegramID(update.Message.Chat.Id)); + string name = _userGetter.GetUserNameByID(contactId); + if (name == "" || !allowedIds.Contains(_userGetter.GetTelegramIDbyUserID(contactId))) + { + await CommonUtilities.AlertMessageAndShowMenu(botClient, update, chatId, Config.GetResourceString("NoUserFoundByID")); + return; + } + await botClient.SendMessage(chatId, string.Format(Config.GetResourceString("WillWorkWithContact"), contactId, name), cancellationToken: cancellationToken, + replyMarkup: ReplyKeyboardUtils.GetSingleButtonKeyboardMarkup(Config.GetResourceString("NextButtonText"))); + } + else + { + string link = update.Message.Text!; + contactId = _contactGetter.GetContactIDByLink(link); + List allowedIds = await _contactGetter.GetAllContactUserTGIds(_userGetter.GetUserIDbyTelegramID(update.Message.Chat.Id)); + + if (contactId == -1 || !allowedIds.Contains(_userGetter.GetTelegramIDbyUserID(contactId))) + { + await CommonUtilities.AlertMessageAndShowMenu(botClient, update, chatId, Config.GetResourceString("NoUserFoundByLink")); + return; + } + string name = _userGetter.GetUserNameByID(contactId); + await botClient.SendMessage(chatId, string.Format(Config.GetResourceString("WillWorkWithContact"), contactId, name), cancellationToken: cancellationToken); + } + userState.userId = _userGetter.GetUserIDbyTelegramID(chatId); + userState.targetContactId = contactId; + await botClient.SendMessage(chatId, Config.GetResourceString("InputNewDisplayName"), cancellationToken: cancellationToken, + replyMarkup: ReplyKeyboardUtils.GetSingleButtonKeyboardMarkup(Config.GetResourceString("ResetDisplayNameButtonText"))); + userState.currentState = UserRenameContactState.WaitingForNewName; + break; + + case UserRenameContactState.WaitingForNewName: + if (await CommonUtilities.HandleStateBreakCommand(botClient, update, chatId)) return; + + string newName = update.Message!.Text!; + string? displayName = newName.Equals(Config.GetResourceString("ResetDisplayNameButtonText"), StringComparison.OrdinalIgnoreCase) + ? null + : newName; + + await ReplyKeyboardUtils.RemoveReplyMarkup(botClient, chatId, cancellationToken); + + bool success = _contactSetter.SetContactDisplayName(userState.userId, userState.targetContactId, displayName); + UserSessionManager.Remove(chatId); + + if (success) + { + string resultText = displayName != null + ? string.Format(Config.GetResourceString("DisplayNameSet"), displayName) + : Config.GetResourceString("DisplayNameReset"); + await CommonUtilities.AlertMessageAndShowMenu(botClient, update, chatId, resultText); + } + else + { + await CommonUtilities.AlertMessageAndShowMenu(botClient, update, chatId, Config.GetResourceString("ActionCancelledError")); + } + break; + } + } +} diff --git a/TelegramBot/States/States.cs b/TelegramBot/States/States.cs index 3608180..704589c 100644 --- a/TelegramBot/States/States.cs +++ b/TelegramBot/States/States.cs @@ -58,6 +58,13 @@ public enum UserInboundState Finish } +public enum UserRenameContactState +{ + WaitingForLinkOrID, + WaitingForNewName, + Finish +} + public enum UsersStandardState { ProcessAction,