mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-27 08:43:17 +00:00
commit
4b9dab52d5
38 changed files with 647 additions and 271 deletions
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -48,4 +48,9 @@ trait User {
|
|||
{
|
||||
return 500;
|
||||
}
|
||||
|
||||
public function getMaxInstanceBansPerDayAttribute()
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">×</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
|
||||
|
||||
@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
|
|
@ -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');
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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"> </p>
|
||||
<p class="small text-dark font-weight-bold mb-0"> </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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> --}}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
86
tests/Unit/APAnnounceStrategyTest.php
Normal file
86
tests/Unit/APAnnounceStrategyTest.php
Normal 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);
|
||||
}
|
||||
}
|
27
tests/Unit/ActivityPub/RemoteFollowTest.php
Normal file
27
tests/Unit/ActivityPub/RemoteFollowTest.php
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue