补齐群组详情:支持查看管理员与复制加入链接

This commit is contained in:
meoacgx committed 2025-12-19 19:10:45 +08:00
1 parent 34bda77560
commit eb3c3ae978
4 files changed
+353 -7

No files matched your search

@@ -16,4 +16,14 @@ public interface IGroupService
/// 获取群组详情
/// </summary>
Task<GroupInfo?> GetGroupInfoAsync(int accountId, long groupId);
/// <summary>
/// 导出加入链接:公开群组返回 t.me 链接;否则导出邀请链接。
/// </summary>
Task<string> ExportJoinLinkAsync(int accountId, long groupId);
/// <summary>
/// 获取群组管理员列表(需要权限)
/// </summary>
Task<List<ChannelAdminInfo>> GetAdminsAsync(int accountId, long groupId);
}
@@ -111,6 +111,148 @@ public class GroupService : IGroupService
return groups.FirstOrDefault(g => g.TelegramId == groupId);
}
public async Task<string> ExportJoinLinkAsync(int accountId, long groupId)
{
var client = await GetOrCreateConnectedClientAsync(accountId);
var dialogs = await client.Messages_GetAllDialogs();
// 基础群组(Chat)
var basic = dialogs.chats.Values.OfType<Chat>().FirstOrDefault(c => c.IsActive && c.id == groupId);
if (basic != null)
{
var invite = await client.Messages_ExportChatInvite(basic);
var link = invite switch
{
ChatInviteExported e => e.link,
_ => null
};
if (string.IsNullOrWhiteSpace(link))
throw new InvalidOperationException("无法导出群组邀请链接(可能无权限)");
return link;
}
// 超级群组(Channel 但 megagroup=true,即 !IsChannel)
var mega = dialogs.chats.Values.OfType<Channel>().FirstOrDefault(c => c.IsActive && !c.IsChannel && c.id == groupId);
if (mega != null)
{
if (!string.IsNullOrWhiteSpace(mega.MainUsername))
return $"https://t.me/{mega.MainUsername}";
var invite = await client.Messages_ExportChatInvite(mega);
var link = invite switch
{
ChatInviteExported e => e.link,
_ => null
};
if (string.IsNullOrWhiteSpace(link))
throw new InvalidOperationException("无法导出群组邀请链接(可能无权限)");
return link;
}
throw new InvalidOperationException($"群组 {groupId} not found");
}
public async Task<List<ChannelAdminInfo>> GetAdminsAsync(int accountId, long groupId)
{
var client = await GetOrCreateConnectedClientAsync(accountId);
var dialogs = await client.Messages_GetAllDialogs();
// 基础群组(Chat)
var basic = dialogs.chats.Values.OfType<Chat>().FirstOrDefault(c => c.IsActive && c.id == groupId);
if (basic != null)
{
var fullChat = await client.Messages_GetFullChat(basic.id);
if (fullChat.full_chat is not ChatFull cf || cf.participants is not ChatParticipants cp)
return new List<ChannelAdminInfo>();
var list = new List<ChannelAdminInfo>();
foreach (var p in cp.participants)
{
long userId;
var isCreator = p is ChatParticipantCreator;
if (p is ChatParticipantCreator creator)
{
userId = creator.user_id;
}
else if (p is ChatParticipantAdmin admin)
{
userId = admin.user_id;
}
else
{
continue;
}
fullChat.users.TryGetValue(userId, out var u);
list.Add(new ChannelAdminInfo(
UserId: userId,
Username: u?.MainUsername,
FirstName: u?.first_name,
LastName: u?.last_name,
IsCreator: isCreator,
Rank: null
));
}
return list
.OrderByDescending(x => x.IsCreator)
.ThenBy(x => x.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
}
// 超级群组(Channel but !IsChannel)
var mega = dialogs.chats.Values.OfType<Channel>().FirstOrDefault(c => c.IsActive && !c.IsChannel && c.id == groupId);
if (mega != null)
{
var participants = await client.Channels_GetParticipants(mega, new ChannelParticipantsAdmins());
var list = new List<ChannelAdminInfo>();
foreach (var p in participants.participants)
{
long userId;
string? rank = null;
var isCreator = p is ChannelParticipantCreator;
if (p is ChannelParticipantAdmin admin)
{
userId = admin.user_id;
rank = string.IsNullOrWhiteSpace(admin.rank) ? null : admin.rank;
}
else if (p is ChannelParticipantCreator creator)
{
userId = creator.user_id;
rank = string.IsNullOrWhiteSpace(creator.rank) ? null : creator.rank;
}
else
{
continue;
}
participants.users.TryGetValue(userId, out var u);
list.Add(new ChannelAdminInfo(
UserId: userId,
Username: u?.MainUsername,
FirstName: u?.first_name,
LastName: u?.last_name,
IsCreator: isCreator,
Rank: rank
));
}
return list
.OrderByDescending(x => x.IsCreator)
.ThenBy(x => x.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
}
throw new InvalidOperationException($"群组 {groupId} not found");
}
private async Task<WTelegram.Client> GetOrCreateConnectedClientAsync(int accountId)
{
var existing = _clientPool.GetClient(accountId);
@@ -0,0 +1,183 @@
@namespace TelegramPanel.Web.Components.Dialogs
@inject GroupManagementService GroupManagement
@inject AccountManagementService AccountManagement
@inject TelegramPanel.Core.Interfaces.IGroupService GroupService
@inject ISnackbar Snackbar
@inject IJSRuntime JS
@using TelegramPanel.Data.Entities
@using TelegramPanel.Core.Models
<MudDialog>
<DialogContent>
@if (loading)
{
<MudProgressLinear Indeterminate="true" />
}
else if (group == null)
{
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined">未找到群组数据</MudAlert>
}
else
{
<MudStack Spacing="2">
<MudText Typo="Typo.h6">@group.Title</MudText>
<MudGrid>
<MudItem xs="12" md="6">
<MudText Typo="Typo.body2"><strong>TelegramId:</strong>@group.TelegramId</MudText>
<MudText Typo="Typo.body2"><strong>用户名:</strong>@(string.IsNullOrWhiteSpace(group.Username) ? "(私密)" : "@" + group.Username)</MudText>
<MudText Typo="Typo.body2"><strong>成员数:</strong>@group.MemberCount</MudText>
</MudItem>
<MudItem xs="12" md="6">
<MudText Typo="Typo.body2"><strong>创建者:</strong>@creatorDisplay</MudText>
<MudText Typo="Typo.body2"><strong>创建时间:</strong>@(group.CreatedAt?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") ?? "(未知)")</MudText>
<MudText Typo="Typo.body2"><strong>最后同步:</strong>@group.SyncedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")</MudText>
</MudItem>
</MudGrid>
@if (!string.IsNullOrWhiteSpace(group.About))
{
<MudText Typo="Typo.body2"><strong>简介:</strong></MudText>
<MudPaper Class="pa-3" Elevation="0" Style="background: rgba(0,0,0,0.03);">
<MudText Typo="Typo.body2" Style="white-space: pre-wrap;">@group.About</MudText>
</MudPaper>
}
<MudDivider />
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudText Typo="Typo.h6">管理员</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh"
Disabled="@adminsLoading" OnClick="LoadAdmins">
刷新
</MudButton>
</MudStack>
@if (adminsLoading)
{
<MudProgressLinear Indeterminate="true" />
}
else if (admins.Count == 0)
{
<MudText Typo="Typo.body2" Color="Color.Secondary">暂无管理员数据(或无权限获取)</MudText>
}
else
{
<MudTable Items="@admins" Dense="true" Hover="true" Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>身份</MudTh>
<MudTh>昵称</MudTh>
<MudTh>用户名</MudTh>
<MudTh>头衔</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="身份">
<MudChip T="string" Size="Size.Small" Color="@(context.IsCreator ? Color.Success : Color.Default)">
@(context.IsCreator ? "创建者" : "管理员")
</MudChip>
</MudTd>
<MudTd DataLabel="昵称">@context.DisplayName</MudTd>
<MudTd DataLabel="用户名">@(!string.IsNullOrWhiteSpace(context.Username) ? "@" + context.Username : "-")</MudTd>
<MudTd DataLabel="头衔">@(string.IsNullOrWhiteSpace(context.Rank) ? "-" : context.Rank)</MudTd>
</RowTemplate>
</MudTable>
}
</MudStack>
}
</DialogContent>
<DialogActions>
@if (group != null)
{
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="CopyLink">复制链接</MudButton>
}
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Close">关闭</MudButton>
</DialogActions>
</MudDialog>
@code
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public int GroupDbId { get; set; }
private bool loading = true;
private Group? group;
private string creatorDisplay = "-";
private bool adminsLoading;
private List<ChannelAdminInfo> admins = new();
protected override async Task OnInitializedAsync()
{
await LoadGroup();
await LoadAdmins();
}
private async Task LoadGroup()
{
loading = true;
try
{
group = await GroupManagement.GetGroupAsync(GroupDbId);
if (group == null)
return;
var creator = await AccountManagement.GetAccountAsync(group.CreatorAccountId);
creatorDisplay = creator == null
? $"账号 {group.CreatorAccountId}"
: $"{creator.Phone}({creator.Nickname ?? creator.Username ?? creator.UserId.ToString()})";
}
catch (Exception ex)
{
Snackbar.Add($"加载群组失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private async Task LoadAdmins()
{
if (group == null)
return;
adminsLoading = true;
try
{
admins = await GroupService.GetAdminsAsync(group.CreatorAccountId, group.TelegramId);
}
catch (Exception ex)
{
admins.Clear();
Snackbar.Add($"加载管理员失败:{ex.Message}", Severity.Error);
}
finally
{
adminsLoading = false;
}
}
private async Task CopyLink()
{
if (group == null)
return;
try
{
var link = await GroupService.ExportJoinLinkAsync(group.CreatorAccountId, group.TelegramId);
await JS.InvokeVoidAsync("telegramPanel.copyText", link);
Snackbar.Add("已复制链接", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"复制失败:{ex.Message}", Severity.Error);
}
}
private void Close()
{
MudDialog.Close();
}
}
@@ -1,10 +1,13 @@
@page "/groups"
@inject GroupManagementService GroupManagement
@inject AccountManagementService AccountManagement
@inject IGroupService GroupService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject IJSRuntime JS
@using TelegramPanel.Data.Entities
@using TelegramPanel.Web.Components.Dialogs
<PageTitle>群组列表 - Telegram Panel</PageTitle>
@@ -73,7 +76,7 @@
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small">
<MudMenuItem Icon="@Icons.Material.Filled.Info" OnClick="@(() => ShowDetails(context))">查看详情</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PersonAdd" OnClick="@(() => InviteMembers(context))">邀请成员</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => CopyLink(context))">复制链接</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(async () => await CopyLink(context))">复制链接</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(() => DeleteGroup(context.Id))">删除群组</MudMenuItem>
</MudMenu>
@@ -152,7 +155,9 @@
private void ShowDetails(Group group)
{
Snackbar.Add($"查看群组详情:{group.Title}", Severity.Info);
var parameters = new DialogParameters { ["GroupDbId"] = group.Id };
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
DialogService.Show<GroupDetailsDialog>("群组详情", parameters, options);
}
private void InviteMembers(Group group)
@@ -160,12 +165,18 @@
Navigation.NavigateTo($"/batch/invite?groupId={group.Id}");
}
private void CopyLink(Group group)
private async Task CopyLink(Group group)
{
var link = string.IsNullOrEmpty(group.Username)
? $"私密群组(邀请链接待实现)"
: $"https://t.me/{group.Username}";
Snackbar.Add($"链接:{link}", Severity.Info);
try
{
var link = await GroupService.ExportJoinLinkAsync(group.CreatorAccountId, group.TelegramId);
await JS.InvokeVoidAsync("telegramPanel.copyText", link);
Snackbar.Add("已复制链接", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"复制失败:{ex.Message}", Severity.Error);
}
}
private async Task DeleteGroup(int id)