Skip to content

Commit 73724d9

Browse files
Thiritinclaude
andcommitted
Source-specific chat implementation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 55381ff commit 73724d9

15 files changed

Lines changed: 305 additions & 58 deletions

File tree

app/Console/Commands/Chat/DeleteCommand.php

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
namespace App\Console\Commands\Chat;
44

5-
use App\Models\User;
6-
use App\Models\Message;
75
use App\Events\Chat\Broadcasts\BroadcastMessageDeletionIdsEvent;
6+
use App\Models\Message;
7+
use App\Models\User;
88
use Carbon\Carbon;
99
use Illuminate\Support\Facades\Log;
1010

1111
class DeleteCommand extends AbstractChatCommand
1212
{
1313
protected string $name = 'delete';
14+
1415
protected array $aliases = ['del', 'remove', 'purge'];
16+
1517
protected string $description = 'Delete all messages from a user within a time period';
18+
1619
protected string $signature = '/delete <username> <duration>';
1720

1821
protected array $parameters = [
@@ -30,7 +33,7 @@ class DeleteCommand extends AbstractChatCommand
3033

3134
public function authorize(User $user): bool
3235
{
33-
return $user->hasPermission('chat.moderate') ||
36+
return $user->hasPermission('chat.moderate') ||
3437
$user->hasRole('admin') ||
3538
$user->hasRole('moderator');
3639
}
@@ -42,16 +45,18 @@ protected function execute(User $user, array $parameters): void
4245

4346
// Find the target user
4447
$targetUser = User::where('name', $username)->first();
45-
46-
if (!$targetUser) {
48+
49+
if (! $targetUser) {
4750
$this->feedback($user, "User '{$username}' not found.", 'error');
51+
4852
return;
4953
}
5054

5155
// Parse duration to get the time range
5256
$cutoffTime = $this->parseDurationToTime($duration);
53-
if (!$cutoffTime) {
57+
if (! $cutoffTime) {
5458
$this->feedback($user, "Invalid duration format. Use formats like '5m', '1h', '1d'.", 'error');
59+
5560
return;
5661
}
5762

@@ -63,28 +68,34 @@ protected function execute(User $user, array $parameters): void
6368

6469
if ($messages->isEmpty()) {
6570
$this->feedback($user, "No messages found from '{$username}' in the last {$duration}.", 'info');
71+
6672
return;
6773
}
6874

69-
// Collect message IDs for broadcasting
70-
$messageIds = $messages->pluck('id')->toArray();
71-
$messageCount = count($messageIds);
75+
// Group messages by source_id for broadcasting
76+
$messagesBySource = $messages->groupBy('source_id');
77+
$messageCount = $messages->count();
7278

7379
// Log the IDs being deleted for debugging
7480
Log::info('Deleting messages with IDs', [
75-
'message_ids' => $messageIds,
81+
'message_ids' => $messages->pluck('id')->toArray(),
7682
'count' => $messageCount,
7783
'target_user' => $username,
7884
]);
7985

8086
// Soft delete all messages
81-
Message::whereIn('id', $messageIds)->update([
87+
Message::whereIn('id', $messages->pluck('id'))->update([
8288
'deleted_at' => now(),
8389
'deleted_by_user_id' => $user->id,
8490
]);
8591

86-
// Broadcast deletion event with message IDs to ALL users (including the moderator)
87-
broadcast(new BroadcastMessageDeletionIdsEvent($messageIds));
92+
// Broadcast deletion event to each source channel
93+
foreach ($messagesBySource as $sourceId => $sourceMessages) {
94+
broadcast(new BroadcastMessageDeletionIdsEvent(
95+
$sourceMessages->pluck('id')->toArray(),
96+
$sourceId
97+
));
98+
}
8899

89100
// Calculate human-readable duration
90101
$durationText = $this->humanizeDuration($duration);
@@ -114,16 +125,16 @@ protected function execute(User $user, array $parameters): void
114125
private function parseDurationToTime(string $duration): ?Carbon
115126
{
116127
$matches = [];
117-
if (!preg_match('/^(\d+)([smhd])$/i', $duration, $matches)) {
128+
if (! preg_match('/^(\d+)([smhd])$/i', $duration, $matches)) {
118129
return null;
119130
}
120131

121132
$value = (int) $matches[1];
122133
$unit = strtolower($matches[2]);
123134

124135
$now = now();
125-
126-
return match($unit) {
136+
137+
return match ($unit) {
127138
's' => $now->subSeconds($value),
128139
'm' => $now->subMinutes($value),
129140
'h' => $now->subHours($value),
@@ -135,18 +146,18 @@ private function parseDurationToTime(string $duration): ?Carbon
135146
private function humanizeDuration(string $duration): string
136147
{
137148
$matches = [];
138-
if (!preg_match('/^(\d+)([smhd])$/i', $duration, $matches)) {
149+
if (! preg_match('/^(\d+)([smhd])$/i', $duration, $matches)) {
139150
return $duration;
140151
}
141152

142153
$value = (int) $matches[1];
143154
$unit = strtolower($matches[2]);
144155

145-
return match($unit) {
146-
's' => $value . ' second' . ($value > 1 ? 's' : ''),
147-
'm' => $value . ' minute' . ($value > 1 ? 's' : ''),
148-
'h' => $value . ' hour' . ($value > 1 ? 's' : ''),
149-
'd' => $value . ' day' . ($value > 1 ? 's' : ''),
156+
return match ($unit) {
157+
's' => $value.' second'.($value > 1 ? 's' : ''),
158+
'm' => $value.' minute'.($value > 1 ? 's' : ''),
159+
'h' => $value.' hour'.($value > 1 ? 's' : ''),
160+
'd' => $value.' day'.($value > 1 ? 's' : ''),
150161
default => $duration,
151162
};
152163
}
@@ -160,4 +171,4 @@ public function examples(): array
160171
'/purge SpamUser 30m' => 'Using alias to purge messages from last 30 minutes',
161172
];
162173
}
163-
}
174+
}

app/Events/Chat/Broadcasts/BroadcastMessageDeletionIdsEvent.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@ class BroadcastMessageDeletionIdsEvent implements ShouldBroadcast
1212
{
1313
use Dispatchable, InteractsWithSockets, SerializesModels;
1414

15-
public function __construct(public array $ids) {}
15+
public function __construct(public array $ids, public ?int $sourceId = null) {}
1616

1717
public function broadcastOn(): array
1818
{
1919
return [
20-
new Channel('chat'),
20+
new Channel('chat.source.'.$this->sourceId),
2121
];
2222
}
2323

2424
public function broadcastWith(): array
2525
{
2626
return [
2727
'ids' => $this->ids,
28+
'source_id' => $this->sourceId,
2829
];
2930
}
3031

app/Events/Chat/Broadcasts/ChatMessageEvent.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public function __construct(public readonly Message $message, public readonly Us
1919
public function broadcastOn(): array
2020
{
2121
return [
22-
new Channel('chat'),
22+
new Channel('chat.source.'.$this->message->source_id),
2323
];
2424
}
2525

@@ -35,6 +35,7 @@ public function broadcastWith(): array
3535
'type' => $this->message->type,
3636
'priority' => $this->message->priority,
3737
'metadata' => $this->message->metadata,
38+
'source_id' => $this->message->source_id,
3839
];
3940
}
4041

app/Events/Chat/Broadcasts/SystemAnnouncementEvent.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public function __construct(public readonly Message $message) {}
1818
public function broadcastOn(): array
1919
{
2020
return [
21-
new Channel('chat'),
21+
new Channel('chat.source.'.$this->message->source_id),
2222
];
2323
}
2424

@@ -32,17 +32,18 @@ public function broadcastWith(): array
3232
'role' => (object) [
3333
'name' => 'System',
3434
'slug' => 'system',
35-
'chat_color' => '#FFD700'
35+
'chat_color' => '#FFD700',
3636
],
3737
'chat_color' => '#FFD700',
3838
'type' => $this->message->type,
3939
'priority' => $this->message->priority,
4040
'metadata' => $this->message->metadata,
41+
'source_id' => $this->message->source_id,
4142
];
4243
}
4344

4445
public function broadcastAs(): string
4546
{
4647
return 'message';
4748
}
48-
}
49+
}

app/Http/Controllers/MessageController.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
use Illuminate\Support\Facades\Cache;
1212
use Illuminate\Support\Facades\RateLimiter;
1313
use Illuminate\Validation\ValidationException;
14-
use Inertia\Inertia;
1514
use Symfony\Component\HttpFoundation\Response as SymphonyResponse;
1615

1716
class MessageController extends Controller
1817
{
1918
public function send(MessageRequest $request)
2019
{
2120
$message = $request->post('message');
21+
$sourceId = $request->post('source_id');
2222
$user = $request->user();
2323
$maxTries = Cache::get('chat.maxTries', static fn () => config('chat.default.maxTries'));
2424
$rateDecay = Cache::get('chat.rateDecay', static fn () => config('chat.default.rateDecay'));
@@ -28,14 +28,14 @@ public function send(MessageRequest $request)
2828
$activeTimeout = Timeout::where('user_id', $user->id)
2929
->where('expires_at', '>', now())
3030
->first();
31-
31+
3232
if ($activeTimeout) {
3333
$remainingTime = now()->diffInSeconds($activeTimeout->expires_at);
3434
$message = "You are timed out for {$remainingTime} more seconds";
3535
if ($activeTimeout->reason) {
3636
$message .= " (Reason: {$activeTimeout->reason})";
3737
}
38-
38+
3939
return response([
4040
'success' => false,
4141
'error' => 'user_timed_out',
@@ -48,7 +48,7 @@ public function send(MessageRequest $request)
4848
], SymphonyResponse::HTTP_FORBIDDEN);
4949
}
5050

51-
if ($user->cant('chat.ignore.ratelimit') && !$user->isAdmin() && !$user->isModerator()) {
51+
if ($user->cant('chat.ignore.ratelimit') && ! $user->isAdmin() && ! $user->isModerator()) {
5252
if (RateLimiter::tooManyAttempts('send-message:'.$user->id, $maxTries)) {
5353
$seconds = RateLimiter::availableIn('send-message:'.$user->id);
5454

@@ -80,6 +80,7 @@ public function send(MessageRequest $request)
8080

8181
$messageModel = $user->messages()->create([
8282
'message' => $message,
83+
'source_id' => $sourceId,
8384
'is_command' => false,
8485
'type' => 'user',
8586
]);
@@ -98,6 +99,7 @@ public function send(MessageRequest $request)
9899
'chat_color' => $user->chat_color,
99100
'type' => $messageModel->type,
100101
'is_command' => false,
102+
'source_id' => $messageModel->source_id,
101103
],
102104
'rateLimit' => [
103105
'maxTries' => $maxTries,
@@ -108,21 +110,26 @@ public function send(MessageRequest $request)
108110
]);
109111
}
110112

111-
112113
public function loadOlder(Request $request)
113114
{
114115
$user = $request->user();
115116
$beforeId = $request->get('before_id');
117+
$sourceId = $request->get('source_id');
116118
$limit = 50;
117119

118120
$query = Message::with('user')
119121
->where(function ($query) use ($user) {
120122
$query->where('is_command', false)
121-
->orWhere('type', 'announcement')
122-
->orWhere('type', 'system')
123-
->orWhere(fn ($q) => $q->where('is_command', true)->where('user_id', $user->id));
123+
->orWhere('type', 'announcement')
124+
->orWhere('type', 'system')
125+
->orWhere(fn ($q) => $q->where('is_command', true)->where('user_id', $user->id));
124126
});
125127

128+
// Filter by source_id if provided
129+
if ($sourceId) {
130+
$query->where('source_id', $sourceId);
131+
}
132+
126133
if ($beforeId) {
127134
$query->where('id', '<', $beforeId);
128135
}
@@ -142,6 +149,7 @@ public function loadOlder(Request $request)
142149
'type' => $message->type,
143150
'priority' => $message->priority,
144151
'metadata' => $message->metadata,
152+
'source_id' => $message->source_id,
145153
])
146154
->values();
147155

app/Http/Controllers/StreamController.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,9 @@ public function show(Request $request, Show $show)
249249
'initialStatus' => $show->isLive() ? 'online' : \Cache::get('stream.status', static fn () => StreamStatusEnum::OFFLINE->value),
250250
'initialListeners' => $show->viewer_count ?? StreamInfoService::getUserCount(),
251251
'initialOtherDevice' => false, // This feature has been removed with Client model
252+
'sourceId' => $show->source_id,
252253
'chatMessages' => array_values(Message::with('user')
254+
->where('source_id', $show->source_id)
253255
->where(function ($query) use ($user) {
254256
$query->where('is_command', false)
255257
->orWhere('type', 'announcement')
@@ -271,6 +273,70 @@ public function show(Request $request, Show $show)
271273
'type' => $message->type,
272274
'priority' => $message->priority,
273275
'metadata' => $message->metadata,
276+
'source_id' => $message->source_id,
277+
])->toArray()),
278+
'rateLimit' => [
279+
'maxTries' => \Cache::get('chat.maxTries', static fn () => config('chat.default.maxTries')),
280+
'rateDecay' => \Cache::get('chat.rateDecay', static fn () => config('chat.default.rateDecay')),
281+
'slowMode' => \Cache::get('chat.slowMode', static fn () => config('chat.default.slowMode')),
282+
'secondsLeft' => (! $user->isStaff()) ? RateLimiter::availableIn('send-message:'.$user->id) : 0,
283+
],
284+
]);
285+
}
286+
287+
/**
288+
* Pop-out chat window
289+
*/
290+
public function chat(Request $request, Show $show)
291+
{
292+
/** @var User $user */
293+
$user = Auth::user();
294+
295+
// Load show with source relationship
296+
$show->load('source');
297+
298+
// Check access restrictions
299+
if (! $show->canBeAccessedBy($user)) {
300+
abort(403, 'You do not have permission to view this chat');
301+
}
302+
303+
// Check if show is in a valid state for chat
304+
if (! in_array($show->status, ['scheduled', 'live'])) {
305+
abort(404, 'Chat is not available for this show');
306+
}
307+
308+
return Inertia::render('ChatPopout', [
309+
'show' => [
310+
'id' => $show->id,
311+
'title' => $show->title,
312+
'slug' => $show->slug,
313+
'status' => $show->status,
314+
],
315+
'sourceId' => $show->source_id,
316+
'chatMessages' => array_values(Message::with('user')
317+
->where('source_id', $show->source_id)
318+
->where(function ($query) use ($user) {
319+
$query->where('is_command', false)
320+
->orWhere('type', 'announcement')
321+
->orWhere('type', 'system')
322+
->orWhere(fn ($q) => $q->where('is_command', true)->where('user_id', $user->id));
323+
})
324+
->orderBy('created_at', 'desc')
325+
->limit(50)
326+
->get()
327+
->reverse()
328+
->map(fn (Message $message) => [
329+
'id' => $message->id,
330+
'message' => $message->message,
331+
'is_command' => (bool) $message->is_command,
332+
'name' => $message->user->name ?? null,
333+
'role' => $message->user?->role,
334+
'chat_color' => $message->user?->chat_color,
335+
'time' => $message->created_at->format('H:i'),
336+
'type' => $message->type,
337+
'priority' => $message->priority,
338+
'metadata' => $message->metadata,
339+
'source_id' => $message->source_id,
274340
])->toArray()),
275341
'rateLimit' => [
276342
'maxTries' => \Cache::get('chat.maxTries', static fn () => config('chat.default.maxTries')),

0 commit comments

Comments
 (0)