dootask/app/Models/WebSocketDialogMsg.php
2022-07-01 16:57:03 +08:00

579 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Tasks\PushTask;
use App\Tasks\WebSocketDialogMsgTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\WebSocketDialogMsg
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property string|null $dialog_type 对话类型
* @property int|null $userid 发送会员ID
* @property string|null $type 消息类型
* @property string|null $mtype 消息类型(用于搜索)
* @property array|mixed $msg 详细消息
* @property array|mixed $emoji emoji回复
* @property string|null $key 搜索关键词
* @property int|null $read 已阅数量
* @property int|null $send 发送数量
* @property int|null $tag 标注会员ID
* @property int|null $link 是否存在链接
* @property int|null $reply_num 有多少条回复
* @property int|null $reply_id 回复ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int|mixed $percentage
* @property-read \App\Models\WebSocketDialogMsg|null $reply_data
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereEmoji($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereKey($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereLink($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMtype($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg withTrashed()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg withoutTrashed()
* @mixin \Eloquent
*/
class WebSocketDialogMsg extends AbstractModel
{
use SoftDeletes;
protected $appends = [
'percentage',
'reply_data',
];
protected $hidden = [
'key',
'updated_at',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function webSocketDialog(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
}
/**
* 阅读占比
* @return int|mixed
*/
public function getPercentageAttribute()
{
if (!isset($this->appendattrs['percentage'])) {
$this->generatePercentage();
}
return $this->appendattrs['percentage'];
}
/**
* 回复消息详情
* @return WebSocketDialogMsg|null
*/
public function getReplyDataAttribute()
{
if (!isset($this->appendattrs['reply_data'])) {
$this->appendattrs['reply_data'] = null;
if ($this->reply_id > 0) {
$this->appendattrs['reply_data'] = self::find($this->reply_id, ['id', 'userid', 'type', 'msg'])?->cancelAppend() ?: null;
}
}
return $this->appendattrs['reply_data'];
}
/**
* 消息格式化
* @param $value
* @return array|mixed
*/
public function getMsgAttribute($value)
{
if (is_array($value)) {
return $value;
}
$value = Base::json2array($value);
if ($this->type === 'file') {
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'png', 'gif']) ? 'img' : 'file';
$value['path'] = Base::fillUrl($value['path']);
$value['thumb'] = Base::fillUrl($value['thumb'] ?: Base::extIcon($value['ext']));
} else if ($this->type === 'record') {
$value['path'] = Base::fillUrl($value['path']);
}
return $value;
}
/**
* emoji回复格式化
* @param $value
* @return array|mixed
*/
public function getEmojiAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* 获取占比
* @param bool|int $increment 是否新增阅读数
* @return int
*/
public function generatePercentage($increment = false) {
if ($increment) {
$this->increment('read', is_bool($increment) ? 1 : $increment);
}
if ($this->read > $this->send || empty($this->send)) {
return $this->appendattrs['percentage'] = 100;
} else {
return $this->appendattrs['percentage'] = intval($this->read / $this->send * 100);
}
}
/**
* 标记已送达 同时 告诉发送人已送达
* @param $userid
* @return bool
*/
public function readSuccess($userid)
{
if (empty($userid)) {
return false;
}
self::transaction(function() use ($userid) {
$msgRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereUserid($userid)->lockForUpdate()->first();
if (empty($msgRead)) {
$msgRead = WebSocketDialogMsgRead::createInstance([
'dialog_id' => $this->dialog_id,
'msg_id' => $this->id,
'userid' => $userid,
'after' => 1,
]);
if ($msgRead->saveOrIgnore()) {
$this->send = WebSocketDialogMsgRead::whereMsgId($this->id)->count();
$this->save();
} else {
return;
}
}
if (!$msgRead->read_at) {
$msgRead->read_at = Carbon::now();
$msgRead->save();
$this->generatePercentage(true);
PushTask::push([
'userid' => $this->userid,
'msg' => [
'type' => 'dialog',
'mode' => 'readed',
'data' => [
'id' => $this->id,
'read' => $this->read,
'percentage' => $this->percentage,
],
]
]);
}
});
return true;
}
/**
* emoji回复
* @param $symbol
* @param int $sender 发送的会员ID
* @return mixed
*/
public function emojiMsg($symbol, $sender)
{
$exist = false;
$array = $this->emoji;
foreach ($array as $index => &$item) {
if ($item['symbol'] === $symbol) {
if (in_array($sender, $item['userids'])) {
// 已存在 去除
$item['userids'] = array_values(array_diff($item['userids'], [$sender]));
if (empty($item['userids'])) {
unset($array[$index]);
$array = array_values($array);
}
} else {
// 未存在 添加
array_unshift($item['userids'], $sender);
}
$exist = true;
break;
}
}
if (!$exist) {
array_unshift($array, [
'symbol' => $symbol,
'userids' => [$sender]
]);
}
//
$this->emoji = Base::array2json($array);
$this->save();
$resData = [
'id' => $this->id,
'emoji' => $array,
];
//
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->pushMsg('update', $resData);
//
return Base::retSuccess('sucess', $resData);
}
/**
* 标注、取消标注
* @param int $sender 标注的会员ID
* @return mixed
*/
public function toggleTagMsg($sender)
{
if ($this->type === 'tag') {
return Base::retError('此消息不支持标注');
}
$this->tag = $this->tag ? 0 : $sender;
$this->save();
$resData = [
'id' => $this->id,
'tag' => $this->tag,
];
//
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->pushMsg('update', $resData);
//
$data = [
'update' => $resData
];
$res = self::sendMsg($this->dialog_id, 0, 'tag', [
'action' => $this->tag ? 'add' : 'remove',
'data' => [
'id' => $this->id,
'type' => $this->type,
'msg' => $this->msg,
]
], $sender);
if (Base::isSuccess($res)) {
$data['add'] = $res['data'];
}
//
return Base::retSuccess('sucess', $data);
}
/**
* 转发消息
* @param $userids
* @param int $sender 发送的会员ID
* @return mixed
*/
public function forwardMsg($userids, $sender)
{
return AbstractModel::transaction(function() use ($sender, $userids) {
$msgs = [];
foreach ($userids as $userid) {
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($sender, $userid);
if ($dialog) {
$res = self::sendMsg($dialog->id, 0, $this->type, $this->getOriginal('msg'), $sender);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
}
}
return Base::retSuccess('转发成功', [
'msgs' => $msgs
]);
});
}
/**
* 删除消息
* @return void
*/
public function deleteMsg()
{
$send_dt = Carbon::parse($this->created_at)->addDay();
if ($send_dt->lt(Carbon::now())) {
throw new ApiException('已超过24小时此消息不能撤回');
}
AbstractModel::transaction(function() {
$deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
$this->delete();
//
if ($this->reply_id > 0) {
self::whereId($this->reply_id)->decrement('reply_num');
}
//
$last_msg = null;
if ($this->webSocketDialog) {
$last_msg = WebSocketDialogMsg::whereDialogId($this->dialog_id)->orderByDesc('id')->first();
$this->webSocketDialog->last_at = $last_msg->created_at;
$this->webSocketDialog->save();
}
//
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$userids = $dialog->dialogUser->pluck('userid')->toArray();
PushTask::push([
'userid' => $userids,
'msg' => [
'type' => 'dialog',
'mode' => 'delete',
'data' => [
'id' => $this->id,
'dialog_id' => $this->dialog_id,
'last_msg' => $last_msg,
'update_read' => $deleteRead ? 1 : 0
],
]
]);
}
});
}
/**
* 预览消息
* @param bool $preserveHtml 保留html格式
* @param null|array $data
* @return string
*/
public function previewMsg($preserveHtml = false, $data = null)
{
if ($data === null) {
$data = [
'type' => $this->type,
'msg' => $this->msg,
];
}
switch ($data['type']) {
case 'text':
return $this->previewTextMsg($data['msg']['text'], $preserveHtml);
case 'record':
return "[语音]";
case 'meeting':
return "[会议] ${$data['msg']['name']}";
case 'file':
if ($data['msg']['type'] == 'img') {
return "[图片]";
}
return "[文件] {$data['msg']['name']}";
case 'tag':
$action = $data['msg']['action'] === 'remove' ? '取消标注' : '标注';
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
case 'notice':
return $data['msg']['notice'];
default:
return "[未知的消息]";
}
}
/**
* 生成关键词
* @return string
*/
public function generateMsgKey()
{
return match ($this->type) {
'text' => strip_tags($this->msg['text']),
'meeting', 'file' => $this->msg['name'],
default => '',
};
}
/**
* 返回文本预览消息
* @param $text
* @param bool $preserveHtml 保留html格式
* @return string|string[]|null
*/
private function previewTextMsg($text, $preserveHtml = false)
{
if (!$text) return '';
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text);
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[表情]", $text);
$text = preg_replace("/<img\s+class=\"browse\"[^>]*?>/", "[图片]", $text);
if ($preserveHtml) {
return $text;
} else {
return strip_tags($text);
}
}
/**
* 处理文本消息内容,用于发送前
* @param $text
* @param $dialog_id
* @return mixed|string|string[]
*/
public static function formatMsg($text, $dialog_id)
{
@ini_set("pcre.backtrack_limit", 999999999);
// 图片 [:IMAGE:className:width:height:src:alt:]
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|gif);base64,(.*?)\"(.*?)>(<\/img>)*/s", $text, $matchs);
foreach ($matchs[2] as $key => $base64) {
$tmpPath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5s($base64) . "." . $matchs[1][$key];
if (file_put_contents(public_path($tmpPath), base64_decode($base64))) {
$imagesize = getimagesize(public_path($tmpPath));
if (Base::imgThumb(public_path($tmpPath), public_path($tmpPath) . "_thumb.jpg", 320, 0)) {
$tmpPath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imagesize[0]}:{$imagesize[1]}:{$tmpPath}::]", $text);
}
}
// 表情图片
preg_match_all("/<img\s+class=\"emoticon\"(.*?)>/s", $text, $matchs);
foreach ($matchs[1] as $key => $str) {
preg_match("/data-asset=\"(.*?)\"/", $str, $matchAsset);
preg_match("/data-name=\"(.*?)\"/", $str, $matchName);
if (file_exists(public_path($matchAsset[1]))) {
$imagesize = getimagesize(public_path($matchAsset[1]));
$text = str_replace($matchs[0][$key], "[:IMAGE:emoticon:{$imagesize[0]}:{$imagesize[1]}:{$matchAsset[1]}:{$matchName[1]}:]", $text);
}
}
// 其他网络图片
preg_match_all("/<img[^>]*?src=([\"'])(.*?\.(png|jpg|jpeg|gif))\\1[^>]*?>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
$tmpPath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5s($str) . "." . $matchs[3][$key];
if (file_exists(public_path($tmpPath))) {
$imagesize = getimagesize(public_path($tmpPath));
if (Base::imgThumb(public_path($tmpPath), public_path($tmpPath) . "_thumb.jpg", 320, 0)) {
$tmpPath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imagesize[0]}:{$imagesize[1]}:{$tmpPath}::]", $text);
} else {
$image = file_get_contents($str);
if (empty($image)) {
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text);
} else if (file_put_contents(public_path($tmpPath), $image)) {
$imagesize = getimagesize(public_path($tmpPath));
if (Base::imgThumb(public_path($tmpPath), public_path($tmpPath) . "_thumb.jpg", 320, 0)) {
$tmpPath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imagesize[0]}:{$imagesize[1]}:{$tmpPath}::]", $text);
}
}
}
// @成员、#任务
preg_match_all("/<span\s+class=\"mention\"(.*?)>.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs);
foreach ($matchs[1] as $key => $str) {
preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar);
preg_match("/data-id=\"(.*?)\"/", $str, $matchId);
preg_match("/data-value=\"(.*?)\"/", $str, $matchValye);
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$matchId[1]}:{$matchValye[1]}:]", $text);
}
// 处理链接
preg_match_all("/<a[^>]*?href=([\"'])(.*?)\\1[^>]*?>([^<]*?)<\/a>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
$herf = $matchs[2][$key];
$title = $matchs[3][$key] ?: $herf;
$text = str_replace($matchs[0][$key], "<a href=\"{$herf}\" target=\"_blank\">{$title}</a>", $text);
}
// 过滤标签
$text = strip_tags($text, '<blockquote> <strong> <pre> <ol> <ul> <li> <em> <p> <s> <u> <a>');
$text = preg_replace("/\<(blockquote|strong|pre|ol|ul|li|em|p|s|u).*?\>/is", "<$1>", $text); // 不用去除a标签上面已经处理过了
$text = preg_replace("/\[:IMAGE:(.*?):(.*?):(.*?):(.*?):(.*?):\]/i", "<img class=\"$1\" width=\"$2\" height=\"$3\" src=\"{{RemoteURL}}$4\" alt=\"$5\"/>", $text);
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
$text = preg_replace("/\[:#:(.*?):(.*?):\]/i", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
}
/**
* 发送消息
* @param int $dialog_id 会话ID即 聊天室ID
* @param int $reply_id 回复ID
* @param string $type 消息类型
* @param array $msg 发送的消息
* @param int $sender 发送的会员ID默认自己0为系统
* @param bool $push_self 是否推送给自己
* @return array
*/
public static function sendMsg($dialog_id, $reply_id, $type, $msg, $sender = 0, $push_self = false)
{
$link = 0;
$mtype = $type;
if ($type === 'text') {
if (str_contains($msg['text'], '<a ') || preg_match("/https*:\/\//", $msg['text'])) {
$link = 1;
}
if (str_contains($msg['text'], '<img ')) {
$mtype = str_contains($msg['text'], '"emoticon"') ? 'emoticon' : 'image';
}
} elseif ($type === 'file') {
if (in_array($msg['ext'], ['jpg', 'jpeg', 'png', 'gif'])) {
$mtype = 'image';
}
}
//
$dialogMsg = self::createInstance([
'dialog_id' => $dialog_id,
'reply_id' => $reply_id,
'userid' => $sender ?: User::userid(),
'type' => $type,
'mtype' => $mtype,
'link' => $link,
'msg' => $msg,
'read' => 0,
]);
if ($reply_id > 0) {
self::whereId($reply_id)->increment('reply_num');
}
AbstractModel::transaction(function () use ($dialogMsg) {
$dialog = WebSocketDialog::find($dialogMsg->dialog_id);
if (empty($dialog)) {
throw new ApiException('获取会话失败');
}
$dialog->last_at = Carbon::now();
$dialog->save();
$dialogMsg->send = 1;
$dialogMsg->dialog_type = $dialog->type;
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
});
//
$task = new WebSocketDialogMsgTask($dialogMsg->id);
if ($push_self) {
$task->setIgnoreFd(null);
}
Task::deliver($task);
//
return Base::retSuccess('发送成功', $dialogMsg);
}
}