Merge pull request #5 from pixelfed/dev

Sync June 28
This commit is contained in:
okpierre 2019-06-28 15:02:14 -04:00 committed by GitHub
commit 4b9dab52d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 647 additions and 271 deletions

View file

@ -56,9 +56,9 @@ ACTIVITYPUB_SHAREDINBOX=false
# php artisan optimize:clear # php artisan optimize:clear
# php artisan optimize # php artisan optimize
PF_COSTAR_ENABLED=false PF_COSTAR_ENABLED=true
CS_BLOCKED_DOMAINS='example.org,example.net,example.com' CS_BLOCKED_DOMAINS='gab.com,gab.ai,develop.gab.com'
CS_CW_DOMAINS='example.org,example.net,example.com' CS_CW_DOMAINS='switter.at'
CS_UNLISTED_DOMAINS='example.org,example.net,example.com' CS_UNLISTED_DOMAINS='example.org,example.net,example.com'
## Optional ## Optional

View file

@ -191,7 +191,6 @@ class AccountController extends Controller
$pid = $user->id; $pid = $user->id;
Cache::forget("user:filter:list:$pid"); Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:people:$pid");
Cache::forget("feature:discover:posts:$pid"); Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid"); Cache::forget("api:local:exp:rec:$pid");
@ -242,7 +241,6 @@ class AccountController extends Controller
$pid = $user->id; $pid = $user->id;
Cache::forget("user:filter:list:$pid"); Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:people:$pid");
Cache::forget("feature:discover:posts:$pid"); Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid"); Cache::forget("api:local:exp:rec:$pid");
@ -296,7 +294,6 @@ class AccountController extends Controller
$pid = $user->id; $pid = $user->id;
Cache::forget("user:filter:list:$pid"); Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:people:$pid");
Cache::forget("feature:discover:posts:$pid"); Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid"); Cache::forget("api:local:exp:rec:$pid");
@ -348,7 +345,6 @@ class AccountController extends Controller
$pid = $user->id; $pid = $user->id;
Cache::forget("user:filter:list:$pid"); Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:people:$pid");
Cache::forget("feature:discover:posts:$pid"); Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid"); Cache::forget("api:local:exp:rec:$pid");

View file

@ -18,6 +18,7 @@ class PageController extends Controller
'/site/about' => 'site:about', '/site/about' => 'site:about',
'/site/privacy' => 'site:privacy', '/site/privacy' => 'site:privacy',
'/site/terms' => 'site:terms', '/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) public function generatePage(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'page' => 'required|string|in:about,terms,privacy', 'page' => 'required|string|in:about,terms,privacy,community_guidelines',
]); ]);
$page = $request->input('page'); $page = $request->input('page');
@ -98,6 +99,10 @@ class PageController extends Controller
case 'terms': case 'terms':
Page::firstOrCreate(['slug' => '/site/terms']); Page::firstOrCreate(['slug' => '/site/terms']);
break; break;
case 'community_guidelines':
Page::firstOrCreate(['slug' => '/site/kb/community-guidelines']);
break;
} }
return redirect(route('admin.settings.pages')); return redirect(route('admin.settings.pages'));

View file

@ -56,7 +56,7 @@ class SearchController extends Controller
] ]
]]; ]];
} else if ($type == 'Note') { } else if ($type == 'Note') {
$item = Helpers::statusFirstOrFetch($tag, false); $item = Helpers::statusFetch($tag);
$tokens['posts'] = [[ $tokens['posts'] = [[
'count' => 0, 'count' => 0,
'url' => $item->url(), 'url' => $item->url(),

View file

@ -5,11 +5,13 @@ namespace App\Http\Controllers\Settings;
use App\AccountLog; use App\AccountLog;
use App\EmailVerification; use App\EmailVerification;
use App\Instance; use App\Instance;
use App\Follower;
use App\Media; use App\Media;
use App\Profile; use App\Profile;
use App\User; use App\User;
use App\UserFilter; use App\UserFilter;
use App\Util\Lexer\PrettyNumber; use App\Util\Lexer\PrettyNumber;
use App\Util\ActivityPub\Helpers;
use Auth, Cache, DB; use Auth, Cache, DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -134,9 +136,13 @@ trait PrivacySettings
public function blockedInstanceStore(Request $request) public function blockedInstanceStore(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'domain' => 'required|active_url' 'domain' => 'required|url|min:1|max:120'
]); ]);
$domain = $request->input('domain'); $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]); $instance = Instance::firstOrCreate(['domain' => $domain]);
$filter = new UserFilter; $filter = new UserFilter;
$filter->user_id = Auth::user()->profile->id; $filter->user_id = Auth::user()->profile->id;
@ -165,4 +171,47 @@ trait PrivacySettings
{ {
return view('settings.privacy.blocked-keywords'); 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];
}
} }

View file

@ -42,7 +42,7 @@ class SiteController extends Controller
public function about() 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(); $page = Page::whereSlug('/site/about')->whereActive(true)->first();
$stats = [ $stats = [
'posts' => Status::whereLocal(true)->count(), 'posts' => Status::whereLocal(true)->count(),
@ -64,24 +64,25 @@ class SiteController extends Controller
public function communityGuidelines(Request $request) public function communityGuidelines(Request $request)
{ {
$slug = '/site/kb/community-guidelines'; return Cache::remember('site:help:community-guidelines', now()->addDays(120), function() {
$page = Page::whereSlug($slug)->whereActive(true)->first(); $slug = '/site/kb/community-guidelines';
return view('site.help.community-guidelines', compact('page')); $page = Page::whereSlug($slug)->whereActive(true)->first();
return View::make('site.help.community-guidelines')->with(compact('page'))->render();
});
} }
public function privacy(Request $request) 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'; $slug = '/site/privacy';
$page = Page::whereSlug($slug)->whereActive(true)->first(); $page = Page::whereSlug($slug)->whereActive(true)->first();
return View::make('site.privacy')->with(compact('page'))->render(); return View::make('site.privacy')->with(compact('page'))->render();
}); });
} }
public function terms(Request $request) 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'; $slug = '/site/terms';
$page = Page::whereSlug($slug)->whereActive(true)->first(); $page = Page::whereSlug($slug)->whereActive(true)->first();
return View::make('site.terms')->with(compact('page'))->render(); return View::make('site.terms')->with(compact('page'))->render();

View file

@ -30,11 +30,12 @@ class StatusController extends Controller
} }
$status = Status::whereProfileId($user->id) $status = Status::whereProfileId($user->id)
->whereNull('reblog_of_id')
->whereNotIn('visibility',['draft','direct']) ->whereNotIn('visibility',['draft','direct'])
->findOrFail($id); ->findOrFail($id);
if($status->uri) { if($status->uri || $status->url) {
$url = $status->uri; $url = $status->uri ?? $status->url;
if(ends_with($url, '/activity')) { if(ends_with($url, '/activity')) {
$url = str_replace('/activity', '', $url); $url = str_replace('/activity', '', $url);
} }
@ -59,6 +60,11 @@ class StatusController extends Controller
return view($template, compact('user', 'status')); return view($template, compact('user', 'status'));
} }
public function showEmbed(Request $request, $username, int $id)
{
return;
}
public function showObject(Request $request, $username, int $id) public function showObject(Request $request, $username, int $id)
{ {
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
@ -102,109 +108,6 @@ class StatusController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
return; 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 <a href="#">here</a> 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) public function delete(Request $request)
@ -238,7 +141,9 @@ class StatusController extends Controller
$user = Auth::user(); $user = Auth::user();
$profile = $user->profile; $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; $count = $status->shares_count;

View file

@ -61,8 +61,6 @@ class FollowPipeline implements ShouldQueue
$notification->item_type = "App\Profile"; $notification->item_type = "App\Profile";
$notification->save(); $notification->save();
Cache::forever('notification.'.$notification->id, $notification);
Cache::forget('feature:discover:people:'.$actor->id);
$redis = Redis::connection(); $redis = Redis::connection();
$nkey = config('cache.prefix').':user.'.$target->id.'.notifications'; $nkey = config('cache.prefix').':user.'.$target->id.'.notifications';

View file

@ -2,16 +2,17 @@
namespace App\Jobs\LikePipeline; namespace App\Jobs\LikePipeline;
use App\Like; use Cache, Log, Redis;
use App\Notification; use App\{Like, Notification};
use Cache;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Log; use App\Util\ActivityPub\Helpers;
use Redis; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\Like as LikeTransformer;
class LikePipeline implements ShouldQueue class LikePipeline implements ShouldQueue
{ {
@ -48,11 +49,15 @@ class LikePipeline implements ShouldQueue
$status = $this->like->status; $status = $this->like->status;
$actor = $this->like->actor; $actor = $this->like->actor;
if (!$status || $status->url !== null) { if (!$status) {
// Ignore notifications to remote statuses, or deleted statuses // Ignore notifications to deleted statuses
return; return;
} }
if($status->url && $actor->domain == null) {
return $this->remoteLikeDeliver();
}
$exists = Notification::whereProfileId($status->profile_id) $exists = Notification::whereProfileId($status->profile_id)
->whereActorId($actor->id) ->whereActorId($actor->id)
->whereAction('like') ->whereAction('like')
@ -78,4 +83,20 @@ class LikePipeline implements ShouldQueue
} catch (Exception $e) { } 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);
}
} }

View file

@ -50,7 +50,7 @@ class MentionPipeline implements ShouldQueue
$exists = Notification::whereProfileId($target) $exists = Notification::whereProfileId($target)
->whereActorId($actor->id) ->whereActorId($actor->id)
->whereAction('mention') ->whereIn('action', ['mention', 'comment'])
->whereItemId($status->id) ->whereItemId($status->id)
->whereItemType('App\Status') ->whereItemType('App\Status')
->count(); ->count();

View file

@ -2,16 +2,18 @@
namespace App\Jobs\SharePipeline; namespace App\Jobs\SharePipeline;
use App\Status; use Cache, Log, Redis;
use App\Notification; use App\{Status, Notification};
use Cache;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Log; use League\Fractal;
use Redis; 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 class SharePipeline implements ShouldQueue
{ {
@ -63,6 +65,8 @@ class SharePipeline implements ShouldQueue
return true; return true;
} }
$this->remoteAnnounceDeliver();
try { try {
$notification = new Notification; $notification = new Notification;
$notification->profile_id = $target->id; $notification->profile_id = $target->id;
@ -74,8 +78,6 @@ class SharePipeline implements ShouldQueue
$notification->item_type = "App\Status"; $notification->item_type = "App\Status";
$notification->save(); $notification->save();
Cache::forever('notification.'.$notification->id, $notification);
$redis = Redis::connection(); $redis = Redis::connection();
$key = config('cache.prefix').':user.'.$status->profile_id.'.notifications'; $key = config('cache.prefix').':user.'.$status->profile_id.'.notifications';
$redis->lpush($key, $notification->id); $redis->lpush($key, $notification->id);
@ -83,4 +85,56 @@ class SharePipeline implements ShouldQueue
Log::error($e); 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();
}
} }

View file

@ -49,6 +49,7 @@ class StatusActivityPubDeliver implements ShouldQueue
public function handle() public function handle()
{ {
$status = $this->status; $status = $this->status;
$profile = $status->profile;
if($status->local == false || $status->url || $status->uri) { if($status->local == false || $status->url || $status->uri) {
return; return;
@ -56,12 +57,11 @@ class StatusActivityPubDeliver implements ShouldQueue
$audience = $status->profile->getAudienceInbox(); $audience = $status->profile->getAudienceInbox();
if(empty($audience) || $status->visibility != 'public') { if(empty($audience) || $status->scope != 'public') {
// Return on profiles with no remote followers // Return on profiles with no remote followers
return; return;
} }
$profile = $status->profile;
$fractal = new Fractal\Manager(); $fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer()); $fractal->setSerializer(new ArraySerializer());

View file

@ -11,9 +11,16 @@ class Announce extends Fractal\TransformerAbstract
{ {
return [ return [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $status->permalink(),
'type' => 'Announce', 'type' => 'Announce',
'actor' => $status->profile->permalink(), '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(),
]; ];
} }
} }

View file

@ -11,6 +11,7 @@ class Like extends Fractal\TransformerAbstract
{ {
return [ return [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $like->actor->permalink('#likes/'.$like->id),
'type' => 'Like', 'type' => 'Like',
'actor' => $like->actor->permalink(), 'actor' => $like->actor->permalink(),
'object' => $like->status->url() 'object' => $like->status->url()

View file

@ -34,7 +34,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'muted' => null, 'muted' => null,
'sensitive' => (bool) $status->is_nsfw, 'sensitive' => (bool) $status->is_nsfw,
'spoiler_text' => $status->cw_summary, 'spoiler_text' => $status->cw_summary,
'visibility' => $status->visibility, 'visibility' => $status->visibility ?? $status->scope,
'application' => [ 'application' => [
'name' => 'web', 'name' => 'web',
'website' => null 'website' => null

View file

@ -146,9 +146,13 @@ class Helpers {
$host = parse_url($valid, PHP_URL_HOST); $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.enabled') == true) {
if( 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) (config('costar.actor.block') != null && in_array($url, config('costar.actor.block')) == true)
) { ) {
return false; return false;
@ -202,7 +206,7 @@ class Helpers {
return self::fetchFromUrl($url); return self::fetchFromUrl($url);
} }
public static function statusFirstOrFetch($url, $replyTo = true) public static function statusFirstOrFetch($url, $replyTo = false)
{ {
$url = self::validateUrl($url); $url = self::validateUrl($url);
if($url == false) { 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) public static function importNoteAttachment($data, Status $status)
{ {
if(self::verifyAttachments($data) == false) { if(self::verifyAttachments($data) == false) {
@ -399,7 +408,10 @@ class Helpers {
return; return;
} }
$domain = parse_url($res['id'], PHP_URL_HOST); $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}"; $remoteUsername = "@{$username}@{$domain}";
abort_if(!self::validateUrl($res['inbox']), 400); abort_if(!self::validateUrl($res['inbox']), 400);
@ -408,9 +420,9 @@ class Helpers {
$profile = Profile::whereRemoteUrl($res['id'])->first(); $profile = Profile::whereRemoteUrl($res['id'])->first();
if(!$profile) { if(!$profile) {
$profile = new Profile; $profile = new Profile();
$profile->domain = $domain; $profile->domain = $domain;
$profile->username = Purify::clean($remoteUsername); $profile->username = (string) Purify::clean($remoteUsername);
$profile->name = Purify::clean($res['name']) ?? 'user'; $profile->name = Purify::clean($res['name']) ?? 'user';
$profile->bio = Purify::clean($res['summary']); $profile->bio = Purify::clean($res['summary']);
$profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null; $profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null;
@ -428,6 +440,11 @@ class Helpers {
return $profile; return $profile;
} }
public static function profileFetch($url)
{
return self::profileFirstOrNew($url);
}
public static function sendSignedObject($senderProfile, $url, $body) public static function sendSignedObject($senderProfile, $url, $body)
{ {
abort_if(!self::validateUrl($url), 400); abort_if(!self::validateUrl($url), 400);

View file

@ -151,7 +151,7 @@ class Inbox
if(Status::whereUrl($url)->exists()) { if(Status::whereUrl($url)->exists()) {
return; return;
} }
Helpers::statusFirstOrFetch($url, false); Helpers::statusFetch($url);
return; return;
} }
@ -205,21 +205,27 @@ class Inbox
{ {
$actor = $this->actorFirstOrCreate($this->payload['actor']); $actor = $this->actorFirstOrCreate($this->payload['actor']);
$activity = $this->payload['object']; $activity = $this->payload['object'];
if(!$actor || $actor->domain == null) { if(!$actor || $actor->domain == null) {
return; return;
} }
if(Helpers::validateLocalUrl($activity) == false) { if(Helpers::validateLocalUrl($activity) == false) {
return; return;
} }
$parent = Helpers::statusFirstOrFetch($activity, true);
if(!$parent) { $parent = Helpers::statusFetch($activity);
if(empty($parent)) {
return; return;
} }
$status = Status::firstOrCreate([ $status = Status::firstOrCreate([
'profile_id' => $actor->id, 'profile_id' => $actor->id,
'reblog_of_id' => $parent->id, 'reblog_of_id' => $parent->id,
'type' => 'reply' 'type' => 'share'
]); ]);
Notification::firstOrCreate([ Notification::firstOrCreate([
'profile_id' => $parent->profile->id, 'profile_id' => $parent->profile->id,
'actor_id' => $actor->id, 'actor_id' => $actor->id,
@ -229,6 +235,7 @@ class Inbox
'item_id' => $parent->id, 'item_id' => $parent->id,
'item_type' => 'App\Status' 'item_type' => 'App\Status'
]); ]);
$parent->reblogs_count = $parent->shares()->count(); $parent->reblogs_count = $parent->shares()->count();
$parent->save(); $parent->save();
} }
@ -267,8 +274,10 @@ class Inbox
if(is_string($obj) && Helpers::validateUrl($obj)) { if(is_string($obj) && Helpers::validateUrl($obj)) {
// actor object detected // actor object detected
// todo delete actor // todo delete actor
return;
} else if (Helpers::validateUrl($obj['id']) && Helpers::validateObject($obj) && $obj['type'] == 'Tombstone') { } else if (Helpers::validateUrl($obj['id']) && Helpers::validateObject($obj) && $obj['type'] == 'Tombstone') {
// todo delete status or object // todo delete status or object
return;
} }
} }
@ -316,6 +325,21 @@ class Inbox
break; break;
case 'Announce': 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; break;
case 'Block': case 'Block':
@ -347,6 +371,6 @@ class Inbox
->forceDelete(); ->forceDelete();
break; break;
} }
return;
} }
} }

View file

@ -718,7 +718,7 @@ class Autolink extends Regex
// Replace the username // Replace the username
$linkText = Str::startsWith($screen_name, '@') ? $screen_name : '@'.$screen_name; $linkText = Str::startsWith($screen_name, '@') ? $screen_name : '@'.$screen_name;
$class = $this->class_user; $class = $this->class_user;
$url = $this->url_base_user.$screen_name;; $url = $this->url_base_user . $screen_name;
} }
if (!empty($class)) { if (!empty($class)) {
$attributes['class'] = $class; $attributes['class'] = $class;

View file

@ -48,4 +48,9 @@ trait User {
{ {
return 500; return 500;
} }
public function getMaxInstanceBansPerDayAttribute()
{
return 100;
}
} }

View file

@ -4,74 +4,43 @@ namespace App\Util\Webfinger;
class Webfinger class Webfinger
{ {
public $user; protected $user;
public $subject; protected $subject;
public $aliases; protected $aliases;
public $links; protected $links;
public function __construct($user) public function __construct($user)
{ {
$this->user = $user; $this->subject = 'acct:'.$user->username.'@'.parse_url(config('app.url'), PHP_URL_HOST);
$this->subject = ''; $this->aliases = [
$this->aliases = []; $user->url(),
$this->links = []; $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() public function generate()
{ {
$host = parse_url(config('app.url'), PHP_URL_HOST); return [
$username = $this->user->username; 'subject' => $this->subject,
'aliases' => $this->aliases,
$this->subject = 'acct:'.$username.'@'.$host; 'links' => $this->links,
];
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,
];
}
} }

View file

@ -7,4 +7,9 @@ return [
'enabled' => env('INSTANCE_CONTACT_FORM', false), 'enabled' => env('INSTANCE_CONTACT_FORM', false),
'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1), '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.<br><span class="font-weight-normal">Something else here</span>')
]
]; ];

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -179,14 +179,14 @@
<div class="reactions my-1"> <div class="reactions my-1">
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3> <h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3> <h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3> <h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
<h3 v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3> <h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
</div> </div>
<div class="reaction-counts font-weight-bold mb-0"> <div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal"> <span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes <span class="like-count">{{status.favourites_count || 0}}</span> likes
</span> </span>
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal"> <span v-if="status.visibility == 'public'" class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares <span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span> </span>
</div> </div>
@ -268,13 +268,13 @@
<div class="reactions py-2"> <div class="reactions py-2">
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3> <h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3> <h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary float-right cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn float-right cursor-pointer']" title="Share" v-on:click="shareStatus"></h3> <h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary float-right cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn float-right cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
</div> </div>
<div class="reaction-counts font-weight-bold mb-0"> <div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal"> <span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes <span class="like-count">{{status.favourites_count || 0}}</span> likes
</span> </span>
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal"> <span v-if="status.visibility == 'public'" class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares <span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span> </span>
</div> </div>

View file

@ -242,7 +242,7 @@
<div class="reactions my-1" v-if="user.hasOwnProperty('id')"> <div class="reactions my-1" v-if="user.hasOwnProperty('id')">
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3> <h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
<h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3> <h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3> <h3 v-if="status.visibility == 'public'" v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
</div> </div>
<div class="likes font-weight-bold"> <div class="likes font-weight-bold">

View file

@ -117,7 +117,7 @@
<div v-if="!modes.distractionFree" class="reactions my-1"> <div v-if="!modes.distractionFree" class="reactions my-1">
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3> <h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3> <h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3> <h3 v-if="status.visibility == 'public'" v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
</div> </div>
<div class="likes font-weight-bold" v-if="expLc(status) == true && !modes.distractionFree"> <div class="likes font-weight-bold" v-if="expLc(status) == true && !modes.distractionFree">
@ -449,6 +449,13 @@
this.config = res.data; this.config = res.data;
this.fetchProfile(); this.fetchProfile();
this.fetchTimelineApi(); this.fetchTimelineApi();
// if(this.config.announcement.enabled == true) {
// let msg = $('<div>')
// .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', type: 'status',
item: status.id item: status.id
}).then(res => { }).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'); swal('Success', 'You have successfully deleted this post', 'success');
}).catch(err => { }).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error'); swal('Error', 'Something went wrong. Please try again later.', 'error');

View file

@ -49,17 +49,22 @@
<form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create"> <form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create">
@csrf @csrf
<input type="hidden" name="page" value="about"> <input type="hidden" name="page" value="about">
<button type="submit" class="btn btn-outline-secondary font-weight-bold">Create About Page</button> <button type="submit" class="btn btn-outline-secondary font-weight-bold">Create About</button>
</form> </form>
<form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create"> <form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create">
@csrf @csrf
<input type="hidden" name="page" value="privacy"> <input type="hidden" name="page" value="privacy">
<button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Privacy Page</button> <button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Privacy</button>
</form>
<form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create">
@csrf
<input type="hidden" name="page" value="terms">
<button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Terms</button>
</form> </form>
<form class="form-inline" method="post" action="/i/admin/settings/pages/create"> <form class="form-inline" method="post" action="/i/admin/settings/pages/create">
@csrf @csrf
<input type="hidden" name="page" value="terms"> <input type="hidden" name="page" value="community_guidelines">
<button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Terms Page</button> <button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Guidelines</button>
</form> </form>
</div> </div>
@else @else
@ -73,17 +78,22 @@
<form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create"> <form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create">
@csrf @csrf
<input type="hidden" name="page" value="about"> <input type="hidden" name="page" value="about">
<button type="submit" class="btn btn-outline-secondary font-weight-bold">Create About Page</button> <button type="submit" class="btn btn-outline-secondary font-weight-bold">Create About</button>
</form> </form>
<form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create"> <form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create">
@csrf @csrf
<input type="hidden" name="page" value="privacy"> <input type="hidden" name="page" value="privacy">
<button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Privacy Page</button> <button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Privacy</button>
</form>
<form class="form-inline mr-1" method="post" action="/i/admin/settings/pages/create">
@csrf
<input type="hidden" name="page" value="terms">
<button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Terms</button>
</form> </form>
<form class="form-inline" method="post" action="/i/admin/settings/pages/create"> <form class="form-inline" method="post" action="/i/admin/settings/pages/create">
@csrf @csrf
<input type="hidden" name="page" value="terms"> <input type="hidden" name="page" value="community_guidelines">
<button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Terms Page</button> <button type="submit" class="btn btn-outline-secondary font-weight-bold">Create Guidelines</button>
</form> </form>
</div> </div>
@endif @endif

View file

@ -10,10 +10,15 @@
<p> <p>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.muted-users')}}">Muted Users</a> <a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.muted-users')}}">Muted Users</a>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-users')}}">Blocked Users</a> <a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-users')}}">Blocked Users</a>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-keywords')}}">Blocked keywords</a>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-instances')}}">Blocked instances</a>
</p> </p>
</div> </div>
<form method="post"> <form method="post">
@csrf @csrf
<input type="hidden" name="pa_mode" value="">
<input type="hidden" name="pa_duration" value="">
<input type="hidden" name="pa_newrequests" value="">
<div class="form-check pb-3"> <div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="is_private" id="is_private" {{$settings->is_private ? 'checked=""':''}}> <input class="form-check-input" type="checkbox" name="is_private" id="is_private" {{$settings->is_private ? 'checked=""':''}}>
<label class="form-check-label font-weight-bold" for="is_private"> <label class="form-check-label font-weight-bold" for="is_private">
@ -29,6 +34,43 @@
<p class="text-muted small help-text">When your account is visible to search engines, your information can be crawled and stored by search engines.</p> <p class="text-muted small help-text">When your account is visible to search engines, your information can be crawled and stored by search engines.</p>
</div> </div>
{{-- <div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="show_discover" id="show_discover" {{$settings->is_private ? 'disabled=""':''}} {{$settings->show_discover ? 'checked=""':''}}>
<label class="form-check-label font-weight-bold" for="show_discover">
{{__('Visible on discover')}}
</label>
<p class="text-muted small help-text">When this option is enabled, your profile and posts are used for discover recommendations. Only public profiles and posts are used.</p>
</div> --}}
{{--<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" value="" id="dm">
<label class="form-check-label font-weight-bold" for="dm">
{{__('Receive Direct Messages from anyone')}}
</label>
<p class="text-muted small help-text">If selected, you will be able to receive messages from any user even if you do not follow them.</p>
</div>--}}
{{-- <div class="form-check pb-3">
<input class="form-check-input" type="checkbox" value="" id="srs" checked="">
<label class="form-check-label font-weight-bold" for="srs">
{{__('Hide sensitive content from search results')}}
</label>
<p class="text-muted small help-text">This prevents posts with potentially sensitive content from displaying in your search results.</p>
</div> --}}
{{-- <div class="form-check pb-3">
<input class="form-check-input" type="checkbox" value="" id="rbma" checked="">
<label class="form-check-label font-weight-bold" for="rbma">
{{__('Remove blocked and muted accounts')}}
</label>
<p class="text-muted small help-text">Use this to eliminate search results from accounts you've blocked or muted.</p>
</div>
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" value="" id="ssp">
<label class="form-check-label font-weight-bold" for="ssp">
{{__('Display media that may contain sensitive content')}}
</label>
<p class="text-muted small help-text">Show all media, including potentially sensitive content.</p>
</div> --}}
<div class="form-check pb-3"> <div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="show_profile_follower_count" id="show_profile_follower_count" {{$settings->show_profile_follower_count ? 'checked=""':''}}> <input class="form-check-input" type="checkbox" name="show_profile_follower_count" id="show_profile_follower_count" {{$settings->show_profile_follower_count ? 'checked=""':''}}>
<label class="form-check-label font-weight-bold" for="show_profile_follower_count"> <label class="form-check-label font-weight-bold" for="show_profile_follower_count">
@ -49,9 +91,104 @@
<div class="form-group row mt-5 pt-5"> <div class="form-group row mt-5 pt-5">
<div class="col-12 text-right"> <div class="col-12 text-right">
<hr> <hr>
<button type="submit" class="btn btn-primary font-weight-bold">Submit</button> <button type="submit" class="btn btn-primary font-weight-bold py-0 px-5">Submit</button>
</div> </div>
</div> </div>
</form> </form>
<div class="modal" tabindex="-1" role="dialog" id="pac_modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm this action</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body p-3">
<p class="font-weight-bold">Please select the type of private account you would like:</p>
<div>
<div class="form-check">
<input class="form-check-input" type="radio" id="fm-1" name="pfType" value="keep-all" checked>
<label class="form-check-label pb-2 font-weight-bold" for="fm-1">
Keep existing followers
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="fm-2" name="pfType" value="mutual-only">
<label class="form-check-label pb-2 font-weight-bold" for="fm-2">
Only keep mutual followers
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="fm-3" name="pfType" value="only-followers">
<label class="form-check-label pb-2 font-weight-bold" for="fm-3">
Only followers that have followed you for atleast <select name="pfDuration">
<option value="60">1 hour</option>
<option value="1440">1 day</option>
<option value="20160">2 weeks</option>
<option value="43200">1 month</option>
<option value="259200">6 months</option>
<option value="525600">1 year</option>
</select>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="fm-4" name="pfType" value="remove-all">
<label class="form-check-label font-weight-bold text-danger" for="fm-4">
Remove existing followers
</label>
</div>
{{-- <hr>
<div class="form-check pt-3">
<input class="form-check-input" type="checkbox" id="allowFollowRequest">
<label class="form-check-label" for="allowFollowRequest">
Allow new follow requests
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="blockNotifications" id="chk4">
<label class="form-check-label" for="chk4">
Block notifications from accounts I don't follow
</label>
</div> --}}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary font-weight-bold py-0" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary font-weight-bold py-0" id="modal_confirm">Save</button>
</div>
</div>
</div>
</div>
@endsection @endsection
@push('scripts')
<script type="text/javascript">
$(document).ready(function() {
$('#is_private').on('click', function(e) {
let el = $(this);
if(el[0].checked) {
$('#pac_modal').modal('show');
}
});
$('#modal_confirm').on('click', function(e) {
$('#pac_modal').modal('hide')
let mode = $('input[name="pfType"]:checked').val();
let duration = $('select[name="pfDuration"]').val();
// let newrequests = $('#allowFollowRequest')[0].checked;
axios.post("{{route('settings.privacy.account')}}", {
'mode': mode,
'duration': duration,
// 'newrequests': newrequests
}).then(res => {
window.location.href = window.location.href;
}).catch(err => {
swal('Error', 'An error occured. Please try again.', 'error');
});
});
});
</script>
@endpush

View file

@ -64,23 +64,34 @@
}, },
}) })
.then(val => { .then(val => {
if (!val) throw null; if (!val) {
swal.stopLoading();
swal.close();
return;
};
let msg = 'The URL you have entered is not valid, please try again.'
try { try {
let validator = new URL(val); let validator = new URL(val);
if(!validator.hostname) throw null; if(!validator.hostname || validator.protocol != 'https:') {
swal.stopLoading();
swal.close();
swal('Invalid URL', msg, 'error');
return;
};
axios.post(window.location.href, { axios.post(window.location.href, {
domain: validator.hostname domain: validator.href
}).then(res => { }).then(res => {
window.location.href = window.location.href; window.location.href = window.location.href;
}).catch(err => { }).catch(err => {
swal.stopLoading(); swal.stopLoading();
swal.close(); swal.close();
swal('An Error Occured', 'An error occured, please try again later.', 'error'); swal('Invalid URL', msg, 'error');
return;
}); });
} catch(e) { } catch(e) {
swal.stopLoading(); swal.stopLoading();
swal.close(); swal.close();
swal('An Error Occured', 'An error occured, please try again later.', 'error'); swal('Invalid URL', msg, 'error');
} }
}) })
}); });

View file

@ -149,7 +149,7 @@
</a> </a>
</div> </div>
{{-- <div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<a href="{{route('help.community-guidelines')}}" class="text-decoration-none"> <a href="{{route('help.community-guidelines')}}" class="text-decoration-none">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
@ -158,13 +158,13 @@
</p> </p>
<p class="text-center text-muted font-weight-bold h4 mb-0">{{__('helpcenter.communityGuidelines')}}</p> <p class="text-center text-muted font-weight-bold h4 mb-0">{{__('helpcenter.communityGuidelines')}}</p>
<div class="text-center pt-3"> <div class="text-center pt-3">
<p class="small text-dark font-weight-bold mb-0">&nbsp;</p> <p class="small text-dark font-weight-bold mb-0">Content that will be removed</p>
<p class="small text-dark font-weight-bold mb-0">&nbsp;</p> <p class="small text-dark font-weight-bold mb-0">Content that is explicitly disallowed</p>
</div> </div>
</div> </div>
</div> </div>
</a> </a>
</div> --}} </div>
{{-- <div class="col-12 col-md-6 mb-3"> {{-- <div class="col-12 col-md-6 mb-3">
<a href="{{route('help.blocking-accounts')}}" class="text-decoration-none"> <a href="{{route('help.blocking-accounts')}}" class="text-decoration-none">
<div class="card"> <div class="card">

View file

@ -6,21 +6,58 @@
<h3 class="font-weight-bold">Community Guidelines</h3> <h3 class="font-weight-bold">Community Guidelines</h3>
</div> </div>
<hr> <hr>
<div class="card"> @if($page)
<div class="card-body"> <div>
<div class="row"> {!!$page->content!!}
<div class="col-12 col-md-3 text-center"> <hr>
<div class="icon-wrapper"> <p class="">This document was last updated {{$page->created_at->format('M d, Y')}}.</p>
<i class="far fa-question-circle fa-3x text-light"></i> <p class="">Originally adapted from the <a href="https://mastodon.social/about/more">Mastodon</a> Code of Conduct.</p>
</div>
</div>
<div class="col-12 col-md-9 d-flex align-items-center">
<div class="text-center">
<p class="h3 font-weight-bold mb-0">This page isn't available</p>
<p class="font-weight-light mb-0">We haven't finished it yet, it will be updated soon!</p>
</div>
</div>
</div>
</div>
</div> </div>
@else
<div>
<p class="lead mb-5">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:</p>
<div class="py-4">
<h5 class="pb-3">The following types of content will be removed from the public timeline:</h5>
<ul>
<li class="mb-3">Excessive advertising</li>
<li class="mb-3">Uncurated news bots posting from third-party news sources</li>
<li class="mb-3">Untagged nudity, pornography and sexually explicit content, including artistic depictions</li>
<li class="mb-3">Untagged gore and extremely graphic violence, including artistic depictions</li>
</ul>
</div>
<hr>
<div class="py-4">
<h5 class="pb-3">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:</h5>
<ul>
<li class="mb-3">Racism or advocation of racism</li>
<li class="mb-3">Sexism or advocation of sexism</li>
<li class="mb-3">Discrimination against gender and sexual minorities, or advocation thereof</li>
<li class="mb-3">Xenophobic and/or violent nationalism</li>
</ul>
</div>
<hr>
<div class="py-4">
<h5 class="pb-3">The following types of content are explicitly disallowed and will result in revocation of access to the service:</h5>
<ul>
<li class="mb-3">Sexual depictions of children</li>
<li class="mb-3">Content illegal in Canada, Germany and/or France, such as holocaust denial or Nazi symbolism</li>
<li class="mb-3">Conduct promoting the ideology of National Socialism</li>
</ul>
</div>
<hr>
<div class="py-4">
<h5 class="pb-3">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:</h5>
<ul>
<li class="mb-3">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.</li>
<li class="mb-3">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.</li>
<li class="mb-3">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.</li>
</ul>
</div>
<hr>
<p>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.</p>
<hr>
<p class="">This document was last updated Jun 26, 2019.</p>
<p class="">Originally adapted from the <a href="https://mastodon.social/about/more">Mastodon</a> Code of Conduct.</p>
</div>
@endif
@endsection @endsection

View file

@ -30,11 +30,11 @@
<li class="nav-item"> <li class="nav-item">
<hr> <hr>
</li> </li>
{{-- <li class="nav-item {{request()->is('*/community-guidelines')?'active':''}}"> <li class="nav-item {{request()->is('*/community-guidelines')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('help.community-guidelines')}}"> <a class="nav-link font-weight-light text-muted" href="{{route('help.community-guidelines')}}">
{{__('helpcenter.communityGuidelines')}} {{__('helpcenter.communityGuidelines')}}
</a> </a>
</li> --}} </li>
{{-- <li class="nav-item {{request()->is('*/what-is-the-fediverse')?'active':''}}"> {{-- <li class="nav-item {{request()->is('*/what-is-the-fediverse')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('help.what-is-fediverse')}}">{{__('helpcenter.whatIsTheFediverse')}}</a> <a class="nav-link font-weight-light text-muted" href="{{route('help.what-is-fediverse')}}">{{__('helpcenter.whatIsTheFediverse')}}</a>
</li> --}} </li> --}}

View file

@ -43,7 +43,9 @@
<p class="">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.</p> <p class="">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.</p>
<h5 class="font-weight-bold">8. Governing Law</h5> <h5 class="font-weight-bold">8. Governing Law</h5>
<p class="">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.</p> <p class="">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.</p>
<h5 class="font-weight-bold">9. Additional Rules</h5> <h5 class="font-weight-bold">9. Community Guidelines</h5>
<p class="">You can view our Community Guidelines <a href="{{route('help.community-guidelines')}}">here</a>.</p>
<h5 class="font-weight-bold">10. Additional Rules</h5>
<p class="">This website does not have any additional rules.</p> <p class="">This website does not have any additional rules.</p>
@endif @endif
@endsection @endsection

View file

@ -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::get('privacy/blocked-users', 'SettingsController@blockedUsers')->name('settings.privacy.blocked-users');
Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate'); Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate');
Route::get('privacy/blocked-instances', 'SettingsController@blockedInstances')->name('settings.privacy.blocked-instances'); 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::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::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'); Route::get('reports', 'SettingsController@reportsHome')->name('settings.reports');
// Todo: Release in 0.7.2 // Todo: Release in 0.7.2
Route::group(['prefix' => 'remove', 'middleware' => 'dangerzone'], function() { Route::group(['prefix' => 'remove', 'middleware' => 'dangerzone'], function() {

View file

@ -0,0 +1,86 @@
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Util\ActivityPub\Helpers;
class APAnnounceStrategyTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
$this->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);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Tests\Unit;
use App\Util\ActivityPub\Helpers;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class RemoteFollowTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
$this->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);
}
}