messages implemented
This commit is contained in:
246
app/Console/Commands/MigrateMessagesCommand.php
Normal file
246
app/Console/Commands/MigrateMessagesCommand.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\ConversationParticipant;
|
||||
use App\Models\Message;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Migrates legacy `chat` / `messages` tables into the modern conversation-based system.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Load all legacy rows from the `chat` table via the 'legacy' DB connection.
|
||||
* 2. Group by (sender_user_id, receiver_user_id) pair (canonical: min first).
|
||||
* 3. For each pair, find or create a `direct` conversation.
|
||||
* 4. Insert each message in chronological order.
|
||||
* 5. Set last_read_at based on the legacy read_date column (if present).
|
||||
* 6. Skip deleted / inactive rows.
|
||||
* 7. Convert smileys to emoji placeholders.
|
||||
*
|
||||
* Usage:
|
||||
* php artisan skinbase:migrate-messages
|
||||
* php artisan skinbase:migrate-messages --dry-run
|
||||
* php artisan skinbase:migrate-messages --chunk=1000
|
||||
*/
|
||||
class MigrateMessagesCommand extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:migrate-messages
|
||||
{--dry-run : Preview only — no writes to DB}
|
||||
{--chunk=500 : Rows to process per batch}';
|
||||
|
||||
protected $description = 'Migrate legacy chat/messages into the modern conversation system';
|
||||
|
||||
/** Columns we attempt to read; gracefully degrade if missing. */
|
||||
private array $skipped = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$chunk = max(1, (int) $this->option('chunk'));
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('[DRY-RUN] No data will be written.');
|
||||
}
|
||||
|
||||
// ── Check legacy connection ───────────────────────────────────────────
|
||||
try {
|
||||
DB::connection('legacy')->getPdo();
|
||||
} catch (Throwable $e) {
|
||||
$this->error('Cannot connect to legacy database: ' . $e->getMessage());
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$legacySchema = DB::connection('legacy')->getSchemaBuilder();
|
||||
|
||||
if (! $legacySchema->hasTable('chat')) {
|
||||
$this->error('Legacy table `chat` not found on the legacy connection.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$columns = $legacySchema->getColumnListing('chat');
|
||||
$this->info('Legacy chat columns: ' . implode(', ', $columns));
|
||||
|
||||
// Map expected legacy columns (adapt if your legacy schema differs)
|
||||
$hasReadDate = in_array('read_date', $columns, true);
|
||||
$hasSoftDelete = in_array('deleted', $columns, true);
|
||||
|
||||
// ── Count total rows ──────────────────────────────────────────────────
|
||||
$query = DB::connection('legacy')->table('chat');
|
||||
|
||||
if ($hasSoftDelete) {
|
||||
$query->where('deleted', 0);
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$this->info("Total legacy rows to process: {$total}");
|
||||
|
||||
if ($total === 0) {
|
||||
$this->info('Nothing to migrate.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$inserted = 0;
|
||||
$skipped = 0;
|
||||
$offset = 0;
|
||||
|
||||
// ── Chunk processing ──────────────────────────────────────────────────
|
||||
while (true) {
|
||||
$rows = DB::connection('legacy')
|
||||
->table('chat')
|
||||
->when($hasSoftDelete, fn ($q) => $q->where('deleted', 0))
|
||||
->orderBy('id')
|
||||
->offset($offset)
|
||||
->limit($chunk)
|
||||
->get();
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$senderId = (int) ($row->sender_user_id ?? $row->from_user_id ?? $row->user_id ?? 0);
|
||||
$receiverId = (int) ($row->receiver_user_id ?? $row->to_user_id ?? $row->recipient_id ?? 0);
|
||||
$body = trim((string) ($row->message ?? $row->body ?? $row->content ?? ''));
|
||||
$createdAt = $row->created_at ?? $row->date ?? $row->timestamp ?? now();
|
||||
$readDate = $hasReadDate ? $row->read_date : null;
|
||||
|
||||
if ($senderId === 0 || $receiverId === 0 || $body === '') {
|
||||
$skipped++;
|
||||
$this->skipped[] = ['id' => $row->id ?? '?', 'reason' => 'missing sender/receiver/body'];
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip self-messages
|
||||
if ($senderId === $receiverId) {
|
||||
$skipped++;
|
||||
$this->skipped[] = ['id' => $row->id ?? '?', 'reason' => 'self-message'];
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sanitize: strip HTML, convert smileys to emoji
|
||||
$body = $this->sanitize($body);
|
||||
|
||||
if ($dryRun) {
|
||||
$inserted++;
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($senderId, $receiverId, $body, $createdAt, $readDate, &$inserted) {
|
||||
// Find or create direct conversation
|
||||
$conv = Conversation::findDirect($senderId, $receiverId);
|
||||
|
||||
if (! $conv) {
|
||||
$conv = Conversation::create([
|
||||
'type' => 'direct',
|
||||
'created_by' => $senderId,
|
||||
'last_message_at' => $createdAt,
|
||||
]);
|
||||
|
||||
ConversationParticipant::insert([
|
||||
[
|
||||
'conversation_id' => $conv->id,
|
||||
'user_id' => $senderId,
|
||||
'role' => 'admin',
|
||||
'joined_at' => $createdAt,
|
||||
'last_read_at' => $readDate,
|
||||
],
|
||||
[
|
||||
'conversation_id' => $conv->id,
|
||||
'user_id' => $receiverId,
|
||||
'role' => 'member',
|
||||
'joined_at' => $createdAt,
|
||||
'last_read_at' => $readDate,
|
||||
],
|
||||
]);
|
||||
} else {
|
||||
// Update last_read_at on existing participants when available
|
||||
if ($readDate) {
|
||||
ConversationParticipant::where('conversation_id', $conv->id)
|
||||
->where('user_id', $receiverId)
|
||||
->whereNull('last_read_at')
|
||||
->update(['last_read_at' => $readDate]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::create([
|
||||
'conversation_id' => $conv->id,
|
||||
'sender_id' => $senderId,
|
||||
'body' => $body,
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
]);
|
||||
|
||||
// Keep last_message_at up to date
|
||||
if ($conv->last_message_at < $createdAt) {
|
||||
$conv->update(['last_message_at' => $createdAt]);
|
||||
}
|
||||
|
||||
$inserted++;
|
||||
});
|
||||
} catch (Throwable $e) {
|
||||
$skipped++;
|
||||
$this->skipped[] = ['id' => $row->id ?? '?', 'reason' => $e->getMessage()];
|
||||
Log::warning('MigrateMessages: skipped row', [
|
||||
'id' => $row->id ?? '?',
|
||||
'reason' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$offset += $chunk;
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
|
||||
$this->info("Done. Inserted: {$inserted} | Skipped: {$skipped}");
|
||||
|
||||
if ($skipped > 0 && $this->option('verbose')) {
|
||||
$this->table(['ID', 'Reason'], $this->skipped);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip HTML tags and convert common legacy smileys to emoji.
|
||||
*/
|
||||
private function sanitize(string $body): string
|
||||
{
|
||||
// Strip raw HTML
|
||||
$body = strip_tags($body);
|
||||
|
||||
// Decode HTML entities
|
||||
$body = html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
// Common smiley → emoji mapping
|
||||
$smileys = [
|
||||
':)' => '🙂', ':-)' => '🙂',
|
||||
':(' => '🙁', ':-(' => '🙁',
|
||||
':D' => '😀', ':-D' => '😀',
|
||||
':P' => '😛', ':-P' => '😛',
|
||||
';)' => '😉', ';-)' => '😉',
|
||||
':o' => '😮', ':O' => '😮',
|
||||
':|' => '😐', ':-|' => '😐',
|
||||
':/' => '😕', ':-/' => '😕',
|
||||
'<3' => '❤️',
|
||||
'xD' => '😂', 'XD' => '😂',
|
||||
];
|
||||
|
||||
return str_replace(array_keys($smileys), array_values($smileys), $body);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user