create(array_merge(['created_at' => now()->subYear()], $attributes)); $user->refresh(); UserSetting::updateOrCreate(['user_id' => $user->id], ['public_dm' => $publicDm]); return $user; } function dmProfile(User $user): Profile { return Profile::findOrFail($user->profile_id); } function dmRemoteProfile(string $username = 'alice', string $domain = 'remote.example'): Profile { $actor = "https://{$domain}/users/{$username}"; return Profile::factory()->remote()->create([ 'domain' => $domain, 'username' => "@{$username}@{$domain}", 'remote_url' => $actor, 'key_id' => "{$actor}#main-key", 'inbox_url' => "{$actor}/inbox", 'sharedInbox' => "https://{$domain}/inbox", 'last_fetched_at' => now(), ]); } /** * Seed the DNS and banned-domain caches so URL validation passes without * a network lookup. Call after factories, the lazy refresh can flush the * cache. */ function dmSeedHosts(array $hosts = ['remote.example', 'other.example']): void { $hosts[] = config('pixelfed.domain.app'); foreach ($hosts as $host) { Cache::put('helpers:url:public-ips:'.hash('xxh128', $host), ['203.0.113.40'], 3600); } Cache::put('instances:banned:domains', [], 1209600); } /** * Give $b the id right after $a, the way two profiles created in the same * millisecond end up. Ids that close are equal once compared as floats, * so anything that orders or matches ids has to cope with it. */ function dmNeighbour(Profile $a, Profile $b): Profile { DB::table('profiles')->where('id', $b->id)->update(['id' => $a->id + 1]); return Profile::findOrFail($a->id + 1); } function dmFollow(Profile $follower, Profile $target): void { Follower::create([ 'profile_id' => $follower->id, 'following_id' => $target->id, 'local_profile' => $follower->domain === null, 'local_following' => $target->domain === null, ]); } /** * A direct Note the way Mastodon sends one: addressed to people only, * a Mention for each of them, and the thread identifiers. * * @param array $recipients */ function dmNote(Profile $author, string $path, array $recipients, array $overrides = []): array { $id = $author->remote_url.'/statuses/'.$path; $to = array_map(fn (Profile $profile) => $profile->permalink(), $recipients); $mentions = implode(' ', array_map( fn (Profile $profile) => '@'.ltrim(explode('@', ltrim($profile->username, '@'))[0], '@').'', $recipients )); return array_merge([ 'id' => $id, 'type' => 'Note', 'attributedTo' => $author->remote_url, 'url' => "https://{$author->domain}/@user/{$path}", 'content' => "

{$mentions} hello there

", 'published' => now()->subMinute()->toAtomString(), 'inReplyTo' => null, 'to' => $to, 'cc' => [], 'sensitive' => false, 'conversation' => "tag:{$author->domain},2026-09-21:objectId=1:objectType=Conversation", 'context' => "https://{$author->domain}/contexts/1", 'attachment' => [], 'tag' => array_map(fn (Profile $profile) => [ 'type' => 'Mention', 'href' => $profile->permalink(), 'name' => '@'.ltrim($profile->username, '@'), ], $recipients), ], $overrides); } function dmDeliver(Profile $actor, array $object, string $type = 'Create'): void { $payload = [ '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => $object['id'].'/activity', 'type' => $type, 'actor' => $actor->remote_url, 'object' => $object, ]; if ($type === 'Create') { $payload['to'] = $object['to'] ?? []; $payload['cc'] = $object['cc'] ?? []; } $headers = [ 'signature' => ['keyId="'.$actor->key_id.'",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="dGVzdA=="'], 'date' => [now()->toRfc7231String()], ]; (new Inbox($headers, null, $payload))->handle(); } }