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
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

View file

@ -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");

View file

@ -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'));

View file

@ -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(),

View file

@ -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];
}
}

View file

@ -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();

View file

@ -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 <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)
@ -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;

View file

@ -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';

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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();
}
}

View file

@ -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());

View file

@ -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(),
];
}
}

View file

@ -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()

View file

@ -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

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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;

View file

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

View file

@ -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,
];
}
}

View file

@ -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.<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">
<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-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.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.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 class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
</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>
</div>
@ -268,13 +268,13 @@
<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-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 class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
</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>
</div>

View file

@ -242,7 +242,7 @@
<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 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 class="likes font-weight-bold">

View file

@ -117,7 +117,7 @@
<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-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 class="likes font-weight-bold" v-if="expLc(status) == true && !modes.distractionFree">
@ -449,6 +449,13 @@
this.config = res.data;
this.fetchProfile();
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',
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');

View file

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

View file

@ -10,10 +10,15 @@
<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.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>
</div>
<form method="post">
@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">
<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">
@ -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>
</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">
<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">
@ -49,9 +91,104 @@
<div class="form-group row mt-5 pt-5">
<div class="col-12 text-right">
<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>
</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 => {
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 {
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, {
domain: validator.hostname
domain: validator.href
}).then(res => {
window.location.href = window.location.href;
}).catch(err => {
swal.stopLoading();
swal.close();
swal('An Error Occured', 'An error occured, please try again later.', 'error');
swal('Invalid URL', msg, 'error');
return;
});
} catch(e) {
swal.stopLoading();
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>
</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">
<div class="card">
<div class="card-body">
@ -158,13 +158,13 @@
</p>
<p class="text-center text-muted font-weight-bold h4 mb-0">{{__('helpcenter.communityGuidelines')}}</p>
<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">&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">Content that is explicitly disallowed</p>
</div>
</div>
</div>
</a>
</div> --}}
</div>
{{-- <div class="col-12 col-md-6 mb-3">
<a href="{{route('help.blocking-accounts')}}" class="text-decoration-none">
<div class="card">

View file

@ -6,21 +6,58 @@
<h3 class="font-weight-bold">Community Guidelines</h3>
</div>
<hr>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-12 col-md-3 text-center">
<div class="icon-wrapper">
<i class="far fa-question-circle fa-3x text-light"></i>
</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>
@if($page)
<div>
{!!$page->content!!}
<hr>
<p class="">This document was last updated {{$page->created_at->format('M d, Y')}}.</p>
<p class="">Originally adapted from the <a href="https://mastodon.social/about/more">Mastodon</a> Code of Conduct.</p>
</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

View file

@ -30,11 +30,11 @@
<li class="nav-item">
<hr>
</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')}}">
{{__('helpcenter.communityGuidelines')}}
</a>
</li> --}}
</li>
{{-- <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>
</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>
<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>
<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>
@endif
@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::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() {

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);
}
}