mirror of https://github.com/pixelfed/pixelfed
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
236 lines
9.9 KiB
PHP
236 lines
9.9 KiB
PHP
<?php
|
|
|
|
use App\Jobs\Federation\DeliverDirectMessageActivity;
|
|
use App\Models\DirectMessage;
|
|
use App\Models\DmConversation;
|
|
use App\Models\DmConversationParticipant;
|
|
use App\Models\DmMessage;
|
|
use App\Models\Media;
|
|
use App\Models\Notification;
|
|
use App\Models\Profile;
|
|
use App\Models\Status;
|
|
use App\Models\UserFilter;
|
|
use App\Services\DirectMessageService;
|
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Redis;
|
|
|
|
require_once __DIR__.'/helpers.php';
|
|
|
|
uses(LazilyRefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Redis::spy();
|
|
Queue::fake();
|
|
Http::fake();
|
|
config(['snowflake.datacenter_id' => 1, 'snowflake.worker_id' => 1]);
|
|
});
|
|
|
|
function dmLegacy(Profile $from, Profile $to, string $text, array $dm = [], array $status = []): DirectMessage
|
|
{
|
|
$post = Status::factory()->create(array_merge([
|
|
'profile_id' => $from->id,
|
|
'caption' => $text,
|
|
'scope' => 'direct',
|
|
'visibility' => 'direct',
|
|
'in_reply_to_profile_id' => $to->id,
|
|
], $status));
|
|
|
|
$row = new DirectMessage;
|
|
$row->from_id = $from->id;
|
|
$row->to_id = $to->id;
|
|
$row->status_id = $post->id;
|
|
$row->type = $dm['type'] ?? 'text';
|
|
$row->is_hidden = $dm['is_hidden'] ?? false;
|
|
$row->read_at = $dm['read_at'] ?? null;
|
|
$row->meta = $dm['meta'] ?? null;
|
|
$row->save();
|
|
|
|
return $row;
|
|
}
|
|
|
|
describe('dm:backfill-conversations', function () {
|
|
it('turns a legacy thread into one conversation and keeps the status ids', function () {
|
|
$alice = dmProfile(dmLocalUser());
|
|
$bob = dmProfile(dmLocalUser());
|
|
|
|
$one = dmLegacy($alice, $bob, 'first', ['read_at' => now()]);
|
|
$two = dmLegacy($bob, $alice, 'second', ['read_at' => now()]);
|
|
$three = dmLegacy($alice, $bob, 'third');
|
|
|
|
$this->artisan('dm:backfill-conversations', ['--force' => true])->assertSuccessful();
|
|
|
|
$conversation = DmConversation::first();
|
|
|
|
expect(DmConversation::count())->toBe(1)
|
|
->and($conversation->participants_hash)->toBe(DmConversation::dmHash($alice->id, $bob->id))
|
|
->and($conversation->last_message_id)->toBe($three->status_id)
|
|
->and(DmMessage::orderBy('id')->pluck('id')->all())->toBe([$one->status_id, $two->status_id, $three->status_id])
|
|
->and(DmMessage::orderBy('id')->pluck('body')->all())->toBe(['first', 'second', 'third'])
|
|
->and(DmMessage::find($one->status_id)->legacy_dm_id)->toBe($one->id);
|
|
|
|
$bobState = DmConversationParticipant::where('profile_id', $bob->id)->first();
|
|
$aliceState = DmConversationParticipant::where('profile_id', $alice->id)->first();
|
|
|
|
expect($bobState->unread_count)->toBe(1)
|
|
->and($bobState->last_read_message_id)->toBe($one->status_id)
|
|
->and($bobState->last_activity_at)->not->toBeNull()
|
|
->and($aliceState->unread_count)->toBe(0)
|
|
->and($aliceState->last_read_message_id)->toBe($three->status_id);
|
|
});
|
|
|
|
it('can be run again without duplicating anything', function () {
|
|
$alice = dmProfile(dmLocalUser());
|
|
$bob = dmProfile(dmLocalUser());
|
|
|
|
dmLegacy($alice, $bob, 'first');
|
|
|
|
$this->artisan('dm:backfill-conversations', ['--force' => true])->assertSuccessful();
|
|
dmLegacy($bob, $alice, 'second');
|
|
$this->artisan('dm:backfill-conversations', ['--force' => true])->assertSuccessful();
|
|
$this->artisan('dm:backfill-conversations', ['--force' => true, '--full' => true])->assertSuccessful();
|
|
|
|
expect(DmMessage::count())->toBe(2)
|
|
->and(DmConversation::count())->toBe(1)
|
|
->and(DmConversationParticipant::count())->toBe(2);
|
|
});
|
|
|
|
it('carries over media, filtered threads, story metadata and mutes', function () {
|
|
$alice = dmProfile(dmLocalUser());
|
|
$bob = dmProfile(dmLocalUser());
|
|
$carol = dmRemoteProfile('carol');
|
|
|
|
$photo = dmLegacy($alice, $bob, 'with a photo', ['type' => 'photo']);
|
|
$media = Media::create([
|
|
'status_id' => $photo->status_id,
|
|
'profile_id' => $alice->id,
|
|
'media_path' => 'public/m/legacy.jpg',
|
|
'mime' => 'image/jpeg',
|
|
'size' => 1000,
|
|
]);
|
|
|
|
dmLegacy($carol, $bob, 'Tom & Jerry', ['is_hidden' => true], ['uri' => $carol->remote_url.'/statuses/9']);
|
|
|
|
dmLegacy($alice, $bob, '🔥', [
|
|
'type' => 'story:react',
|
|
'meta' => json_encode(['story_id' => 5, 'reaction' => '🔥']),
|
|
], ['type' => 'story:reaction']);
|
|
|
|
UserFilter::create([
|
|
'user_id' => $bob->id,
|
|
'filterable_id' => $alice->id,
|
|
'filterable_type' => 'App\Models\Profile',
|
|
'filter_type' => 'dm.mute',
|
|
]);
|
|
|
|
$this->artisan('dm:backfill-conversations', ['--force' => true])->assertSuccessful();
|
|
|
|
$withPhoto = DmMessage::with('media')->find($photo->status_id);
|
|
$fromCarol = DmMessage::where('profile_id', $carol->id)->first();
|
|
$reaction = DmMessage::where('type', 'story:react')->first();
|
|
|
|
expect($withPhoto->media->pluck('id')->all())->toBe([$media->id])
|
|
->and($withPhoto->body)->toBe('with a photo')
|
|
->and($fromCarol->body)->toBe('Tom & Jerry')
|
|
->and($fromCarol->ap_object_uri)->toBe($carol->remote_url.'/statuses/9')
|
|
->and(DmMessage::whereObjectUri($carol->remote_url.'/statuses/9')->exists())->toBeTrue()
|
|
->and($reaction->meta)->toBe(['story_id' => 5, 'reaction' => '🔥']);
|
|
|
|
$carolThread = DmConversation::where('participants_hash', DmConversation::dmHash($carol->id, $bob->id))->first();
|
|
$aliceThread = DmConversation::where('participants_hash', DmConversation::dmHash($alice->id, $bob->id))->first();
|
|
|
|
expect(DmConversationParticipant::where('conversation_id', $carolThread->id)->where('profile_id', $bob->id)->value('state'))->toBe('request')
|
|
->and(DmConversationParticipant::where('conversation_id', $aliceThread->id)->where('profile_id', $bob->id)->value('muted_at'))->not->toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('cleanup', function () {
|
|
it('removes the message when the status behind it is deleted', function () {
|
|
$alice = dmProfile(dmLocalUser());
|
|
$bob = dmProfile(dmLocalUser());
|
|
|
|
$legacy = dmLegacy($alice, $bob, 'first');
|
|
$this->artisan('dm:backfill-conversations', ['--force' => true])->assertSuccessful();
|
|
|
|
app(DirectMessageService::class)->deleteByStatusId($legacy->status_id);
|
|
|
|
expect(DmMessage::count())->toBe(0)
|
|
->and(DmConversation::first()->last_message_id)->toBeNull();
|
|
});
|
|
|
|
it('clears a deleted profile out of its conversations', function () {
|
|
$alice = dmProfile(dmLocalUser());
|
|
$bob = dmProfile(dmLocalUser());
|
|
$dave = dmProfile(dmLocalUser());
|
|
|
|
$service = app(DirectMessageService::class);
|
|
|
|
$direct = $service->findOrCreateDm($alice, $bob);
|
|
$service->sendMessage($direct, $alice, ['body' => 'one to one']);
|
|
$service->sendMessage($direct, $bob, ['body' => 'reply']);
|
|
|
|
$group = $service->findOrCreateConversation($alice, collect([$bob, $dave]));
|
|
$service->sendMessage($group, $alice, ['body' => 'from alice']);
|
|
$service->sendMessage($group, $bob, ['body' => 'from bob']);
|
|
|
|
$service->purgeProfile($alice->id);
|
|
|
|
expect(DmConversation::find($direct->id))->toBeNull()
|
|
->and(DmMessage::where('conversation_id', $direct->id)->count())->toBe(0)
|
|
->and(DmMessage::where('conversation_id', $group->id)->pluck('body')->all())->toBe(['from bob'])
|
|
->and(DmConversationParticipant::where('conversation_id', $group->id)->where('profile_id', $alice->id)->value('state'))->toBe('left');
|
|
});
|
|
|
|
it('keeps direct message media away from the orphan media collector', function () {
|
|
$alice = dmLocalUser();
|
|
$bob = dmLocalUser();
|
|
|
|
$attached = Media::create(['profile_id' => $alice->profile_id, 'user_id' => $alice->id, 'media_path' => 'public/m/a.jpg', 'mime' => 'image/jpeg', 'size' => 1]);
|
|
$orphan = Media::create(['profile_id' => $alice->profile_id, 'user_id' => $alice->id, 'media_path' => 'public/m/b.jpg', 'mime' => 'image/jpeg', 'size' => 1]);
|
|
|
|
DB::table('media')->whereIn('id', [$attached->id, $orphan->id])->update(['created_at' => now()->subDay()]);
|
|
|
|
$service = app(DirectMessageService::class);
|
|
$conversation = $service->findOrCreateDm(dmProfile($alice), dmProfile($bob));
|
|
$service->sendMessage($conversation, dmProfile($alice), ['media' => collect([$attached])]);
|
|
|
|
$collectable = Media::whereNull('status_id')
|
|
->notInDirectMessage()
|
|
->where('created_at', '<', now()->subHours(2))
|
|
->pluck('id')
|
|
->all();
|
|
|
|
expect($collectable)->toBe([$orphan->id]);
|
|
});
|
|
});
|
|
|
|
describe('story replies', function () {
|
|
it('lands in the conversation with the story author', function () {
|
|
$alice = dmProfile(dmLocalUser());
|
|
$bob = dmProfile(dmLocalUser(publicDm: false));
|
|
|
|
$message = app(DirectMessageService::class)->storeStoryMessage(
|
|
$alice,
|
|
$bob,
|
|
'story:comment',
|
|
'nice one',
|
|
['story_id' => 1, 'caption' => 'nice one'],
|
|
12345
|
|
);
|
|
|
|
$state = DmConversationParticipant::where('profile_id', $bob->id)->first();
|
|
|
|
expect($message->type)->toBe('story:comment')
|
|
->and($message->status_id)->toBe(12345)
|
|
->and($message->meta['caption'])->toBe('nice one')
|
|
->and($state->state)->toBe('active')
|
|
->and($state->unread_count)->toBe(1)
|
|
->and(Notification::where('profile_id', $bob->id)->where('action', 'story:comment')->count())->toBe(1);
|
|
|
|
// The story pipeline federates these, not the direct message one
|
|
Queue::assertNotPushed(DeliverDirectMessageActivity::class);
|
|
});
|
|
});
|