Импорт групповых чатом, мелкие правки, внешний вид чата, GIF тип

This commit is contained in:
Халимов Рустам
2026-03-17 00:57:01 +03:00
parent 97b70250de
commit 1fd6ef0c48
19 changed files with 358 additions and 144 deletions
@@ -62,7 +62,8 @@ public class AdminController : ControllerBase
DisplayName = u.DisplayName,
Avatar = u.Avatar,
CreatedAt = u.CreatedAt,
LastOnlineAt = u.CreatedAt // fallback
IsOnline = Knot.Modules.Chats.Infrastructure.SignalR.ChatHub.IsUserOnline(u.Id.ToString()),
LastOnlineAt = Knot.Modules.Chats.Infrastructure.SignalR.ChatHub.IsUserOnline(u.Id.ToString()) ? DateTime.UtcNow : u.CreatedAt
}));
}
@@ -101,7 +102,8 @@ public class AdminController : ControllerBase
Bio = targetUser.Bio,
Avatar = targetUser.Avatar,
CreatedAt = targetUser.CreatedAt,
LastOnlineAt = targetUser.CreatedAt, // fallback
IsOnline = Knot.Modules.Chats.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString()),
LastOnlineAt = Knot.Modules.Chats.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString()) ? DateTime.UtcNow : targetUser.CreatedAt,
Stats = new
{
MessagesCount = messagesCount,
@@ -93,8 +93,8 @@ public sealed class MessagesController : ControllerBase
if (rUser != null) senders[r.UserId] = rUser;
}
var userObj = senders.TryGetValue(r.UserId, out var ru)
? new { id = ru.Id, username = ru.Username, displayName = ru.DisplayName }
: new { id = r.UserId, username = "unknown", displayName = "Unknown" };
? new { id = ru.Id, username = ru.Username, displayName = ru.DisplayName, avatar = ru.Avatar }
: new { id = r.UserId, username = "unknown", displayName = "Unknown", avatar = (string?)null };
reactionsWithUser.Add(new
{
@@ -141,7 +141,7 @@ public sealed class MessagesController : ControllerBase
displayName = s.DisplayName,
avatar = s.Avatar
} : null,
readBy = m.ReadBy.Select(r => r.UserId).ToList(),
readBy = m.ReadBy.Select(r => new { userId = r.UserId }).ToList(),
reactions = reactionsWithUser
});
}
@@ -266,7 +266,10 @@ public sealed class MessagesController : ControllerBase
var filteredMedia = m.Media.Where(media => {
var mediaType = media.Type?.ToLower() ?? "file";
if (filterType == "media") return mediaType == "image" || mediaType == "video";
var isGif = mediaType == "image" && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase));
if (filterType == "media") return (mediaType == "image" || mediaType == "video") && !isGif;
if (filterType == "gifs") return isGif;
if (filterType == "files") return mediaType != "image" && mediaType != "video" && mediaType != "link";
return true;
}).ToList();
@@ -117,7 +117,7 @@ public sealed class TelegramImportController : ControllerBase
});
}
public sealed record ExecuteImportRequest(Guid Token, Dictionary<string, Guid> Mapping);
public sealed record ExecuteImportRequest(Guid Token, Dictionary<string, Guid> Mapping, string? GroupName);
[HttpPost("execute")]
public async Task<IActionResult> Execute([FromBody] ExecuteImportRequest req, CancellationToken ct)
@@ -161,7 +161,7 @@ public sealed class TelegramImportController : ControllerBase
else
{
// Create a group
var command = new CreateChatCommand("Импортированный чат", ChatType.Group, chatMembers);
var command = new CreateChatCommand(req.GroupName ?? "Импортированный чат", ChatType.Group, chatMembers);
var res = await _sender.Send(command, ct);
if (res.IsFailure) return BadRequest(res.Error);
chatId = res.Value;
@@ -224,15 +224,25 @@ public sealed class TelegramImportController : ControllerBase
Guid senderGuid = lastSenderGuid;
string content = "";
if (textNode != null)
var mainBodyNode = node.QuerySelector(".body");
var isForwarded = node.QuerySelector(".forwarded") != null;
// To avoid grabbing text from inside the forwarded block as main text,
// we can look for .text that is a direct child of the main .body
// The .forwarded block has its own .text
var contentTextNode = isForwarded
? (node.QuerySelector(".body > .text") ?? node.QuerySelector(".text:not(.forwarded .text)"))
: node.QuerySelector(".text");
if (contentTextNode != null)
{
var html = textNode.InnerHtml
var html = contentTextNode.InnerHtml
.Replace("<br>", "\n", StringComparison.OrdinalIgnoreCase)
.Replace("<br/>", "\n", StringComparison.OrdinalIgnoreCase)
.Replace("<br />", "\n", StringComparison.OrdinalIgnoreCase);
var tempParser = new AngleSharp.Html.Parser.HtmlParser();
var tempDoc = tempParser.ParseDocument("<div>" + html + "</div>");
content = tempDoc.Body.TextContent.Trim();
content = tempDoc.Body?.TextContent.Trim() ?? "";
}
DateTime createdAt = lastCreatedAt;
@@ -282,6 +292,7 @@ public sealed class TelegramImportController : ControllerBase
}
// Parse forwards
Guid? forwardedFromId = null;
var forwardedNode = node.QuerySelector(".forwarded.body");
if (forwardedNode != null)
{
@@ -294,6 +305,15 @@ public sealed class TelegramImportController : ControllerBase
}
var fwdName = fwdNameText != null ? fwdNameText.TextContent.Trim() : "Неизвестного";
if (req.Mapping.TryGetValue(fwdName, out var mappedFwdId) && mappedFwdId != Guid.Empty)
{
forwardedFromId = mappedFwdId;
}
else if (fwdName == "Это я" || fwdName == req.Mapping.FirstOrDefault(x => x.Value == myId).Key)
{
forwardedFromId = myId;
}
var fwdTextNode = forwardedNode.QuerySelector(".text");
string fwdContent = "";
if (fwdTextNode != null)
@@ -304,12 +324,20 @@ public sealed class TelegramImportController : ControllerBase
.Replace("<br />", "\n", StringComparison.OrdinalIgnoreCase);
var tempParser = new AngleSharp.Html.Parser.HtmlParser();
var tempDoc = tempParser.ParseDocument("<div>" + fHtml + "</div>");
fwdContent = tempDoc.Body.TextContent.Trim();
fwdContent = tempDoc.Body?.TextContent.Trim() ?? "";
}
content = string.IsNullOrEmpty(content)
? $"[Переслано от {fwdName}]:\n{fwdContent}"
: $"{content}\n\n[Переслано от {fwdName}]:\n{fwdContent}";
if (forwardedFromId == null)
{
// We don't have this user in the app, map to generic string
content = string.IsNullOrEmpty(content)
? $"[Переслано от {fwdName}]:\n{fwdContent}"
: $"{content}\n\n[Переслано от {fwdName}]:\n{fwdContent}";
}
else if (string.IsNullOrEmpty(content))
{
content = fwdContent;
}
}
// Parse replies
@@ -356,6 +384,15 @@ public sealed class TelegramImportController : ControllerBase
if (firstHref != null)
{
messageType = GetMediaTypes(firstHref).mType;
if (mediaNodes[0].ClassName?.Contains("animated") == true || firstHref.EndsWith(".mp4"))
{
// Treat telegram animated gifs as image format in app (we auto-loop mp4 images)
// But web app specifically handles "image" and "video" and microlink crashes on mp4 video
// Let's just keep "video" or "image". Actually, telegram exports gifs as mp4.
// We should let our app player handle it.
if (mediaNodes[0].ClassName?.Contains("animated") == true)
messageType = "image";
}
}
}
@@ -370,7 +407,7 @@ public sealed class TelegramImportController : ControllerBase
else
{
string finalContent = content;
targetMessage = Message.Import(chatId, senderGuid, finalContent, messageType, createdAt, replyToId, null);
targetMessage = Message.Import(chatId, senderGuid, finalContent, messageType, createdAt, replyToId, forwardedFromId);
var idAttr = node.GetAttribute("id");
if (!string.IsNullOrEmpty(idAttr))
@@ -396,8 +433,10 @@ public sealed class TelegramImportController : ControllerBase
ms.Position = 0;
var types = GetMediaTypes(href);
var finalMType = types.mType;
if (mediaNode.ClassName?.Contains("animated") == true) finalMType = "image";
var fileId = await _fileStorage.UploadFileAsync(ms, Path.GetFileName(href), types.cType);
targetMessage.AddMedia(types.mType, $"/api/files/{fileId}", Path.GetFileName(href), zipEntry.Length);
targetMessage.AddMedia(finalMType, $"/api/files/{fileId}", Path.GetFileName(href), zipEntry.Length);
}
}
}
@@ -23,6 +23,7 @@ public sealed class ChatHub : Hub
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, ParticipantInfo>> _groupCallParticipants = new();
public static int OnlineUsersCount => _userConnections.Count;
public static bool IsUserOnline(string userId) => _userConnections.ContainsKey(userId);
private readonly ISender _sender;
private readonly IUserContext _userContext;
@@ -8,14 +8,14 @@ public class SystemSettingsDto
public int MaxFileSizeMb { get; set; } = 100;
// Calls
public bool EnableCalls { get; set; } = true;
public bool EnableCalls { get; set; } = false;
public string TurnHost { get; set; } = string.Empty;
public int TurnPort { get; set; } = 3478;
public string TurnUser { get; set; } = string.Empty;
public string TurnSecret { get; set; } = string.Empty;
// Klipy
public bool EnableKlipy { get; set; } = true;
public bool EnableKlipy { get; set; } = false;
public string KlipyApiKey { get; set; } = string.Empty;
public string KlipyCustomerId { get; set; } = string.Empty;