1049 lines
39 KiB
PHP
1049 lines
39 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
header('Cache-Control: no-store');
|
||
|
||
require __DIR__ . '/im_web_bootstrap.php';
|
||
|
||
function webActionList(mixed $value): array
|
||
{
|
||
if (is_array($value)) {
|
||
$items = $value;
|
||
} else {
|
||
$items = preg_split('/[\s,,;;]+/u', (string)$value) ?: [];
|
||
}
|
||
|
||
return array_values(array_unique(array_filter(array_map(static function ($item): string {
|
||
return trim((string)$item);
|
||
}, $items))));
|
||
}
|
||
|
||
function appPlaceholders(array $values): string
|
||
{
|
||
return implode(',', array_fill(0, count($values), '?'));
|
||
}
|
||
|
||
function resolveWebUserIds(PDO $db, array $values): array
|
||
{
|
||
$values = webActionList($values);
|
||
if (!$values) {
|
||
return [];
|
||
}
|
||
|
||
$resolved = [];
|
||
foreach ($values as $value) {
|
||
$statement = $db->prepare(
|
||
'select `_uid` from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ? limit 1'
|
||
);
|
||
$statement->execute([$value, $value, $value]);
|
||
$uid = $statement->fetchColumn();
|
||
if (is_string($uid) && $uid !== '') {
|
||
$resolved[] = $uid;
|
||
}
|
||
}
|
||
|
||
return array_values(array_unique($resolved));
|
||
}
|
||
|
||
function webGroupConversation(array $group, string $role = 'member'): array
|
||
{
|
||
$name = (string)($group['name'] ?? $group['_name'] ?? $group['group_id'] ?? $group['_gid'] ?? '');
|
||
$groupId = (string)($group['group_id'] ?? $group['_gid'] ?? '');
|
||
$category = (string)($group['category'] ?? 'group');
|
||
$title = $name !== '' ? $name : $groupId;
|
||
$updatedAt = (int)($group['updated_at_ms'] ?? $group['_dt'] ?? webImNowMs());
|
||
|
||
return [
|
||
'id' => 'group-' . $groupId,
|
||
'type' => $category === 'channel' ? 'channel' : 'group',
|
||
'target' => $groupId,
|
||
'title' => $title,
|
||
'subtitle' => $category === 'channel' ? '公告群' : '群组',
|
||
'unread' => 0,
|
||
'avatarText' => mb_strtoupper(mb_substr($title, 0, 1, 'UTF-8'), 'UTF-8'),
|
||
'updatedAt' => $updatedAt,
|
||
'role' => $role,
|
||
'raw' => [
|
||
'type' => 'group',
|
||
'target' => $groupId,
|
||
'line' => 0,
|
||
'category' => $category,
|
||
'role' => $role,
|
||
],
|
||
];
|
||
}
|
||
|
||
function fetchWebGroup(PDO $db, string $groupKey): ?array
|
||
{
|
||
$statement = $db->prepare(
|
||
'select * from `t_web_groups`'
|
||
. ' where `deleted` = 0 and (`group_id` = ? or `name` = ?)'
|
||
. ' order by (`group_id` = ?) desc limit 1'
|
||
);
|
||
$statement->execute([$groupKey, $groupKey, $groupKey]);
|
||
$group = $statement->fetch();
|
||
|
||
return is_array($group) ? $group : null;
|
||
}
|
||
|
||
function fetchWebGroupRole(PDO $db, string $groupId, string $userId): string
|
||
{
|
||
$statement = $db->prepare(
|
||
'select `role` from `t_web_group_members`'
|
||
. ' where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1'
|
||
);
|
||
$statement->execute([$groupId, $userId]);
|
||
$role = $statement->fetchColumn();
|
||
|
||
return is_string($role) && $role !== '' ? $role : 'member';
|
||
}
|
||
|
||
function fetchWebGroupMember(PDO $db, string $groupId, string $userId): array
|
||
{
|
||
$statement = $db->prepare(
|
||
'select * from `t_web_group_members`'
|
||
. ' where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1'
|
||
);
|
||
$statement->execute([$groupId, $userId]);
|
||
$member = $statement->fetch();
|
||
|
||
return is_array($member) ? $member : [];
|
||
}
|
||
|
||
function requireWebGroupMember(PDO $db, string $groupId, string $userId): array
|
||
{
|
||
$member = fetchWebGroupMember($db, $groupId, $userId);
|
||
if (!$member) {
|
||
respond(403, '你不在该群组中');
|
||
}
|
||
|
||
return $member;
|
||
}
|
||
|
||
function requireWebGroupRole(PDO $db, string $groupId, string $userId, array $roles): array
|
||
{
|
||
$member = requireWebGroupMember($db, $groupId, $userId);
|
||
if (!in_array((string)$member['role'], $roles, true)) {
|
||
respond(403, '没有群组管理权限');
|
||
}
|
||
|
||
return $member;
|
||
}
|
||
|
||
function upsertWebGroupMember(PDO $db, string $groupId, string $userId, string $role): void
|
||
{
|
||
$now = webImNowMs();
|
||
$statement = $db->prepare(
|
||
'insert into `t_web_group_members`'
|
||
. ' (`group_id`, `user_id`, `role`, `joined_at_ms`, `updated_at_ms`, `status`)'
|
||
. ' values (?, ?, ?, ?, ?, 0)'
|
||
. ' on duplicate key update'
|
||
. ' `role` = if(`role` = "owner", `role`, values(`role`)),'
|
||
. ' `updated_at_ms` = values(`updated_at_ms`),'
|
||
. ' `status` = 0'
|
||
);
|
||
$statement->execute([$groupId, $userId, $role, $now, $now]);
|
||
}
|
||
|
||
function createNativeGroup(string $operator, string $groupId, string $name, string $category, array $members): array
|
||
{
|
||
$nativeMembers = [];
|
||
foreach ($members as $memberId) {
|
||
$nativeMembers[] = [
|
||
'member_id' => $memberId,
|
||
'type' => $memberId === $operator ? 2 : 0,
|
||
];
|
||
}
|
||
|
||
return requireImSuccess(imAdminPost('/admin/group/create', [
|
||
'operator' => $operator,
|
||
'group' => [
|
||
'group_info' => [
|
||
'target_id' => $groupId,
|
||
'name' => $name,
|
||
'owner' => $operator,
|
||
'type' => 0,
|
||
'mute' => $category === 'channel' ? 1 : 0,
|
||
'join_type' => 0,
|
||
'searchable' => 1,
|
||
'history_message' => 1,
|
||
'extra' => json_encode(['webCategory' => $category], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||
],
|
||
'members' => $nativeMembers,
|
||
],
|
||
'to_lines' => [0],
|
||
]), '原生群组创建失败');
|
||
}
|
||
|
||
function addNativeGroupMember(string $operator, string $groupId, string $userId): void
|
||
{
|
||
requireImSuccess(imAdminPost('/admin/group/member/add', [
|
||
'operator' => $operator,
|
||
'group_id' => $groupId,
|
||
'members' => [
|
||
['member_id' => $userId, 'type' => 0],
|
||
],
|
||
'to_lines' => [0],
|
||
]), '加入原生群组失败');
|
||
}
|
||
|
||
function removeNativeGroupMember(string $operator, string $groupId, array $userIds): void
|
||
{
|
||
if (!$userIds) {
|
||
return;
|
||
}
|
||
|
||
requireImSuccess(imAdminPost('/admin/group/member/del', [
|
||
'operator' => $operator,
|
||
'group_id' => $groupId,
|
||
'members' => array_values($userIds),
|
||
'to_lines' => [0],
|
||
]), '移除原生成员失败');
|
||
}
|
||
|
||
function dismissNativeGroup(string $operator, string $groupId): void
|
||
{
|
||
requireImSuccess(imAdminPost('/admin/group/del', [
|
||
'operator' => $operator,
|
||
'group_id' => $groupId,
|
||
'to_lines' => [0],
|
||
]), '解散原生群组失败');
|
||
}
|
||
|
||
function transferNativeGroup(string $operator, string $groupId, string $newOwner): void
|
||
{
|
||
requireImSuccess(imAdminPost('/admin/group/transfer', [
|
||
'operator' => $operator,
|
||
'group_id' => $groupId,
|
||
'new_owner' => $newOwner,
|
||
'to_lines' => [0],
|
||
]), '转让原生群组失败');
|
||
}
|
||
|
||
function sendWebGroupWelcomeMessage(PDO $appDb, string $groupId, string $senderId, string $welcomeText): void
|
||
{
|
||
$text = trim($welcomeText);
|
||
if ($text === '') {
|
||
return;
|
||
}
|
||
|
||
$now = webImNowMs();
|
||
$messageUid = webImMessageUid();
|
||
$statement = $appDb->prepare(
|
||
'insert into `t_web_group_messages`'
|
||
. ' (`message_uid`, `group_id`, `sender_uid`, `text`, `created_at_ms`, `edited_at_ms`, `recalled`)'
|
||
. ' values (?, ?, ?, ?, ?, 0, 0)'
|
||
);
|
||
$statement->execute([$messageUid, $groupId, $senderId, $text, $now]);
|
||
$update = $appDb->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?');
|
||
$update->execute([$now, $groupId]);
|
||
|
||
try {
|
||
requireImSuccess(imAdminPost('/admin/message/send', [
|
||
'sender' => $senderId,
|
||
'conv' => [
|
||
'type' => 1,
|
||
'target' => $groupId,
|
||
'line' => 0,
|
||
],
|
||
'payload' => [
|
||
'type' => 1,
|
||
'searchableContent' => $text,
|
||
'pushContent' => $text,
|
||
'content' => $text,
|
||
'persistFlag' => 3,
|
||
],
|
||
'userMessage' => true,
|
||
]), '欢迎词发送失败');
|
||
} catch (Throwable) {
|
||
}
|
||
}
|
||
|
||
function createWebGroupAction(PDO $imDb, PDO $appDb, string $userId, array $payload, string $category): array
|
||
{
|
||
$name = trim((string)($payload['name'] ?? ''));
|
||
$announcement = trim((string)($payload['announcement'] ?? ''));
|
||
$welcomeText = trim((string)($payload['welcomeText'] ?? ''));
|
||
if ($name === '') {
|
||
respond(400, $category === 'channel' ? '请输入公告群名称' : '请输入群组名称');
|
||
}
|
||
|
||
$memberIds = resolveWebUserIds($imDb, webActionList($payload['memberIds'] ?? []));
|
||
$memberIds[] = $userId;
|
||
$memberIds = array_values(array_unique(array_filter($memberIds)));
|
||
if ($category === 'group' && count($memberIds) < 2) {
|
||
respond(400, '请选择至少一位联系人');
|
||
}
|
||
|
||
$groupId = 'webg_' . bin2hex(random_bytes(8));
|
||
$now = webImNowMs();
|
||
$nativeStatus = 'pending';
|
||
$nativeGroupId = '';
|
||
$nativeError = '';
|
||
|
||
try {
|
||
$nativeResult = createNativeGroup($userId, $groupId, $name, $category, $memberIds);
|
||
$nativeGroupId = (string)($nativeResult['group_id'] ?? $nativeResult['groupId'] ?? $groupId);
|
||
$nativeStatus = 'ok';
|
||
} catch (Throwable $error) {
|
||
$nativeStatus = 'fallback';
|
||
$nativeError = mb_substr($error->getMessage() ?: '原生群组创建失败', 0, 255, 'UTF-8');
|
||
}
|
||
|
||
$statement = $appDb->prepare(
|
||
'insert into `t_web_groups`'
|
||
. ' (`group_id`, `category`, `name`, `portrait`, `owner_uid`, `join_mode`, `announcement`,'
|
||
. ' `welcome_text`, `native_group_id`, `native_status`, `native_error`, `created_by`, `created_at_ms`, `updated_at_ms`, `deleted`)'
|
||
. ' values (?, ?, ?, "", ?, "open", ?, ?, ?, ?, ?, ?, ?, ?, 0)'
|
||
);
|
||
$statement->execute([
|
||
$groupId,
|
||
$category,
|
||
$name,
|
||
$userId,
|
||
$announcement,
|
||
$welcomeText,
|
||
$nativeGroupId,
|
||
$nativeStatus,
|
||
$nativeError,
|
||
$userId,
|
||
$now,
|
||
$now,
|
||
]);
|
||
|
||
foreach ($memberIds as $memberId) {
|
||
upsertWebGroupMember($appDb, $groupId, $memberId, $memberId === $userId ? 'owner' : 'member');
|
||
}
|
||
sendWebGroupWelcomeMessage($appDb, $groupId, $userId, $welcomeText);
|
||
|
||
$group = fetchWebGroup($appDb, $groupId);
|
||
return [
|
||
'group' => $group,
|
||
'conversation' => webGroupConversation($group ?: [
|
||
'group_id' => $groupId,
|
||
'category' => $category,
|
||
'name' => $name,
|
||
'updated_at_ms' => $now,
|
||
], 'owner'),
|
||
'nativeStatus' => $nativeStatus,
|
||
'nativeError' => $nativeError,
|
||
];
|
||
}
|
||
|
||
function fetchWebUserMap(PDO $imDb, array $userIds, string $ownerId = ''): array
|
||
{
|
||
$userIds = webActionList($userIds);
|
||
if (!$userIds) {
|
||
return [];
|
||
}
|
||
|
||
$statement = $imDb->prepare(
|
||
'select `_uid`, `_name`, `_display_name`, `_mobile`, `_portrait` from `t_user`'
|
||
. ' where `_uid` in (' . appPlaceholders($userIds) . ')'
|
||
);
|
||
$statement->execute($userIds);
|
||
|
||
$users = [];
|
||
foreach ($statement->fetchAll() as $row) {
|
||
$uid = (string)$row['_uid'];
|
||
$displayName = trim((string)($row['_display_name'] ?? ''))
|
||
?: trim((string)($row['_name'] ?? ''))
|
||
?: trim((string)($row['_mobile'] ?? ''))
|
||
?: $uid;
|
||
$users[$uid] = [
|
||
'uid' => $uid,
|
||
'name' => (string)($row['_name'] ?? ''),
|
||
'displayName' => $displayName,
|
||
'mobile' => (string)($row['_mobile'] ?? ''),
|
||
'portrait' => (string)($row['_portrait'] ?? ''),
|
||
'contactAlias' => '',
|
||
'friendAlias' => '',
|
||
'remarkAlias' => '',
|
||
'isContact' => false,
|
||
];
|
||
}
|
||
|
||
if ($ownerId !== '') {
|
||
$aliasStatement = $imDb->prepare(
|
||
'select `_friend_uid`, `_alias` from `t_friend` where `_uid` = ?'
|
||
. ' and `_friend_uid` in (' . appPlaceholders($userIds) . ')'
|
||
);
|
||
$aliasStatement->execute(array_merge([$ownerId], $userIds));
|
||
foreach ($aliasStatement->fetchAll() as $row) {
|
||
$friendUid = (string)$row['_friend_uid'];
|
||
if (!isset($users[$friendUid])) {
|
||
continue;
|
||
}
|
||
$users[$friendUid]['isContact'] = true;
|
||
$users[$friendUid]['friendAlias'] = trim((string)($row['_alias'] ?? ''));
|
||
}
|
||
|
||
$remarkMap = fetchWebUserRemarkMap(imAppDb(), $ownerId, $userIds);
|
||
foreach ($remarkMap as $targetUid => $alias) {
|
||
if (!isset($users[$targetUid])) {
|
||
continue;
|
||
}
|
||
$users[$targetUid]['remarkAlias'] = $alias;
|
||
}
|
||
|
||
foreach ($users as $uid => $user) {
|
||
$friendAlias = trim((string)($user['friendAlias'] ?? ''));
|
||
$remarkAlias = trim((string)($user['remarkAlias'] ?? ''));
|
||
$users[$uid]['contactAlias'] = $friendAlias !== '' ? $friendAlias : $remarkAlias;
|
||
}
|
||
}
|
||
|
||
return $users;
|
||
}
|
||
|
||
function currentMuteSetting(PDO $appDb, string $groupId): array
|
||
{
|
||
$statement = $appDb->prepare('select * from `t_web_group_mute_rules` where `group_id` = ? limit 1');
|
||
$statement->execute([$groupId]);
|
||
$row = $statement->fetch();
|
||
if (!is_array($row)) {
|
||
return [
|
||
'mode' => 'off',
|
||
'scheduleType' => 'once',
|
||
'startsAtMs' => 0,
|
||
'endsAtMs' => 0,
|
||
'dailyStart' => '',
|
||
'dailyEnd' => '',
|
||
'enabled' => false,
|
||
];
|
||
}
|
||
|
||
return [
|
||
'mode' => (string)$row['mode'],
|
||
'scheduleType' => (string)$row['schedule_type'],
|
||
'startsAtMs' => (int)$row['starts_at_ms'],
|
||
'endsAtMs' => (int)$row['ends_at_ms'],
|
||
'dailyStart' => (string)$row['daily_start'],
|
||
'dailyEnd' => (string)$row['daily_end'],
|
||
'enabled' => (bool)$row['enabled'],
|
||
];
|
||
}
|
||
|
||
function groupDetailAction(PDO $imDb, PDO $appDb, string $userId, string $groupId): array
|
||
{
|
||
$group = fetchWebGroup($appDb, $groupId);
|
||
if (!$group) {
|
||
$groupStatement = $imDb->prepare('select * from `t_group` where `_gid` = ? and `_deleted` = 0 limit 1');
|
||
$groupStatement->execute([$groupId]);
|
||
$nativeGroup = $groupStatement->fetch();
|
||
$memberStatement = $imDb->prepare('select `_type`, `_alias` from `t_group_member` where `_gid` = ? and `_mid` = ? limit 1');
|
||
$memberStatement->execute([$groupId, $userId]);
|
||
$nativeMember = $memberStatement->fetch();
|
||
if (!$nativeGroup || !$nativeMember) {
|
||
respond(403, '你不在该群组中');
|
||
}
|
||
$ownerStatement = $imDb->prepare('select `_mid` from `t_group_member` where `_gid` = ? and `_type` = 2 limit 1');
|
||
$ownerStatement->execute([$groupId]);
|
||
$ownerId = (string)($ownerStatement->fetchColumn() ?: '');
|
||
$users = fetchWebUserMap($imDb, array_filter([$ownerId, $userId]), $userId);
|
||
return ['group' => [
|
||
'groupId' => $groupId,
|
||
'publicId' => (string)($nativeGroup['id'] ?? ''),
|
||
'name' => (string)($nativeGroup['_name'] ?? $groupId),
|
||
'category' => 'group',
|
||
'announcement' => (string)($nativeGroup['_bulletin'] ?? ''),
|
||
'welcomeText' => '',
|
||
'ownerUid' => $ownerId,
|
||
'ownerName' => (string)($users[$ownerId]['displayName'] ?? $ownerId),
|
||
'myRole' => ((int)($nativeMember['_type'] ?? 0) === 2 ? 'owner' : ((int)($nativeMember['_type'] ?? 0) === 1 ? 'admin' : 'member')),
|
||
'myAlias' => (string)($nativeMember['_alias'] ?? ''),
|
||
'myDisplayName' => (string)($nativeMember['_alias'] ?? '') ?: (string)($users[$userId]['displayName'] ?? $userId),
|
||
'muteSetting' => ['enabled' => false, 'mode' => 'off'],
|
||
]];
|
||
}
|
||
$member = fetchWebGroupMember($appDb, $groupId, $userId);
|
||
if (!$member) {
|
||
respond(403, '你不在该群组中');
|
||
}
|
||
|
||
$ownerId = (string)$group['owner_uid'];
|
||
$users = fetchWebUserMap($imDb, [$ownerId, $userId], $userId);
|
||
$owner = $users[$ownerId] ?? ['uid' => $ownerId, 'displayName' => $ownerId, 'contactAlias' => ''];
|
||
$me = $users[$userId] ?? ['uid' => $userId, 'displayName' => $userId];
|
||
$myAlias = (string)($member['alias'] ?? '');
|
||
|
||
return [
|
||
'group' => [
|
||
'groupId' => (string)$group['group_id'],
|
||
'publicId' => (string)($group['id'] ?? ''),
|
||
'name' => (string)$group['name'],
|
||
'category' => (string)$group['category'],
|
||
'announcement' => (string)($group['announcement'] ?? ''),
|
||
'welcomeText' => (string)($group['welcome_text'] ?? ''),
|
||
'ownerUid' => $ownerId,
|
||
'ownerName' => (string)($owner['contactAlias'] ?? '') !== '' ? (string)$owner['contactAlias'] : (string)$owner['displayName'],
|
||
'myRole' => (string)$member['role'],
|
||
'myAlias' => $myAlias,
|
||
'myDisplayName' => $myAlias !== '' ? $myAlias : (string)$me['displayName'],
|
||
'muteSetting' => currentMuteSetting($appDb, (string)$group['group_id']),
|
||
],
|
||
];
|
||
}
|
||
|
||
function groupMembersAction(PDO $imDb, PDO $appDb, string $userId, string $groupId): array
|
||
{
|
||
if (!fetchWebGroupMember($appDb, $groupId, $userId)) {
|
||
$membership = $imDb->prepare('select 1 from `t_group_member` where `_gid` = ? and `_mid` = ? limit 1');
|
||
$membership->execute([$groupId, $userId]);
|
||
if (!$membership->fetchColumn()) {
|
||
respond(403, '你不在该群组中');
|
||
}
|
||
$nativeStatement = $imDb->prepare(
|
||
'select `_mid` as `user_id`, `_type`, `_alias`, `_create_dt` from `t_group_member`'
|
||
. ' where `_gid` = ? order by (`_type` = 2) desc, `_create_dt` asc'
|
||
);
|
||
$nativeStatement->execute([$groupId]);
|
||
$nativeMembers = $nativeStatement->fetchAll();
|
||
$nativeUsers = fetchWebUserMap($imDb, array_column($nativeMembers, 'user_id'), $userId);
|
||
return ['members' => array_map(static function (array $member) use ($nativeUsers): array {
|
||
$uid = (string)$member['user_id'];
|
||
$user = $nativeUsers[$uid] ?? ['displayName' => $uid, 'mobile' => '', 'portrait' => '', 'isContact' => false];
|
||
$alias = trim((string)($member['_alias'] ?? ''));
|
||
$type = (int)($member['_type'] ?? 0);
|
||
return [
|
||
'uid' => $uid,
|
||
'role' => $type === 2 ? 'owner' : ($type === 1 ? 'admin' : 'member'),
|
||
'alias' => $alias,
|
||
'contactAlias' => trim((string)($user['contactAlias'] ?? '')),
|
||
'friendAlias' => trim((string)($user['friendAlias'] ?? '')),
|
||
'remarkAlias' => trim((string)($user['remarkAlias'] ?? '')),
|
||
'isContact' => (bool)($user['isContact'] ?? false),
|
||
'displayName' => trim((string)($user['contactAlias'] ?? '')) ?: ($alias ?: (string)$user['displayName']),
|
||
'rawDisplayName' => (string)$user['displayName'],
|
||
'groupDisplayName' => $alias,
|
||
'mobile' => (string)($user['mobile'] ?? ''),
|
||
'portrait' => (string)($user['portrait'] ?? ''),
|
||
];
|
||
}, $nativeMembers)];
|
||
}
|
||
|
||
$statement = $appDb->prepare(
|
||
'select `user_id`, `role`, `alias`, `joined_at_ms` from `t_web_group_members`'
|
||
. ' where `group_id` = ? and `status` = 0 order by (`role` = "owner") desc, `joined_at_ms` asc'
|
||
);
|
||
$statement->execute([$groupId]);
|
||
$members = $statement->fetchAll();
|
||
$users = fetchWebUserMap($imDb, array_column($members, 'user_id'), $userId);
|
||
|
||
return [
|
||
'members' => array_map(static function (array $member) use ($users): array {
|
||
$uid = (string)$member['user_id'];
|
||
$user = $users[$uid] ?? [
|
||
'uid' => $uid,
|
||
'displayName' => $uid,
|
||
'mobile' => '',
|
||
'portrait' => '',
|
||
'contactAlias' => '',
|
||
'friendAlias' => '',
|
||
'remarkAlias' => '',
|
||
'isContact' => false,
|
||
];
|
||
$alias = trim((string)($member['alias'] ?? ''));
|
||
$contactAlias = trim((string)($user['contactAlias'] ?? ''));
|
||
return [
|
||
'uid' => $uid,
|
||
'role' => (string)$member['role'],
|
||
'alias' => $alias,
|
||
'contactAlias' => $contactAlias,
|
||
'friendAlias' => trim((string)($user['friendAlias'] ?? '')),
|
||
'remarkAlias' => trim((string)($user['remarkAlias'] ?? '')),
|
||
'isContact' => (bool)($user['isContact'] ?? false),
|
||
'displayName' => $contactAlias !== '' ? $contactAlias : ($alias !== '' ? $alias : (string)$user['displayName']),
|
||
'rawDisplayName' => (string)$user['displayName'],
|
||
'groupDisplayName' => $alias,
|
||
'mobile' => (string)($user['mobile'] ?? ''),
|
||
'portrait' => (string)($user['portrait'] ?? ''),
|
||
];
|
||
}, $members),
|
||
];
|
||
}
|
||
|
||
function updateGroupAliasAction(PDO $appDb, string $userId, string $groupId, string $alias): array
|
||
{
|
||
if (mb_strlen($alias, 'UTF-8') > 64) {
|
||
respond(400, '本群昵称过长');
|
||
}
|
||
if (!fetchWebGroupMember($appDb, $groupId, $userId)) {
|
||
respond(403, '你不在该群组中');
|
||
}
|
||
|
||
$now = webImNowMs();
|
||
$statement = $appDb->prepare(
|
||
'update `t_web_group_members` set `alias` = ?, `updated_at_ms` = ?'
|
||
. ' where `group_id` = ? and `user_id` = ? and `status` = 0'
|
||
);
|
||
$statement->execute([$alias, $now, $groupId, $userId]);
|
||
|
||
return ['alias' => $alias];
|
||
}
|
||
|
||
function leaveGroupAction(PDO $appDb, string $userId, string $groupId): array
|
||
{
|
||
$group = fetchWebGroup($appDb, $groupId);
|
||
if (!$group) {
|
||
respond(404, '没有找到该群组');
|
||
}
|
||
$member = fetchWebGroupMember($appDb, $groupId, $userId);
|
||
if (!$member) {
|
||
respond(403, '你不在该群组中');
|
||
}
|
||
|
||
$now = webImNowMs();
|
||
$statement = $appDb->prepare(
|
||
'update `t_web_group_members` set `status` = 1, `updated_at_ms` = ?'
|
||
. ' where `group_id` = ? and `user_id` = ?'
|
||
);
|
||
$statement->execute([$now, $groupId, $userId]);
|
||
$groupUpdate = $appDb->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?');
|
||
$groupUpdate->execute([$now, $groupId]);
|
||
|
||
try {
|
||
requireImSuccess(imAdminPost('/admin/group/member/quit', [
|
||
'operator' => $userId,
|
||
'group_id' => $groupId,
|
||
'to_lines' => [0],
|
||
]), '退出原生群组失败');
|
||
} catch (Throwable) {
|
||
}
|
||
|
||
return ['groupId' => $groupId];
|
||
}
|
||
|
||
function dismissGroupAction(PDO $appDb, string $userId, string $groupId): array
|
||
{
|
||
$group = fetchWebGroup($appDb, $groupId);
|
||
if (!$group) {
|
||
respond(404, '没有找到该群组');
|
||
}
|
||
requireWebGroupRole($appDb, $groupId, $userId, ['owner']);
|
||
|
||
$now = webImNowMs();
|
||
$update = $appDb->prepare('update `t_web_groups` set `deleted` = 1, `updated_at_ms` = ? where `group_id` = ?');
|
||
$update->execute([$now, $groupId]);
|
||
$memberUpdate = $appDb->prepare('update `t_web_group_members` set `status` = 1, `updated_at_ms` = ? where `group_id` = ?');
|
||
$memberUpdate->execute([$now, $groupId]);
|
||
|
||
try {
|
||
dismissNativeGroup($userId, $groupId);
|
||
} catch (Throwable) {
|
||
}
|
||
|
||
return ['groupId' => $groupId];
|
||
}
|
||
|
||
function transferGroupAction(PDO $appDb, string $userId, string $groupId, string $targetUserId): array
|
||
{
|
||
if ($targetUserId === '' || $targetUserId === $userId) {
|
||
respond(400, '请选择新的群主');
|
||
}
|
||
$group = fetchWebGroup($appDb, $groupId);
|
||
if (!$group) {
|
||
respond(404, '没有找到该群组');
|
||
}
|
||
requireWebGroupRole($appDb, $groupId, $userId, ['owner']);
|
||
$target = requireWebGroupMember($appDb, $groupId, $targetUserId);
|
||
if ((string)$target['role'] === 'owner') {
|
||
respond(400, '该成员已经是群主');
|
||
}
|
||
|
||
$now = webImNowMs();
|
||
$appDb->beginTransaction();
|
||
try {
|
||
$groupUpdate = $appDb->prepare('update `t_web_groups` set `owner_uid` = ?, `updated_at_ms` = ? where `group_id` = ?');
|
||
$groupUpdate->execute([$targetUserId, $now, $groupId]);
|
||
$oldOwnerUpdate = $appDb->prepare(
|
||
'update `t_web_group_members` set `role` = "member", `updated_at_ms` = ?'
|
||
. ' where `group_id` = ? and `user_id` = ?'
|
||
);
|
||
$oldOwnerUpdate->execute([$now, $groupId, $userId]);
|
||
$newOwnerUpdate = $appDb->prepare(
|
||
'update `t_web_group_members` set `role` = "owner", `updated_at_ms` = ?'
|
||
. ' where `group_id` = ? and `user_id` = ?'
|
||
);
|
||
$newOwnerUpdate->execute([$now, $groupId, $targetUserId]);
|
||
$appDb->commit();
|
||
} catch (Throwable $error) {
|
||
$appDb->rollBack();
|
||
throw $error;
|
||
}
|
||
|
||
try {
|
||
transferNativeGroup($userId, $groupId, $targetUserId);
|
||
} catch (Throwable) {
|
||
}
|
||
|
||
return ['groupId' => $groupId, 'newOwner' => $targetUserId];
|
||
}
|
||
|
||
function setGroupAdminsAction(PDO $appDb, string $userId, string $groupId, array $adminIds): array
|
||
{
|
||
requireWebGroupRole($appDb, $groupId, $userId, ['owner']);
|
||
$adminIds = array_values(array_unique(array_filter(array_map('strval', $adminIds), static fn(string $uid): bool => $uid !== $userId)));
|
||
$now = webImNowMs();
|
||
|
||
$reset = $appDb->prepare(
|
||
'update `t_web_group_members` set `role` = "member", `updated_at_ms` = ?'
|
||
. ' where `group_id` = ? and `role` = "admin" and `status` = 0'
|
||
);
|
||
$reset->execute([$now, $groupId]);
|
||
|
||
if ($adminIds) {
|
||
$update = $appDb->prepare(
|
||
'update `t_web_group_members` set `role` = "admin", `updated_at_ms` = ?'
|
||
. ' where `group_id` = ? and `user_id` in (' . appPlaceholders($adminIds) . ')'
|
||
. ' and `role` <> "owner" and `status` = 0'
|
||
);
|
||
$update->execute(array_merge([$now, $groupId], $adminIds));
|
||
}
|
||
|
||
return ['adminIds' => $adminIds];
|
||
}
|
||
|
||
function updateGroupProfileAction(PDO $appDb, string $userId, string $groupId, array $payload): array
|
||
{
|
||
requireWebGroupRole($appDb, $groupId, $userId, ['owner', 'admin']);
|
||
$name = trim((string)($payload['name'] ?? ''));
|
||
$announcement = trim((string)($payload['announcement'] ?? ''));
|
||
$welcomeText = trim((string)($payload['welcomeText'] ?? ''));
|
||
if ($name === '') {
|
||
respond(400, '请输入群名称');
|
||
}
|
||
if (mb_strlen($name, 'UTF-8') > 128) {
|
||
respond(400, '群名称过长');
|
||
}
|
||
|
||
$now = webImNowMs();
|
||
$statement = $appDb->prepare(
|
||
'update `t_web_groups` set `name` = ?, `announcement` = ?, `welcome_text` = ?, `updated_at_ms` = ?'
|
||
. ' where `group_id` = ? and `deleted` = 0'
|
||
);
|
||
$statement->execute([$name, $announcement, $welcomeText, $now, $groupId]);
|
||
|
||
return [
|
||
'groupId' => $groupId,
|
||
'name' => $name,
|
||
'announcement' => $announcement,
|
||
'welcomeText' => $welcomeText,
|
||
'updatedAt' => $now,
|
||
];
|
||
}
|
||
|
||
function kickGroupMembersAction(PDO $appDb, string $userId, string $groupId, array $targetIds): array
|
||
{
|
||
$operator = requireWebGroupRole($appDb, $groupId, $userId, ['owner', 'admin']);
|
||
$operatorRole = (string)$operator['role'];
|
||
$targetIds = array_values(array_unique(array_filter(array_map('strval', $targetIds), static fn(string $uid): bool => $uid !== '')));
|
||
$targetIds = array_values(array_filter($targetIds, static fn(string $uid): bool => $uid !== $userId));
|
||
if (!$targetIds) {
|
||
respond(400, '请选择要移除的群员');
|
||
}
|
||
|
||
$statement = $appDb->prepare(
|
||
'select `user_id`, `role` from `t_web_group_members`'
|
||
. ' where `group_id` = ? and `user_id` in (' . appPlaceholders($targetIds) . ') and `status` = 0'
|
||
);
|
||
$statement->execute(array_merge([$groupId], $targetIds));
|
||
$targets = $statement->fetchAll();
|
||
if (!$targets) {
|
||
respond(400, '没有可移除的群员');
|
||
}
|
||
|
||
$allowedIds = [];
|
||
foreach ($targets as $target) {
|
||
$role = (string)$target['role'];
|
||
if ($role === 'owner') {
|
||
continue;
|
||
}
|
||
if ($operatorRole === 'admin' && $role !== 'member') {
|
||
continue;
|
||
}
|
||
$allowedIds[] = (string)$target['user_id'];
|
||
}
|
||
if (!$allowedIds) {
|
||
respond(403, '没有可移除的群员');
|
||
}
|
||
|
||
$now = webImNowMs();
|
||
$update = $appDb->prepare(
|
||
'update `t_web_group_members` set `status` = 2, `updated_at_ms` = ?'
|
||
. ' where `group_id` = ? and `user_id` in (' . appPlaceholders($allowedIds) . ')'
|
||
);
|
||
$update->execute(array_merge([$now, $groupId], $allowedIds));
|
||
|
||
try {
|
||
removeNativeGroupMember($userId, $groupId, $allowedIds);
|
||
} catch (Throwable) {
|
||
}
|
||
|
||
return ['removedIds' => $allowedIds];
|
||
}
|
||
|
||
function updateMuteSettingAction(PDO $appDb, string $userId, string $groupId, array $payload): array
|
||
{
|
||
requireWebGroupRole($appDb, $groupId, $userId, ['owner', 'admin']);
|
||
$mode = trim((string)($payload['mode'] ?? 'off'));
|
||
$allowedModes = ['off', 'all_include_admins', 'all_except_admins'];
|
||
if (!in_array($mode, $allowedModes, true)) {
|
||
respond(400, '禁言模式不正确');
|
||
}
|
||
$scheduleType = trim((string)($payload['scheduleType'] ?? 'once'));
|
||
if (!in_array($scheduleType, ['manual', 'once', 'daily'], true)) {
|
||
respond(400, '禁言时段类型不正确');
|
||
}
|
||
|
||
$startsAtMs = max(0, (int)($payload['startsAtMs'] ?? 0));
|
||
$endsAtMs = max(0, (int)($payload['endsAtMs'] ?? 0));
|
||
$dailyStart = trim((string)($payload['dailyStart'] ?? ''));
|
||
$dailyEnd = trim((string)($payload['dailyEnd'] ?? ''));
|
||
$enabled = $mode !== 'off';
|
||
if ($enabled && $scheduleType === 'once' && ($startsAtMs <= 0 || $endsAtMs <= $startsAtMs)) {
|
||
respond(400, '请设置有效的单次禁言时段');
|
||
}
|
||
if ($enabled && $scheduleType === 'daily' && (!preg_match('/^\d{2}:\d{2}$/', $dailyStart) || !preg_match('/^\d{2}:\d{2}$/', $dailyEnd))) {
|
||
respond(400, '请设置有效的每日禁言时段');
|
||
}
|
||
|
||
$now = webImNowMs();
|
||
$statement = $appDb->prepare(
|
||
'insert into `t_web_group_mute_rules`'
|
||
. ' (`group_id`, `mode`, `schedule_type`, `starts_at_ms`, `ends_at_ms`, `daily_start`, `daily_end`, `enabled`, `created_by`, `updated_at_ms`)'
|
||
. ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||
. ' on duplicate key update'
|
||
. ' `mode` = values(`mode`),'
|
||
. ' `schedule_type` = values(`schedule_type`),'
|
||
. ' `starts_at_ms` = values(`starts_at_ms`),'
|
||
. ' `ends_at_ms` = values(`ends_at_ms`),'
|
||
. ' `daily_start` = values(`daily_start`),'
|
||
. ' `daily_end` = values(`daily_end`),'
|
||
. ' `enabled` = values(`enabled`),'
|
||
. ' `created_by` = values(`created_by`),'
|
||
. ' `updated_at_ms` = values(`updated_at_ms`)'
|
||
);
|
||
$statement->execute([$groupId, $mode, $scheduleType, $startsAtMs, $endsAtMs, $dailyStart, $dailyEnd, $enabled ? 1 : 0, $userId, $now]);
|
||
|
||
return ['muteSetting' => currentMuteSetting($appDb, $groupId)];
|
||
}
|
||
|
||
function joinWebGroupAction(PDO $appDb, string $userId, string $groupKey): array
|
||
{
|
||
if ($groupKey === '') {
|
||
respond(400, '请输入群组ID或群名称');
|
||
}
|
||
|
||
$group = fetchWebGroup($appDb, $groupKey);
|
||
if (!$group) {
|
||
try {
|
||
$native = requireImSuccess(imAdminPost('/admin/group/get_info', ['groupId' => $groupKey]), '群组不存在');
|
||
$now = webImNowMs();
|
||
$statement = $appDb->prepare(
|
||
'insert into `t_web_groups`'
|
||
. ' (`group_id`, `category`, `name`, `portrait`, `owner_uid`, `join_mode`, `announcement`,'
|
||
. ' `native_group_id`, `native_status`, `created_by`, `created_at_ms`, `updated_at_ms`, `deleted`)'
|
||
. ' values (?, "group", ?, ?, ?, "open", "", ?, "ok", ?, ?, ?, 0)'
|
||
. ' on duplicate key update `updated_at_ms` = values(`updated_at_ms`)'
|
||
);
|
||
$statement->execute([
|
||
$groupKey,
|
||
(string)($native['name'] ?? $groupKey),
|
||
(string)($native['portrait'] ?? ''),
|
||
(string)($native['owner'] ?? ''),
|
||
$groupKey,
|
||
$userId,
|
||
$now,
|
||
$now,
|
||
]);
|
||
$group = fetchWebGroup($appDb, $groupKey);
|
||
} catch (Throwable) {
|
||
respond(404, '没有找到该群组');
|
||
}
|
||
}
|
||
|
||
$groupId = (string)$group['group_id'];
|
||
upsertWebGroupMember($appDb, $groupId, $userId, 'member');
|
||
try {
|
||
addNativeGroupMember((string)($group['owner_uid'] ?: $userId), $groupId, $userId);
|
||
} catch (Throwable $error) {
|
||
// Web membership is authoritative for this web build; native group sync is best-effort.
|
||
}
|
||
|
||
$role = fetchWebGroupRole($appDb, $groupId, $userId);
|
||
return [
|
||
'group' => fetchWebGroup($appDb, $groupId),
|
||
'conversation' => webGroupConversation($group, $role),
|
||
];
|
||
}
|
||
|
||
function sendBroadcastAction(PDO $imDb, PDO $appDb, string $userId, array $payload): array
|
||
{
|
||
$text = trim((string)($payload['text'] ?? ''));
|
||
if ($text === '') {
|
||
respond(400, '请输入群发内容');
|
||
}
|
||
|
||
$targetIds = resolveWebUserIds($imDb, webActionList($payload['targetIds'] ?? []));
|
||
$targetIds = array_values(array_filter(array_unique($targetIds), static fn(string $uid): bool => $uid !== $userId));
|
||
if (!$targetIds) {
|
||
respond(400, '请选择群发对象');
|
||
}
|
||
|
||
$results = [];
|
||
foreach ($targetIds as $targetId) {
|
||
try {
|
||
$result = requireImSuccess(imAdminPost('/admin/message/send', [
|
||
'sender' => $userId,
|
||
'conv' => [
|
||
'type' => 0,
|
||
'target' => $targetId,
|
||
'line' => 0,
|
||
],
|
||
'payload' => [
|
||
'type' => 1,
|
||
'searchableContent' => $text,
|
||
'pushContent' => $text,
|
||
'content' => $text,
|
||
'persistFlag' => 3,
|
||
],
|
||
'userMessage' => true,
|
||
]), '发送失败');
|
||
$results[] = [
|
||
'targetId' => $targetId,
|
||
'ok' => true,
|
||
'messageUid' => isset($result['messageUid']) ? (string)$result['messageUid'] : '',
|
||
];
|
||
} catch (Throwable $error) {
|
||
$results[] = [
|
||
'targetId' => $targetId,
|
||
'ok' => false,
|
||
'error' => $error->getMessage() ?: '发送失败',
|
||
];
|
||
}
|
||
}
|
||
|
||
$successCount = count(array_filter($results, static fn(array $item): bool => !empty($item['ok'])));
|
||
$failedCount = count($results) - $successCount;
|
||
$now = webImNowMs();
|
||
$statement = $appDb->prepare(
|
||
'insert into `t_web_broadcast_logs`'
|
||
. ' (`sender_uid`, `target_count`, `success_count`, `failed_count`, `text`, `target_json`, `result_json`, `created_at_ms`)'
|
||
. ' values (?, ?, ?, ?, ?, ?, ?, ?)'
|
||
);
|
||
$statement->execute([
|
||
$userId,
|
||
count($targetIds),
|
||
$successCount,
|
||
$failedCount,
|
||
$text,
|
||
json_encode($targetIds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||
json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||
$now,
|
||
]);
|
||
|
||
return [
|
||
'targetCount' => count($targetIds),
|
||
'successCount' => $successCount,
|
||
'failedCount' => $failedCount,
|
||
'results' => $results,
|
||
];
|
||
}
|
||
|
||
$payload = readJsonPayload();
|
||
$action = trim((string)($payload['action'] ?? ''));
|
||
$userId = trim((string)($payload['userId'] ?? ''));
|
||
|
||
if ($userId === '') {
|
||
respond(400, '缺少当前用户ID');
|
||
}
|
||
|
||
try {
|
||
ensureWebImAppTables();
|
||
$imDb = imDb();
|
||
$appDb = imAppDb();
|
||
|
||
if ($action === 'create_group') {
|
||
respond(0, 'success', createWebGroupAction($imDb, $appDb, $userId, $payload, 'group'));
|
||
}
|
||
if ($action === 'create_channel') {
|
||
respond(0, 'success', createWebGroupAction($imDb, $appDb, $userId, $payload, 'channel'));
|
||
}
|
||
if ($action === 'join_group') {
|
||
respond(0, 'success', joinWebGroupAction($appDb, $userId, trim((string)($payload['groupKey'] ?? ''))));
|
||
}
|
||
if ($action === 'group_detail') {
|
||
respond(0, 'success', groupDetailAction($imDb, $appDb, $userId, trim((string)($payload['groupId'] ?? ''))));
|
||
}
|
||
if ($action === 'group_members') {
|
||
respond(0, 'success', groupMembersAction($imDb, $appDb, $userId, trim((string)($payload['groupId'] ?? ''))));
|
||
}
|
||
if ($action === 'update_group_alias') {
|
||
respond(0, 'success', updateGroupAliasAction(
|
||
$appDb,
|
||
$userId,
|
||
trim((string)($payload['groupId'] ?? '')),
|
||
trim((string)($payload['alias'] ?? ''))
|
||
));
|
||
}
|
||
if ($action === 'leave_group') {
|
||
respond(0, 'success', leaveGroupAction($appDb, $userId, trim((string)($payload['groupId'] ?? ''))));
|
||
}
|
||
if ($action === 'dismiss_group') {
|
||
respond(0, 'success', dismissGroupAction($appDb, $userId, trim((string)($payload['groupId'] ?? ''))));
|
||
}
|
||
if ($action === 'transfer_group') {
|
||
respond(0, 'success', transferGroupAction(
|
||
$appDb,
|
||
$userId,
|
||
trim((string)($payload['groupId'] ?? '')),
|
||
trim((string)($payload['targetUserId'] ?? ''))
|
||
));
|
||
}
|
||
if ($action === 'set_group_admins') {
|
||
respond(0, 'success', setGroupAdminsAction(
|
||
$appDb,
|
||
$userId,
|
||
trim((string)($payload['groupId'] ?? '')),
|
||
is_array($payload['adminIds'] ?? null) ? $payload['adminIds'] : []
|
||
));
|
||
}
|
||
if ($action === 'update_group_profile') {
|
||
respond(0, 'success', updateGroupProfileAction(
|
||
$appDb,
|
||
$userId,
|
||
trim((string)($payload['groupId'] ?? '')),
|
||
$payload
|
||
));
|
||
}
|
||
if ($action === 'kick_group_members') {
|
||
respond(0, 'success', kickGroupMembersAction(
|
||
$appDb,
|
||
$userId,
|
||
trim((string)($payload['groupId'] ?? '')),
|
||
is_array($payload['targetIds'] ?? null) ? $payload['targetIds'] : []
|
||
));
|
||
}
|
||
if ($action === 'update_mute_setting') {
|
||
respond(0, 'success', updateMuteSettingAction(
|
||
$appDb,
|
||
$userId,
|
||
trim((string)($payload['groupId'] ?? '')),
|
||
$payload
|
||
));
|
||
}
|
||
if ($action === 'broadcast') {
|
||
$result = sendBroadcastAction($imDb, $appDb, $userId, $payload);
|
||
if ((int)$result['successCount'] <= 0) {
|
||
respond(500, '群发失败', $result);
|
||
}
|
||
respond(0, 'success', $result);
|
||
}
|
||
|
||
respond(400, '未知操作');
|
||
} catch (Throwable $error) {
|
||
respond(500, $error->getMessage() ?: '会话操作失败');
|
||
}
|