diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index 354536f41..c2da5ef88 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -12,6 +12,7 @@ use App\Module\AI; use App\Module\Doo; use App\Models\File; use App\Models\User; +use App\Models\UserBot; use App\Module\Base; use App\Module\Timer; use App\Models\Setting; @@ -443,6 +444,29 @@ class DialogController extends AbstractController return Base::retError('打开会话失败'); } $data = WebSocketDialog::synthesizeData($dialog->id, $user->userid); + + if ($userid > 0) { + $botTarget = User::whereUserid($userid)->whereBot(1)->first(); + if ($botTarget) { + $userBot = UserBot::whereBotId($botTarget->userid)->first(); + if ($userBot) { + $userBot->dispatchWebhook(UserBot::WEBHOOK_EVENT_DIALOG_OPEN, [ + 'dialog_id' => $dialog->id, + 'dialog_type' => $dialog->type, + 'session_id' => $dialog->session_id, + 'dialog_name' => $dialog->getGroupName(), + 'user' => [ + 'userid' => $user->userid, + 'email' => $user->email, + 'nickname' => $user->nickname, + ], + ], 10, [ + 'dialog' => $dialog->id, + 'operator' => $user->userid, + ]); + } + } + } return Base::retSuccess('success', $data); } @@ -1256,6 +1280,9 @@ class DialogController extends AbstractController if ($model_name) { $msgData['model_name'] = $model_name; } + if (User::isBot($user->userid)) { + $msgData['force_webhook'] = true; // 强制使用webhook发送 + } $result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key); } } diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 436933e8c..1207de77d 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -34,6 +34,8 @@ use App\Models\WebSocketDialogUser; use App\Models\UserTaskBrowse; use App\Models\UserFavorite; use App\Models\UserRecentItem; +use App\Models\UserTag; +use App\Models\UserTagRecognition; use Illuminate\Support\Facades\DB; use App\Models\UserEmailVerification; use App\Module\AgoraIO\AgoraTokenGenerator; @@ -386,6 +388,9 @@ class UsersController extends AbstractController $data['nickname_original'] = $user->getRawOriginal('nickname'); $data['department_name'] = $user->getDepartmentName(); $data['department_owner'] = UserDepartment::where('parent_id',0)->where('owner_userid', $user->userid)->exists(); // 适用默认部门下第1级负责人才能添加部门OKR + $tagMeta = UserTag::listWithMeta($user->userid, $user); + $data['personal_tags'] = $tagMeta['top']; + $data['personal_tags_total'] = $tagMeta['total']; return Base::retSuccess('success', $data); } @@ -446,6 +451,9 @@ class UsersController extends AbstractController * @apiParam {String} [tel] 电话 * @apiParam {String} [nickname] 昵称 * @apiParam {String} [profession] 职位/职称 + * @apiParam {String} [birthday] 生日(格式:YYYY-MM-DD) + * @apiParam {String} [address] 地址 + * @apiParam {String} [introduction] 个人简介 * @apiParam {String} [lang] 语言(比如:zh/en) * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) @@ -509,6 +517,40 @@ class UsersController extends AbstractController $upLdap['employeeType'] = $profession; } } + // 生日 + if (Arr::exists($data, 'birthday')) { + $birthday = trim((string) Request::input('birthday')); + if ($birthday === '') { + $user->birthday = null; + } else { + try { + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthday)) { + $birthdayDate = Carbon::createFromFormat('Y-m-d', $birthday); + } else { + $birthdayDate = Carbon::parse($birthday); + } + } catch (\Exception $e) { + return Base::retError('生日格式错误'); + } + $user->birthday = $birthdayDate->format('Y-m-d'); + } + } + // 地址 + if (Arr::exists($data, 'address')) { + $address = trim((string) Request::input('address')); + if (mb_strlen($address) > 100) { + return Base::retError('地址最多只能设置100个字'); + } + $user->address = $address ?: null; + } + // 个人简介 + if (Arr::exists($data, 'introduction')) { + $introduction = trim((string) Request::input('introduction')); + if (mb_strlen($introduction) > 500) { + return Base::retError('个人简介最多只能设置500个字'); + } + $user->introduction = $introduction ?: null; + } // 语言 if (Arr::exists($data, 'lang')) { $lang = trim(Request::input('lang')); @@ -767,8 +809,12 @@ class UsersController extends AbstractController public function basic() { $sharekey = Request::header('sharekey'); - if (empty($sharekey) || !Meeting::getShareInfo($sharekey)) { - User::auth(); + $shareInfo = $sharekey ? Meeting::getShareInfo($sharekey) : null; + $viewer = null; + if (empty($shareInfo)) { + $viewer = User::auth(); + } elseif (Doo::userId() > 0) { + $viewer = User::whereUserid(Doo::userId())->first(); } // $userid = Request::input('userid'); @@ -786,6 +832,9 @@ class UsersController extends AbstractController $basic = UserDelete::userid2basic($id); } if ($basic) { + $tagMeta = UserTag::listWithMeta($basic->userid, $viewer); + $basic->personal_tags = $tagMeta['top']; + $basic->personal_tags_total = $tagMeta['total']; // $retArray[] = $basic; } @@ -1450,6 +1499,233 @@ class UsersController extends AbstractController return Base::retSuccess('success', $data); } + protected function buildUserTagResponse(?User $viewer, int $targetUserId, string $message = 'success') + { + return Base::retSuccess($message, UserTag::listWithMeta($targetUserId, $viewer)); + } + + /** + * @api {get} api/users/tags/lists 10.1. 获取个性标签列表 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName tags__lists + * + * @apiParam {Number} [userid] 会员ID(不传默认为当前用户) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + * @apiSuccessExample {json} data: + { + "list": [ + { + "id": 1, + "name": "认真负责", + "recognition_total": 3, + "recognized": true, + "can_edit": true, + "can_delete": true + } + ], + "top": [ ], + "total": 1 + } + */ + public function tags__lists() + { + $viewer = User::auth(); + $userid = intval(Request::input('userid')) ?: $viewer->userid; + $target = User::whereUserid($userid)->first(); + if (empty($target)) { + return Base::retError('会员不存在'); + } + return $this->buildUserTagResponse($viewer, $target->userid); + } + + /** + * @api {post} api/users/tags/add 10.2. 新增个性标签 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName tags__add + * + * @apiParam {Number} [userid] 会员ID(不传默认为当前用户) + * @apiParam {String} name 标签名称(1-20个字符) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据,同“获取个性标签列表” + */ + public function tags__add() + { + $viewer = User::auth(); + $userid = intval(Request::input('userid')) ?: $viewer->userid; + $target = User::whereUserid($userid)->first(); + if (empty($target)) { + return Base::retError('会员不存在'); + } + + $name = trim((string) Request::input('name')); + if ($name === '') { + return Base::retError('请输入个性标签'); + } + if (mb_strlen($name) > 20) { + return Base::retError('标签名称最多只能设置20个字'); + } + if (UserTag::where('user_id', $userid)->where('name', $name)->exists()) { + return Base::retError('标签已存在'); + } + if (UserTag::where('user_id', $userid)->count() >= 100) { + return Base::retError('每位会员最多添加100个标签'); + } + + $tag = UserTag::create([ + 'user_id' => $userid, + 'name' => $name, + 'created_by' => $viewer->userid, + 'updated_by' => $viewer->userid, + ]); + $tag->save(); + + return $this->buildUserTagResponse($viewer, $userid, '添加成功'); + } + + /** + * @api {post} api/users/tags/update 10.3. 修改个性标签 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName tags__update + * + * @apiParam {Number} tag_id 标签ID + * @apiParam {String} name 标签名称(1-20个字符) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据,同“获取个性标签列表” + */ + public function tags__update() + { + $viewer = User::auth(); + $tagId = intval(Request::input('tag_id')); + $name = trim((string) Request::input('name')); + if ($tagId <= 0) { + return Base::retError('参数错误'); + } + if ($name === '') { + return Base::retError('请输入个性标签'); + } + if (mb_strlen($name) > 20) { + return Base::retError('标签名称最多只能设置20个字'); + } + $tag = UserTag::find($tagId); + if (empty($tag)) { + return Base::retError('标签不存在'); + } + if (!$tag->canManage($viewer)) { + return Base::retError('无权操作该标签'); + } + if ($name !== $tag->name && UserTag::where('user_id', $tag->user_id)->where('name', $name)->where('id', '!=', $tag->id)->exists()) { + return Base::retError('标签已存在'); + } + + if ($name !== $tag->name) { + $tag->updateInstance([ + 'name' => $name, + 'updated_by' => $viewer->userid, + ]); + } else { + $tag->updateInstance([ + 'updated_by' => $viewer->userid, + ]); + } + $tag->save(); + + return $this->buildUserTagResponse($viewer, $tag->user_id, '保存成功'); + } + + /** + * @api {post} api/users/tags/delete 10.4. 删除个性标签 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName tags__delete + * + * @apiParam {Number} tag_id 标签ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据,同“获取个性标签列表” + */ + public function tags__delete() + { + $viewer = User::auth(); + $tagId = intval(Request::input('tag_id')); + if ($tagId <= 0) { + return Base::retError('参数错误'); + } + $tag = UserTag::find($tagId); + if (empty($tag)) { + return Base::retError('标签不存在'); + } + if (!$tag->canManage($viewer)) { + return Base::retError('无权操作该标签'); + } + + $userId = $tag->user_id; + $tag->delete(); + + return $this->buildUserTagResponse($viewer, $userId, '删除成功'); + } + + /** + * @api {post} api/users/tags/recognize 10.5. 认可个性标签 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName tags__recognize + * + * @apiParam {Number} tag_id 标签ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据,同“获取个性标签列表” + */ + public function tags__recognize() + { + $viewer = User::auth(); + $tagId = intval(Request::input('tag_id')); + if ($tagId <= 0) { + return Base::retError('参数错误'); + } + $tag = UserTag::find($tagId); + if (empty($tag)) { + return Base::retError('标签不存在'); + } + + $recognition = UserTagRecognition::where('tag_id', $tagId) + ->where('user_id', $viewer->userid) + ->first(); + if ($recognition) { + $recognition->delete(); + $message = '已取消认可'; + } else { + UserTagRecognition::create([ + 'tag_id' => $tagId, + 'user_id' => $viewer->userid, + ]); + $message = '认可成功'; + } + + return $this->buildUserTagResponse($viewer, $tag->user_id, $message); + } + /** * @api {get} api/users/meeting/link 【会议】获取分享链接 * @@ -2181,7 +2457,8 @@ class UsersController extends AbstractController 'users.nickname', 'users.userimg', 'user_bots.clear_day', - 'user_bots.webhook_url' + 'user_bots.webhook_url', + 'user_bots.webhook_events' ]) ->orderByDesc('id') ->get() @@ -2191,6 +2468,7 @@ class UsersController extends AbstractController $bot['name'] = $bot['nickname']; $bot['avatar'] = $bot['userimg']; $bot['system_name'] = UserBot::systemBotName($bot['name']); + $bot['webhook_events'] = UserBot::normalizeWebhookEvents($bot['webhook_events'] ?? null, empty($bot['webhook_events'])); unset($bot['userid'], $bot['nickname'], $bot['userimg']); } @@ -2242,11 +2520,13 @@ class UsersController extends AbstractController 'avatar' => $botUser->userimg, 'clear_day' => 0, 'webhook_url' => '', + 'webhook_events' => [UserBot::WEBHOOK_EVENT_MESSAGE], 'system_name' => UserBot::systemBotName($botUser->email), ]; if ($userBot) { $data['clear_day'] = $userBot->clear_day; $data['webhook_url'] = $userBot->webhook_url; + $data['webhook_events'] = $userBot->webhook_events; } return Base::retSuccess('success', $data); } @@ -2327,6 +2607,9 @@ class UsersController extends AbstractController if (Arr::exists($data, 'webhook_url')) { $upBot['webhook_url'] = trim($data['webhook_url']); } + if (Arr::exists($data, 'webhook_events')) { + $upBot['webhook_events'] = UserBot::normalizeWebhookEvents($data['webhook_events'], false); + } // if ($upUser) { $botUser->updateInstance($upUser); @@ -2343,11 +2626,13 @@ class UsersController extends AbstractController 'avatar' => $botUser->userimg, 'clear_day' => 0, 'webhook_url' => '', + 'webhook_events' => [UserBot::WEBHOOK_EVENT_MESSAGE], 'system_name' => UserBot::systemBotName($botUser->email), ]; if ($userBot) { $data['clear_day'] = $userBot->clear_day; $data['webhook_url'] = $userBot->webhook_url; + $data['webhook_events'] = $userBot->webhook_events; } return Base::retSuccess($botId ? '修改成功' : '添加成功', $data); } diff --git a/app/Models/User.php b/app/Models/User.php index e8394abe6..f02d19206 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -22,6 +22,9 @@ use Carbon\Carbon; * @property string|null $tel 联系电话 * @property string $nickname 昵称 * @property string|null $profession 职位/职称 + * @property \Illuminate\Support\Carbon|null $birthday 生日 + * @property string|null $address 地址 + * @property string|null $introduction 个人简介 * @property string $userimg 头像 * @property string|null $encrypt * @property string|null $password 登录密码 @@ -89,7 +92,7 @@ class User extends AbstractModel public static $defaultAvatarMode = 'auto'; // 基本信息的字段 - public static $basicField = ['userid', 'email', 'nickname', 'profession', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_at']; + public static $basicField = ['userid', 'email', 'nickname', 'profession', 'birthday', 'address', 'introduction', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_at']; /** * 昵称 diff --git a/app/Models/UserBot.php b/app/Models/UserBot.php index b1c4c38e1..d3cd78eb8 100644 --- a/app/Models/UserBot.php +++ b/app/Models/UserBot.php @@ -5,10 +5,12 @@ namespace App\Models; use App\Module\Base; use App\Module\Doo; use App\Module\Extranet; +use App\Module\Ihttp; use App\Module\Timer; use App\Tasks\JokeSoupTask; use Cache; use Carbon\Carbon; +use Throwable; /** * App\Models\UserBot @@ -20,6 +22,7 @@ use Carbon\Carbon; * @property \Illuminate\Support\Carbon|null $clear_at 下一次清理时间 * @property string|null $webhook_url 消息webhook地址 * @property int|null $webhook_num 消息webhook请求次数 + * @property array|null $webhook_events Webhook事件配置 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend() @@ -44,6 +47,131 @@ use Carbon\Carbon; */ class UserBot extends AbstractModel { + public const WEBHOOK_EVENT_MESSAGE = 'message'; + public const WEBHOOK_EVENT_DIALOG_OPEN = 'dialog_open'; + public const WEBHOOK_EVENT_MEMBER_JOIN = 'member_join'; + public const WEBHOOK_EVENT_MEMBER_LEAVE = 'member_leave'; + + protected $casts = [ + 'webhook_events' => 'array', + ]; + + /** + * 获取可选的 webhook 事件 + * + * @return string[] + */ + public static function webhookEventOptions(): array + { + return [ + self::WEBHOOK_EVENT_MESSAGE, + self::WEBHOOK_EVENT_DIALOG_OPEN, + self::WEBHOOK_EVENT_MEMBER_JOIN, + self::WEBHOOK_EVENT_MEMBER_LEAVE, + ]; + } + + /** + * 标准化 webhook 事件配置 + * + * @param mixed $events + * @return array + */ + public static function normalizeWebhookEvents(mixed $events, bool $useFallback = true): array + { + if (is_string($events)) { + $events = Base::json2array($events); + } + if ($events === null) { + $events = []; + } + if (!is_array($events)) { + $events = [$events]; + } + $events = array_filter(array_map('strval', $events)); + $events = array_values(array_intersect($events, self::webhookEventOptions())); + return $events ?: ($useFallback ? [self::WEBHOOK_EVENT_MESSAGE] : []); + } + + /** + * 获取 webhook 事件配置 + * + * @param mixed $value + * @return array + */ + public function getWebhookEventsAttribute(mixed $value): array + { + if ($value === null || $value === '') { + return self::normalizeWebhookEvents(null, true); + } + return self::normalizeWebhookEvents($value, false); + } + + /** + * 设置 webhook 事件配置 + * + * @param mixed $value + * @return void + */ + public function setWebhookEventsAttribute(mixed $value): void + { + $useFallback = $value === null; + $this->attributes['webhook_events'] = Base::array2json(self::normalizeWebhookEvents($value, $useFallback)); + } + + /** + * 判断是否需要触发指定 webhook 事件 + * + * @param string $event + * @return bool + */ + public function shouldDispatchWebhook(string $event): bool + { + if (!$this->webhook_url) { + return false; + } + if (!preg_match('/^https?:\/\//', $this->webhook_url)) { + return false; + } + return in_array($event, $this->webhook_events ?? [], true); + } + + /** + * 发送 webhook + * + * @param string $event + * @param array $payload + * @param int $timeout + * @param array $context + * @return array|null + */ + public function dispatchWebhook(string $event, array $payload, int $timeout = 30, array $context = []): ?array + { + if (!$this->shouldDispatchWebhook($event)) { + return null; + } + + $payload = array_merge([ + 'event' => $event, + 'timestamp' => time(), + 'bot_uid' => $this->bot_id, + 'owner_uid' => $this->userid, + ], $payload); + + try { + $result = Ihttp::ihttp_post($this->webhook_url, $payload, $timeout); + $this->increment('webhook_num'); + return $result; + } catch (Throwable $th) { + info(Base::array2json(array_merge($context, [ + 'bot_userid' => $this->bot_id, + 'event' => $event, + 'webhook_url' => $this->webhook_url, + 'error' => $th->getMessage(), + ]))); + return null; + } + } /** * 判断是否系统机器人 diff --git a/app/Models/UserTag.php b/app/Models/UserTag.php new file mode 100644 index 000000000..70c8740f6 --- /dev/null +++ b/app/Models/UserTag.php @@ -0,0 +1,84 @@ +belongsTo(User::class, 'created_by', 'userid') + ->select(['userid', 'nickname']); + } + + public function recognitions(): HasMany + { + return $this->hasMany(UserTagRecognition::class, 'tag_id'); + } + + public function canManage(User $viewer): bool + { + return $viewer->isAdmin() + || $viewer->userid === $this->user_id + || $viewer->userid === $this->created_by; + } + + public static function listWithMeta(int $targetUserId, ?User $viewer): array + { + $query = static::query() + ->where('user_id', $targetUserId) + ->with(['creator']) + ->withCount(['recognitions as recognition_total']) + ->orderByDesc('recognition_total') + ->orderBy('id'); + + $tags = $query->get(); + + $viewerId = $viewer?->userid ?? 0; + $viewerIsAdmin = $viewer?->isAdmin() ?? false; + $viewerIsOwner = $viewerId > 0 && $viewerId === $targetUserId; + + $recognizedIds = []; + if ($viewerId > 0 && $tags->isNotEmpty()) { + $recognizedIds = UserTagRecognition::query() + ->where('user_id', $viewerId) + ->whereIn('tag_id', $tags->pluck('id')) + ->pluck('tag_id') + ->all(); + } + $recognizedLookup = array_flip($recognizedIds); + + $list = $tags->map(function (self $tag) use ($viewerId, $viewerIsAdmin, $viewerIsOwner, $recognizedLookup) { + $canManage = $viewerIsAdmin || $viewerIsOwner || $viewerId === $tag->created_by; + + return [ + 'id' => $tag->id, + 'user_id' => $tag->user_id, + 'name' => $tag->name, + 'created_by' => $tag->created_by, + 'created_by_name' => $tag->creator?->nickname ?: '', + 'recognition_total' => (int) $tag->recognition_total, + 'recognized' => isset($recognizedLookup[$tag->id]), + 'can_edit' => $canManage, + 'can_delete' => $canManage, + ]; + })->values()->toArray(); + + return [ + 'list' => $list, + 'top' => array_slice($list, 0, 10), + 'total' => count($list), + ]; + } +} diff --git a/app/Models/UserTagRecognition.php b/app/Models/UserTagRecognition.php new file mode 100644 index 000000000..1599e2faf --- /dev/null +++ b/app/Models/UserTagRecognition.php @@ -0,0 +1,26 @@ +belongsTo(UserTag::class, 'tag_id'); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id', 'userid') + ->select(['userid', 'nickname']); + } +} diff --git a/app/Models/WebSocketDialog.php b/app/Models/WebSocketDialog.php index 02aaba5b5..0b6f3267d 100644 --- a/app/Models/WebSocketDialog.php +++ b/app/Models/WebSocketDialog.php @@ -461,7 +461,8 @@ class WebSocketDialog extends AbstractModel */ public function joinGroup($userid, $inviter, $important = null) { - AbstractModel::transaction(function () use ($important, $inviter, $userid) { + $addedUserIds = []; + AbstractModel::transaction(function () use ($important, $inviter, $userid, &$addedUserIds) { foreach (is_array($userid) ? $userid : [$userid] as $value) { if ($value > 0) { $updateData = [ @@ -480,6 +481,7 @@ class WebSocketDialog extends AbstractModel ]); }, $isInsert); if ($isInsert) { + $addedUserIds[] = $value; WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [ 'notice' => User::userid2nickname($value) . " 已加入群组" ], $inviter, true, true); @@ -490,6 +492,16 @@ class WebSocketDialog extends AbstractModel $data = WebSocketDialog::generatePeople($this->id); $data['id'] = $this->id; $this->pushMsg("groupUpdate", $data); + if ($addedUserIds) { + $meta = ['action' => 'join']; + if ($inviter > 0) { + $actor = $this->getUserSnapshots([$inviter]); + if (!empty($actor)) { + $meta['actor'] = $actor[0]; + } + } + $this->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_JOIN, $addedUserIds, $meta); + } return true; } @@ -503,14 +515,15 @@ class WebSocketDialog extends AbstractModel public function exitGroup($userid, $type = 'exit', $checkDelete = true, $pushMsg = true) { $typeDesc = $type === 'remove' ? '移出' : '退出'; - AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid) { + $removedUserIds = []; + AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid, &$removedUserIds) { $builder = WebSocketDialogUser::whereDialogId($this->id); if (is_array($userid)) { $builder->whereIn('userid', $userid); } else { $builder->whereUserid($userid); } - $builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type) { + $builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type, &$removedUserIds) { /** @var WebSocketDialogUser $item */ foreach ($list as $item) { if ($checkDelete) { @@ -531,6 +544,7 @@ class WebSocketDialog extends AbstractModel } // $item->delete(); + $removedUserIds[] = $item->userid; // if ($pushMsg) { if ($type === 'remove') { @@ -549,6 +563,87 @@ class WebSocketDialog extends AbstractModel $data = WebSocketDialog::generatePeople($this->id); $data['id'] = $this->id; $this->pushMsg("groupUpdate", $data); + if ($removedUserIds) { + $meta = ['action' => $type]; + $operatorId = User::userid(); + if ($operatorId > 0) { + $actor = $this->getUserSnapshots([$operatorId]); + if (!empty($actor)) { + $meta['actor'] = $actor[0]; + } + } + $this->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_LEAVE, $removedUserIds, $meta); + } + } + + /** + * 获取用户快照 + * @param array $userIds + * @return array + */ + protected function getUserSnapshots(array $userIds): array + { + $userIds = array_values(array_unique(array_filter($userIds))); + if (empty($userIds)) { + return []; + } + return User::whereIn('userid', $userIds) + ->get(['userid', 'nickname', 'email', 'bot']) + ->map(function (User $user) { + return [ + 'userid' => $user->userid, + 'nickname' => $user->nickname, + 'email' => $user->email, + 'is_bot' => (bool)$user->bot, + ]; + }) + ->values() + ->all(); + } + + /** + * 推送成员事件到机器人 webhook + * @param string $event + * @param array $memberIds + * @param array $meta + * @return void + */ + protected function dispatchMemberWebhook(string $event, array $memberIds, array $meta = []): void + { + $memberIds = array_values(array_unique(array_filter($memberIds))); + if (empty($memberIds)) { + return; + } + + $botIds = $this->dialogUser()->where('bot', 1)->pluck('userid')->toArray(); + if (empty($botIds)) { + return; + } + + $userBots = UserBot::whereIn('bot_id', $botIds)->get(); + if ($userBots->isEmpty()) { + return; + } + + $members = $this->getUserSnapshots($memberIds); + if (empty($members)) { + return; + } + + $payload = array_merge([ + 'dialog_id' => $this->id, + 'dialog_type' => $this->type, + 'group_type' => $this->group_type, + 'dialog_name' => $this->getGroupName(), + 'members' => $members, + ], array_filter($meta, fn ($value) => $value !== null)); + + foreach ($userBots as $userBot) { + $userBot->dispatchWebhook($event, $payload, 10, [ + 'dialog' => $this->id, + 'event_members' => $memberIds, + ]); + } } /** diff --git a/app/Tasks/BotReceiveMsgTask.php b/app/Tasks/BotReceiveMsgTask.php index 4e0012856..b6193d4b6 100644 --- a/app/Tasks/BotReceiveMsgTask.php +++ b/app/Tasks/BotReceiveMsgTask.php @@ -66,7 +66,7 @@ class BotReceiveMsgTask extends AbstractTask } // 判断消息是否存在 - $msg = WebSocketDialogMsg::with(['user'])->find($this->msgId); + $msg = WebSocketDialogMsg::with(['user', 'webSocketDialog'])->find($this->msgId); if (empty($msg)) { return; } @@ -75,7 +75,11 @@ class BotReceiveMsgTask extends AbstractTask $msg->readSuccess($botUser->userid); // 判断消息是否是机器人发送的则不处理,避免循环 - if (!$msg->user || $msg->user->bot) { + if ((!$msg->user || $msg->user->bot)) { + $msgData = Base::json2array($msg->msg); + if (Base::val($msgData, 'force_webhook') && $msg->webSocketDialog) { + $this->handleWebhookRequest($msgData['text'], null, $msg, $msg->webSocketDialog, $botUser); + } return; } @@ -427,6 +431,7 @@ class BotReceiveMsgTask extends AbstractTask private function handleWebhookRequest($sendText, $replyText, WebSocketDialogMsg $msg, WebSocketDialog $dialog, User $botUser) { $webhookUrl = null; + $userBot = null; $extras = ['timestamp' => time()]; try { @@ -530,13 +535,11 @@ class BotReceiveMsgTask extends AbstractTask return; } $userBot = UserBot::whereBotId($botUser->userid)->first(); - if ($userBot) { - $userBot->webhook_num++; - $userBot->save(); - $webhookUrl = $userBot->webhook_url; + if (!$userBot || !$userBot->shouldDispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE)) { + return; } } - if (!preg_match("/^https?:\/\//", $webhookUrl)) { + if (!$userBot && !preg_match("/^https?:\/\//", $webhookUrl)) { return; } } catch (\Exception $e) { @@ -547,50 +550,60 @@ class BotReceiveMsgTask extends AbstractTask return; } // - try { - $data = [ - 'text' => $sendText, - 'reply_text' => $replyText, - 'token' => User::generateToken($botUser), - 'session_id' => $dialog->session_id, - 'dialog_id' => $dialog->id, - 'dialog_type' => $dialog->type, - 'msg_id' => $msg->id, - 'msg_uid' => $msg->userid, - 'mention' => $this->mention ? 1 : 0, - 'bot_uid' => $botUser->userid, - 'version' => Base::getVersion(), - 'extras' => Base::array2json($extras) + $data = [ + 'text' => $sendText, + 'reply_text' => $replyText, + 'token' => User::generateToken($botUser), + 'session_id' => $dialog->session_id, + 'dialog_id' => $dialog->id, + 'dialog_type' => $dialog->type, + 'msg_id' => $msg->id, + 'msg_uid' => $msg->userid, + 'mention' => $this->mention ? 1 : 0, + 'bot_uid' => $botUser->userid, + 'version' => Base::getVersion(), + 'extras' => Base::array2json($extras) + ]; + // 添加用户信息 + $userInfo = User::find($msg->userid); + if ($userInfo) { + $data['msg_user'] = [ + 'userid' => $userInfo->userid, + 'email' => $userInfo->email, + 'nickname' => $userInfo->nickname, + 'profession' => $userInfo->profession, + 'lang' => $userInfo->lang, + 'token' => User::generateTokenNoDevice($userInfo, now()->addHour()), ]; - // 添加用户信息 - $userInfo = User::find($msg->userid); - if ($userInfo) { - $data['msg_user'] = [ - 'userid' => $userInfo->userid, - 'email' => $userInfo->email, - 'nickname' => $userInfo->nickname, - 'profession' => $userInfo->profession, - 'lang' => $userInfo->lang, - 'token' => User::generateTokenNoDevice($userInfo, now()->addHour()), - ]; - } - // 请求Webhook - $result = Ihttp::ihttp_post($webhookUrl, $data, 30); - if ($result['data'] && $data = Base::json2array($result['data'])) { - if ($data['code'] != 200 && $data['message']) { - WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [ - 'text' => $result['data']['message'] - ], $botUser->userid, false, false, true); - } - } - } catch (\Throwable $th) { - info(Base::array2json([ - 'bot_userid' => $botUser->userid, + } + + $result = null; + if ($userBot) { + $result = $userBot->dispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE, $data, 30, [ 'dialog' => $dialog->id, 'msg' => $msg->id, - 'webhook_url' => $webhookUrl, - 'error' => $th->getMessage(), - ])); + ]); + } else { + try { + $result = Ihttp::ihttp_post($webhookUrl, $data, 30); + } catch (\Throwable $th) { + info(Base::array2json([ + 'bot_userid' => $botUser->userid, + 'dialog' => $dialog->id, + 'msg' => $msg->id, + 'webhook_url' => $webhookUrl, + 'error' => $th->getMessage(), + ])); + } + } + + if ($result && isset($result['data'])) { + $responseData = Base::json2array($result['data']); + if (($responseData['code'] ?? 0) != 200 && !empty($responseData['message'])) { + WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [ + 'text' => $responseData['message'] + ], $botUser->userid, false, false, true); + } } } diff --git a/app/Tasks/WebSocketDialogMsgTask.php b/app/Tasks/WebSocketDialogMsgTask.php index 4dcf271b6..fd3507aa3 100644 --- a/app/Tasks/WebSocketDialogMsgTask.php +++ b/app/Tasks/WebSocketDialogMsgTask.php @@ -138,11 +138,11 @@ class WebSocketDialogMsgTask extends AbstractTask 'dot' => $dot, 'updated' => $updated, ]; - // 机器人收到消处理 - $botUser = User::whereUserid($userid)->whereBot(1)->first(); - if ($botUser) { - $this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id, $mentions, $this->client); - } + } + // 机器人收到消处理 + $botUser = User::whereUserid($userid)->whereBot(1)->first(); + if ($botUser) { // 避免机器人处理自己发送的消息 + $this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id, $mentions, $this->client); } } // 更新已发送数量 diff --git a/database/migrations/2025_10_01_000001_add_user_bots_webhook_events.php b/database/migrations/2025_10_01_000001_add_user_bots_webhook_events.php new file mode 100644 index 000000000..5f471d75c --- /dev/null +++ b/database/migrations/2025_10_01_000001_add_user_bots_webhook_events.php @@ -0,0 +1,39 @@ +text('webhook_events')->nullable()->after('webhook_num')->comment('Webhook事件配置'); + } + }); + + DB::table('user_bots') + ->where(function ($query) { + $query->whereNull('webhook_events')->orWhere('webhook_events', ''); + }) + ->update(['webhook_events' => json_encode(['message'])]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('user_bots', function (Blueprint $table) { + if (Schema::hasColumn('user_bots', 'webhook_events')) { + $table->dropColumn('webhook_events'); + } + }); + } +}; diff --git a/database/migrations/2025_10_12_090000_add_profile_fields_to_users_table.php b/database/migrations/2025_10_12_090000_add_profile_fields_to_users_table.php new file mode 100644 index 000000000..ca04966cb --- /dev/null +++ b/database/migrations/2025_10_12_090000_add_profile_fields_to_users_table.php @@ -0,0 +1,34 @@ +date('birthday')->nullable()->after('profession'); + $table->string('address', 255)->nullable()->after('birthday'); + $table->text('introduction')->nullable()->after('address'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['birthday', 'address', 'introduction']); + }); + } +}; diff --git a/database/migrations/2025_10_13_000000_create_user_tags_table.php b/database/migrations/2025_10_13_000000_create_user_tags_table.php new file mode 100644 index 000000000..16d5407da --- /dev/null +++ b/database/migrations/2025_10_13_000000_create_user_tags_table.php @@ -0,0 +1,40 @@ +bigIncrements('id'); + $table->unsignedBigInteger('user_id')->index()->comment('被标签用户ID'); + $table->string('name', 50)->comment('标签名称'); + $table->unsignedBigInteger('created_by')->index()->comment('创建人'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('最后更新人'); + $table->timestamps(); + + $table->unique(['user_id', 'name'], 'user_tags_unique_name'); + $table->foreign('user_id')->references('userid')->on('users')->onDelete('cascade'); + $table->foreign('created_by')->references('userid')->on('users')->onDelete('cascade'); + $table->foreign('updated_by')->references('userid')->on('users')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_tags'); + } +} diff --git a/database/migrations/2025_10_13_000100_create_user_tag_recognitions_table.php b/database/migrations/2025_10_13_000100_create_user_tag_recognitions_table.php new file mode 100644 index 000000000..138747bbb --- /dev/null +++ b/database/migrations/2025_10_13_000100_create_user_tag_recognitions_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->unsignedBigInteger('tag_id')->index()->comment('标签ID'); + $table->unsignedBigInteger('user_id')->index()->comment('认可人ID'); + $table->timestamps(); + + $table->unique(['tag_id', 'user_id'], 'user_tag_recognitions_unique'); + $table->foreign('tag_id')->references('id')->on('user_tags')->onDelete('cascade'); + $table->foreign('user_id')->references('userid')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_tag_recognitions'); + } +} diff --git a/resources/assets/js/pages/manage/application.vue b/resources/assets/js/pages/manage/application.vue index fbc0459e9..3384b5c1f 100644 --- a/resources/assets/js/pages/manage/application.vue +++ b/resources/assets/js/pages/manage/application.vue @@ -106,6 +106,7 @@
ID:{{ item.id }}
{{ $L('清理时间') }}:{{ item.clear_day }}
Webhook:{{ item.webhook_url || '-' }}
+{{ $L('Webhook事件') }}:{{ formatWebhookEvents(item.webhook_events) }}
{{$L('还没有个性标签,快来添加吧~')}}
+