140 lines
5.0 KiB
PHP
140 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Messaging;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\ConversationParticipant;
|
|
use App\Models\Message;
|
|
use App\Services\Messaging\MessageSearchIndexer;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Meilisearch\Client;
|
|
|
|
class MessageSearchController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly MessageSearchIndexer $indexer,
|
|
) {}
|
|
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
$data = $request->validate([
|
|
'q' => 'required|string|min:1|max:200',
|
|
'conversation_id' => 'nullable|integer|exists:conversations,id',
|
|
'cursor' => 'nullable|integer|min:0',
|
|
]);
|
|
|
|
$allowedConversationIds = ConversationParticipant::query()
|
|
->where('user_id', $user->id)
|
|
->whereNull('left_at')
|
|
->pluck('conversation_id')
|
|
->map(fn ($id) => (int) $id)
|
|
->all();
|
|
|
|
$conversationId = isset($data['conversation_id']) ? (int) $data['conversation_id'] : null;
|
|
if ($conversationId !== null && ! in_array($conversationId, $allowedConversationIds, true)) {
|
|
abort(403, 'You are not a participant of this conversation.');
|
|
}
|
|
|
|
if (empty($allowedConversationIds)) {
|
|
return response()->json(['data' => [], 'next_cursor' => null]);
|
|
}
|
|
|
|
$limit = max(1, (int) config('messaging.search.page_size', 20));
|
|
$offset = max(0, (int) ($data['cursor'] ?? 0));
|
|
|
|
$hits = collect();
|
|
$estimated = 0;
|
|
|
|
try {
|
|
$client = new Client(
|
|
config('scout.meilisearch.host'),
|
|
config('scout.meilisearch.key')
|
|
);
|
|
|
|
$prefix = (string) config('scout.prefix', '');
|
|
$indexName = $prefix . (string) config('messaging.search.index', 'messages');
|
|
|
|
$conversationFilter = $conversationId !== null
|
|
? "conversation_id = {$conversationId}"
|
|
: 'conversation_id IN [' . implode(',', $allowedConversationIds) . ']';
|
|
|
|
$result = $client
|
|
->index($indexName)
|
|
->search((string) $data['q'], [
|
|
'limit' => $limit,
|
|
'offset' => $offset,
|
|
'sort' => ['created_at:desc'],
|
|
'filter' => $conversationFilter,
|
|
]);
|
|
|
|
$hits = collect($result->getHits() ?? []);
|
|
$estimated = (int) ($result->getEstimatedTotalHits() ?? $hits->count());
|
|
} catch (\Throwable) {
|
|
$query = Message::query()
|
|
->select('id')
|
|
->whereNull('deleted_at')
|
|
->whereIn('conversation_id', $allowedConversationIds)
|
|
->when($conversationId !== null, fn ($q) => $q->where('conversation_id', $conversationId))
|
|
->where('body', 'like', '%' . (string) $data['q'] . '%')
|
|
->orderByDesc('created_at')
|
|
->orderByDesc('id');
|
|
|
|
$estimated = (clone $query)->count();
|
|
$hits = $query->offset($offset)->limit($limit)->get()->map(fn ($row) => ['id' => (int) $row->id]);
|
|
}
|
|
$messageIds = $hits->pluck('id')->map(fn ($id) => (int) $id)->all();
|
|
|
|
$messages = Message::query()
|
|
->whereIn('id', $messageIds)
|
|
->whereIn('conversation_id', $allowedConversationIds)
|
|
->whereNull('deleted_at')
|
|
->with(['sender:id,username', 'attachments'])
|
|
->get()
|
|
->keyBy('id');
|
|
|
|
$ordered = $hits
|
|
->map(function (array $hit) use ($messages) {
|
|
$message = $messages->get((int) ($hit['id'] ?? 0));
|
|
if (! $message) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'id' => $message->id,
|
|
'conversation_id' => $message->conversation_id,
|
|
'sender_id' => $message->sender_id,
|
|
'sender' => $message->sender,
|
|
'body' => $message->body,
|
|
'created_at' => optional($message->created_at)?->toISOString(),
|
|
'has_attachments' => $message->attachments->isNotEmpty(),
|
|
];
|
|
})
|
|
->filter()
|
|
->values();
|
|
|
|
$nextCursor = ($offset + $limit) < $estimated ? ($offset + $limit) : null;
|
|
|
|
return response()->json([
|
|
'data' => $ordered,
|
|
'next_cursor' => $nextCursor,
|
|
]);
|
|
}
|
|
|
|
public function rebuild(Request $request): JsonResponse
|
|
{
|
|
abort_unless($request->user()?->isAdmin(), 403, 'Admin access required.');
|
|
|
|
$conversationId = $request->integer('conversation_id');
|
|
if ($conversationId > 0) {
|
|
$this->indexer->rebuildConversation($conversationId);
|
|
return response()->json(['queued' => true, 'scope' => 'conversation']);
|
|
}
|
|
|
|
$this->indexer->rebuildAll();
|
|
|
|
return response()->json(['queued' => true, 'scope' => 'all']);
|
|
}
|
|
}
|