messages implemented
This commit is contained in:
139
app/Http/Controllers/Api/Messaging/MessageSearchController.php
Normal file
139
app/Http/Controllers/Api/Messaging/MessageSearchController.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user