Merge branch 'frontend-ui-refactor' into 598-avatar-sizing

This commit is contained in:
daniel 2019-01-23 14:04:18 -07:00 committed by GitHub
commit e5fe7eb8a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 5684 additions and 5930 deletions

View file

@ -18,7 +18,7 @@ testing and development.
## Requirements
- PHP >= 7.1.3 < 7.3 (7.2.x recommended for stable version)
- MySQL >= 5.7, Postgres (MariaDB and sqlite are not supported yet)
- MySQL >= 5.7 (Postgres, MariaDB and sqlite are not supported)
- Redis
- Composer
- GD or ImageMagick

View file

@ -5,6 +5,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\{Profile, User};
use DB;
use App\Util\Lexer\RestrictedNames;
class FixUsernames extends Command
{
@ -43,8 +44,13 @@ class FixUsernames extends Command
$affected = collect([]);
$users = User::chunk(100, function($users) use($affected) {
$restricted = RestrictedNames::get();
$users = User::chunk(100, function($users) use($affected, $restricted) {
foreach($users as $user) {
if(in_array($user->username, $restricted)) {
$affected->push($user);
}
$val = str_replace(['-', '_'], '', $user->username);
if(!ctype_alnum($val)) {
$this->info('Found invalid username: ' . $user->username);
@ -58,11 +64,13 @@ class FixUsernames extends Command
$opts = [
'Random replace (assigns random username)',
'Best try replace (assigns alpha numeric username)',
'Manual replace (manually set username)'
'Manual replace (manually set username)',
'Skip (do not replace. Use at your own risk)'
];
foreach($affected as $u) {
$old = $u->username;
$this->info("Found user: {$old}");
$opt = $this->choice('Select fix method:', $opts, 0);
switch ($opt) {
@ -83,23 +91,31 @@ class FixUsernames extends Command
$new = $this->ask('Enter new username:');
$this->info('New username: ' . $new);
break;
case $opts[3]:
$new = false;
break;
default:
$new = "user_" . str_random(6);
break;
}
DB::transaction(function() use($u, $new) {
$profile = $u->profile;
$profile->username = $new;
$u->username = $new;
$u->save();
$profile->save();
});
if($new) {
DB::transaction(function() use($u, $new) {
$profile = $u->profile;
$profile->username = $new;
$u->username = $new;
$u->save();
$profile->save();
});
}
$this->info('Selected: ' . $opt);
}
$this->info('Fixed ' . $affected->count() . ' usernames!');
} else {
$this->info('No affected usernames found!');
}
}
}

View file

@ -17,4 +17,14 @@ class FollowRequest extends Model
{
return $this->belongsTo(Profile::class, 'following_id', 'id');
}
public function actor()
{
return $this->belongsTo(Profile::class, 'follower_id', 'id');
}
public function target()
{
return $this->belongsTo(Profile::class, 'following_id', 'id');
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers\Admin;
use DB, Cache;
use App\{Instance, Profile};
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
trait AdminInstanceController
{
public function instances(Request $request)
{
$this->validate($request, [
'filter' => [
'nullable',
'string',
'min:1',
'max:20',
Rule::in(['autocw', 'unlisted', 'banned'])
],
]);
if($request->has('filter') && $request->filled('filter')) {
switch ($request->filter) {
case 'autocw':
$instances = Instance::whereAutoCw(true)->orderByDesc('id')->paginate(5);
break;
case 'unlisted':
$instances = Instance::whereUnlisted(true)->orderByDesc('id')->paginate(5);
break;
case 'banned':
$instances = Instance::whereBanned(true)->orderByDesc('id')->paginate(5);
break;
}
} else {
$instances = Instance::orderByDesc('id')->paginate(5);
}
return view('admin.instances.home', compact('instances'));
}
public function instanceScan(Request $request)
{
DB::transaction(function() {
Profile::whereNotNull('domain')
->groupBy('domain')
->chunk(50, function($domains) {
foreach($domains as $domain) {
Instance::firstOrCreate([
'domain' => $domain->domain
]);
}
});
});
return redirect()->back();
}
public function instanceShow(Request $request, $id)
{
$instance = Instance::findOrFail($id);
return view('admin.instances.show', compact('instance'));
}
public function instanceEdit(Request $request, $id)
{
$this->validate($request, [
'action' => [
'required',
'string',
'min:1',
'max:20',
Rule::in(['autocw', 'unlist', 'ban'])
],
]);
$instance = Instance::findOrFail($id);
$unlisted = $instance->unlisted;
$autocw = $instance->auto_cw;
$banned = $instance->banned;
switch ($request->action) {
case 'autocw':
$instance->auto_cw = $autocw == true ? false : true;
$instance->save();
break;
case 'unlist':
$instance->unlisted = $unlisted == true ? false : true;
$instance->save();
break;
case 'ban':
$instance->banned = $banned == true ? false : true;
$instance->save();
break;
}
return response()->json([]);
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Admin;
use DB, Cache;
use App\{
Media,
Profile,
Status
};
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
trait AdminMediaController
{
public function media(Request $request)
{
$this->validate($request, [
'layout' => [
'nullable',
'string',
'min:1',
'max:4',
Rule::in(['grid','list'])
],
'search' => 'nullable|string|min:1|max:20'
]);
if($request->filled('search')) {
$profiles = Profile::where('username', 'like', '%'.$request->input('search').'%')->pluck('id')->toArray();
$media = Media::whereHas('status')
->with('status')
->orderby('id', 'desc')
->whereIn('profile_id', $profiles)
->orWhere('mime', $request->input('search'))
->paginate(12);
} else {
$media = Media::whereHas('status')->with('status')->orderby('id', 'desc')->paginate(12);
}
return view('admin.media.home', compact('media'));
}
public function mediaShow(Request $request, $id)
{
$media = Media::findOrFail($id);
return view('admin.media.show', compact('media'));
}
}

View file

@ -13,7 +13,8 @@ use App\{
Avatar,
Notification,
Media,
Profile
Profile,
Status
};
use App\Transformer\Api\{
AccountTransformer,
@ -23,6 +24,7 @@ use App\Transformer\Api\{
};
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Jobs\AvatarPipeline\AvatarOptimize;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\VideoPipeline\{
@ -97,18 +99,52 @@ class BaseApiController extends Controller
public function accountStatuses(Request $request, $id)
{
$pid = Auth::user()->profile->id;
$profile = Profile::findOrFail($id);
$statuses = $profile->statuses();
if($pid === $profile->id) {
$statuses = $statuses->orderBy('id', 'desc')->paginate(20);
$this->validate($request, [
'only_media' => 'nullable',
'pinned' => 'nullable',
'exclude_replies' => 'nullable',
'max_id' => 'nullable|integer|min:1',
'since_id' => 'nullable|integer|min:1',
'min_id' => 'nullable|integer|min:1',
'limit' => 'nullable|integer|min:1|max:24'
]);
$limit = $request->limit ?? 20;
$max_id = $request->max_id ?? false;
$min_id = $request->min_id ?? false;
$since_id = $request->since_id ?? false;
$only_media = $request->only_media ?? false;
$user = Auth::user();
$account = Profile::findOrFail($id);
$statuses = $account->statuses()->getQuery();
if($only_media == true) {
$statuses = $statuses
->whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id');
}
if($id == $account->id && !$max_id && !$min_id && !$since_id) {
$statuses = $statuses->orderBy('id', 'desc')
->paginate($limit);
} else if($since_id) {
$statuses = $statuses->where('id', '>', $since_id)
->orderBy('id', 'DESC')
->paginate($limit);
} else if($min_id) {
$statuses = $statuses->where('id', '>', $min_id)
->orderBy('id', 'ASC')
->paginate($limit);
} else if($max_id) {
$statuses = $statuses->where('id', '<', $max_id)
->orderBy('id', 'DESC')
->paginate($limit);
} else {
$statuses = $statuses->whereVisibility('public')->orderBy('id', 'desc')->paginate(20);
$statuses = $statuses->whereVisibility('public')->orderBy('id', 'desc')->paginate($limit);
}
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
//$resource->setPaginator(new IlluminatePaginatorAdapter($statuses));
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
}
public function followSuggestions(Request $request)
@ -265,4 +301,13 @@ class BaseApiController extends Controller
return response()->json($res);
}
public function showAccount(Request $request, $id)
{
$profile = Profile::whereNull('domain')->whereNull('status')->findOrFail($id);
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\{Profile, Status, User};
use Cache;
class InstanceApiController extends Controller {
protected function getData()
{
$contact = Cache::remember('api:v1:instance:contact', 1440, function() {
$admin = User::whereIsAdmin(true)->first()->profile;
return [
'id' => $admin->id,
'username' => $admin->username,
'acct' => $admin->username,
'display_name' => e($admin->name),
'locked' => (bool) $admin->is_private,
'bot' => false,
'created_at' => $admin->created_at->format('c'),
'note' => e($admin->bio),
'url' => $admin->url(),
'avatar' => $admin->avatarUrl(),
'avatar_static' => $admin->avatarUrl(),
'header' => null,
'header_static' => null,
'moved' => null,
'fields' => null,
'bot' => null,
];
});
$res = [
'uri' => config('pixelfed.domain.app'),
'title' => config('app.name'),
'description' => '',
'version' => config('pixelfed.version'),
'urls' => [],
'stats' => [
'user_count' => User::count(),
'status_count' => Status::whereNull('uri')->count(),
'domain_count' => Profile::whereNotNull('domain')
->groupBy('domain')
->pluck('domain')
->count()
],
'thumbnail' => '',
'languages' => [],
'contact_account' => $contact
];
return $res;
}
public function instance()
{
$res = Cache::remember('api:v1:instance', 60, function() {
return json_encode($this->getData());
});
return response($res)->header('Content-Type', 'application/json');
}
}

View file

@ -2,13 +2,18 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use App\Comment;
use App\Jobs\CommentPipeline\CommentPipeline;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Profile;
use App\Status;
use Auth;
use Illuminate\Http\Request;
use League\Fractal;
use App\Transformer\Api\StatusTransformer;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
class CommentController extends Controller
{
@ -57,7 +62,19 @@ class CommentController extends Controller
CommentPipeline::dispatch($status, $reply);
if ($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Comment saved', 'username' => $profile->username, 'url' => $reply->url(), 'profile' => $profile->url(), 'comment' => $reply->caption];
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$entity = new Fractal\Resource\Item($reply, new StatusTransformer());
$entity = $fractal->createData($entity)->toArray();
$response = [
'code' => 200,
'msg' => 'Comment saved',
'username' => $profile->username,
'url' => $reply->url(),
'profile' => $profile->url(),
'comment' => $reply->caption,
'entity' => $entity,
];
} else {
$response = redirect($status->url());
}

View file

@ -15,6 +15,7 @@ use Illuminate\Http\Request;
use League\Fractal;
use App\Util\ActivityPub\Helpers;
use App\Util\ActivityPub\HttpSignature;
use \Zttp\Zttp;
class FederationController extends Controller
{
@ -81,37 +82,38 @@ class FederationController extends Controller
{
$res = Cache::remember('api:nodeinfo', 60, function () {
return [
'metadata' => [
'nodeName' => config('app.name'),
'software' => [
'homepage' => 'https://pixelfed.org',
'github' => 'https://github.com/pixelfed',
'follow' => 'https://mastodon.social/@pixelfed',
],
],
'openRegistrations' => config('pixelfed.open_registration'),
'protocols' => [
'activitypub',
],
'services' => [
'inbound' => [],
'outbound' => [],
],
'software' => [
'name' => 'pixelfed',
'version' => config('pixelfed.version'),
],
'usage' => [
'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
'users' => [
'total' => \App\User::count(),
'activeHalfyear' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(6)->toDateTimeString())->groupBy('user_id')->get()->count(),
'activeMonth' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(1)->toDateTimeString())->groupBy('user_id')->get()->count(),
],
],
'version' => '2.0',
];
'metadata' => [
'nodeName' => config('app.name'),
'software' => [
'homepage' => 'https://pixelfed.org',
'github' => 'https://github.com/pixelfed',
'follow' => 'https://mastodon.social/@pixelfed',
],
'captcha' => (bool) config('pixelfed.recaptcha'),
],
'openRegistrations' => config('pixelfed.open_registration'),
'protocols' => [
'activitypub',
],
'services' => [
'inbound' => [],
'outbound' => [],
],
'software' => [
'name' => 'pixelfed',
'version' => config('pixelfed.version'),
],
'usage' => [
'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
'users' => [
'total' => \App\User::count(),
'activeHalfyear' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(6)->toDateTimeString())->groupBy('user_id')->get()->count(),
'activeMonth' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(1)->toDateTimeString())->groupBy('user_id')->get()->count(),
],
],
'version' => '2.0',
];
});
return response()->json($res, 200, [
@ -238,7 +240,7 @@ XML;
}
$signatureData = HttpSignature::parseSignatureHeader($signature);
$keyId = Helpers::validateUrl($signatureData['keyId']);
$actor = Profile::whereKeyId($keyId)->first();
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->firstOrFail();
$res = Zttp::timeout(5)->withHeaders([
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => 'PixelFedBot v0.1 - https://pixelfed.org',

View file

@ -39,6 +39,7 @@ class FollowerController extends Controller
$user = Auth::user()->profile;
$target = Profile::where('id', '!=', $user->id)->whereNull('status')->findOrFail($item);
$private = (bool) $target->is_private;
$remote = (bool) $target->domain;
$blocked = UserFilter::whereUserId($target->id)
->whereFilterType('block')
->whereFilterableId($user->id)
@ -51,7 +52,7 @@ class FollowerController extends Controller
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->count();
if($private == true && $isFollowing == 0) {
if($private == true && $isFollowing == 0 || $remote == true) {
$follow = FollowRequest::firstOrCreate([
'follower_id' => $user->id,
'following_id' => $target->id

View file

@ -54,6 +54,7 @@ class InternalApiController extends Controller
$attachments = [];
$status = new Status;
$mimes = [];
$cw = false;
foreach($medias as $k => $media) {
$m = Media::findOrFail($media['id']);
@ -64,7 +65,8 @@ class InternalApiController extends Controller
$m->license = $media['license'];
$m->caption = strip_tags($media['alt']);
$m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
if($media['cw'] == true) {
if($media['cw'] == true || $profile->cw == true) {
$cw = true;
$m->is_nsfw = true;
$status->is_nsfw = true;
}
@ -84,6 +86,9 @@ class InternalApiController extends Controller
$media->save();
}
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
$cw = $profile->cw == true ? true : $cw;
$status->is_nsfw = $cw;
$status->visibility = $visibility;
$status->scope = $visibility;
$status->type = StatusController::mimeTypeCheck($mimes);

View file

@ -187,7 +187,7 @@ class ProfileController extends Controller
return view('profile.private', compact('user', 'is_following'));
}
}
$followers = $profile->followers()->whereNull('status')->orderBy('created_at', 'desc')->simplePaginate(12);
$followers = $profile->followers()->whereNull('status')->orderBy('followers.created_at', 'desc')->simplePaginate(12);
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
if ($user->remote_url) {
$settings = new \StdClass;
@ -217,7 +217,7 @@ class ProfileController extends Controller
return view('profile.private', compact('user', 'is_following'));
}
}
$following = $profile->following()->whereNull('status')->orderBy('created_at', 'desc')->simplePaginate(12);
$following = $profile->following()->whereNull('status')->orderBy('followers.created_at', 'desc')->simplePaginate(12);
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
if ($user->remote_url) {
$settings = new \StdClass;

View file

@ -233,6 +233,8 @@ class PublicApiController extends Controller
$dir = $min ? '>' : '<';
$id = $min ?? $max;
$timeline = Status::whereHas('media')
->whereLocal(true)
->whereNull('uri')
->where('id', $dir, $id)
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')
@ -244,6 +246,8 @@ class PublicApiController extends Controller
->get();
} else {
$timeline = Status::whereHas('media')
->whereLocal(true)
->whereNull('uri')
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
@ -295,6 +299,8 @@ class PublicApiController extends Controller
$dir = $min ? '>' : '<';
$id = $min ?? $max;
$timeline = Status::whereHas('media')
->whereLocal(true)
->whereNull('uri')
->where('id', $dir, $id)
->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered)
@ -307,6 +313,8 @@ class PublicApiController extends Controller
->get();
} else {
$timeline = Status::whereHas('media')
->whereLocal(true)
->whereNull('uri')
->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')

View file

@ -149,11 +149,17 @@ class SettingsController extends Controller
public function removeAccountPermanent(Request $request)
{
if(config('pixelfed.account_deletion') == false) {
abort(404);
}
return view('settings.remove.permanent');
}
public function removeAccountPermanentSubmit(Request $request)
{
if(config('pixelfed.account_deletion') == false) {
abort(404);
}
$user = Auth::user();
if($user->is_admin == true) {
return abort(400, 'You cannot delete an admin account.');

View file

@ -5,10 +5,12 @@ namespace App\Http\Controllers;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\SharePipeline\SharePipeline;
use App\Media;
use App\Profile;
use App\Status;
use App\Transformer\ActivityPub\StatusTransformer;
use App\Transformer\ActivityPub\Verb\Note;
use App\User;
use Auth;
use Cache;
@ -19,7 +21,7 @@ class StatusController extends Controller
{
public function show(Request $request, $username, int $id)
{
$user = Profile::whereUsername($username)->firstOrFail();
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if($user->status != null) {
return ProfileController::accountCheck($user);
@ -55,6 +57,39 @@ class StatusController extends Controller
return view($template, compact('user', 'status'));
}
public function showObject(Request $request, $username, int $id)
{
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if($user->status != null) {
return ProfileController::accountCheck($user);
}
$status = Status::whereProfileId($user->id)
->whereNotIn('visibility',['draft','direct'])
->findOrFail($id);
if($status->uri) {
$url = $status->uri;
if(ends_with($url, '/activity')) {
$url = str_replace('/activity', '', $url);
}
return redirect($url);
}
if($status->visibility == 'private' || $user->is_private) {
if(!Auth::check()) {
abort(403);
}
$pid = Auth::user()->profile;
if($user->followedBy($pid) == false && $user->id !== $pid->id) {
abort(403);
}
}
return $this->showActivityPub($request, $status);
}
public function compose()
{
$this->authCheck();
@ -91,6 +126,9 @@ class StatusController extends Controller
$profile = $user->profile;
$visibility = $this->validateVisibility($request->visibility);
$cw = $profile->cw == true ? true : $cw;
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
$status = new Status();
$status->profile_id = $profile->id;
$status->caption = strip_tags($request->caption);
@ -197,8 +235,10 @@ class StatusController extends Controller
$share = new Status();
$share->profile_id = $profile->id;
$share->reblog_of_id = $status->id;
$share->in_reply_to_profile_id = $status->profile_id;
$share->save();
$count++;
SharePipeline::dispatch($share);
}
if ($request->ajax()) {
@ -213,7 +253,7 @@ class StatusController extends Controller
public function showActivityPub(Request $request, $status)
{
$fractal = new Fractal\Manager();
$resource = new Fractal\Resource\Item($status, new StatusTransformer());
$resource = new Fractal\Resource\Item($status, new Note());
$res = $fractal->createData($resource)->toArray();
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');

View file

@ -6,5 +6,63 @@ use Illuminate\Database\Eloquent\Model;
class Instance extends Model
{
//
protected $fillable = ['domain'];
public function profiles()
{
return $this->hasMany(Profile::class, 'domain', 'domain');
}
public function statuses()
{
return $this->hasManyThrough(
Status::class,
Profile::class,
'domain',
'profile_id',
'domain',
'id'
);
}
public function reported()
{
return $this->hasManyThrough(
Report::class,
Profile::class,
'domain',
'reported_profile_id',
'domain',
'id'
);
}
public function reports()
{
return $this->hasManyThrough(
Report::class,
Profile::class,
'domain',
'profile_id',
'domain',
'id'
);
}
public function media()
{
return $this->hasManyThrough(
Media::class,
Profile::class,
'domain',
'profile_id',
'domain',
'id'
);
}
public function getUrl()
{
return url("/i/admin/instances/show/{$this->id}");
}
}

View file

@ -19,6 +19,13 @@ class AvatarOptimize implements ShouldQueue
protected $profile;
protected $current;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -20,6 +20,13 @@ class CreateAvatar implements ShouldQueue
protected $profile;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -20,6 +20,13 @@ class CommentPipeline implements ShouldQueue
protected $status;
protected $comment;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -0,0 +1,64 @@
<?php
namespace App\Jobs\FollowPipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Cache, Log, Redis;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\FollowRequest;
use App\Util\ActivityPub\Helpers;
use App\Transformer\ActivityPub\Verb\Follow;
class FollowActivityPubDeliver implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $followRequest;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(FollowRequest $followRequest)
{
$this->followRequest = $followRequest;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$follow = $this->followRequest;
$actor = $follow->actor;
$target = $follow->target;
if($target->domain == null || $target->inbox_url == null) {
return;
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($follow, new Follow());
$activity = $fractal->createData($resource)->toArray();
$url = $target->sharedInbox ?? $target->inbox_url;
Helpers::sendSignedObject($actor, $url, $activity);
}
}

View file

@ -18,6 +18,13 @@ class FollowPipeline implements ShouldQueue
protected $follower;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -15,6 +15,13 @@ class ImageOptimize implements ShouldQueue
protected $media;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -16,6 +16,13 @@ class ImageResize implements ShouldQueue
protected $media;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -17,6 +17,13 @@ class ImageThumbnail implements ShouldQueue
protected $media;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -2,6 +2,7 @@
namespace App\Jobs\ImageOptimizePipeline;
use Storage;
use App\Media;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -9,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use ImageOptimizer;
use Illuminate\Http\File;
class ImageUpdate implements ShouldQueue
{
@ -17,11 +19,17 @@ class ImageUpdate implements ShouldQueue
protected $media;
protected $protectedMimes = [
'image/gif',
'image/bmp',
'video/mp4',
'image/jpeg',
'image/png',
];
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
@ -43,21 +51,31 @@ class ImageUpdate implements ShouldQueue
$path = storage_path('app/'.$media->media_path);
$thumb = storage_path('app/'.$media->thumbnail_path);
try {
if (!in_array($media->mime, $this->protectedMimes)) {
ImageOptimizer::optimize($thumb);
ImageOptimizer::optimize($path);
}
} catch (Exception $e) {
return;
if (in_array($media->mime, $this->protectedMimes) == true) {
ImageOptimizer::optimize($thumb);
ImageOptimizer::optimize($path);
}
if (!is_file($path) || !is_file($thumb)) {
return;
}
$photo_size = filesize($path);
$thumb_size = filesize($thumb);
$total = ($photo_size + $thumb_size);
$media->size = $total;
$media->save();
if(config('pixelfed.cloud_storage') == true) {
$p = explode('/', $media->media_path);
$monthHash = $p[2];
$userHash = $p[3];
$storagePath = "public/m/{$monthHash}/{$userHash}";
$file = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($path), 'public');
$url = Storage::disk(config('filesystems.cloud'))->url($file);
$media->cdn_url = $url;
$media->optimized_url = $url;
$media->save();
}
}
}

View file

@ -25,7 +25,14 @@ class ImportInstagram implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $job;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -19,6 +19,13 @@ class LikePipeline implements ShouldQueue
protected $like;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -18,6 +18,13 @@ class MentionPipeline implements ShouldQueue
protected $status;
protected $mention;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -17,7 +17,14 @@ class SharePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $like;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
@ -37,32 +44,32 @@ class SharePipeline implements ShouldQueue
public function handle()
{
$status = $this->status;
$actor = $this->status->profile;
$target = $this->status->parent()->profile;
$actor = $status->profile;
$target = $status->parent()->profile;
if ($status->url !== null) {
if ($status->uri !== null) {
// Ignore notifications to remote statuses
return;
}
$exists = Notification::whereProfileId($status->profile_id)
->whereActorId($actor->id)
->whereAction('like')
->whereItemId($status->id)
$exists = Notification::whereProfileId($target->id)
->whereActorId($status->profile_id)
->whereAction('share')
->whereItemId($status->reblog_of_id)
->whereItemType('App\Status')
->count();
if ($actor->id === $status->profile_id || $exists !== 0) {
if ($target->id === $status->profile_id || $exists !== 0) {
return true;
}
try {
$notification = new Notification();
$notification->profile_id = $status->profile_id;
$notification = new Notification;
$notification->profile_id = $target->id;
$notification->actor_id = $actor->id;
$notification->action = 'like';
$notification->message = $like->toText();
$notification->rendered = $like->toHtml();
$notification->action = 'share';
$notification->message = $status->shareToText();
$notification->rendered = $status->shareToHtml();
$notification->item_id = $status->id;
$notification->item_type = "App\Status";
$notification->save();

View file

@ -16,7 +16,14 @@ class NewStatusPipeline implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
@ -37,7 +44,10 @@ class NewStatusPipeline implements ShouldQueue
$status = $this->status;
StatusEntityLexer::dispatch($status);
StatusActivityPubDeliver::dispatch($status);
if(config('pixelfed.activitypub_enabled') == true) {
StatusActivityPubDeliver::dispatch($status);
}
// Cache::forever('post.'.$status->id, $status);
// $redis = Redis::connection();

View file

@ -18,7 +18,14 @@ class StatusActivityPubDeliver implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -19,7 +19,14 @@ class StatusDelete implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -24,7 +24,14 @@ class StatusEntityLexer implements ShouldQueue
protected $status;
protected $entities;
protected $autolink;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
@ -42,7 +49,10 @@ class StatusEntityLexer implements ShouldQueue
*/
public function handle()
{
$this->parseEntities();
$profile = $this->status->profile;
if($profile->no_autolink == false) {
$this->parseEntities();
}
}
public function parseEntities()

View file

@ -38,32 +38,11 @@ class AuthLogin
});
}
if(empty($user->profile)) {
DB::transaction(function() use($user) {
$profile = new Profile();
$profile->user_id = $user->id;
$profile->username = $user->username;
$profile->name = $user->name;
$pkiConfig = [
'digest_alg' => 'sha512',
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
$pki = openssl_pkey_new($pkiConfig);
openssl_pkey_export($pki, $pki_private);
$pki_public = openssl_pkey_get_details($pki);
$pki_public = $pki_public['key'];
$profile->private_key = $pki_private;
$profile->public_key = $pki_public;
$profile->save();
CreateAvatar::dispatch($profile);
});
}
if($user->status != null) {
$profile = $user->profile;
if(!$profile) {
return;
}
switch ($user->status) {
case 'disabled':
$profile->status = null;

View file

@ -17,13 +17,23 @@ class Media extends Model
*/
protected $dates = ['deleted_at'];
public function status()
{
return $this->belongsTo(Status::class);
}
public function profile()
{
return $this->belongsTo(Profile::class);
}
public function url()
{
if(!empty($this->remote_media) && $this->remote_url) {
$url = $this->remote_url;
} else {
$path = $this->media_path;
$url = Storage::url($path);
$url = $this->cdn_url ?? Storage::url($path);
}
return url($url);
@ -37,6 +47,11 @@ class Media extends Model
return url($url);
}
public function thumb()
{
return $this->thumbnailUrl();
}
public function mimeType()
{
return explode('/', $this->mime)[0];

View file

@ -12,7 +12,7 @@ class Profile extends Model
protected $dates = ['deleted_at'];
protected $hidden = ['private_key'];
protected $visible = ['username', 'name'];
protected $visible = ['id', 'user_id', 'username', 'name'];
public function user()
{

View file

@ -3,6 +3,7 @@
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
class AuthServiceProvider extends ServiceProvider
{
@ -24,6 +25,8 @@ class AuthServiceProvider extends ServiceProvider
{
$this->registerPolicies();
//
Passport::routes();
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
}
}

View file

@ -118,7 +118,11 @@ class Status extends Model
$media = $this->firstMedia();
$path = $media->media_path;
$hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
$url = Storage::url($path)."?v={$hash}";
if(config('pixelfed.cloud_storage') == true) {
$url = Storage::disk(config('filesystems.cloud'))->url($path)."?v={$hash}";
} else {
$url = Storage::url($path)."?v={$hash}";
}
return url($url);
}
@ -270,6 +274,22 @@ class Status extends Model
__('notification.commented');
}
public function shareToText()
{
$actorName = $this->profile->username;
return "{$actorName} ".__('notification.shared');
}
public function shareToHtml()
{
$actorName = $this->profile->username;
$actorUrl = $this->profile->url();
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
__('notification.shared');
}
public function recentComments()
{
return $this->comments()->orderBy('created_at', 'desc')->take(3);

View file

@ -2,9 +2,36 @@
namespace App;
use Auth;
use Illuminate\Database\Eloquent\Model;
class Story extends Model
{
//
protected $visible = ['id'];
public function profile()
{
return $this->belongsTo(Profile::class);
}
public function items()
{
return $this->hasMany(StoryItem::class);
}
public function reactions()
{
return $this->hasMany(StoryReaction::class);
}
public function views()
{
return $this->hasMany(StoryView::class);
}
public function seen($pid = false)
{
$id = $pid ?? Auth::user()->profile->id;
return $this->views()->whereProfileId($id)->exists();
}
}

19
app/StoryItem.php Normal file
View file

@ -0,0 +1,19 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Storage;
class StoryItem extends Model
{
public function story()
{
return $this->belongsTo(Story::class);
}
public function url()
{
return Storage::url($this->media_path);
}
}

View file

@ -6,5 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class StoryReaction extends Model
{
//
public function story()
{
return $this->belongsTo(Story::class);
}
}

13
app/StoryView.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class StoryView extends Model
{
public function story()
{
return $this->belongsTo(Story::class);
}
}

View file

@ -50,7 +50,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'type' => 'Document',
'mediaType' => $media->mime,
'url' => $media->url(),
'name' => null,
'name' => $media->caption
];
}),
'tag' => [],

View file

@ -7,7 +7,7 @@ use League\Fractal;
class Follow extends Fractal\TransformerAbstract
{
public function transform(Follower $follower)
public function transform($follower)
{
return [
'@context' => 'https://www.w3.org/ns/activitystreams',

View file

@ -0,0 +1,56 @@
<?php
namespace App\Transformer\ActivityPub\Verb;
use App\Status;
use League\Fractal;
class Note extends Fractal\TransformerAbstract
{
public function transform(Status $status)
{
$mentions = $status->mentions->map(function ($mention) {
return [
'type' => 'Mention',
'href' => $mention->permalink(),
'name' => $mention->emailUrl()
];
})->toArray();
$hashtags = $status->hashtags->map(function ($hashtag) {
return [
'type' => 'Hashtag',
'href' => $hashtag->url(),
'name' => "#{$hashtag->name}",
];
})->toArray();
$tags = array_merge($mentions, $hashtags);
return [
'@context' => [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
],
'id' => $status->url(),
'type' => 'Note',
'summary' => null,
'content' => $status->rendered ?? $status->caption,
'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
'published' => $status->created_at->toAtomString(),
'url' => $status->url(),
'attributedTo' => $status->profile->permalink(),
'to' => $status->scopeToAudience('to'),
'cc' => $status->scopeToAudience('cc'),
'sensitive' => (bool) $status->is_nsfw,
'attachment' => $status->media->map(function ($media) {
return [
'type' => $media->activityVerb(),
'mediaType' => $media->mime,
'url' => $media->url(),
'name' => null,
];
})->toArray(),
'tag' => $tags,
];
}
}

View file

@ -7,27 +7,28 @@ use League\Fractal;
class AccountTransformer extends Fractal\TransformerAbstract
{
public function transform(Profile $profile)
{
return [
'id' => $profile->id,
'username' => $profile->username,
'acct' => $profile->username,
'display_name' => $profile->name,
'locked' => (bool) $profile->is_private,
'created_at' => $profile->created_at->format('c'),
'followers_count' => $profile->followerCount(),
'following_count' => $profile->followingCount(),
'statuses_count' => $profile->statusCount(),
'note' => $profile->bio,
'url' => $profile->url(),
'avatar' => $profile->avatarUrl(),
'avatar_static' => $profile->avatarUrl(),
'header' => null,
'header_static' => null,
'moved' => null,
'fields' => null,
'bot' => null,
];
}
public function transform(Profile $profile)
{
return [
'id' => $profile->id,
'username' => $profile->username,
'acct' => $profile->username,
'display_name' => $profile->name,
'locked' => (bool) $profile->is_private,
'created_at' => $profile->created_at->format('c'),
'followers_count' => $profile->followerCount(),
'following_count' => $profile->followingCount(),
'statuses_count' => $profile->statusCount(),
'note' => $profile->bio,
'url' => $profile->url(),
'avatar' => $profile->avatarUrl(),
'avatar_static' => $profile->avatarUrl(),
'header' => null,
'header_static' => null,
'moved' => null,
'fields' => null,
'bot' => null,
'software' => 'pixelfed'
];
}
}

View file

@ -6,11 +6,11 @@ use League\Fractal;
class ApplicationTransformer extends Fractal\TransformerAbstract
{
public function transform()
{
return [
'name' => '',
'website' => null,
];
}
public function transform()
{
return [
'name' => '',
'website' => null,
];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Transformer\Api;
use League\Fractal;
class AttachmentTransformer extends Fractal\TransformerAbstract
{
public function transform(Media $media)
{
return [
'id' => $media->id,
'type' => $media->activityVerb(),
'url' => $media->url(),
'remote_url' => null,
'preview_url' => $media->thumbnailUrl(),
'text_url' => null,
'meta' => null,
'description' => $media->caption,
'license' => $media->license,
'is_nsfw' => $media->is_nsfw,
'orientation' => $media->orientation,
'filter_name' => $media->filter_name,
'filter_class' => $media->filter_class,
'mime' => $media->mime,
];
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Transformer\Api;
use League\Fractal;
class ContextTransformer extends Fractal\TransformerAbstract
{
public function transform()
{
return [
'ancestors' => [],
'descendants' => []
];
}
}

View file

@ -6,13 +6,13 @@ use League\Fractal;
class EmojiTransformer extends Fractal\TransformerAbstract
{
public function transform($emoji)
{
return [
'shortcode' => '',
'static_url' => '',
'url' => '',
'visible_in_picker' => false
];
}
public function transform($emoji)
{
return [
'shortcode' => '',
'static_url' => '',
'url' => '',
'visible_in_picker' => false
];
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Transformer\Api;
use League\Fractal;
class FilterTransformer extends Fractal\TransformerAbstract
{
public function transform()
{
return [
'id' => (string) '',
'phrase' => (string) '',
'context' => [],
'expires_at' => null,
'irreversible' => (bool) false,
'whole_word' => (bool) false
];
}
}

View file

@ -7,11 +7,11 @@ use League\Fractal;
class HashtagTransformer extends Fractal\TransformerAbstract
{
public function transform(Hashtag $hashtag)
{
return [
'name' => $hashtag->name,
'url' => $hashtag->url(),
];
}
public function transform(Hashtag $hashtag)
{
return [
'name' => $hashtag->name,
'url' => $hashtag->url(),
];
}
}

View file

@ -10,20 +10,20 @@ class MediaTransformer extends Fractal\TransformerAbstract
public function transform(Media $media)
{
return [
'id' => $media->id,
'type' => $media->activityVerb(),
'url' => $media->url(),
'remote_url' => null,
'preview_url' => $media->thumbnailUrl(),
'text_url' => null,
'meta' => null,
'description' => $media->caption,
'license' => $media->license,
'is_nsfw' => $media->is_nsfw,
'orientation' => $media->orientation,
'filter_name' => $media->filter_name,
'filter_class' => $media->filter_class,
'mime' => $media->mime,
'id' => $media->id,
'type' => $media->activityVerb(),
'url' => $media->url(),
'remote_url' => null,
'preview_url' => $media->thumbnailUrl(),
'text_url' => null,
'meta' => null,
'description' => $media->caption,
'license' => $media->license,
'is_nsfw' => $media->is_nsfw,
'orientation' => $media->orientation,
'filter_name' => $media->filter_name,
'filter_class' => $media->filter_class,
'mime' => $media->mime,
];
}
}

View file

@ -44,6 +44,7 @@ class NotificationTransformer extends Fractal\TransformerAbstract
'follow' => 'follow',
'mention' => 'mention',
'reblog' => 'share',
'share' => 'share',
'like' => 'favourite',
'comment' => 'comment',
];

View file

@ -0,0 +1,42 @@
<?php
namespace App\Transformer\Api;
use League\Fractal;
class ResultsTransformer extends Fractal\TransformerAbstract
{
protected $defaultIncludes = [
'accounts',
'statuses',
'hashtags',
];
public function transform($results)
{
return [
'accounts' => [],
'statuses' => [],
'hashtags' => []
];
}
public function includeAccounts($results)
{
$accounts = $results->accounts;
return $this->collection($accounts, new AccountTransformer());
}
public function includeStatuses($results)
{
$statuses = $results->statuses;
return $this->collection($statuses, new StatusTransformer());
}
public function includeTags($results)
{
$hashtags = $status->hashtags;
return $this->collection($hashtags, new HashtagTransformer());
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Transformer\Api;
use App\StoryItem;
use League\Fractal;
use Illuminate\Support\Str;
class StoryItemTransformer extends Fractal\TransformerAbstract
{
public function transform(StoryItem $item)
{
return [
'id' => (string) Str::uuid(),
'type' => $item->type,
'length' => $item->duration,
'src' => $item->url(),
'preview' => null,
'link' => null,
'linkText' => null,
'time' => $item->updated_at->format('U'),
'seen' => $item->story->seen(),
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Transformer\Api;
use App\Story;
use League\Fractal;
class StoryTransformer extends Fractal\TransformerAbstract
{
protected $defaultIncludes = [
'items',
];
public function transform(Story $story)
{
return [
'id' => $story->id,
'photo' => $story->profile->avatarUrl(),
'name' => '',
'link' => '',
'lastUpdated' => $story->updated_at->format('U'),
'seen' => $story->seen(),
'items' => [],
];
}
public function includeItems(Story $story)
{
$items = $story->items;
return $this->collection($items, new StoryItemTransformer());
}
}

View file

@ -2,7 +2,7 @@
namespace App\Util\ActivityPub;
use Cache, DB, Log, Redis, Validator;
use Cache, DB, Log, Purify, Redis, Validator;
use App\{
Activity,
Follower,
@ -16,6 +16,10 @@ use Carbon\Carbon;
use App\Util\ActivityPub\Helpers;
use App\Jobs\LikePipeline\LikePipeline;
use App\Util\ActivityPub\Validator\{
Follow
};
class Inbox
{
protected $headers;
@ -35,30 +39,6 @@ class Inbox
$this->handleVerb();
}
public function authenticatePayload()
{
try {
$signature = Helpers::validateSignature($this->headers, $this->payload);
$payload = Helpers::validateObject($this->payload);
if($signature == false) {
return;
}
} catch (Exception $e) {
return;
}
$this->payloadLogger();
}
public function payloadLogger()
{
$logger = new Activity;
$logger->data = json_encode($this->payload);
$logger->save();
$this->logger = $logger;
Log::info('AP:inbox:activity:new:'.$this->logger->id);
$this->handleVerb();
}
public function handleVerb()
{
$verb = $this->payload['type'];
@ -76,6 +56,7 @@ class Inbox
break;
case 'Accept':
if(Accept::validate($this->payload) == false) { return; }
$this->handleAcceptActivity();
break;
@ -171,7 +152,8 @@ class Inbox
$caption = str_limit(strip_tags($activity['content']), config('pixelfed.max_caption_length'));
$status = new Status;
$status->profile_id = $actor->id;
$status->caption = $caption;
$status->caption = strip_tags($caption);
$status->rendered = Purify::clean($caption);
$status->visibility = $status->scope = 'public';
$status->uri = $url;
$status->url = $url;
@ -275,13 +257,10 @@ class Inbox
$obj = $this->payload['object'];
if(is_string($obj) && Helpers::validateUrl($obj)) {
// actor object detected
// todo delete actor
} else if (is_array($obj) && isset($obj['type']) && $obj['type'] == 'Tombstone') {
// tombstone detected
$status = Status::whereUri($obj['id'])->first();
if($status == null) {
return;
}
$status = Status::whereUri($obj['id'])->firstOrFail();
$status->forceDelete();
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Util\ActivityPub\Validator;
use Validator;
use Illuminate\Validation\Rule;
class Announce {
public static function validate($payload)
{
$valid = Validator::make($payload, [
'@context' => 'required',
'id' => 'required|string',
'type' => [
'required',
Rule::in(['Announce'])
],
'actor' => 'required|url|active_url',
'published' => 'required|date',
'to' => 'required',
'cc' => 'required',
'object' => 'required|url|active_url'
])->passes();
return $valid;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Util\ActivityPub\Validator;
use Validator;
use Illuminate\Validation\Rule;
class Follow {
public static function validate($payload)
{
$valid = Validator::make($payload, [
'@context' => 'required',
'id' => 'required|string',
'type' => [
'required',
Rule::in(['Follow'])
],
'actor' => 'required|url|active_url',
'object' => 'required|url|active_url'
])->passes();
return $valid;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Util\ActivityPub\Validator;
use Validator;
use Illuminate\Validation\Rule;
class Like {
public static function validate($payload)
{
$valid = Validator::make($payload, [
'@context' => 'required',
'id' => 'required|string',
'type' => [
'required',
Rule::in(['Like'])
],
'actor' => 'required|url|active_url',
'object' => 'required|url|active_url'
])->passes();
return $valid;
}
}

View file

@ -17,15 +17,6 @@ class RestrictedNames
'contact-us',
'contact_us',
'copyright',
'd',
'dashboard',
'dev',
'developer',
'developers',
'discover',
'discovers',
'doc',
'docs',
'download',
'domainadmin',
'domainadministrator',
@ -41,10 +32,7 @@ class RestrictedNames
'guests',
'hostmaster',
'hostmaster',
'image',
'images',
'imap',
'img',
'info',
'info',
'is',
@ -57,7 +45,6 @@ class RestrictedNames
'mailerdaemon',
'marketing',
'me',
'media',
'mis',
'mx',
'new',
@ -82,7 +69,6 @@ class RestrictedNames
'pop3',
'postmaster',
'pricing',
'privacy',
'root',
'sales',
'security',
@ -96,7 +82,6 @@ class RestrictedNames
'sys',
'sysadmin',
'system',
'terms',
'tutorial',
'tutorials',
'usenet',
@ -121,34 +106,65 @@ class RestrictedNames
'account',
'api',
'auth',
'bartender',
'broadcast',
'broadcaster',
'booth',
'bouncer',
'c',
'css',
'checkpoint',
'collection',
'collections',
'c',
'costar',
'costars',
'cdn',
'd',
'dashboard',
'deck',
'dev',
'developer',
'developers',
'discover',
'discovers',
'dj',
'doc',
'docs',
'docs',
'drive',
'driver',
'error',
'explore',
'font',
'fonts',
'gdpr',
'home',
'help',
'helpcenter',
'help-center',
'help_center',
'help_center_',
'help-center-',
'help-center_',
'help_center-',
'i',
'img',
'imgs',
'image',
'images',
'js',
'legal',
'live',
'login',
'logout',
'media',
'menu',
'official',
'p',
'photo',
'photos',
'password',
'privacy',
'reset',
'report',
'reports',
@ -161,20 +177,28 @@ class RestrictedNames
'statuses',
'site',
'sites',
'stage',
'static',
'story',
'stories',
'support',
'svg',
'svgs',
'terms',
'telescope',
'timeline',
'timelines',
'tour',
'user',
'users',
'username',
'usernames',
'vendor',
'waiter',
'ws',
'wss',
'www',
'valet',
'400',
'401',
'403',

View file

@ -6,6 +6,12 @@
"type": "project",
"require": {
"php": "^7.1.3",
"ext-bcmath": "*",
"ext-ctype": "*",
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"beyondcode/laravel-self-diagnosis": "^1.0.2",
"bitverse/identicon": "^1.1",
"doctrine/dbal": "^2.7",

View file

@ -42,7 +42,7 @@ return [
],
'api' => [
'driver' => 'token',
'driver' => 'passport',
'provider' => 'users',
],
],

View file

@ -65,6 +65,21 @@ return [
'endpoint' => env('AWS_ENDPOINT'),
],
'spaces' => [
'driver' => 's3',
'key' => env('DO_SPACES_KEY'),
'secret' => env('DO_SPACES_SECRET'),
'endpoint' => env('DO_SPACES_ENDPOINT'),
'region' => env('DO_SPACES_REGION'),
'bucket' => env('DO_SPACES_BUCKET'),
'visibility' => 'public',
'options' => [
'CacheControl' => 'max-age=31536000'
],
'root' => env('DO_SPACES_ROOT','/'),
'url' => str_replace(env('DO_SPACES_REGION'),env('DO_SPACES_BUCKET').'.'.env('DO_SPACES_REGION'),str_replace("digitaloceanspaces","cdn.digitaloceanspaces",env('DO_SPACES_ENDPOINT'))),
],
],
];

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.7.7',
'version' => '0.7.10',
/*
|--------------------------------------------------------------------------
@ -177,6 +177,38 @@ return [
*/
'image_quality' => (int) env('IMAGE_QUALITY', 80),
/*
|--------------------------------------------------------------------------
| Account deletion
|--------------------------------------------------------------------------
|
| Enable account deletion.
|
*/
'account_deletion' => env('ACCOUNT_DELETION', true),
/*
|--------------------------------------------------------------------------
| Account deletion after X days
|--------------------------------------------------------------------------
|
| Set account deletion queue after X days, set to false to delete accounts
| immediately.
|
*/
'account_delete_after' => env('ACCOUNT_DELETE_AFTER', false),
/*
|--------------------------------------------------------------------------
| Enable Cloud Storage
|--------------------------------------------------------------------------
|
| Store media on object storage like S3, Digital Ocean Spaces, Rackspace
|
*/
'cloud_storage' => env('PF_ENABLE_CLOUD', false),
'media_types' => env('MEDIA_TYPES', 'image/jpeg,image/png,image/gif'),
'enforce_account_limit' => env('LIMIT_ACCOUNT_SIZE', true),
'ap_inbox' => env('ACTIVITYPUB_INBOX', false),

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateProfilesTableUseTextForBio extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('profiles', function (Blueprint $table) {
$table->text('bio')->nullable()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('profiles', function (Blueprint $table) {
$table->string('bio')->nullable()->change();
});
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateProfilesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('profiles', function (Blueprint $table) {
$table->boolean('unlisted')->default(false)->index()->after('bio');
$table->boolean('cw')->default(false)->index()->after('unlisted');
$table->boolean('no_autolink')->default(false)->index()->after('cw');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$table->dropColumn('unlisted');
$table->dropColumn('cw');
$table->dropColumn('no_autolink');
}
}

View file

@ -0,0 +1,77 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class Stories extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('story_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('story_id')->unsigned()->index();
$table->string('media_path')->nullable();
$table->string('media_url')->nullable();
$table->tinyInteger('duration')->unsigned();
$table->string('filter')->nullable();
$table->string('link_url')->nullable()->index();
$table->string('link_text')->nullable();
$table->tinyInteger('order')->unsigned()->nullable();
$table->string('type')->default('photo');
$table->json('layers')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
Schema::create('story_views', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('story_id')->unsigned()->index();
$table->bigInteger('profile_id')->unsigned()->index();
$table->unique(['story_id', 'profile_id']);
$table->timestamps();
});
Schema::table('stories', function (Blueprint $table) {
$table->string('title')->nullable()->after('profile_id');
$table->boolean('preview_photo')->default(false)->after('title');
$table->boolean('local_only')->default(false)->after('preview_photo');
$table->boolean('is_live')->default(false)->after('local_only');
$table->string('broadcast_url')->nullable()->after('is_live');
$table->string('broadcast_key')->nullable()->after('broadcast_url');
});
Schema::table('story_reactions', function (Blueprint $table) {
$table->bigInteger('story_id')->unsigned()->index()->after('profile_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('story_items');
Schema::dropIfExists('story_views');
Schema::table('stories', function (Blueprint $table) {
$table->dropColumn('title');
$table->dropColumn('preview_photo');
$table->dropColumn('local_only');
$table->dropColumn('is_live');
$table->dropColumn('broadcast_url');
$table->dropColumn('broadcast_key');
});
Schema::table('story_reactions', function (Blueprint $table) {
$table->dropColumn('story_id');
});
}
}

9047
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,9 +15,11 @@
"bootstrap": "^4.2.1",
"cross-env": "^5.2.0",
"jquery": "^3.2",
"laravel-mix": "^2.1.14",
"lodash": "^4.17.11",
"popper.js": "^1.14.6",
"resolve-url-loader": "^2.3.1",
"sass": "^1.15.2",
"sass-loader": "^7.1.0",
"vue": "^2.5.21",
"vue-template-compiler": "^2.5.21"
},
@ -27,9 +29,12 @@
"filesize": "^3.6.1",
"infinite-scroll": "^3.0.4",
"laravel-echo": "^1.5.2",
"laravel-mix": "^4.0.12",
"node-sass": "^4.11.0",
"opencollective": "^1.0.3",
"opencollective-postinstall": "^2.0.1",
"plyr": "^3.4.7",
"promise-polyfill": "8.1.0",
"pusher-js": "^4.2.2",
"readmore-js": "^2.2.1",
"socket.io-client": "^2.2.0",

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/js/activity.js vendored

Binary file not shown.

BIN
public/js/app.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

1
public/svg/403.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

1
public/svg/404.svg Executable file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024"><defs><linearGradient id="a" x1="50.31%" x2="50%" y1="74.74%" y2="0%"><stop offset="0%" stop-color="#FFE98A"/><stop offset="67.7%" stop-color="#B63E59"/><stop offset="100%" stop-color="#68126F"/></linearGradient><circle id="c" cx="603" cy="682" r="93"/><filter id="b" width="203.2%" height="203.2%" x="-51.6%" y="-51.6%" filterUnits="objectBoundingBox"><feOffset in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="32"/><feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><linearGradient id="d" x1="49.48%" x2="49.87%" y1="11.66%" y2="77.75%"><stop offset="0%" stop-color="#F7EAB9"/><stop offset="100%" stop-color="#E5765E"/></linearGradient><linearGradient id="e" x1="91.59%" x2="66.97%" y1="5.89%" y2="100%"><stop offset="0%" stop-color="#A22A50"/><stop offset="100%" stop-color="#EE7566"/></linearGradient><linearGradient id="f" x1="49.48%" x2="49.61%" y1="11.66%" y2="98.34%"><stop offset="0%" stop-color="#F7EAB9"/><stop offset="100%" stop-color="#E5765E"/></linearGradient><linearGradient id="g" x1="78.5%" x2="36.4%" y1="106.76%" y2="26.41%"><stop offset="0%" stop-color="#A22A50"/><stop offset="100%" stop-color="#EE7566"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><rect width="1024" height="1024" fill="url(#a)"/><use fill="black" filter="url(#b)" xlink:href="#c"/><use fill="#FFF6CB" xlink:href="#c"/><g fill="#FFFFFF" opacity=".3" transform="translate(14 23)"><circle cx="203" cy="255" r="3" fill-opacity=".4"/><circle cx="82" cy="234" r="2"/><circle cx="22" cy="264" r="2" opacity=".4"/><circle cx="113" cy="65" r="3"/><circle cx="202" cy="2" r="2"/><circle cx="2" cy="114" r="2"/><circle cx="152" cy="144" r="2"/><circle cx="362" cy="224" r="2"/><circle cx="453" cy="65" r="3" opacity=".4"/><circle cx="513" cy="255" r="3"/><circle cx="593" cy="115" r="3"/><circle cx="803" cy="5" r="3" opacity=".4"/><circle cx="502" cy="134" r="2"/><circle cx="832" cy="204" r="2"/><circle cx="752" cy="114" r="2"/><circle cx="933" cy="255" r="3" opacity=".4"/><circle cx="703" cy="225" r="3"/><circle cx="903" cy="55" r="3"/><circle cx="982" cy="144" r="2"/><circle cx="632" cy="14" r="2"/></g><g transform="translate(0 550)"><path fill="#8E2C15" d="M259 5.47c0 5.33 3.33 9.5 10 12.5s9.67 9.16 9 18.5h1c.67-6.31 1-11.8 1-16.47 8.67 0 13.33-1.33 14-4 .67 4.98 1.67 8.3 3 9.97 1.33 1.66 2 5.16 2 10.5h1c0-5.65.33-9.64 1-11.97 1-3.5 4-10.03-1-14.53S295 7 290 3c-5-4-10-3-13 2s-5 7-9 7-5-3.53-5-5.53c0-2 2-5-1.5-5s-7.5 0-7.5 2c0 1.33 1.67 2 5 2z"/><path fill="url(#d)" d="M1024 390H0V105.08C77.3 71.4 155.26 35 297.4 35c250 0 250.76 125.25 500 125 84.03-.08 160.02-18.2 226.6-40.93V390z"/><path fill="url(#d)" d="M1024 442H0V271.82c137.51-15.4 203.1-50.49 356.67-60.1C555.24 199.3 606.71 86.59 856.74 86.59c72.78 0 124.44 10.62 167.26 25.68V442z"/><path fill="url(#e)" d="M1024 112.21V412H856.91c99.31-86.5 112.63-140.75 39.97-162.78C710.24 192.64 795.12 86.58 856.9 86.58c72.7 0 124.3 10.6 167.09 25.63z"/><path fill="url(#e)" d="M1024 285.32V412H857c99.31-86.6 112.63-140.94 39.97-163L1024 285.32z"/><path fill="url(#f)" d="M0 474V223.93C67.12 190.69 129.55 155 263 155c250 0 331.46 162.6 530 175 107.42 6.71 163-26.77 231-58.92V474H0z"/><path fill="url(#e)" d="M353.02 474H0V223.93C67.12 190.69 129.55 155 263 155c71.14 0 151.5 12.76 151.5 70.5 0 54.5-45.5 79.72-112.5 109-82.26 35.95-54.57 111.68 51.02 139.5z"/><path fill="url(#g)" d="M353.02 474H0v-14.8l302-124.7c-82.26 35.95-54.57 111.68 51.02 139.5z"/></g><g fill="#FFFFFF" opacity=".2" transform="translate(288 523)"><circle cx="250" cy="110" r="110"/><circle cx="420" cy="78" r="60"/><circle cx="70" cy="220" r="70"/></g><g fill="#FFFFFF" fill-rule="nonzero" opacity=".08" transform="translate(135 316)"><path d="M10 80.22a14.2 14.2 0 0 1 20 0 14.2 14.2 0 0 0 20 0l20-19.86a42.58 42.58 0 0 1 60 0l15 14.9a21.3 21.3 0 0 0 30 0 21.3 21.3 0 0 1 30 0l.9.9A47.69 47.69 0 0 1 220 110H0v-5.76c0-9.02 3.6-17.67 10-24.02zm559.1-66.11l5.9-5.86c11.07-11 28.93-11 40 0l10 9.94a14.19 14.19 0 0 0 20 0 14.19 14.19 0 0 1 20 0 16.36 16.36 0 0 0 21.3 1.5l8.7-6.47a33.47 33.47 0 0 1 40 0l4.06 3.03A39.6 39.6 0 0 1 755 48H555a47.77 47.77 0 0 1 14.1-33.89z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/svg/500.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

1
public/svg/503.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -3,6 +3,7 @@ import BootstrapVue from 'bootstrap-vue'
import InfiniteLoading from 'vue-infinite-loading';
import Loading from 'vue-loading-overlay';
import VueTimeago from 'vue-timeago';
//import {Howl, Howler} from 'howler';
Vue.use(BootstrapVue);
Vue.use(InfiniteLoading);
@ -29,7 +30,6 @@ try {
document.createEvent("TouchEvent");
$('body').addClass('touch');
} catch (e) {
return false;
}
window.InfiniteScroll = require('infinite-scroll');
@ -67,44 +67,49 @@ window.pixelfed.n = {};
Vue.component(
'photo-presenter',
require('./components/presenter/PhotoPresenter.vue')
require('./components/presenter/PhotoPresenter.vue').default
);
Vue.component(
'video-presenter',
require('./components/presenter/VideoPresenter.vue')
require('./components/presenter/VideoPresenter.vue').default
);
Vue.component(
'photo-album-presenter',
require('./components/presenter/PhotoAlbumPresenter.vue')
require('./components/presenter/PhotoAlbumPresenter.vue').default
);
Vue.component(
'video-album-presenter',
require('./components/presenter/VideoAlbumPresenter.vue')
require('./components/presenter/VideoAlbumPresenter.vue').default
);
Vue.component(
'mixed-album-presenter',
require('./components/presenter/MixedAlbumPresenter.vue')
require('./components/presenter/MixedAlbumPresenter.vue').default
);
// Vue.component(
// 'micro',
// require('./components/Micro.vue')
// require('./components/Micro.vue').default
// );
Vue.component(
'follow-suggestions',
require('./components/FollowSuggestions.vue')
require('./components/FollowSuggestions.vue').default
);
Vue.component(
'discover-component',
require('./components/DiscoverComponent.vue')
require('./components/DiscoverComponent.vue').default
);
// Vue.component(
// 'profile',
// require('./components/Profile.vue').default
// );
// Vue.component(
// 'circle-panel',
// require('./components/CirclePanel.vue')
@ -112,53 +117,55 @@ Vue.component(
Vue.component(
'post-component',
require('./components/PostComponent.vue')
require('./components/PostComponent.vue').default
);
Vue.component(
'post-comments',
require('./components/PostComments.vue')
require('./components/PostComments.vue').default
);
Vue.component(
'timeline',
require('./components/Timeline.vue')
require('./components/Timeline.vue').default
);
Vue.component(
'passport-clients',
require('./components/passport/Clients.vue')
);
// Vue.component(
// 'passport-clients',
// require('./components/passport/Clients.vue').default
// );
Vue.component(
'passport-authorized-clients',
require('./components/passport/AuthorizedClients.vue')
);
// Vue.component(
// 'passport-authorized-clients',
// require('./components/passport/AuthorizedClients.vue').default
// );
Vue.component(
'passport-personal-access-tokens',
require('./components/passport/PersonalAccessTokens.vue')
);
// Vue.component(
// 'passport-personal-access-tokens',
// require('./components/passport/PersonalAccessTokens.vue').default
// );
window.pixelfed.copyToClipboard = (str) => {
const el = document.createElement('textarea');
el.value = str;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
const selected =
document.getSelection().rangeCount > 0
? document.getSelection().getRangeAt(0)
: false;
el.select();
document.execCommand('copy');
document.body.removeChild(el);
if (selected) {
document.getSelection().removeAllRanges();
document.getSelection().addRange(selected);
}
};
//import 'promise-polyfill/src/polyfill';
// window.pixelfed.copyToClipboard = (str) => {
// const el = document.createElement('textarea');
// el.value = str;
// el.setAttribute('readonly', '');
// el.style.position = 'absolute';
// el.style.left = '-9999px';
// document.body.appendChild(el);
// const selected =
// document.getSelection().rangeCount > 0
// ? document.getSelection().getRangeAt(0)
// : false;
// el.select();
// document.execCommand('copy');
// document.body.removeChild(el);
// if (selected) {
// document.getSelection().removeAllRanges();
// document.getSelection().addRange(selected);
// }
// };
$(document).ready(function() {
$(function () {

View file

@ -1,4 +1,4 @@
<style>
<style scoped>
span {
font-size: 14px;
}
@ -92,7 +92,7 @@ export default {
axios.get(url)
.then(response => {
let self = this;
this.results = response.data.data;
this.results = _.reverse(response.data.data);
this.pagination = response.data.meta.pagination;
if(this.results.length > 0) {
$('.load-more-link').removeClass('d-none');

View file

@ -1,4 +1,4 @@
<style>
<style scoped>
#l-modal .modal-body,
#s-modal .modal-body {
max-height: 70vh;
@ -496,18 +496,21 @@ export default {
},
deletePost() {
if($('body').hasClass('loggedIn') == false) {
return;
}
var result = confirm('Are you sure you want to delete this post?');
if (result) {
if($('body').hasClass('loggedIn') == false) {
return;
}
axios.post('/i/delete', {
type: 'status',
item: this.status.id
}).then(res => {
swal('Success', 'You have successfully deleted this post', 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
axios.post('/i/delete', {
type: 'status',
item: this.status.id
}).then(res => {
swal('Success', 'You have successfully deleted this post', 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
}
}
}
}

View file

@ -0,0 +1,40 @@
<template>
<div>
<div class="card">
<div class="card-body" id="stories">
</div>
</div>
</div>
</template>
<style type="text/css" scoped>
</style>
<script type="text/javascript">
export default {
data() {
return {
stories: [],
}
},
beforeMount() {
},
mounted() {
},
methods: {
fetchStories() {
axios.get('/api/v2/stories')
.then(res => {
this.stories = res.data
});
}
}
}
</script>

View file

@ -2,11 +2,6 @@
<div class="container" style="">
<div class="row">
<div class="col-md-8 col-lg-8 pt-2 px-0 my-3 timeline order-2 order-md-1">
<div class="loader text-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div class="card mb-4 status-card card-md-rounded-0" :data-status-id="status.id" v-for="(status, index) in feed" :key="status.id">
<div class="card-header d-inline-flex align-items-center bg-white">
@ -97,6 +92,15 @@
</form>
</div>
</div>
<!--
<infinite-loading @infinite="infiniteTimeline">
<div slot="no-more" class="font-weight-bold text-light">No more posts to load</div>
<div slot="no-results" class="font-weight-bold text-light">No posts found</div>
</infinite-loading>
-->
<div class="pagination d-none">
<p class="btn btn-outline-secondary font-weight-bold btn-block" v-on:click="loadMore">Load more posts</p>
</div>
</div>
<div class="col-md-4 col-lg-4 pt-2 my-3 order-1 order-md-2">
@ -185,7 +189,7 @@
<div class="container pb-5">
<p class="mb-0 text-uppercase font-weight-bold text-muted small">
<a href="/site/about" class="text-dark pr-2">About Us</a>
<a href="/site/help" class="text-dark pr-2">Support</a>
<a href="/site/help" class="text-dark pr-2">Help</a>
<a href="/site/open-source" class="text-dark pr-2">Open Source</a>
<a href="/site/language" class="text-dark pr-2">Language</a>
<a href="/site/terms" class="text-dark pr-2">Terms</a>
@ -202,7 +206,7 @@
</div>
</template>
<style type="text/css">
<style type="text/css" scoped>
.postPresenterContainer {
display: flex;
align-items: center;
@ -220,7 +224,7 @@
export default {
data() {
return {
page: 1,
page: 2,
feed: [],
profile: {},
scope: window.location.pathname,
@ -229,6 +233,7 @@
notifications: {},
stories: {},
suggestions: {},
loading: true,
}
},
@ -241,7 +246,6 @@
},
updated() {
this.scroll();
},
methods: {
@ -262,11 +266,10 @@
},
fetchTimelineApi() {
let homeTimeline = '/api/v1/timelines/home?page=' + this.page;
let localTimeline = '/api/v1/timelines/public?page=' + this.page;
let homeTimeline = '/api/v1/timelines/home?page=1';
let localTimeline = '/api/v1/timelines/public?page=1';
let apiUrl = this.scope == '/' ? homeTimeline : localTimeline;
axios.get(apiUrl).then(res => {
$('.timeline .loader').addClass('d-none');
let data = res.data;
this.feed.push(...data);
let ids = data.map(status => status.id);
@ -274,11 +277,62 @@
if(this.page == 1) {
this.max_id = Math.max(...ids);
}
this.page++;
$('.timeline .pagination').removeClass('d-none');
this.loading = false;
}).catch(err => {
});
},
infiniteTimeline($state) {
let homeTimeline = '/api/v1/timelines/home';
let localTimeline = '/api/v1/timelines/public';
let apiUrl = this.scope == '/' ? homeTimeline : localTimeline;
axios.get(apiUrl, {
params: {
page: this.page,
},
}).then(res => {
if (res.data.length && this.loading == false) {
let data = res.data;
this.feed.push(...data);
let ids = data.map(status => status.id);
this.min_id = Math.min(...ids);
if(this.page == 1) {
this.max_id = Math.max(...ids);
}
this.page += 1;
$state.loaded();
this.loading = false;
} else {
$state.complete();
}
});
},
loadMore() {
let homeTimeline = '/api/v1/timelines/home';
let localTimeline = '/api/v1/timelines/public';
let apiUrl = this.scope == '/' ? homeTimeline : localTimeline;
axios.get(apiUrl, {
params: {
page: this.page,
},
}).then(res => {
if (res.data.length && this.loading == false) {
let data = res.data;
this.feed.push(...data);
let ids = data.map(status => status.id);
this.min_id = Math.min(...ids);
if(this.page == 1) {
this.max_id = Math.max(...ids);
}
this.page += 1;
this.loading = false;
} else {
}
});
},
fetchNotifications() {
axios.get('/api/v1/notifications')
.then(res => {
@ -288,16 +342,6 @@
});
},
scroll() {
window.onscroll = () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight == document.documentElement.offsetHeight;
if (bottomOfWindow) {
this.fetchTimelineApi();
}
};
},
reportUrl(status) {
let type = status.in_reply_to ? 'comment' : 'post';
let id = status.id;

View file

@ -18,7 +18,7 @@
<source :src="media.url" :type="media.mime">
</video>
<img v-else-if="media.type == 'Image'" slot="img" class="d-block img-fluid w-100" :src="media.url" :alt="media.description">
<img v-else-if="media.type == 'Image'" slot="img" class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description">
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
@ -40,7 +40,7 @@
<source :src="media.url" :type="media.mime">
</video>
<img v-else-if="media.type == 'Image'" slot="img" class="d-block img-fluid w-100" :src="media.url" :alt="media.description">
<img v-else-if="media.type == 'Image'" slot="img" class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description">
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>

View file

@ -13,7 +13,7 @@
:interval="0"
>
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id">
<img slot="img" class="d-block img-fluid w-100" :src="img.url" :alt="img.description">
<img slot="img" class="d-block img-fluid w-100" :src="img.url" :alt="img.description" :title="img.description">
</b-carousel-slide>
</b-carousel>
</details>
@ -27,7 +27,7 @@
:interval="0"
>
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id">
<img slot="img" class="d-block img-fluid w-100" :src="img.url" :alt="img.description">
<img slot="img" class="d-block img-fluid w-100" :src="img.url" :alt="img.description" :title="img.description">
</b-carousel-slide>
</b-carousel>
</div>

View file

@ -6,13 +6,13 @@
<p class="font-weight-light">(click to show)</p>
</summary>
<a class="max-hide-overflow" :href="status.url" :class="status.media_attachments[0].filter_class">
<img class="card-img-top" :src="status.media_attachments[0].url">
<img class="card-img-top" :src="status.media_attachments[0].url" :alt="status.media_attachments[0].description" :title="status.media_attachments[0].description">
</a>
</details>
</div>
<div v-else>
<div :class="status.media_attachments[0].filter_class">
<img class="card-img-top" :src="status.media_attachments[0].url">
<img class="card-img-top" :src="status.media_attachments[0].url" :alt="status.media_attachments[0].description" :title="status.media_attachments[0].description">
</div>
</div>
</template>

View file

@ -1,9 +1,9 @@
/*! Instagram.css v0.1.3 | MIT License | github.com/picturepan2/instagram.css */
[class*="filter"] {
[class*="filter-"] {
position: relative;
}
[class*="filter"]::before {
[class*="filter-"]::before {
display: block;
height: 100%;
left: 0;

View file

@ -456,6 +456,13 @@ details summary::-webkit-details-marker {
border-bottom-color:#dc3545 !important;
}
#previewAvatar {
img {
max-width: 100%;
height: auto;
}
}
.img-thumbnail {
box-sizing: content-box;
}

View file

@ -0,0 +1,223 @@
@extends('admin.partial.template')
@section('section')
<div class="title">
<h3 class="font-weight-bold d-inline-block">Instances</h3>
<span class="float-right">
<div class="dropdown">
<button class="btn btn-light btn-sm dropdown-toggle font-weight-bold" type="button" id="filterDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-filter"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="filterDropdown">
<a class="dropdown-item font-weight-light" href="{{route('admin.instances')}}?filter=unlisted">Show only Unlisted</a>
<a class="dropdown-item font-weight-light" href="{{route('admin.instances')}}?filter=autocw">Show only Auto CW</a>
<a class="dropdown-item font-weight-light" href="{{route('admin.instances')}}?filter=banned">Show only Banned</a>
<a class="dropdown-item font-weight-light" href="{{route('admin.instances')}}">Show all</a>
</div>
</div>
</span>
</div>
<hr>
@if($instances->count() == 0 && request()->has('filter') == false)
<div class="alert alert-warning mb-3">
<p class="lead font-weight-bold mb-0">Warning</p>
<p class="font-weight-lighter mb-0">No instances were found.</p>
</div>
<p class="font-weight-lighter">Do you want to scan and populate instances from Profiles and Statuses?</p>
<p>
<form method="post">
@csrf
<button type="submit" class="btn btn-primary py-1 font-weight-bold">Run Scan</button>
</form>
</p>
@else
<ul class="list-group">
@foreach($instances as $instance)
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="h4 font-weight-normal mb-1">
{{$instance->domain}}
</p>
<p class="mb-0">
<a class="btn btn-outline-primary btn-sm py-0 font-weight-normal" href="{{$instance->getUrl()}}">Overview</a>
<button class="btn btn-outline-secondary btn-sm py-0 font-weight-normal btn-action mr-3"
data-instance-id="{{$instance->id}}"
data-instance-domain="{{$instance->domain}}"
data-instance-unlisted="{{$instance->unlisted}}"
data-instance-autocw="{{$instance->auto_cw}}"
data-instance-banned="{{$instance->banned}}"
>Actions</button>
@if($instance->unlisted)
<i class="fas fa-minus-circle text-danger" data-toggle="tooltip" title="Unlisted from timelines"></i>
@endif
@if($instance->auto_cw)
<i class="fas fa-eye-slash text-danger" data-toggle="tooltip" title="CW applied to all media"></i>
@endif
@if($instance->banned)
<i class="fas fa-shield-alt text-danger" data-toggle="tooltip" title="Instance is banned"></i>
@endif
</p>
</div>
<div>
<div class="d-inline-block pr-4">
<p class="h4 font-weight-light text-center">{{$instance->profiles()->count()}}</p>
<p class="mb-0 small font-weight-normal text-muted">Profiles</p>
</div>
<div class="d-inline-block pr-4">
<p class="h4 font-weight-light text-center">{{$instance->statuses()->count()}}</p>
<p class="mb-0 small font-weight-normal text-muted">Statuses</p>
</div>
<div class="d-inline-block pr-4">
<p class="h4 font-weight-light text-center text-muted">{{$instance->reported()->count()}}</p>
<p class="mb-0 small font-weight-normal text-muted">Reports</p>
</div>
<div class="d-inline-block">
<p class="h4 font-weight-light text-center text-muted filesize" data-size="{{$instance->media()->sum('size')}}">0</p>
<p class="mb-0 small font-weight-normal text-muted">Storage Used</p>
</div>
</div>
</div>
</li>
@endforeach
</ul>
<div class="d-flex justify-content-center mt-5 small">
{{$instances->links()}}
</div>
@endif
@endsection
@push('scripts')
<script type="text/javascript">
$(document).ready(function() {
$('.filesize').each(function(k,v) {
$(this).text(filesize(v.getAttribute('data-size')))
});
$('.btn-action').on('click', function(e) {
let id = this.getAttribute('data-instance-id');
let instanceDomain = this.getAttribute('data-instance-domain');
let text = 'Domain: ' + instanceDomain;
let unlisted = this.getAttribute('data-instance-unlisted');
let autocw = this.getAttribute('data-instance-autocw');
let banned = this.getAttribute('data-instance-banned');
swal({
title: 'Instance Actions',
text: text,
icon: 'warning',
buttons: {
unlist: {
text: unlisted == 0 ? "Unlist" : "Re-list",
className: "bg-warning",
value: "unlisted",
},
cw: {
text: autocw == 0 ? "CW Media" : "Remove AutoCW",
text: autocw == 0 ? "CW Media" : "Remove AutoCW",
className: "bg-warning",
value: "autocw",
},
ban: {
text: banned == 0 ? "Ban" : "Unban",
className: "bg-danger",
value: "ban",
},
},
})
.then((value) => {
switch (value) {
case "unlisted":
swal({
title: "Are you sure?",
text: unlisted == 0 ?
"Are you sure you want to unlist " + instanceDomain + " ?" :
"Are you sure you want to remove the unlisted rule of " + instanceDomain + " ?",
icon: "warning",
buttons: true,
dangerMode: true,
})
.then((unlist) => {
if (unlist) {
axios.post('/i/admin/instances/edit/' + id, {
action: 'unlist'
}).then((res) => {
swal("Domain action was successful! The page will now refresh.", {
icon: "success",
});
setTimeout(function() {
window.location.href = window.location.href;
}, 5000);
}).catch((err) => {
swal("Something went wrong!", "Please try again later.", "error");
})
} else {
swal("Action Cancelled", "You successfully cancelled this action.", "error");
}
});
break;
case "autocw":
swal({
title: "Are you sure?",
text: autocw == 0 ?
"Are you sure you want to auto CW all media from " + instanceDomain + " ?" :
"Are you sure you want to remove the auto cw rule for " + instanceDomain + " ?",
icon: "warning",
buttons: true,
dangerMode: true,
})
.then((res) => {
if (res) {
axios.post('/i/admin/instances/edit/' + id, {
action: 'autocw'
}).then((res) => {
swal("Domain action was successful! The page will now refresh.", {
icon: "success",
});
setTimeout(function() {
window.location.href = window.location.href;
}, 5000);
}).catch((err) => {
swal("Something went wrong!", "Please try again later.", "error");
})
} else {
swal("Action Cancelled", "You successfully cancelled this action.", "error");
}
});
break;
case "ban":
swal({
title: "Are you sure?",
text: autocw == 0 ?
"Are you sure you want to ban " + instanceDomain + " ?" :
"Are you sure you want unban " + instanceDomain + " ?",
icon: "warning",
buttons: true,
dangerMode: true,
})
.then((res) => {
if (res) {
axios.post('/i/admin/instances/edit/' + id, {
action: 'ban'
}).then((res) => {
swal("Domain action was successful! The page will now refresh.", {
icon: "success",
});
setTimeout(function() {
window.location.href = window.location.href;
}, 5000);
}).catch((err) => {
swal("Something went wrong!", "Please try again later.", "error");
})
} else {
swal("Action Cancelled", "You successfully cancelled this action.", "error");
}
});
break;
}
});
})
});
</script>
@endpush

View file

@ -0,0 +1,113 @@
@extends('admin.partial.template')
@section('section')
<div class="title">
<div class="d-flex justify-content-between">
<div>
<h3 class="font-weight-bold mb-0">Instance Overview</h3>
<p class="font-weight-lighter mb-0">domain: {{$instance->domain}}</p>
</div>
<div>
<a class="btn btn-outline-primary btn-sm py-1" href="{{route('admin.instances')}}">Back</a>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between">
<div>
<p class="font-weight-lighter mb-0">unlisted: {{$instance->unlisted ? 'true' : 'false'}}</p>
</div>
<div>
<p class="font-weight-lighter mb-0">CW media: {{$instance->auto_cw ? 'true' : 'false'}}</p>
</div>
<div>
<p class="font-weight-lighter mb-0">banned: {{$instance->banned ? 'true' : 'false'}}</p>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-md-6">
<div class="card mb-3">
<div class="card-body text-center">
<p class="mb-0 font-weight-lighter display-4">
{{$instance->profiles->count()}}
</p>
<p class="mb-0 text-muted">Profiles</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body text-center">
<p class="mb-0 font-weight-lighter display-4">
{{$instance->reports->count()}}
</p>
<p class="mb-0 text-muted">Reports</p>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card mb-3">
<div class="card-body text-center">
<p class="mb-0 font-weight-lighter display-4">
{{$instance->statuses->count()}}
</p>
<p class="mb-0 text-muted">Statuses</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body text-center">
<p class="mb-0 font-weight-lighter display-4 filesize" data-size="{{$instance->media()->sum('size')}}">
0
</p>
<p class="mb-0 text-muted">Storage Used</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light h4 font-weight-lighter">
Profiles
<span class="float-right">
<a class="btn btn-outline-secondary btn-sm py-0" href="#">View All</a>
</span>
</div>
<ul class="list-group list-group-flush">
@foreach($instance->profiles()->latest()->take(5)->get() as $profile)
<li class="list-group-item">
<a class="btn btn-outline-primary btn-block btn-sm" href="{{$profile->url()}}">{{$profile->emailUrl()}}</a>
</li>
@endforeach
</ul>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light h4 font-weight-lighter">
Statuses
<span class="float-right">
<a class="btn btn-outline-secondary btn-sm py-0" href="#">View All</a>
</span>
</div>
<ul class="list-group list-group-flush">
@foreach($instance->statuses()->latest()->take(5)->get() as $status)
<li class="list-group-item">
<a class="btn btn-outline-primary btn-block btn-sm" href="{{$status->url()}}">Status ID: {{$status->id}}</a>
</li>
@endforeach
</ul>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
$(document).ready(function() {
$('.filesize').each(function(k,v) {
$(this).text(filesize(v.getAttribute('data-size')))
});
});
</script>
@endpush

View file

@ -0,0 +1,41 @@
@extends('admin.partial.template')
@section('section')
<div class="title">
<h3 class="font-weight-bold d-inline-block">Media</h3>
<p class="font-weight-lighter mb-0">ID: {{$media->id}}</p>
</div>
<hr>
<div class="row">
<div class="col-12 col-md-8 offset-md-2">
<div class="card">
<img class="card-img-top" src="{{$media->thumb()}}">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between">
<div>
<p class="mb-0 small">status id: <a href="{{$media->status->url()}}" class="font-weight-bold">{{$media->status_id}}</a></p>
<p class="mb-0 small">username: <a href="{{$media->profile->url()}}" class="font-weight-bold">{{$media->profile->username}}</a></p>
<p class="mb-0 small">size: <span class="filesize font-weight-bold" data-size="{{$media->size}}">0</span></p>
</div>
<div>
<p class="mb-0 small">mime: <span class="font-weight-bold">{{$media->mime}}</span></p>
<p class="mb-0 small">content warning: <i class="fas {{$media->is_nsfw ? 'fa-check text-danger':'fa-times text-success'}}"></i></p>
<p class="mb-0 small">
remote media: <i class="fas {{$media->remote_media ? 'fa-check text-danger':'fa-times text-success'}}"></i></p>
</div>
</li>
</ul>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
$(document).ready(function() {
$('.filesize').each(function(k,v) {
$(this).text(filesize(v.getAttribute('data-size')))
});
});
</script>
@endpush

View file

@ -1,7 +1,7 @@
@extends('layouts.app')
@section('content')
@yield('header')
<div class="container">
<div class="col-12 mt-5">
<div class="card">

View file

@ -38,13 +38,14 @@
<div class="square {{$status->firstMedia()->filter_class}}">
@switch($status->viewType())
@case('album')
<span class="float-right mr-3" style="color:#fff;position:relative;margin-top:10px;z-index: 999999;opacity:0.6"><i class="fas fa-images fa-2x"></i></span>
@case('photo:album')
<span class="float-right mr-3" style="color:#fff;position:relative;margin-top:10px;z-index: 999999;opacity:0.6;text-shadow: 3px 3px 16px #272634;"><i class="fas fa-images fa-2x"></i></span>
@break
@case('video')
<span class="float-right mr-3" style="color:#fff;position:relative;margin-top:10px;z-index: 999999;opacity:0.6"><i class="fas fa-video fa-2x"></i></span>
<span class="float-right mr-3" style="color:#fff;position:relative;margin-top:10px;z-index: 999999;opacity:0.6;text-shadow: 3px 3px 16px #272634;"><i class="fas fa-video fa-2x"></i></span>
@break
@case('video-album')
<span class="float-right mr-3" style="color:#fff;position:relative;margin-top:10px;z-index: 999999;opacity:0.6"><i class="fas fa-film fa-2x"></i></span>
<span class="float-right mr-3" style="color:#fff;position:relative;margin-top:10px;z-index: 999999;opacity:0.6;text-shadow: 3px 3px 16px #272634;"><i class="fas fa-film fa-2x"></i></span>
@break
@endswitch
<div class="square-content" style="background-image: url('{{$status->thumb()}}')">
@ -109,7 +110,7 @@
let elem = document.querySelector('.profile-timeline');
let infScroll = new InfiniteScroll( elem, {
path: '.pagination__next',
append: '.profile-timeline',
append: '.profile-timeline .row',
status: '.page-load-status',
history: false,
});

View file

@ -22,6 +22,7 @@
<label class="custom-file-label" for="avatarInput">Select a profile photo</label>
</div>
<p><span class="small font-weight-bold">Must be a jpeg or png. Max avatar size: <span id="maxAvatarSize"></span></span></p>
<div id="previewAvatar"></div>
<p class="mb-0"><button type="submit" class="btn btn-primary px-4 py-0 font-weight-bold">Upload</button></p>
</div>
</form>
@ -130,5 +131,18 @@
});
$('#maxAvatarSize').text(filesize({{config('pixelfed.max_avatar_size') * 1024}}, {round: 0}));
$('#avatarInput').on('change', function(e) {
var file = document.getElementById('avatarInput').files[0];
var reader = new FileReader();
reader.addEventListener("load", function() {
$('#previewAvatar').html('<img src="' + reader.result + '" class="rounded-circle box-shadow mb-3" width="100%" height="100%"/>');
}, false);
if (file) {
reader.readAsDataURL(file);
}
});
</script>
@endpush

View file

@ -140,7 +140,7 @@
</div>
</div>
</p>
{{-- <hr>
<hr>
<p class="h5 text-muted font-weight-light" id="delete-your-account">Delete Your Account</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#del-collapse1" role="button" aria-expanded="false" aria-controls="del-collapse1">
@ -159,6 +159,7 @@
</div>
</div>
</p>
@if(config('pixelfed.account_deletion'))
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#del-collapse2" role="button" aria-expanded="false" aria-controls="del-collapse2">
<i class="fas fa-chevron-down mr-2"></i>
@ -166,9 +167,15 @@
</a>
<div class="collapse" id="del-collapse2">
<div>
@if(config('pixelfed.account_delete_after') == false)
<div class="bg-light p-3 mb-4">
<p class="mb-0">When you delete your account, your profile, photos, videos, comments, likes and followers will be permanently removed. If you'd just like to take a break, you can <a href="{{route('settings.remove.temporary')}}">temporarily disable</a> your account instead.</p>
<p class="mb-0">When you delete your account, your profile, photos, videos, comments, likes and followers will be <b>permanently removed</b>. If you'd just like to take a break, you can <a href="{{route('settings.remove.temporary')}}">temporarily disable</a> your account instead.</p>
</div>
@else
<div class="bg-light p-3 mb-4">
<p class="mb-0">When you delete your account, your profile, photos, videos, comments, likes and followers will be <b>permanently removed</b> after {{config('pixelfed.account_delete_after')}} days. You can log in during that period to prevent your account from permanent deletion. If you'd just like to take a break, you can <a href="{{route('settings.remove.temporary')}}">temporarily disable</a> your account instead.</p>
</div>
@endif
<p>After you delete your account, you can't sign up again with the same username on this instance or add that username to another account on this instance, and we can't reactivate deleted accounts.</p>
<p>To permanently delete your account:</p>
<ol class="font-weight-light">
@ -178,5 +185,6 @@
</ol>
</div>
</div>
</p> --}}
</p>
@endif
@endsection

View file

@ -20,7 +20,7 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () {
Route::get('/', 'SiteController@home')->name('timeline.personal');
Route::post('/', 'StatusController@store')->middleware('throttle:500,1440');
Route::post('/', 'StatusController@store');
Auth::routes();
@ -41,7 +41,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials');
Route::post('avatar/update', 'ApiController@avatarUpdate');
Route::get('likes', 'ApiController@hydrateLikes');
Route::post('media', 'ApiController@uploadMedia')->middleware('throttle:500,1440');
Route::post('media', 'ApiController@uploadMedia');
Route::get('notifications', 'ApiController@notifications');
Route::get('timelines/public', 'PublicApiController@publicTimelineApi');
Route::get('timelines/home', 'PublicApiController@homeTimelineApi');
@ -58,7 +58,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::group(['prefix' => 'local'], function () {
Route::get('i/follow-suggestions', 'ApiController@followSuggestions');
Route::post('i/more-comments', 'ApiController@loadMoreComments');
Route::post('status/compose', 'InternalApiController@compose')->middleware('throttle:500,1440');
Route::post('status/compose', 'InternalApiController@compose');
});
});
@ -67,20 +67,20 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::group(['prefix' => 'i'], function () {
Route::redirect('/', '/');
Route::get('compose', 'StatusController@compose')->name('compose');
Route::post('comment', 'CommentController@store')->middleware('throttle:1000,1440');
Route::post('delete', 'StatusController@delete')->middleware('throttle:1000,1440');
Route::post('comment', 'CommentController@store');
Route::post('delete', 'StatusController@delete');
Route::post('mute', 'AccountController@mute');
Route::post('block', 'AccountController@block');
Route::post('like', 'LikeController@store')->middleware('throttle:1000,1440');
Route::post('share', 'StatusController@storeShare')->middleware('throttle:1000,1440');
Route::post('follow', 'FollowerController@store')->middleware('throttle:250,1440');
Route::post('bookmark', 'BookmarkController@store')->middleware('throttle:250,1440');
Route::post('like', 'LikeController@store');
Route::post('share', 'StatusController@storeShare');
Route::post('follow', 'FollowerController@store');
Route::post('bookmark', 'BookmarkController@store');
Route::get('lang/{locale}', 'SiteController@changeLocale');
Route::get('restored', 'AccountController@accountRestored');
Route::get('verify-email', 'AccountController@verifyEmail');
Route::post('verify-email', 'AccountController@sendVerifyEmail')->middleware('throttle:10,1440');
Route::get('confirm-email/{userToken}/{randomToken}', 'AccountController@confirmVerifyEmail')->middleware('throttle:10,1440');
Route::post('verify-email', 'AccountController@sendVerifyEmail');
Route::get('confirm-email/{userToken}/{randomToken}', 'AccountController@confirmVerifyEmail');
Route::get('auth/sudo', 'AccountController@sudoMode');
Route::post('auth/sudo', 'AccountController@sudoModeVerify');
@ -92,7 +92,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::group(['prefix' => 'report'], function () {
Route::get('/', 'ReportController@showForm')->name('report.form');
Route::post('/', 'ReportController@formStore')->middleware('throttle:10,5');
Route::post('/', 'ReportController@formStore');
Route::get('not-interested', 'ReportController@notInterestedForm')->name('report.not-interested');
Route::get('spam', 'ReportController@spamForm')->name('report.spam');
Route::get('spam/comment', 'ReportController@spamCommentForm')->name('report.spam.comment');
@ -118,19 +118,19 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::redirect('/', '/settings/home');
Route::get('home', 'SettingsController@home')
->name('settings');
Route::post('home', 'SettingsController@homeUpdate')->middleware('throttle:250,1440');
Route::post('home', 'SettingsController@homeUpdate');
Route::get('avatar', 'SettingsController@avatar')->name('settings.avatar');
Route::post('avatar', 'AvatarController@store');
Route::get('password', 'SettingsController@password')->name('settings.password')->middleware('dangerzone');
Route::post('password', 'SettingsController@passwordUpdate')->middleware(['throttle:2,1440','dangerzone']);
Route::post('password', 'SettingsController@passwordUpdate')->middleware('dangerzone');
Route::get('email', 'SettingsController@email')->name('settings.email');
Route::get('notifications', 'SettingsController@notifications')->name('settings.notifications');
Route::get('privacy', 'SettingsController@privacy')->name('settings.privacy');
Route::post('privacy', 'SettingsController@privacyStore')->middleware('throttle:250,1440');
Route::post('privacy', 'SettingsController@privacyStore');
Route::get('privacy/muted-users', 'SettingsController@mutedUsers')->name('settings.privacy.muted-users');
Route::post('privacy/muted-users', 'SettingsController@mutedUsersUpdate')->middleware('throttle:100,1440');
Route::post('privacy/muted-users', 'SettingsController@mutedUsersUpdate');
Route::get('privacy/blocked-users', 'SettingsController@blockedUsers')->name('settings.privacy.blocked-users');
Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate')->middleware('throttle:100,1440');
Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate');
Route::get('privacy/blocked-instances', 'SettingsController@blockedInstances')->name('settings.privacy.blocked-instances');
// Todo: Release in 0.7.2

View file

@ -0,0 +1,168 @@
<?php
namespace Tests\Unit\ActivityPub\Verb;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Util\ActivityPub\Validator\Announce;
class AnnounceTest extends TestCase
{
public function setUp()
{
parent::setUp();
$this->validAnnounce = [
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://example.org/users/alice/statuses/100000000000001/activity",
"type" => "Announce",
"actor" => "https://example.org/users/alice",
"published" => "2018-12-31T23:59:59Z",
"to" => [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc" => [
"https://example.org/users/bob",
"https://example.org/users/alice/followers"
],
"object" => "https://example.org/p/bob/100000000000000",
];
$this->invalidAnnounce = [
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://example.org/users/alice/statuses/100000000000001/activity",
"type" => "Announce2",
"actor" => "https://example.org/users/alice",
"published" => "2018-12-31T23:59:59Z",
"to" => [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc" => [
"https://example.org/users/bob",
"https://example.org/users/alice/followers"
],
"object" => "https://example.org/p/bob/100000000000000",
];
$this->invalidDate = [
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://example.org/users/alice/statuses/100000000000001/activity",
"type" => "Announce",
"actor" => "https://example.org/users/alice",
"published" => "2018-12-31T23:59:59ZEZE",
"to" => [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc" => [
"https://example.org/users/bob",
"https://example.org/users/alice/followers"
],
"object" => "https://example.org/p/bob/100000000000000",
];
$this->contextMissing = [
"id" => "https://example.org/users/alice/statuses/100000000000001/activity",
"type" => "Announce",
"actor" => "https://example.org/users/alice",
"published" => "2018-12-31T23:59:59Z",
"to" => [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc" => [
"https://example.org/users/bob",
"https://example.org/users/alice/followers"
],
"object" => "https://example.org/p/bob/100000000000000",
];
$this->audienceMissing = [
"id" => "https://example.org/users/alice/statuses/100000000000001/activity",
"type" => "Announce",
"actor" => "https://example.org/users/alice",
"published" => "2018-12-31T23:59:59Z",
"object" => "https://example.org/p/bob/100000000000000",
];
$this->audienceMissing2 = [
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://example.org/users/alice/statuses/100000000000001/activity",
"type" => "Announce",
"actor" => "https://example.org/users/alice",
"published" => "2018-12-31T23:59:59Z",
"to" => null,
"cc" => null,
"object" => "https://example.org/p/bob/100000000000000",
];
$this->invalidActor = [
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://example.org/users/alice/statuses/100000000000001/activity",
"type" => "Announce",
"actor" => "10000",
"published" => "2018-12-31T23:59:59Z",
"to" => [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc" => [
"https://example.org/users/bob",
"https://example.org/users/alice/followers"
],
"object" => "https://example.org/p/bob/100000000000000",
];
$this->invalidActor2 = [
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://example.org/users/alice/statuses/100000000000001/activity",
"type" => "Announce",
"published" => "2018-12-31T23:59:59Z",
"to" => [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc" => [
"https://example.org/users/bob",
"https://example.org/users/alice/followers"
],
"object" => "https://example.org/p/bob/100000000000000",
];
}
/** @test */
public function basic_accept()
{
$this->assertTrue(Announce::validate($this->validAnnounce));
}
/** @test */
public function invalid_accept()
{
$this->assertFalse(Announce::validate($this->invalidAnnounce));
}
/** @test */
public function invalid_date()
{
$this->assertFalse(Announce::validate($this->invalidDate));
}
/** @test */
public function context_missing()
{
$this->assertFalse(Announce::validate($this->contextMissing));
}
/** @test */
public function audience_missing()
{
$this->assertFalse(Announce::validate($this->audienceMissing));
$this->assertFalse(Announce::validate($this->audienceMissing2));
}
/** @test */
public function invalid_actor()
{
$this->assertFalse(Announce::validate($this->invalidActor));
$this->assertFalse(Announce::validate($this->invalidActor2));
}
}