diff --git a/.env.example b/.env.example index 2a8604cc7..f5ed15a93 100644 --- a/.env.example +++ b/.env.example @@ -56,9 +56,9 @@ ACTIVITYPUB_SHAREDINBOX=false # php artisan optimize:clear # php artisan optimize -PF_COSTAR_ENABLED=false -CS_BLOCKED_DOMAINS='example.org,example.net,example.com' -CS_CW_DOMAINS='example.org,example.net,example.com' +PF_COSTAR_ENABLED=true +CS_BLOCKED_DOMAINS='gab.com,gab.ai,develop.gab.com' +CS_CW_DOMAINS='switter.at' CS_UNLISTED_DOMAINS='example.org,example.net,example.com' ## Optional diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 13d5dc030..8ddc88751 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -191,7 +191,6 @@ class AccountController extends Controller $pid = $user->id; Cache::forget("user:filter:list:$pid"); - Cache::forget("feature:discover:people:$pid"); Cache::forget("feature:discover:posts:$pid"); Cache::forget("api:local:exp:rec:$pid"); @@ -242,7 +241,6 @@ class AccountController extends Controller $pid = $user->id; Cache::forget("user:filter:list:$pid"); - Cache::forget("feature:discover:people:$pid"); Cache::forget("feature:discover:posts:$pid"); Cache::forget("api:local:exp:rec:$pid"); @@ -296,7 +294,6 @@ class AccountController extends Controller $pid = $user->id; Cache::forget("user:filter:list:$pid"); - Cache::forget("feature:discover:people:$pid"); Cache::forget("feature:discover:posts:$pid"); Cache::forget("api:local:exp:rec:$pid"); @@ -348,7 +345,6 @@ class AccountController extends Controller $pid = $user->id; Cache::forget("user:filter:list:$pid"); - Cache::forget("feature:discover:people:$pid"); Cache::forget("feature:discover:posts:$pid"); Cache::forget("api:local:exp:rec:$pid"); diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index b3d8869ac..94d149410 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -18,6 +18,7 @@ class PageController extends Controller '/site/about' => 'site:about', '/site/privacy' => 'site:privacy', '/site/terms' => 'site:terms', + '/site/kb/community-guidelines' => 'site:help:community-guidelines' ]; } @@ -81,7 +82,7 @@ class PageController extends Controller public function generatePage(Request $request) { $this->validate($request, [ - 'page' => 'required|string|in:about,terms,privacy', + 'page' => 'required|string|in:about,terms,privacy,community_guidelines', ]); $page = $request->input('page'); @@ -98,6 +99,10 @@ class PageController extends Controller case 'terms': Page::firstOrCreate(['slug' => '/site/terms']); break; + + case 'community_guidelines': + Page::firstOrCreate(['slug' => '/site/kb/community-guidelines']); + break; } return redirect(route('admin.settings.pages')); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index d8d13d477..6c9d4b83c 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -56,7 +56,7 @@ class SearchController extends Controller ] ]]; } else if ($type == 'Note') { - $item = Helpers::statusFirstOrFetch($tag, false); + $item = Helpers::statusFetch($tag); $tokens['posts'] = [[ 'count' => 0, 'url' => $item->url(), diff --git a/app/Http/Controllers/Settings/PrivacySettings.php b/app/Http/Controllers/Settings/PrivacySettings.php index d3283c921..99f7d4100 100644 --- a/app/Http/Controllers/Settings/PrivacySettings.php +++ b/app/Http/Controllers/Settings/PrivacySettings.php @@ -5,11 +5,13 @@ namespace App\Http\Controllers\Settings; use App\AccountLog; use App\EmailVerification; use App\Instance; +use App\Follower; use App\Media; use App\Profile; use App\User; use App\UserFilter; use App\Util\Lexer\PrettyNumber; +use App\Util\ActivityPub\Helpers; use Auth, Cache, DB; use Illuminate\Http\Request; @@ -134,9 +136,13 @@ trait PrivacySettings public function blockedInstanceStore(Request $request) { $this->validate($request, [ - 'domain' => 'required|active_url' + 'domain' => 'required|url|min:1|max:120' ]); $domain = $request->input('domain'); + if(Helpers::validateUrl($domain) == false) { + return abort(400, 'Invalid domain'); + } + $domain = parse_url($domain, PHP_URL_HOST); $instance = Instance::firstOrCreate(['domain' => $domain]); $filter = new UserFilter; $filter->user_id = Auth::user()->profile->id; @@ -165,4 +171,47 @@ trait PrivacySettings { return view('settings.privacy.blocked-keywords'); } + + public function privateAccountOptions(Request $request) + { + $this->validate($request, [ + 'mode' => 'required|string|in:keep-all,mutual-only,only-followers,remove-all', + 'duration' => 'required|integer|min:60|max:525600', + ]); + $mode = $request->input('mode'); + $duration = $request->input('duration'); + // $newRequests = $request->input('newrequests'); + + $profile = Auth::user()->profile; + $settings = Auth::user()->settings; + + if($mode !== 'keep-all') { + switch ($mode) { + case 'mutual-only': + $following = $profile->following()->pluck('profiles.id'); + Follower::whereFollowingId($profile->id)->whereNotIn('profile_id', $following)->delete(); + break; + + case 'only-followers': + $ts = now()->subMinutes($duration); + Follower::whereFollowingId($profile->id)->where('created_at', '>', $ts)->delete(); + break; + + case 'remove-all': + Follower::whereFollowingId($profile->id)->delete(); + break; + + default: + # code... + break; + } + } + $profile->is_private = true; + $settings->show_guests = false; + $settings->show_discover = false; + $settings->save(); + $profile->save(); + Cache::forget('profiles:private'); + return [200]; + } } \ No newline at end of file diff --git a/app/Http/Controllers/SiteController.php b/app/Http/Controllers/SiteController.php index c14dc1ddb..58b092b83 100644 --- a/app/Http/Controllers/SiteController.php +++ b/app/Http/Controllers/SiteController.php @@ -42,7 +42,7 @@ class SiteController extends Controller public function about() { - return Cache::remember('site:about', now()->addMinutes(120), function() { + return Cache::remember('site:about', now()->addHours(12), function() { $page = Page::whereSlug('/site/about')->whereActive(true)->first(); $stats = [ 'posts' => Status::whereLocal(true)->count(), @@ -64,24 +64,25 @@ class SiteController extends Controller public function communityGuidelines(Request $request) { - $slug = '/site/kb/community-guidelines'; - $page = Page::whereSlug($slug)->whereActive(true)->first(); - return view('site.help.community-guidelines', compact('page')); + return Cache::remember('site:help:community-guidelines', now()->addDays(120), function() { + $slug = '/site/kb/community-guidelines'; + $page = Page::whereSlug($slug)->whereActive(true)->first(); + return View::make('site.help.community-guidelines')->with(compact('page'))->render(); + }); } public function privacy(Request $request) { - return Cache::remember('site:privacy', now()->addMinutes(120), function() { + return Cache::remember('site:privacy', now()->addDays(120), function() { $slug = '/site/privacy'; $page = Page::whereSlug($slug)->whereActive(true)->first(); return View::make('site.privacy')->with(compact('page'))->render(); }); } - public function terms(Request $request) { - return Cache::remember('site:terms', now()->addMinutes(120), function() { + return Cache::remember('site:terms', now()->addDays(120), function() { $slug = '/site/terms'; $page = Page::whereSlug($slug)->whereActive(true)->first(); return View::make('site.terms')->with(compact('page'))->render(); diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index 65d6a2da5..c5a1c9975 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -30,11 +30,12 @@ class StatusController extends Controller } $status = Status::whereProfileId($user->id) + ->whereNull('reblog_of_id') ->whereNotIn('visibility',['draft','direct']) ->findOrFail($id); - if($status->uri) { - $url = $status->uri; + if($status->uri || $status->url) { + $url = $status->uri ?? $status->url; if(ends_with($url, '/activity')) { $url = str_replace('/activity', '', $url); } @@ -59,6 +60,11 @@ class StatusController extends Controller return view($template, compact('user', 'status')); } + public function showEmbed(Request $request, $username, int $id) + { + return; + } + public function showObject(Request $request, $username, int $id) { $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); @@ -102,109 +108,6 @@ class StatusController extends Controller public function store(Request $request) { return; - - $this->authCheck(); - $user = Auth::user(); - - $size = Media::whereUserId($user->id)->sum('size') / 1000; - $limit = (int) config('pixelfed.max_account_size'); - if ($size >= $limit) { - return redirect()->back()->with('error', 'You have exceeded your storage limit. Please click here for more info.'); - } - - $this->validate($request, [ - 'photo.*' => 'required|mimetypes:' . config('pixelfed.media_types').'|max:' . config('pixelfed.max_photo_size'), - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length'), - 'cw' => 'nullable|string', - 'filter_class' => 'nullable|alpha_dash|max:30', - 'filter_name' => 'nullable|string', - 'visibility' => 'required|string|min:5|max:10', - ]); - - if (count($request->file('photo')) > config('pixelfed.max_album_length')) { - return redirect()->back()->with('error', 'Too many files, max limit per post: '.config('pixelfed.max_album_length')); - } - $cw = $request->filled('cw') && $request->cw == 'on' ? true : false; - $monthHash = hash('sha1', date('Y').date('m')); - $userHash = hash('sha1', $user->id.(string) $user->created_at); - $profile = $user->profile; - $visibility = $this->validateVisibility($request->visibility); - - $cw = $profile->cw == true ? true : $cw; - $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; - - if(config('costar.enabled') == true) { - $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null) { - $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->caption, $kw) == true) { - abort(400, 'Invalid object'); - } - } - } - } - - $status = new Status(); - $status->profile_id = $profile->id; - $status->caption = strip_tags($request->caption); - $status->is_nsfw = $cw; - - // TODO: remove deprecated visibility in favor of scope - $status->visibility = $visibility; - $status->scope = $visibility; - - $status->save(); - - $photos = $request->file('photo'); - $order = 1; - $mimes = []; - $medias = 0; - - foreach ($photos as $k => $v) { - - $allowedMimes = explode(',', config('pixelfed.media_types')); - if(in_array($v->getMimeType(), $allowedMimes) == false) { - continue; - } - $filter_class = $request->input('filter_class'); - $filter_name = $request->input('filter_name'); - - $storagePath = "public/m/{$monthHash}/{$userHash}"; - $path = $v->store($storagePath); - $hash = \hash_file('sha256', $v); - $media = new Media(); - $media->status_id = $status->id; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $v->getSize(); - $media->mime = $v->getMimeType(); - - $media->filter_class = in_array($filter_class, Filter::classes()) ? $filter_class : null; - $media->filter_name = in_array($filter_name, Filter::names()) ? $filter_name : null; - $media->order = $order; - $media->save(); - array_push($mimes, $media->mime); - ImageOptimize::dispatch($media); - $order++; - $medias++; - } - - if($medias == 0) { - $status->delete(); - return; - } - $status->type = (new self)::mimeTypeCheck($mimes); - $status->save(); - - Cache::forget('profile:status_count:'.$profile->id); - NewStatusPipeline::dispatch($status); - - // TODO: Send to subscribers - - return redirect($status->url()); } public function delete(Request $request) @@ -238,7 +141,9 @@ class StatusController extends Controller $user = Auth::user(); $profile = $user->profile; - $status = Status::withCount('shares')->findOrFail($request->input('item')); + $status = Status::withCount('shares') + ->whereIn('scope', ['public', 'unlisted']) + ->findOrFail($request->input('item')); $count = $status->shares_count; diff --git a/app/Jobs/FollowPipeline/FollowPipeline.php b/app/Jobs/FollowPipeline/FollowPipeline.php index 17ad47889..57ed1d2e4 100644 --- a/app/Jobs/FollowPipeline/FollowPipeline.php +++ b/app/Jobs/FollowPipeline/FollowPipeline.php @@ -61,8 +61,6 @@ class FollowPipeline implements ShouldQueue $notification->item_type = "App\Profile"; $notification->save(); - Cache::forever('notification.'.$notification->id, $notification); - Cache::forget('feature:discover:people:'.$actor->id); $redis = Redis::connection(); $nkey = config('cache.prefix').':user.'.$target->id.'.notifications'; diff --git a/app/Jobs/LikePipeline/LikePipeline.php b/app/Jobs/LikePipeline/LikePipeline.php index f8aeb5c0a..896827917 100644 --- a/app/Jobs/LikePipeline/LikePipeline.php +++ b/app/Jobs/LikePipeline/LikePipeline.php @@ -2,16 +2,17 @@ namespace App\Jobs\LikePipeline; -use App\Like; -use App\Notification; -use Cache; +use Cache, Log, Redis; +use App\{Like, Notification}; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Log; -use Redis; +use App\Util\ActivityPub\Helpers; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; +use App\Transformer\ActivityPub\Verb\Like as LikeTransformer; class LikePipeline implements ShouldQueue { @@ -48,11 +49,15 @@ class LikePipeline implements ShouldQueue $status = $this->like->status; $actor = $this->like->actor; - if (!$status || $status->url !== null) { - // Ignore notifications to remote statuses, or deleted statuses + if (!$status) { + // Ignore notifications to deleted statuses return; } + if($status->url && $actor->domain == null) { + return $this->remoteLikeDeliver(); + } + $exists = Notification::whereProfileId($status->profile_id) ->whereActorId($actor->id) ->whereAction('like') @@ -78,4 +83,20 @@ class LikePipeline implements ShouldQueue } catch (Exception $e) { } } + + public function remoteLikeDeliver() + { + $like = $this->like; + $status = $this->like->status; + $actor = $this->like->actor; + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($like, new LikeTransformer()); + $activity = $fractal->createData($resource)->toArray(); + + $url = $status->profile->sharedInbox ?? $status->profile->inbox_url; + + Helpers::sendSignedObject($actor, $url, $activity); + } } diff --git a/app/Jobs/MentionPipeline/MentionPipeline.php b/app/Jobs/MentionPipeline/MentionPipeline.php index e75ede04f..ea833345d 100644 --- a/app/Jobs/MentionPipeline/MentionPipeline.php +++ b/app/Jobs/MentionPipeline/MentionPipeline.php @@ -50,7 +50,7 @@ class MentionPipeline implements ShouldQueue $exists = Notification::whereProfileId($target) ->whereActorId($actor->id) - ->whereAction('mention') + ->whereIn('action', ['mention', 'comment']) ->whereItemId($status->id) ->whereItemType('App\Status') ->count(); diff --git a/app/Jobs/SharePipeline/SharePipeline.php b/app/Jobs/SharePipeline/SharePipeline.php index 6d581eb73..b62c51268 100644 --- a/app/Jobs/SharePipeline/SharePipeline.php +++ b/app/Jobs/SharePipeline/SharePipeline.php @@ -2,16 +2,18 @@ namespace App\Jobs\SharePipeline; -use App\Status; -use App\Notification; -use Cache; +use Cache, Log, Redis; +use App\{Status, Notification}; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Log; -use Redis; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; +use App\Transformer\ActivityPub\Verb\Announce; +use GuzzleHttp\{Pool, Client, Promise}; +use App\Util\ActivityPub\HttpSignature; class SharePipeline implements ShouldQueue { @@ -63,6 +65,8 @@ class SharePipeline implements ShouldQueue return true; } + $this->remoteAnnounceDeliver(); + try { $notification = new Notification; $notification->profile_id = $target->id; @@ -74,8 +78,6 @@ class SharePipeline implements ShouldQueue $notification->item_type = "App\Status"; $notification->save(); - Cache::forever('notification.'.$notification->id, $notification); - $redis = Redis::connection(); $key = config('cache.prefix').':user.'.$status->profile_id.'.notifications'; $redis->lpush($key, $notification->id); @@ -83,4 +85,56 @@ class SharePipeline implements ShouldQueue Log::error($e); } } + + public function remoteAnnounceDeliver() + { + $status = $this->status; + $profile = $status->profile; + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($status, new Announce()); + $activity = $fractal->createData($resource)->toArray(); + + $audience = $status->profile->getAudienceInbox(); + + if(empty($audience) || $status->scope != 'public') { + // Return on profiles with no remote followers + return; + } + + $payload = json_encode($activity); + + $client = new Client([ + 'timeout' => config('federation.activitypub.delivery.timeout') + ]); + + $requests = function($audience) use ($client, $activity, $profile, $payload) { + foreach($audience as $url) { + $headers = HttpSignature::sign($profile, $url, $activity); + yield function() use ($client, $url, $headers, $payload) { + return $client->postAsync($url, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true + ] + ]); + }; + } + }; + + $pool = new Pool($client, $requests($audience), [ + 'concurrency' => config('federation.activitypub.delivery.concurrency'), + 'fulfilled' => function ($response, $index) { + }, + 'rejected' => function ($reason, $index) { + } + ]); + + $promise = $pool->promise(); + + $promise->wait(); + + } } diff --git a/app/Jobs/StatusPipeline/StatusActivityPubDeliver.php b/app/Jobs/StatusPipeline/StatusActivityPubDeliver.php index a2b6d6f93..ec4243107 100644 --- a/app/Jobs/StatusPipeline/StatusActivityPubDeliver.php +++ b/app/Jobs/StatusPipeline/StatusActivityPubDeliver.php @@ -49,6 +49,7 @@ class StatusActivityPubDeliver implements ShouldQueue public function handle() { $status = $this->status; + $profile = $status->profile; if($status->local == false || $status->url || $status->uri) { return; @@ -56,12 +57,11 @@ class StatusActivityPubDeliver implements ShouldQueue $audience = $status->profile->getAudienceInbox(); - if(empty($audience) || $status->visibility != 'public') { + if(empty($audience) || $status->scope != 'public') { // Return on profiles with no remote followers return; } - $profile = $status->profile; $fractal = new Fractal\Manager(); $fractal->setSerializer(new ArraySerializer()); diff --git a/app/Transformer/ActivityPub/Verb/Announce.php b/app/Transformer/ActivityPub/Verb/Announce.php index b6acb31d1..682885b77 100644 --- a/app/Transformer/ActivityPub/Verb/Announce.php +++ b/app/Transformer/ActivityPub/Verb/Announce.php @@ -11,9 +11,16 @@ class Announce extends Fractal\TransformerAbstract { return [ '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $status->permalink(), 'type' => 'Announce', 'actor' => $status->profile->permalink(), - 'object' => $status->parent()->url() + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], + 'cc' => [ + $status->profile->permalink(), + $status->profile->follower_url ?? $status->profile->permalink('/followers') + ], + 'published' => $status->created_at->format(DATE_ISO8601), + 'object' => $status->parent()->url(), ]; } } \ No newline at end of file diff --git a/app/Transformer/ActivityPub/Verb/Like.php b/app/Transformer/ActivityPub/Verb/Like.php index 5662ab758..b6f699158 100644 --- a/app/Transformer/ActivityPub/Verb/Like.php +++ b/app/Transformer/ActivityPub/Verb/Like.php @@ -11,6 +11,7 @@ class Like extends Fractal\TransformerAbstract { return [ '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $like->actor->permalink('#likes/'.$like->id), 'type' => 'Like', 'actor' => $like->actor->permalink(), 'object' => $like->status->url() diff --git a/app/Transformer/Api/StatusTransformer.php b/app/Transformer/Api/StatusTransformer.php index 1c033c1f1..8a16e4dfd 100644 --- a/app/Transformer/Api/StatusTransformer.php +++ b/app/Transformer/Api/StatusTransformer.php @@ -34,7 +34,7 @@ class StatusTransformer extends Fractal\TransformerAbstract 'muted' => null, 'sensitive' => (bool) $status->is_nsfw, 'spoiler_text' => $status->cw_summary, - 'visibility' => $status->visibility, + 'visibility' => $status->visibility ?? $status->scope, 'application' => [ 'name' => 'web', 'website' => null diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index f2c1169db..49a881740 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -146,9 +146,13 @@ class Helpers { $host = parse_url($valid, PHP_URL_HOST); + if(count(dns_get_record($host, DNS_A | DNS_AAAA)) == 0) { + return false; + } + if(config('costar.enabled') == true) { if( - (config('costar.domain.block') != null && in_array($host, config('costar.domain.block')) == true) || + (config('costar.domain.block') != null && Str::contains($host, config('costar.domain.block')) == true) || (config('costar.actor.block') != null && in_array($url, config('costar.actor.block')) == true) ) { return false; @@ -202,7 +206,7 @@ class Helpers { return self::fetchFromUrl($url); } - public static function statusFirstOrFetch($url, $replyTo = true) + public static function statusFirstOrFetch($url, $replyTo = false) { $url = self::validateUrl($url); if($url == false) { @@ -333,6 +337,11 @@ class Helpers { } } + public static function statusFetch($url) + { + return self::statusFirstOrFetch($url); + } + public static function importNoteAttachment($data, Status $status) { if(self::verifyAttachments($data) == false) { @@ -399,7 +408,10 @@ class Helpers { return; } $domain = parse_url($res['id'], PHP_URL_HOST); - $username = Purify::clean($res['preferredUsername']); + $username = (string) Purify::clean($res['preferredUsername']); + if(empty($username)) { + return; + } $remoteUsername = "@{$username}@{$domain}"; abort_if(!self::validateUrl($res['inbox']), 400); @@ -408,9 +420,9 @@ class Helpers { $profile = Profile::whereRemoteUrl($res['id'])->first(); if(!$profile) { - $profile = new Profile; + $profile = new Profile(); $profile->domain = $domain; - $profile->username = Purify::clean($remoteUsername); + $profile->username = (string) Purify::clean($remoteUsername); $profile->name = Purify::clean($res['name']) ?? 'user'; $profile->bio = Purify::clean($res['summary']); $profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null; @@ -428,6 +440,11 @@ class Helpers { return $profile; } + public static function profileFetch($url) + { + return self::profileFirstOrNew($url); + } + public static function sendSignedObject($senderProfile, $url, $body) { abort_if(!self::validateUrl($url), 400); diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 4731811f9..54aaa4af4 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -151,7 +151,7 @@ class Inbox if(Status::whereUrl($url)->exists()) { return; } - Helpers::statusFirstOrFetch($url, false); + Helpers::statusFetch($url); return; } @@ -205,21 +205,27 @@ class Inbox { $actor = $this->actorFirstOrCreate($this->payload['actor']); $activity = $this->payload['object']; + if(!$actor || $actor->domain == null) { return; } + if(Helpers::validateLocalUrl($activity) == false) { return; } - $parent = Helpers::statusFirstOrFetch($activity, true); - if(!$parent) { + + $parent = Helpers::statusFetch($activity); + + if(empty($parent)) { return; } + $status = Status::firstOrCreate([ 'profile_id' => $actor->id, 'reblog_of_id' => $parent->id, - 'type' => 'reply' + 'type' => 'share' ]); + Notification::firstOrCreate([ 'profile_id' => $parent->profile->id, 'actor_id' => $actor->id, @@ -229,6 +235,7 @@ class Inbox 'item_id' => $parent->id, 'item_type' => 'App\Status' ]); + $parent->reblogs_count = $parent->shares()->count(); $parent->save(); } @@ -267,8 +274,10 @@ class Inbox if(is_string($obj) && Helpers::validateUrl($obj)) { // actor object detected // todo delete actor + return; } else if (Helpers::validateUrl($obj['id']) && Helpers::validateObject($obj) && $obj['type'] == 'Tombstone') { // todo delete status or object + return; } } @@ -316,6 +325,21 @@ class Inbox break; case 'Announce': + $obj = $obj['object']; + abort_if(!Helpers::validateLocalUrl($obj), 400); + $status = Helpers::statusFetch($obj); + if(!$status) { + return; + } + Status::whereProfileId($profile->id) + ->whereReblogOfId($status->id) + ->forceDelete(); + Notification::whereProfileId($status->profile->id) + ->whereActorId($profile->id) + ->whereAction('share') + ->whereItemId($status->reblog_of_id) + ->whereItemType('App\Status') + ->forceDelete(); break; case 'Block': @@ -347,6 +371,6 @@ class Inbox ->forceDelete(); break; } - + return; } } diff --git a/app/Util/Lexer/Autolink.php b/app/Util/Lexer/Autolink.php index 4ec8c2ef7..4aa38d7f1 100755 --- a/app/Util/Lexer/Autolink.php +++ b/app/Util/Lexer/Autolink.php @@ -718,7 +718,7 @@ class Autolink extends Regex // Replace the username $linkText = Str::startsWith($screen_name, '@') ? $screen_name : '@'.$screen_name; $class = $this->class_user; - $url = $this->url_base_user.$screen_name;; + $url = $this->url_base_user . $screen_name; } if (!empty($class)) { $attributes['class'] = $class; diff --git a/app/Util/RateLimit/User.php b/app/Util/RateLimit/User.php index 75e4b1c6e..c93aa6c4f 100644 --- a/app/Util/RateLimit/User.php +++ b/app/Util/RateLimit/User.php @@ -48,4 +48,9 @@ trait User { { return 500; } + + public function getMaxInstanceBansPerDayAttribute() + { + return 100; + } } \ No newline at end of file diff --git a/app/Util/Webfinger/Webfinger.php b/app/Util/Webfinger/Webfinger.php index e8f10c31c..879103332 100644 --- a/app/Util/Webfinger/Webfinger.php +++ b/app/Util/Webfinger/Webfinger.php @@ -4,74 +4,43 @@ namespace App\Util\Webfinger; class Webfinger { - public $user; - public $subject; - public $aliases; - public $links; + protected $user; + protected $subject; + protected $aliases; + protected $links; - public function __construct($user) - { - $this->user = $user; - $this->subject = ''; - $this->aliases = []; - $this->links = []; - } + public function __construct($user) + { + $this->subject = 'acct:'.$user->username.'@'.parse_url(config('app.url'), PHP_URL_HOST); + $this->aliases = [ + $user->url(), + $user->permalink(), + ]; + $this->links = [ + [ + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => $user->url(), + ], + [ + 'rel' => 'http://schemas.google.com/g/2010#updates-from', + 'type' => 'application/atom+xml', + 'href' => $user->permalink('.atom'), + ], + [ + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => $user->permalink(), + ], + ]; + } - public function setSubject() - { - $host = parse_url(config('app.url'), PHP_URL_HOST); - $username = $this->user->username; - - $this->subject = 'acct:'.$username.'@'.$host; - - return $this; - } - - public function generateAliases() - { - $this->aliases = [ - $this->user->url(), - $this->user->permalink(), - ]; - - return $this; - } - - public function generateLinks() - { - $user = $this->user; - - $this->links = [ - [ - 'rel' => 'http://webfinger.net/rel/profile-page', - 'type' => 'text/html', - 'href' => $user->url(), - ], - [ - 'rel' => 'http://schemas.google.com/g/2010#updates-from', - 'type' => 'application/atom+xml', - 'href' => $user->permalink('.atom'), - ], - [ - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => $user->permalink(), - ], - ]; - - return $this; - } - - public function generate() - { - $this->setSubject(); - $this->generateAliases(); - $this->generateLinks(); - - return [ - 'subject' => $this->subject, - 'aliases' => $this->aliases, - 'links' => $this->links, - ]; - } + public function generate() + { + return [ + 'subject' => $this->subject, + 'aliases' => $this->aliases, + 'links' => $this->links, + ]; + } } diff --git a/config/instance.php b/config/instance.php index 3842de358..6fc04c902 100644 --- a/config/instance.php +++ b/config/instance.php @@ -7,4 +7,9 @@ return [ 'enabled' => env('INSTANCE_CONTACT_FORM', false), 'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1), ], + + 'announcement' => [ + 'enabled' => env('INSTANCE_ANNOUNCEMENT_ENABLED', true), + 'message' => env('INSTANCE_ANNOUNCEMENT_MESSAGE', 'Example announcement message.
Something else here') + ] ]; \ No newline at end of file diff --git a/public/js/profile.js b/public/js/profile.js index 338f6bd7d..0ddcd0afe 100644 Binary files a/public/js/profile.js and b/public/js/profile.js differ diff --git a/public/js/status.js b/public/js/status.js index b2c231a7c..ba4af018e 100644 Binary files a/public/js/status.js and b/public/js/status.js differ diff --git a/public/js/timeline.js b/public/js/timeline.js index 4ec4f9432..f4c3175df 100644 Binary files a/public/js/timeline.js and b/public/js/timeline.js differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 4512a2b1b..cbe51aa43 100644 Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ diff --git a/resources/assets/js/components/PostComponent.vue b/resources/assets/js/components/PostComponent.vue index 7902fd6a7..f0be53b1c 100644 --- a/resources/assets/js/components/PostComponent.vue +++ b/resources/assets/js/components/PostComponent.vue @@ -179,14 +179,14 @@

-

-

+

+

likes - + shares
@@ -268,13 +268,13 @@

-

+

likes - + shares
diff --git a/resources/assets/js/components/Profile.vue b/resources/assets/js/components/Profile.vue index 95ebe907a..2ce177e8a 100644 --- a/resources/assets/js/components/Profile.vue +++ b/resources/assets/js/components/Profile.vue @@ -242,7 +242,7 @@

-

+

diff --git a/resources/assets/js/components/Timeline.vue b/resources/assets/js/components/Timeline.vue index c4c834638..cb3be6da7 100644 --- a/resources/assets/js/components/Timeline.vue +++ b/resources/assets/js/components/Timeline.vue @@ -117,7 +117,7 @@

-

+

@@ -449,6 +449,13 @@ this.config = res.data; this.fetchProfile(); this.fetchTimelineApi(); + + // if(this.config.announcement.enabled == true) { + // let msg = $('
') + // .addClass('alert alert-warning mb-0 rounded-0 text-center font-weight-bold') + // .html(this.config.announcement.message); + // $('body').prepend(msg); + // } }); }, @@ -748,7 +755,9 @@ type: 'status', item: status.id }).then(res => { - this.feed.splice(index,1); + this.feed = this.feed.filter(s => { + return s.id != status.id; + }) swal('Success', 'You have successfully deleted this post', 'success'); }).catch(err => { swal('Error', 'Something went wrong. Please try again later.', 'error'); diff --git a/resources/views/admin/pages/home.blade.php b/resources/views/admin/pages/home.blade.php index b1cdaa025..0fe9bf325 100644 --- a/resources/views/admin/pages/home.blade.php +++ b/resources/views/admin/pages/home.blade.php @@ -49,17 +49,22 @@
@csrf - +
@csrf - + +
+
+ @csrf + +
@csrf - - + +
@else @@ -73,17 +78,22 @@
@csrf - +
@csrf - + +
+
+ @csrf + +
@csrf - - + +
@endif diff --git a/resources/views/settings/privacy.blade.php b/resources/views/settings/privacy.blade.php index cff620c98..584529f49 100644 --- a/resources/views/settings/privacy.blade.php +++ b/resources/views/settings/privacy.blade.php @@ -10,10 +10,15 @@

Muted Users Blocked Users + Blocked keywords + Blocked instances

@csrf + + +
is_private ? 'checked=""':''}}>
+ + {{--
+ is_private ? 'disabled=""':''}} {{$settings->show_discover ? 'checked=""':''}}> + +

When this option is enabled, your profile and posts are used for discover recommendations. Only public profiles and posts are used.

+
--}} + {{--
+ + +

If selected, you will be able to receive messages from any user even if you do not follow them.

+
--}} + {{--
+ + +

This prevents posts with potentially sensitive content from displaying in your search results.

+
--}} + {{--
+ + +

Use this to eliminate search results from accounts you've blocked or muted.

+
+
+ + +

Show all media, including potentially sensitive content.

+
--}} +
show_profile_follower_count ? 'checked=""':''}}>
- {{--
+
@@ -158,13 +158,13 @@

{{__('helpcenter.communityGuidelines')}}

-

 

-

 

+

Content that will be removed

+

Content that is explicitly disallowed

-
--}} +
{{--
diff --git a/resources/views/site/help/community-guidelines.blade.php b/resources/views/site/help/community-guidelines.blade.php index 2ece56545..122113203 100644 --- a/resources/views/site/help/community-guidelines.blade.php +++ b/resources/views/site/help/community-guidelines.blade.php @@ -6,21 +6,58 @@

Community Guidelines


-
-
-
-
-
- -
-
-
-
-

This page isn't available

-

We haven't finished it yet, it will be updated soon!

-
-
-
-
+ @if($page) +
+ {!!$page->content!!} +
+

This document was last updated {{$page->created_at->format('M d, Y')}}.

+

Originally adapted from the Mastodon Code of Conduct.

+ @else +
+

The following guidelines are not a legal document, and final interpretation is up to the administration of {{config('pixelfed.domain.app')}}; they are here to provide you with an insight into our content moderation policies:

+
+
The following types of content will be removed from the public timeline:
+
    +
  • Excessive advertising
  • +
  • Uncurated news bots posting from third-party news sources
  • +
  • Untagged nudity, pornography and sexually explicit content, including artistic depictions
  • +
  • Untagged gore and extremely graphic violence, including artistic depictions
  • +
+
+
+
+
The following types of content will be removed from the public timeline, and may result in account suspension and revocation of access to the service:
+
    +
  • Racism or advocation of racism
  • +
  • Sexism or advocation of sexism
  • +
  • Discrimination against gender and sexual minorities, or advocation thereof
  • +
  • Xenophobic and/or violent nationalism
  • +
+
+
+
+
The following types of content are explicitly disallowed and will result in revocation of access to the service:
+
    +
  • Sexual depictions of children
  • +
  • Content illegal in Canada, Germany and/or France, such as holocaust denial or Nazi symbolism
  • +
  • Conduct promoting the ideology of National Socialism
  • +
+
+
+
+
Any conduct intended to stalk or harass other users, or to impede other users from utilizing the service, or to degrade the performance of the service, or to harass other users, or to incite other users to perform any of the aforementioned actions, is also disallowed, and subject to punishment up to and including revocation of access to the service. This includes, but is not limited to, the following behaviors:
+
    +
  • Continuing to engage in conversation with a user that has specifically has requested for said engagement with that user to cease and desist may be considered harassment, regardless of platform-specific privacy tools employed.
  • +
  • Aggregating, posting, and/or disseminating a person's demographic, personal, or private data without express permission (informally called doxing or dropping dox) may be considered harassment.
  • +
  • Inciting users to engage another user in continued interaction or discussion after a user has requested for said engagement with that user to cease and desist (informally called brigading or dogpiling) may be considered harassment.
  • +
+
+
+

These provisions notwithstanding, the administration of the service reserves the right to revoke any user's access permissions, at any time, for any reason, except as limited by law.

+
+

This document was last updated Jun 26, 2019.

+

Originally adapted from the Mastodon Code of Conduct.

+
+ @endif @endsection diff --git a/resources/views/site/help/partial/sidebar.blade.php b/resources/views/site/help/partial/sidebar.blade.php index 05f6ec99f..2278eb7a3 100644 --- a/resources/views/site/help/partial/sidebar.blade.php +++ b/resources/views/site/help/partial/sidebar.blade.php @@ -30,11 +30,11 @@ - {{-- --}} + {{-- --}} diff --git a/resources/views/site/terms.blade.php b/resources/views/site/terms.blade.php index 6ede08305..5a2e51fb2 100644 --- a/resources/views/site/terms.blade.php +++ b/resources/views/site/terms.blade.php @@ -43,7 +43,9 @@

Pixelfed may revise these terms of service for its website at any time without notice. By using this website you are agreeing to be bound by the then current version of these terms of service.

8. Governing Law

These terms and conditions are governed by and construed in accordance with the laws of Canada and you irrevocably submit to the exclusive jurisdiction of the courts in that State or location.

-
9. Additional Rules
+
9. Community Guidelines
+

You can view our Community Guidelines here.

+
10. Additional Rules

This website does not have any additional rules.

@endif @endsection diff --git a/routes/web.php b/routes/web.php index ee4da9733..a7a591c21 100644 --- a/routes/web.php +++ b/routes/web.php @@ -192,10 +192,10 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('privacy/blocked-users', 'SettingsController@blockedUsers')->name('settings.privacy.blocked-users'); Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate'); Route::get('privacy/blocked-instances', 'SettingsController@blockedInstances')->name('settings.privacy.blocked-instances'); - Route::post('privacy/blocked-instances', 'SettingsController@blockedInstanceStore'); + Route::post('privacy/blocked-instances', 'SettingsController@blockedInstanceStore')->middleware('throttle:maxInstanceBansPerDay,1440'); Route::post('privacy/blocked-instances/unblock', 'SettingsController@blockedInstanceUnblock')->name('settings.privacy.blocked-instances.unblock'); Route::get('privacy/blocked-keywords', 'SettingsController@blockedKeywords')->name('settings.privacy.blocked-keywords'); - + Route::post('privacy/account', 'SettingsController@privateAccountOptions')->name('settings.privacy.account'); Route::get('reports', 'SettingsController@reportsHome')->name('settings.reports'); // Todo: Release in 0.7.2 Route::group(['prefix' => 'remove', 'middleware' => 'dangerzone'], function() { diff --git a/tests/Unit/APAnnounceStrategyTest.php b/tests/Unit/APAnnounceStrategyTest.php new file mode 100644 index 000000000..30b2bf1e3 --- /dev/null +++ b/tests/Unit/APAnnounceStrategyTest.php @@ -0,0 +1,86 @@ +invalid = [ + 'id' => 'test', + 'type' => 'Announce', + 'actor' => null, + 'published' => '', + 'to' => ['test'], + 'cc' => 'test', + 'object' => 'test' + ]; + + $this->mastodon = json_decode('{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":{"@id":"as:movedTo","@type":"@id"},"Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"},"featured":{"@id":"toot:featured","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value"}],"id":"https://mastodon.social/users/dansup/statuses/100784657480587830/activity","type":"Announce","actor":"https://mastodon.social/users/dansup","published":"2018-09-25T05:03:49Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://pleroma.site/users/pixeldev","https://mastodon.social/users/dansup/followers"],"object":"https://pleroma.site/objects/68b5c876-f52b-4819-8d81-de6839d73fbc","atomUri":"https://mastodon.social/users/dansup/statuses/100784657480587830/activity"}', true); + + $this->pleroma = json_decode('{"@context":"https://www.w3.org/ns/activitystreams","actor":"https://pleroma.site/users/pixeldev","cc":["https://www.w3.org/ns/activitystreams#Public"],"context":"tag:mastodon.social,2018-10-14:objectId=59146153:objectType=Conversation","context_id":12325955,"id":"https://pleroma.site/activities/db2273eb-d504-4e3a-8f74-c343d069755a","object":"https://mastodon.social/users/dansup/statuses/100891324792793720","published":"2018-10-14T01:22:18.554227Z","to":["https://pleroma.site/users/pixeldev/followers","https://mastodon.social/users/dansup"],"type":"Announce"}', true); + } + + public function testBasicValidation() + { + $this->assertFalse(Helpers::validateObject($this->invalid)); + } + + public function testMastodonValidation() + { + $this->assertTrue(Helpers::validateObject($this->mastodon)); + } + + public function testPleromaValidation() + { + $this->assertTrue(Helpers::validateObject($this->pleroma)); + } + + public function testMastodonAudienceScope() + { + $scope = Helpers::normalizeAudience($this->mastodon, false); + $actual = [ + "to" => [], + "cc" => [ + "https://pleroma.site/users/pixeldev", + "https://mastodon.social/users/dansup/followers", + ], + "scope" => "public", + ]; + + $this->assertEquals($scope, $actual); + } + + public function testPleromaAudienceScope() + { + $scope = Helpers::normalizeAudience($this->pleroma, false); + $actual = [ + "to" => [ + "https://pleroma.site/users/pixeldev/followers", + "https://mastodon.social/users/dansup", + ], + "cc" => [], + "scope" => "unlisted", + ]; + + $this->assertEquals($scope, $actual); + } + + public function testInvalidAudienceScope() + { + $scope = Helpers::normalizeAudience($this->invalid, false); + $actual = [ + 'to' => [], + 'cc' => [], + 'scope' => 'private' + ]; + $this->assertEquals($scope, $actual); + } +} diff --git a/tests/Unit/ActivityPub/RemoteFollowTest.php b/tests/Unit/ActivityPub/RemoteFollowTest.php new file mode 100644 index 000000000..5d97bd55e --- /dev/null +++ b/tests/Unit/ActivityPub/RemoteFollowTest.php @@ -0,0 +1,27 @@ +mastodon = '{"type":"Follow","signature":{"type":"RsaSignature2017","signatureValue":"Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==","creator":"http://mastodon.example.org/users/admin#main-key","created":"2018-02-17T13:29:31Z"},"object":"http://localtesting.pleroma.lol/users/lain","nickname":"lain","id":"http://mastodon.example.org/users/admin#follows/2","actor":"http://mastodon.example.org/users/admin","@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"toot":"http://joinmastodon.org/ns#","sensitive":"as:sensitive","ostatus":"http://ostatus.org#","movedTo":"as:movedTo","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","atomUri":"ostatus:atomUri","Hashtag":"as:Hashtag","Emoji":"toot:Emoji"}]}'; + + } + + /** @test */ + public function validateMastodonFollowObject() + { + $mastodon = json_decode($this->mastodon, true); + $mastodon = Helpers::validateObject($mastodon); + $this->assertTrue($mastodon); + } +} \ No newline at end of file