Merge pull request #2571 from pixelfed/staging

v0.10.10
This commit is contained in:
daniel 2021-01-28 21:36:08 -07:00 committed by GitHub
commit 4deec1f6c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 15130 additions and 1127 deletions

View file

@ -1,16 +1,19 @@
# Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.9...dev)
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.10...dev)
### Added
## [v0.10.10 (2021-01-28)](https://github.com/pixelfed/pixelfed/compare/v0.10.9...v0.10.10)
### Added
- Direct Messages ([d63569c](https://github.com/pixelfed/pixelfed/commit/d63569c))
- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5))
- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5)) ([3ee1215a](https://github.com/pixelfed/pixelfed/commit/3ee1215a))
- Custom content warnings for remote posts ([6afc61a4](https://github.com/pixelfed/pixelfed/commit/6afc61a4))
- Thai translations ([74cd536](https://github.com/pixelfed/pixelfed/commit/74cd536))
- Added Bookmarks to v1 api ([99cb48c5](https://github.com/pixelfed/pixelfed/commit/99cb48c5))
- Added New Post notification to Timeline ([a0e7c4d5](https://github.com/pixelfed/pixelfed/commit/a0e7c4d5))
- Add Instagram Import ([e2a6bdd0](https://github.com/pixelfed/pixelfed/commit/e2a6bdd0))
- Add notification preview to NotificationCard ([28445e27](https://github.com/pixelfed/pixelfed/commit/28445e27))
- Add Grid Mode to Timelines ([c1853ca8](https://github.com/pixelfed/pixelfed/commit/c1853ca8))
- Add MediaPathService ([c54b29c5](https://github.com/pixelfed/pixelfed/commit/c54b29c5))
- Add Media Tags ([711fc020](https://github.com/pixelfed/pixelfed/commit/711fc020))
- Add MediaTagService ([524c6d45](https://github.com/pixelfed/pixelfed/commit/524c6d45))
@ -24,6 +27,7 @@
- Add autospam feature ([b892bcf0](https://github.com/pixelfed/pixelfed/commit/b892bcf0))
- Add hCaptcha ([082c1ccb](https://github.com/pixelfed/pixelfed/commit/082c1ccb))
- Add StatusView model to store views for discover algorithm ([7a68ee94](https://github.com/pixelfed/pixelfed/commit/7a68ee94))
- Add Year in Review feature (mysql only) ([f32072a3](https://github.com/pixelfed/pixelfed/commit/f32072a3))
### Updated
- Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))
@ -145,8 +149,8 @@
- Updated avatars, use jpeg default. ([f6528c84](https://github.com/pixelfed/pixelfed/commit/f6528c84))
- Updated antispam bouncer, change recent from 1 week to 3 months. ([7d818197](https://github.com/pixelfed/pixelfed/commit/7d818197))
- Updated Post components, fix remote post and profile urls. ([cfcf17f3](https://github.com/pixelfed/pixelfed/commit/cfcf17f3))
- Update migrations, fix broken oauth change. ([4a885c88](https://github.com/pixelfed/pixelfed/commit/4a885c88))
- Update LikeController, store status_profile_id and is_comment attributes. ([799a4cba](https://github.com/pixelfed/pixelfed/commit/799a4cba))
- Updated migrations, fix broken oauth change. ([4a885c88](https://github.com/pixelfed/pixelfed/commit/4a885c88))
- Updated LikeController, store status_profile_id and is_comment attributes. ([799a4cba](https://github.com/pixelfed/pixelfed/commit/799a4cba))
- Updated Profile, fix status count. ([6dcd472b](https://github.com/pixelfed/pixelfed/commit/6dcd472b))
- Updated StatusService, cast response to array. ([0fbde91e](https://github.com/pixelfed/pixelfed/commit/0fbde91e))
- Updated status model, use scope over deprecated visibility attribute. ([f70826e1](https://github.com/pixelfed/pixelfed/commit/f70826e1))
@ -155,7 +159,44 @@
- Updated AP helpers, fixed federation bug. ([a52564f3](https://github.com/pixelfed/pixelfed/commit/a52564f3))
- Updated Helpers, cache profiles. ([1f672ecf](https://github.com/pixelfed/pixelfed/commit/1f672ecf))
- Updated DiscoverController, improve trending api performance. ([d8d3331f](https://github.com/pixelfed/pixelfed/commit/d8d3331f))
- Update InboxWorker, fix race condition in account deletes. ([4a4d8f00](https://github.com/pixelfed/pixelfed/commit/4a4d8f00))
- Updated InboxWorker, fix race condition in account deletes. ([4a4d8f00](https://github.com/pixelfed/pixelfed/commit/4a4d8f00))
- Updated StoryItemTransformer, increase story duration from 5 seconds to 10 seconds. ([5b0b14fc](https://github.com/pixelfed/pixelfed/commit/5b0b14fc))
- Updated StatusController, add view method. ([0cfc12c5](https://github.com/pixelfed/pixelfed/commit/0cfc12c5))
- Updated MediaPathService, add story method. ([aac44309](https://github.com/pixelfed/pixelfed/commit/aac44309))
- Updated StatusDelete job, handle cloud storage media deletes. ([4b1a0fd7](https://github.com/pixelfed/pixelfed/commit/4b1a0fd7))
- Updated ImageOptimizePipeline, add skip_optimize and MediaStorageService support. ([234f72f3](https://github.com/pixelfed/pixelfed/commit/234f72f3))
- Updated Media model, add cdn support to url and thumbnailUrl methods. ([57fa889d](https://github.com/pixelfed/pixelfed/commit/57fa889d))
- Updated MediaController, remove deprecated endpoint. ([8132db74](https://github.com/pixelfed/pixelfed/commit/8132db74))
- Updated api controllers, deprecate old endpoints. ([4415af1b](https://github.com/pixelfed/pixelfed/commit/4415af1b))
- Updated mobile apis, add blurhash. ([cf40526e](https://github.com/pixelfed/pixelfed/commit/cf40526e))
- Updated Image media util, store dimensions of media not thumbnail. ([40bd64aa](https://github.com/pixelfed/pixelfed/commit/40bd64aa))
- Updated MediaTransformers, include meta attribute with focus and dimensions. ([f8cbe1e4](https://github.com/pixelfed/pixelfed/commit/f8cbe1e4))
- Updated storage, add remote media cache directory. ([0eabbfdd](https://github.com/pixelfed/pixelfed/commit/0eabbfdd))
- Updated backup config, prevents gateway timeouts for large databases using mysql. ([9cd4bd74](https://github.com/pixelfed/pixelfed/commit/9cd4bd74))
- Updated MediaPipeline, handle cloud object storage. ([be6d12fc](https://github.com/pixelfed/pixelfed/commit/be6d12fc))
- Updated AP Helpers, use MediaStoragePipeline. ([01a1ffd6](https://github.com/pixelfed/pixelfed/commit/01a1ffd6))
- Updated RemoteProfile component, change thumbnail url. ([c1118956](https://github.com/pixelfed/pixelfed/commit/c1118956))
- Updated blade views. ([9683e846](https://github.com/pixelfed/pixelfed/commit/9683e846))
- Updated cache config, use phpredis by default. ([ed6877df](https://github.com/pixelfed/pixelfed/commit/ed6877df))
- Updated components, fix url rewriter. Closes #2538. ([e8cc66dc](https://github.com/pixelfed/pixelfed/commit/e8cc66dc))
- Updated UserCreate command, closes #2581. ([b2b8c9f9](https://github.com/pixelfed/pixelfed/commit/b2b8c9f9))
- Updated AvatarController, remove deprecated thumb_path. ([889c3d87](https://github.com/pixelfed/pixelfed/commit/889c3d87))
- Updated VideoThumbnail, add MediaStoragePipeline. ([98c44f7b](https://github.com/pixelfed/pixelfed/commit/98c44f7b))
- Updated StatusDelete pipeline, fix object storage thumbnail deletion. ([f930c4bd](https://github.com/pixelfed/pixelfed/commit/f930c4bd))
- Updated MediaStorageService, clear transformer cache after storing media. ([ce6ab80d](https://github.com/pixelfed/pixelfed/commit/ce6ab80d))
- Updated MediaTransformer, remove cache busting. ([258b2729](https://github.com/pixelfed/pixelfed/commit/258b2729))
- Updated AP helpers, only run MediaStoragePipeline if using cloud storage. ([77f21b4b](https://github.com/pixelfed/pixelfed/commit/77f21b4b))
- Updated AvatarObserver, add logic to delete avatars stored in S3. ([9eafc31e](https://github.com/pixelfed/pixelfed/commit/9eafc31e))
- Updated Profile model, use cdn_url for avatars. ([ea8e4261](https://github.com/pixelfed/pixelfed/commit/ea8e4261))
- Updated ActivityPubFetchService, add url validation. ([654b08d3](https://github.com/pixelfed/pixelfed/commit/654b08d3))
- Updated MediaStorageService, add avatar method. ([94a9f685](https://github.com/pixelfed/pixelfed/commit/94a9f685))
- Updated AvatarPipeline, add remote avatar fetch. ([4c148055](https://github.com/pixelfed/pixelfed/commit/4c148055))
- Updated ComposeController, update media version. ([cc2d4bf8](https://github.com/pixelfed/pixelfed/commit/cc2d4bf8))
- Updated AP Helpers, add blurhash and RemoteAvatarFetch. ([de8828e8](https://github.com/pixelfed/pixelfed/commit/de8828e8))
- Updated Timeline, prevent nextTick() when reloading same comment modal. Fixes #2584. ([cc84125b](https://github.com/pixelfed/pixelfed/commit/cc84125b))
- Updated site config, add labels to config. ([abe9cb3d](https://github.com/pixelfed/pixelfed/commit/abe9cb3d))
- Update StatusLabelService, change config key. ([4abfe76a](https://github.com/pixelfed/pixelfed/commit/4abfe76a))
## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
### Added
@ -213,7 +254,7 @@
- Updated StatusTransformer, fixes #[2113](https://github.com/pixelfed/pixelfed/issues/2113) ([eefa6e0d](https://github.com/pixelfed/pixelfed/commit/eefa6e0d))
- Updated InternalApiController, limit remote profile ui to remote profiles ([d918a68e](https://github.com/pixelfed/pixelfed/commit/d918a68e))
- Updated NotificationCard, fix pagination bug #[2019](https://github.com/pixelfed/pixelfed/issues/2019) ([32beaad5](https://github.com/pixelfed/pixelfed/commit/32beaad5))
-
## [v0.10.8 (2020-01-29)](https://github.com/pixelfed/pixelfed/compare/v0.10.7...v0.10.8)
### Added

View file

@ -14,9 +14,21 @@ class Avatar extends Model
*
* @var array
*/
protected $dates = ['deleted_at'];
protected $dates = [
'deleted_at',
'last_fetched_at',
'last_processed_at'
];
protected $fillable = ['profile_id'];
protected $visible = [
'id',
'profile_id',
'media_path',
'size',
];
public function profile()
{
return $this->belongsTo(Profile::class);

View file

@ -12,7 +12,7 @@ class UserCreate extends Command
*
* @var string
*/
protected $signature = 'user:create';
protected $signature = 'user:create {--name=} {--username=} {--email=} {--password=} {--is_admin=0} {--confirm_email=0}';
/**
* The console command description.
@ -40,6 +40,26 @@ class UserCreate extends Command
{
$this->info('Creating a new user...');
$o = $this->options();
if( $o['name'] &&
$o['username'] &&
$o['email'] &&
$o['password']
) {
$user = new User;
$user->username = $o['username'];
$user->name = $o['name'];
$user->email = $o['email'];
$user->password = bcrypt($o['password']);
$user->is_admin = (bool) $o['is_admin'];
$user->email_verified_at = (bool) $o['confirm_email'] ? now() : null;
$user->save();
$this->info('Successfully created user!');
return;
}
$name = $this->ask('Name');
$username = $this->ask('Username');

View file

@ -139,6 +139,9 @@ class AdminController extends Controller
$appeal->appeal_handled_at = now();
$appeal->save();
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $status->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $status->profile_id);
return redirect('/i/admin/reports/autospam');
}
@ -151,6 +154,9 @@ class AdminController extends Controller
$appeal->appeal_handled_at = now();
$appeal->save();
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $status->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $status->profile_id);
return redirect('/i/admin/reports/autospam');
}

View file

@ -53,7 +53,6 @@ use App\Services\{
MediaBlocklistService
};
class ApiV1Controller extends Controller
{
protected $fractal;
@ -98,6 +97,7 @@ class ApiV1Controller extends Controller
'client_secret' => $client->secret,
'vapid_key' => null
];
return response()->json($res, 200, [
'Access-Control-Allow-Origin' => '*'
]);
@ -113,14 +113,18 @@ class ApiV1Controller extends Controller
{
abort_if(!$request->user(), 403);
$id = $request->user()->id;
$key = 'user:last_active_at:id:'.$id;
$ttl = now()->addMinutes(5);
Cache::remember($key, $ttl, function() use($id) {
$user = User::findOrFail($id);
$user->last_active_at = now();
$user->save();
return;
});
if($request->user()->last_active_at) {
$key = 'user:last_active_at:id:'.$id;
$ttl = now()->addMinutes(5);
Cache::remember($key, $ttl, function() use($id) {
$user = User::findOrFail($id);
$user->last_active_at = now();
$user->save();
return;
});
}
$profile = Profile::whereNull('status')->whereUserId($id)->firstOrFail();
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
@ -1031,6 +1035,11 @@ class ApiV1Controller extends Controller
]);
$user = $request->user();
if($user->last_active_at == null) {
return [];
}
$profile = $user->profile;
if(config('pixelfed.enforce_account_limit') == true) {
@ -1087,8 +1096,8 @@ class ApiV1Controller extends Controller
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
$res = $this->fractal->createData($resource)->toArray();
$res['preview_url'] = url('/storage/no-preview.png');
$res['url'] = url('/storage/no-preview.png');
$res['preview_url'] = $media->url(). '?cb=1&_v=' . time();
$res['url'] = $media->url(). '?cb=1&_v=' . time();
return response()->json($res);
}
@ -1322,13 +1331,15 @@ class ApiV1Controller extends Controller
$limit = $request->input('limit') ?? 3;
$user = $request->user();
$key = 'user:last_active_at:id:'.$user->id;
$ttl = now()->addMinutes(5);
Cache::remember($key, $ttl, function() use($user) {
$user->last_active_at = now();
$user->save();
return;
});
if($user->last_active_at) {
$key = 'user:last_active_at:id:'.$user->id;
$ttl = now()->addMinutes(5);
Cache::remember($key, $ttl, function() use($user) {
$user->last_active_at = now();
$user->save();
return;
});
}
$pid = $request->user()->profile_id;
@ -1739,6 +1750,10 @@ class ApiV1Controller extends Controller
$in_reply_to_id = $request->input('in_reply_to_id');
$user = $request->user();
if($user->last_active_at == null) {
return [];
}
if($in_reply_to_id) {
$parent = Status::findOrFail($in_reply_to_id);
@ -1752,6 +1767,13 @@ class ApiV1Controller extends Controller
$status->in_reply_to_profile_id = $parent->profile_id;
$status->save();
} else if($ids) {
if(Media::whereUserId($user->id)
->whereNull('status_id')
->find($ids)
->count() == 0
) {
abort(400, 'Invalid media_ids');
}
$status = new Status;
$status->caption = strip_tags($request->input('status'));
$status->profile_id = $user->profile_id;
@ -1765,7 +1787,7 @@ class ApiV1Controller extends Controller
if($k + 1 > config('pixelfed.max_album_length')) {
continue;
}
$m = Media::findOrFail($v);
$m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v);
if($m->profile_id !== $user->profile_id || $m->status_id) {
abort(403, 'Invalid media id');
}
@ -1776,7 +1798,7 @@ class ApiV1Controller extends Controller
if(empty($mimes)) {
$status->delete();
abort(500, 'Invalid media ids');
abort(400, 'Invalid media ids');
}
$status->scope = $request->input('visibility', 'public');
@ -1786,8 +1808,7 @@ class ApiV1Controller extends Controller
}
if(!$status) {
$oops = 'An error occured. RefId: '.time().'-'.$user->profile_id.':'.Str::random(5).':'.Str::random(10);
abort(500, $oops);
abort(500, 'An error occured.');
}
NewStatusPipeline::dispatch($status);

View file

@ -183,7 +183,6 @@ class BaseApiController extends Controller
$avatar = Avatar::whereProfileId($profile->id)->firstOrFail();
$opath = $avatar->media_path;
$avatar->media_path = "$public/$name";
$avatar->thumb_path = null;
$avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = null;
$avatar->save();
@ -201,117 +200,17 @@ class BaseApiController extends Controller
public function showTempMedia(Request $request, $profileId, $mediaId, $timestamp)
{
abort_if(!$request->user(), 403);
abort_if(!$request->hasValidSignature(), 404);
abort_if(Auth::user()->profile_id != $profileId, 404);
$media = Media::whereProfileId(Auth::user()->profile_id)->findOrFail($mediaId);
$path = storage_path('app/'.$media->media_path);
return response()->file($path);
abort(400, 'Endpoint deprecated');
}
public function uploadMedia(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'file.*' => function() {
return [
'required',
'mimes:' . config('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'),
];
},
'filter_name' => 'nullable|string|max:24',
'filter_class' => 'nullable|alpha_dash|max:24'
]);
$user = Auth::user();
$profile = $user->profile;
if(config('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000;
});
$limit = (int) config('pixelfed.max_account_size');
if ($size >= $limit) {
abort(403, 'Account size limit reached.');
}
}
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
$photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) {
return;
}
$storagePath = MediaPathService::get($user, 2);
$path = $photo->store($storagePath);
$hash = \hash_file('sha256', $photo);
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$media = new Media();
$media->status_id = null;
$media->profile_id = $profile->id;
$media->user_id = $user->id;
$media->media_path = $path;
$media->original_sha256 = $hash;
$media->size = $photo->getSize();
$media->mime = $photo->getMimeType();
$media->filter_class = $filterClass;
$media->filter_name = $filterName;
$media->save();
$url = URL::temporarySignedRoute(
'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id, 'timestamp' => time()]
);
switch ($media->mime) {
case 'image/jpeg':
case 'image/png':
ImageOptimize::dispatch($media);
break;
case 'video/mp4':
VideoThumbnail::dispatch($media);
$preview_url = '/storage/no-preview.png';
$url = '/storage/no-preview.png';
break;
default:
break;
}
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
$res = $this->fractal->createData($resource)->toArray();
$res['preview_url'] = $url;
$res['url'] = $url;
return response()->json($res);
abort(400, 'Endpoint deprecated');
}
public function deleteMedia(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'id' => 'required|integer|min:1|exists:media,id'
]);
$media = Media::whereNull('status_id')
->whereUserId(Auth::id())
->findOrFail($request->input('id'));
Storage::delete($media->media_path);
Storage::delete($media->thumbnail_path);
$media->forceDelete();
return response()->json([
'msg' => 'Successfully deleted',
'code' => 200
]);
abort(400, 'Endpoint deprecated');
}
public function verifyCredentials(Request $request)

View file

@ -35,7 +35,6 @@ class AvatarController extends Controller
$avatar = Avatar::firstOrNew(['profile_id' => $profile->id]);
$currentAvatar = $avatar->recentlyCreated ? null : storage_path('app/'.$profile->avatar->media_path);
$avatar->media_path = "$public/$name";
$avatar->thumb_path = null;
$avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = null;
$avatar->save();
@ -121,10 +120,7 @@ class AvatarController extends Controller
$avatar = $profile->avatar;
if( $avatar->media_path == 'public/avatars/default.png' ||
$avatar->thumb_path == 'public/avatars/default.png' ||
$avatar->media_path == 'public/avatars/default.jpg' ||
$avatar->thumb_path == 'public/avatars/default.jpg'
$avatar->media_path == 'public/avatars/default.jpg'
) {
return;
}
@ -133,12 +129,7 @@ class AvatarController extends Controller
@unlink(storage_path('app/' . $avatar->media_path));
}
if(is_file(storage_path('app/' . $avatar->thumb_path))) {
@unlink(storage_path('app/' . $avatar->thumb_path));
}
$avatar->media_path = 'public/avatars/default.jpg';
$avatar->thumb_path = 'public/avatars/default.jpg';
$avatar->change_count = $avatar->change_count + 1;
$avatar->save();

View file

@ -0,0 +1,515 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth, Cache, Storage, URL;
use Carbon\Carbon;
use App\{
Avatar,
Like,
Media,
MediaTag,
Notification,
Profile,
Place,
Status,
UserFilter
};
use App\Transformer\Api\{
MediaTransformer,
MediaDraftTransformer,
StatusTransformer,
StatusStatelessTransformer
};
use League\Fractal;
use App\Util\Media\Filter;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Jobs\AvatarPipeline\AvatarOptimize;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\VideoPipeline\{
VideoOptimize,
VideoPostProcess,
VideoThumbnail
};
use App\Services\NotificationService;
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Services\MediaTagService;
use Illuminate\Support\Str;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\Extractor;
class ComposeController extends Controller
{
protected $fractal;
public function __construct()
{
$this->middleware('auth');
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
public function show(Request $request)
{
return view('status.compose');
}
public function mediaUpload(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'file.*' => function() {
return [
'required',
'mimes:' . config('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'),
];
},
'filter_name' => 'nullable|string|max:24',
'filter_class' => 'nullable|alpha_dash|max:24'
]);
$user = Auth::user();
$profile = $user->profile;
if(config('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000;
});
$limit = (int) config('pixelfed.max_account_size');
if ($size >= $limit) {
abort(403, 'Account size limit reached.');
}
}
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
$photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) {
return;
}
$storagePath = MediaPathService::get($user, 2);
$path = $photo->store($storagePath);
$hash = \hash_file('sha256', $photo);
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$media = new Media();
$media->status_id = null;
$media->profile_id = $profile->id;
$media->user_id = $user->id;
$media->media_path = $path;
$media->original_sha256 = $hash;
$media->size = $photo->getSize();
$media->mime = $photo->getMimeType();
$media->filter_class = $filterClass;
$media->filter_name = $filterName;
$media->version = 3;
$media->save();
// $url = URL::temporarySignedRoute(
// 'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id, 'timestamp' => time()]
// );
switch ($media->mime) {
case 'image/jpeg':
case 'image/png':
ImageOptimize::dispatch($media);
break;
case 'video/mp4':
VideoThumbnail::dispatch($media);
$preview_url = '/storage/no-preview.png';
$url = '/storage/no-preview.png';
break;
default:
break;
}
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
$res = $this->fractal->createData($resource)->toArray();
$res['preview_url'] = $media->url() . '?v=' . time();
$res['url'] = $media->url() . '?v=' . time();
return response()->json($res);
}
public function mediaUpdate(Request $request)
{
$this->validate($request, [
'id' => 'required',
'file' => function() {
return [
'required',
'mimes:' . config('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'),
];
},
]);
$user = Auth::user();
$photo = $request->file('file');
$id = $request->input('id');
$media = Media::whereUserId($user->id)
->whereProfileId($user->profile_id)
->whereNull('status_id')
->findOrFail($id);
$media->save();
$fragments = explode('/', $media->media_path);
$name = last($fragments);
array_pop($fragments);
$dir = implode('/', $fragments);
$path = $photo->storeAs($dir, $name);
$res = [
'url' => $media->url() . '?v=' . time()
];
ImageOptimize::dispatch($media);
return $res;
}
public function mediaDelete(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'id' => 'required|integer|min:1|exists:media,id'
]);
$media = Media::whereNull('status_id')
->whereUserId(Auth::id())
->findOrFail($request->input('id'));
Storage::delete($media->media_path);
Storage::delete($media->thumbnail_path);
$media->forceDelete();
return response()->json([
'msg' => 'Successfully deleted',
'code' => 200
]);
}
public function searchTag(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:1|max:50'
]);
$q = $request->input('q');
if(Str::of($q)->startsWith('@')) {
if(strlen($q) < 3) {
return [];
}
$q = mb_substr($q, 1);
}
$blocked = UserFilter::whereFilterableType('App\Profile')
->whereFilterType('block')
->whereFilterableId($request->user()->profile_id)
->pluck('user_id');
$blocked->push($request->user()->profile_id);
$results = Profile::select('id','domain','username')
->whereNotIn('id', $blocked)
->whereNull('domain')
->where('username','like','%'.$q.'%')
->limit(15)
->get()
->map(function($r) {
return [
'id' => (string) $r->id,
'name' => $r->username,
'privacy' => true,
'avatar' => $r->avatarUrl()
];
});
return $results;
}
public function searchUntag(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'status_id' => 'required',
'profile_id' => 'required'
]);
$user = $request->user();
$status_id = $request->input('status_id');
$profile_id = (int) $request->input('profile_id');
abort_if((int) $user->profile_id !== $profile_id, 400);
$tag = MediaTag::whereStatusId($status_id)
->whereProfileId($profile_id)
->first();
if(!$tag) {
return [];
}
Notification::whereItemType('App\MediaTag')
->whereItemId($tag->id)
->whereProfileId($profile_id)
->whereAction('tagged')
->delete();
MediaTagService::untag($status_id, $profile_id);
return [200];
}
public function searchLocation(Request $request)
{
abort_if(!Auth::check(), 403);
$this->validate($request, [
'q' => 'required|string|max:100'
]);
$q = filter_var($request->input('q'), FILTER_SANITIZE_STRING);
$hash = hash('sha256', $q);
$key = 'search:location:id:' . $hash;
$places = Cache::remember($key, now()->addMinutes(15), function() use($q) {
$q = '%' . $q . '%';
return Place::where('name', 'like', $q)
->take(80)
->get()
->map(function($r) {
return [
'id' => $r->id,
'name' => $r->name,
'country' => $r->country,
'url' => $r->url()
];
});
});
return $places;
}
public function store(Request $request)
{
$this->validate($request, [
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'media.*' => 'required',
'media.*.id' => 'required|integer|min:1',
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
'media.*.license' => 'nullable|string|max:140',
'media.*.alt' => 'nullable|string|max:140',
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
'place' => 'nullable',
'comments_disabled' => 'nullable',
'tagged' => 'nullable',
// 'optimize_media' => 'nullable'
]);
if(config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null && $request->caption) {
$keywords = config('costar.keyword.block');
foreach($keywords as $kw) {
if(Str::contains($request->caption, $kw) == true) {
abort(400, 'Invalid object');
}
}
}
}
$user = Auth::user();
$profile = $user->profile;
$visibility = $request->input('visibility');
$medias = $request->input('media');
$attachments = [];
$status = new Status;
$mimes = [];
$place = $request->input('place');
$cw = $request->input('cw');
$tagged = $request->input('tagged');
$optimize_media = (bool) $request->input('optimize_media');
foreach($medias as $k => $media) {
if($k + 1 > config('pixelfed.max_album_length')) {
continue;
}
$m = Media::findOrFail($media['id']);
if($m->profile_id !== $profile->id || $m->status_id) {
abort(403, 'Invalid media id');
}
$m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
$m->license = $media['license'];
$m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null;
$m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
// if($optimize_media == false) {
// $m->skip_optimize = true;
// ImageThumbnail::dispatch($m);
// } else {
// ImageOptimize::dispatch($m);
// }
if($cw == true || $profile->cw == true) {
$m->is_nsfw = $cw;
$status->is_nsfw = $cw;
}
$m->save();
$attachments[] = $m;
array_push($mimes, $m->mime);
}
$mediaType = StatusController::mimeTypeCheck($mimes);
if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
abort(400, __('exception.compose.invalid.album'));
}
if($place && is_array($place)) {
$status->place_id = $place['id'];
}
if($request->filled('comments_disabled')) {
$status->comments_disabled = (bool) $request->input('comments_disabled');
}
$status->caption = strip_tags($request->caption);
$status->scope = 'draft';
$status->profile_id = $profile->id;
$status->save();
foreach($attachments as $media) {
$media->status_id = $status->id;
$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 = $mediaType;
$status->save();
foreach($tagged as $tg) {
$mt = new MediaTag;
$mt->status_id = $status->id;
$mt->media_id = $status->media->first()->id;
$mt->profile_id = $tg['id'];
$mt->tagged_username = $tg['name'];
$mt->is_public = true;
$mt->metadata = json_encode([
'_v' => 1,
]);
$mt->save();
MediaTagService::set($mt->status_id, $mt->profile_id);
MediaTagService::sendNotification($mt);
}
NewStatusPipeline::dispatch($status);
Cache::forget('user:account:id:'.$profile->user_id);
Cache::forget('_api:statuses:recent_9:'.$profile->id);
Cache::forget('profile:status_count:'.$profile->id);
Cache::forget('status:transformer:media:attachments:'.$status->id);
Cache::forget($user->storageUsedKey());
return $status->url();
}
public function storeText(Request $request)
{
abort_unless(config('exp.top'), 404);
$this->validate($request, [
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
'place' => 'nullable',
'comments_disabled' => 'nullable',
'tagged' => 'nullable',
]);
if(config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null && $request->caption) {
$keywords = config('costar.keyword.block');
foreach($keywords as $kw) {
if(Str::contains($request->caption, $kw) == true) {
abort(400, 'Invalid object');
}
}
}
}
$user = Auth::user();
$profile = $user->profile;
$visibility = $request->input('visibility');
$status = new Status;
$place = $request->input('place');
$cw = $request->input('cw');
$tagged = $request->input('tagged');
if($place && is_array($place)) {
$status->place_id = $place['id'];
}
if($request->filled('comments_disabled')) {
$status->comments_disabled = (bool) $request->input('comments_disabled');
}
$status->caption = strip_tags($request->caption);
$status->profile_id = $profile->id;
$entities = Extractor::create()->extract($status->caption);
$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 = 'text';
$status->rendered = Autolink::create()->autolink($status->caption);
$status->entities = json_encode(array_merge([
'timg' => [
'version' => 0,
'bg_id' => 1,
'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
'length' => strlen($status->caption),
]
], $entities), JSON_UNESCAPED_SLASHES);
$status->save();
foreach($tagged as $tg) {
$mt = new MediaTag;
$mt->status_id = $status->id;
$mt->media_id = $status->media->first()->id;
$mt->profile_id = $tg['id'];
$mt->tagged_username = $tg['name'];
$mt->is_public = true;
$mt->metadata = json_encode([
'_v' => 1,
]);
$mt->save();
MediaTagService::set($mt->status_id, $mt->profile_id);
MediaTagService::sendNotification($mt);
}
Cache::forget('user:account:id:'.$profile->user_id);
Cache::forget('_api:statuses:recent_9:'.$profile->id);
Cache::forget('profile:status_count:'.$profile->id);
return $status->url();
}
}

View file

@ -144,12 +144,12 @@ class DiscoverController extends Controller
public function profilesDirectoryApi(Request $request)
{
return ['error' => 'Temporarily unavailable.'];
$this->validate($request, [
'page' => 'integer|max:10'
]);
return ['error' => 'Temporarily unavailable.'];
$page = $request->input('page') ?? 1;
$key = 'discover:profiles:page:' . $page;
$ttl = now()->addHours(12);
@ -214,6 +214,8 @@ class DiscoverController extends Controller
public function trendingHashtags(Request $request)
{
return [];
$res = StatusHashtag::select('hashtag_id', \DB::raw('count(*) as total'))
->groupBy('hashtag_id')
->orderBy('total','desc')
@ -234,6 +236,8 @@ class DiscoverController extends Controller
public function trendingPlaces(Request $request)
{
return [];
$res = Status::select('place_id',DB::raw('count(place_id) as total'))
->whereNotNull('place_id')
->where('created_at','>',now()->subDays(14))
@ -250,6 +254,6 @@ class DiscoverController extends Controller
];
});
return $res;
return [];
}
}

View file

@ -22,39 +22,6 @@ class MediaController extends Controller
public function composeUpdate(Request $request, $id)
{
$this->validate($request, [
'file' => function() {
return [
'required',
'mimes:' . config('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'),
];
},
]);
$user = Auth::user();
$photo = $request->file('file');
$media = Media::whereUserId($user->id)
->whereProfileId($user->profile_id)
->whereNull('status_id')
->findOrFail($id);
$media->version = 2;
$media->save();
$fragments = explode('/', $media->media_path);
$name = last($fragments);
array_pop($fragments);
$dir = implode('/', $fragments);
$path = $photo->storeAs($dir, $name);
$res = [];
$res['url'] = URL::temporarySignedRoute(
'temp-media', now()->addHours(1), ['profileId' => $media->profile_id, 'mediaId' => $media->id, 'timestamp' => time()]
);
ImageOptimize::dispatch($media);
return $res;
abort(400, 'Endpoint deprecated');
}
}

View file

@ -20,44 +20,7 @@ class MediaTagController extends Controller
public function usernameLookup(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:1|max:50'
]);
$q = $request->input('q');
if(Str::of($q)->startsWith('@')) {
if(strlen($q) < 3) {
return [];
}
$q = mb_substr($q, 1);
}
$blocked = UserFilter::whereFilterableType('App\Profile')
->whereFilterType('block')
->whereFilterableId($request->user()->profile_id)
->pluck('user_id');
$blocked->push($request->user()->profile_id);
$results = Profile::select('id','domain','username')
->whereNotIn('id', $blocked)
->whereNull('domain')
->where('username','like','%'.$q.'%')
->limit(15)
->get()
->map(function($r) {
return [
'id' => (string) $r->id,
'name' => $r->username,
'privacy' => true,
'avatar' => $r->avatarUrl()
];
});
return $results;
abort(404);
}
public function untagProfile(Request $request)

View file

@ -4,17 +4,235 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use App\AccountLog;
use App\Follower;
use App\Like;
use App\Status;
use App\StatusHashtag;
use Illuminate\Support\Facades\Cache;
class SeasonalController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function __construct()
{
$this->middleware('auth');
}
public function yearInReview()
{
$profile = Auth::user()->profile;
return view('account.yir', compact('profile'));
}
public function yearInReview()
{
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
abort_if(config('database.default') != 'mysql', 404);
$profile = Auth::user()->profile;
return view('account.yir', compact('profile'));
}
public function getData(Request $request)
{
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
abort_if(config('database.default') != 'mysql', 404);
$uid = $request->user()->id;
$pid = $request->user()->profile_id;
$epoch = '2020-01-01 00:00:00';
$epochStart = '2020-01-01 00:00:00';
$epochEnd = '2020-12-31 23:59:59';
$siteKey = 'seasonal:my2020:shared';
$siteTtl = now()->addMonths(3);
$userKey = 'seasonal:my2020:user:' . $uid;
$userTtl = now()->addMonths(3);
$shared = Cache::remember($siteKey, $siteTtl, function() use($epochStart, $epochEnd) {
return [
'average' => [
'posts' => round(Status::selectRaw('*, count(profile_id) as count')
->whereNull('uri')
->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->pluck('count')
->avg()),
'likes' => round(Like::selectRaw('*, count(profile_id) as count')
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->pluck('count')
->avg()),
],
'popular' => [
'hashtag' => StatusHashtag::selectRaw('*,count(hashtag_id) as count')
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('hashtag_id')
->orderByDesc('count')
->take(1)
->get()
->map(function($sh) {
return [
'name' => $sh->hashtag->name,
'count' => $sh->count
];
})
->first(),
'post' => Status::whereScope('public')
->where('likes_count', '>', 1)
->whereIsNsfw(false)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->orderByDesc('likes_count')
->take(1)
->get()
->map(function($status) {
return [
'id' => (string) $status->id,
'username' => (string) $status->profile->username,
'created_at' => $status->created_at->format('M d, Y'),
'type' => $status->type,
'url' => $status->url(),
'thumb' => $status->thumb(),
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
'reply_count' => $status->reply_count ?? 0,
];
})
->first(),
'places' => Status::selectRaw('*, count(place_id) as count')
->whereNotNull('place_id')
->having('count', '>', 1)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('place_id')
->orderByDesc('count')
->take(1)
->get()
->map(function($sh) {
return [
'name' => $sh->place->getName(),
'url' => $sh->place->url(),
'count' => $sh->count
];
})
->first()
],
];
});
$res = Cache::remember($userKey, $userTtl, function() use($uid, $pid, $epochStart, $epochEnd, $request) {
return [
'account' => [
'user_id' => $request->user()->id,
'created_at' => $request->user()->created_at->format('M d, Y'),
'created_this_year' => $request->user()->created_at->gt('2020-01-01 00:00:00'),
'created_months_ago' => $request->user()->created_at->diffInMonths(now()),
'followers_this_year' => Follower::whereFollowingId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->count(),
'followed_this_year' => Follower::whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->count(),
'most_popular' => Status::whereProfileId($pid)
->where('likes_count', '>', 1)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->orderByDesc('likes_count')
->take(1)
->get()
->map(function($status) {
return [
'id' => (string) $status->id,
'username' => (string) $status->profile->username,
'created_at' => $status->created_at->format('M d, Y'),
'type' => $status->type,
'url' => $status->url(),
'thumb' => $status->thumb(),
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
'reply_count' => $status->reply_count ?? 0,
];
})
->first(),
'posts_count' => Status::whereProfileId($pid)
->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->count(),
'likes_count' => Like::whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->count(),
'hashtag' => StatusHashtag::selectRaw('*, count(hashtag_id) as count')
->whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->orderByDesc('count')
->take(1)
->get()
->map(function($sh) {
return [
'name' => $sh->hashtag->name,
'count' => $sh->count
];
})
->first(),
'places' => Status::selectRaw('*, count(place_id) as count')
->whereNotNull('place_id')
->having('count', '>', 1)
->whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('place_id')
->orderByDesc('count')
->take(1)
->get()
->map(function($sh) {
return [
'name' => $sh->place->getName(),
'url' => $sh->place->url(),
'count' => $sh->count
];
})
->first(),
'places_total' => Status::whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->whereNotNull('place_id')
->count()
]
];
});
return response()->json(array_merge($res, $shared));
}
public function store(Request $request)
{
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
abort_if(config('database.default') != 'mysql', 404);
$this->validate($request, [
'profile_id' => 'required',
'type' => 'required|string|in:view,hide'
]);
$user = $request->user();
$log = new AccountLog();
$log->user_id = $user->id;
$log->item_type = 'App\User';
$log->item_id = $user->id;
$log->action = $request->input('type') == 'view' ? 'seasonal.my2020.view' : 'seasonal.my2020.hide';
$log->ip_address = $request->ip();
$log->user_agent = $request->user_agent();
$log->save();
}
}

View file

@ -9,6 +9,7 @@ use App\Util\Lexer\PrettyNumber;
use App\{Follower, Page, Profile, Status, User, UserFilter};
use App\Util\Localization\Localization;
use App\Services\FollowerService;
use App\Util\ActivityPub\Helpers;
class SiteController extends Controller
{
@ -108,10 +109,12 @@ class SiteController extends Controller
public function redirectUrl(Request $request)
{
abort_if(!$request->user(), 404);
$this->validate($request, [
'url' => 'required|url'
]);
$url = request()->input('url');
abort_if(Helpers::validateUrl($url) == false, 404);
return view('site.redirect', compact('url'));
}

View file

@ -282,7 +282,7 @@ class StatusController extends Controller
$resource = new Fractal\Resource\Item($status, new Note());
$res = $fractal->createData($resource)->toArray();
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT);
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function edit(Request $request, $username, $id)
@ -408,4 +408,25 @@ class StatusController extends Controller
return response()->json([200]);
}
public function storeView(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'status_id' => 'required|integer|exists:statuses,id',
'profile_id' => 'required|integer|exists:profiles,id'
]);
$sid = (int) $request->input('status_id');
$pid = (int) $request->input('profile_id');
StatusView::firstOrCreate([
'status_id' => $sid,
'status_profile_id' => $pid,
'profile_id' => $request->user()->profile_id
]);
return response()->json(1);
}
}

View file

@ -58,7 +58,6 @@ class AvatarOptimize implements ShouldQueue
$img->save($file, $quality);
$avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
$avatar->thumb_path = $avatar->media_path;
$avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = Carbon::now();
$avatar->save();

View file

@ -45,7 +45,6 @@ class CreateAvatar implements ShouldQueue
$avatar = new Avatar();
$avatar->profile_id = $profile->id;
$avatar->media_path = $path;
$avatar->thumb_path = $path;
$avatar->change_count = 0;
$avatar->last_processed_at = \Carbon\Carbon::now();
$avatar->save();

View file

@ -0,0 +1,98 @@
<?php
namespace App\Jobs\AvatarPipeline;
use App\Avatar;
use App\Profile;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Str;
use Zttp\Zttp;
use App\Http\Controllers\AvatarController;
use Storage;
use Log;
use Illuminate\Http\File;
use App\Services\MediaStorageService;
use App\Services\ActivityPubFetchService;
class RemoteAvatarFetch implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Profile $profile)
{
$this->profile = $profile;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$profile = $this->profile;
if($profile->domain == null || $profile->private_key) {
return 1;
}
$avatar = Avatar::firstOrCreate([
'profile_id' => $profile->id
]);
if($avatar->media_path == null && $avatar->remote_url == null) {
$avatar->media_path = 'public/avatars/default.jpg';
$avatar->is_remote = true;
$avatar->save();
}
$person = Helpers::fetchFromUrl($profile->remote_url);
if(!$person || !isset($person['@context'])) {
return 1;
}
if( !isset($person['icon']) ||
!isset($person['icon']['type']) ||
!isset($person['icon']['url'])
) {
return 1;
}
if($person['icon']['type'] !== 'Image') {
return 1;
}
if(!Helpers::validateUrl($person['icon']['url'])) {
return 1;
}
$icon = $person['icon'];
$avatar->remote_url = $icon['url'];
$avatar->save();
MediaStorageService::avatar($avatar);
return 1;
}
}

View file

@ -41,7 +41,7 @@ class ImageOptimize implements ShouldQueue
{
$media = $this->media;
$path = storage_path('app/'.$media->media_path);
if (!is_file($path)) {
if (!is_file($path) || $media->skip_optimize) {
return;
}

View file

@ -45,7 +45,7 @@ class ImageResize implements ShouldQueue
return;
}
$path = storage_path('app/'.$media->media_path);
if (!is_file($path)) {
if (!is_file($path) || $media->skip_optimize) {
return;
}

View file

@ -11,6 +11,8 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use ImageOptimizer;
use Illuminate\Http\File;
use App\Services\MediaPathService;
use App\Jobs\MediaPipeline\MediaStoragePipeline;
class ImageUpdate implements ShouldQueue
{
@ -60,7 +62,9 @@ class ImageUpdate implements ShouldQueue
if (in_array($media->mime, $this->protectedMimes) == true) {
ImageOptimizer::optimize($thumb);
ImageOptimizer::optimize($path);
if(!$media->skip_optimize) {
ImageOptimizer::optimize($path);
}
}
if (!is_file($path) || !is_file($thumb)) {
@ -73,19 +77,7 @@ class ImageUpdate implements ShouldQueue
$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);
$thumbFile = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($thumb), 'public');
$thumbUrl = Storage::disk(config('filesystems.cloud'))->url($thumbFile);
$media->thumbnail_url = $thumbUrl;
$media->cdn_url = $url;
$media->optimized_url = $url;
$media->save();
}
MediaStoragePipeline::dispatch($media);
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Jobs\MediaPipeline;
use App\Media;
use Cache;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Redis;
use App\Services\MediaStorageService;
class MediaStoragePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $media;
public function __construct(Media $media)
{
$this->media = $media;
}
public function handle()
{
MediaStorageService::store($this->media);
}
}

View file

@ -2,7 +2,7 @@
namespace App\Jobs\StatusPipeline;
use DB;
use DB, Storage;
use App\{
AccountInterstitial,
MediaTag,
@ -17,6 +17,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use League\Fractal;
use Illuminate\Support\Str;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\DeleteNote;
use App\Util\ActivityPub\Helpers;
@ -89,6 +90,24 @@ class StatusDelete implements ShouldQueue
if (is_file($photo)) {
unlink($photo);
}
if( config('pixelfed.cloud_storage') == true) {
if( Str::of($media->media_path)
->startsWith('public/') &&
Storage::disk(config('filesystems.cloud'))
->exists($media->media_path)
) {
Storage::disk(config('filesystems.cloud'))
->delete($media->media_path);
}
if( Str::of($media->thumbnail_path)
->startsWith('public/') &&
Storage::disk(config('filesystems.cloud'))
->exists($media->thumbnail_path)
) {
Storage::disk(config('filesystems.cloud'))
->delete($media->thumbnail_path);
}
}
$media->delete();
} catch (Exception $e) {
}

View file

@ -12,6 +12,7 @@ use Cache;
use FFMpeg;
use Storage;
use App\Media;
use App\Jobs\MediaPipeline\MediaStoragePipeline;
class VideoThumbnail implements ShouldQueue
{
@ -62,26 +63,10 @@ class VideoThumbnail implements ShouldQueue
}
if(config('pixelfed.cloud_storage') == true) {
$path = storage_path('app/'.$media->media_path);
$thumb = storage_path('app/'.$media->thumbnail_path);
$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);
$thumbFile = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($thumb), 'public');
$thumbUrl = Storage::disk(config('filesystems.cloud'))->url($thumbFile);
$media->thumbnail_url = $thumbUrl;
$media->cdn_url = $url;
$media->optimized_url = $url;
$media->save();
}
if($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id);
}
MediaStoragePipeline::dispatch($media);
}
}

View file

@ -29,25 +29,28 @@ class Media extends Model
public function url()
{
if(!empty($this->remote_media) && $this->remote_url) {
//$url = \App\Services\MediaProxyService::get($this->remote_url, $this->mime);
$url = $this->remote_url;
} else {
$path = $this->media_path;
$url = $this->cdn_url ?? config('app.url') . Storage::url($path);
if($this->cdn_url) {
return $this->cdn_url;
}
return $url;
if($this->remote_media && $this->remote_url) {
return $this->remote_url;
}
return url(Storage::url($this->media_path));
}
public function thumbnailUrl()
{
if($this->remote_media == true) {
return $this->remote_url;
} else {
$path = $this->thumbnail_path ?? 'public/no-preview.png';
return url(Storage::url($path));
if($this->thumbnail_url) {
return $this->thumbnail_url;
}
if(!$this->remote_media && $this->thumbnail_path) {
return url(Storage::url($this->thumbnail_path));
}
return url(Storage::url('public/no-preview.png'));
}
public function thumb()

View file

@ -35,7 +35,7 @@ class InstanceActor extends Model
'publicKeyPem' => $this->public_key
],
'manuallyApprovesFollowers' => true,
'url' => route('help.instance-actor')
'url' => url('/site/kb/instance-actor')
];
}
}

View file

@ -3,6 +3,8 @@
namespace App\Observers;
use App\Avatar;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class AvatarObserver
{
@ -54,12 +56,13 @@ class AvatarObserver
) {
@unlink($path);
}
$path = storage_path('app/'.$avatar->thumb_path);
if( is_file($path) &&
$avatar->thumb_path != 'public/avatars/default.png' &&
$avatar->media_path != 'public/avatars/default.jpg'
) {
@unlink($path);
if($avatar->cdn_url) {
$disk = Storage::disk(config('filesystems.cloud'));
$base = Str::startsWith($avatar->media_path, 'cache/avatars/');
if($base && $disk->exists($avatar->media_path)) {
$disk->delete($avatar->media_path);
}
}
}

View file

@ -151,6 +151,15 @@ class Profile extends Model
{
$url = Cache::remember('avatar:'.$this->id, now()->addYears(1), function () {
$avatar = $this->avatar;
if($avatar->cdn_url) {
return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
}
if($avatar->is_remote) {
return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
}
$path = $avatar->media_path;
$path = "{$path}?v={$avatar->change_count}";

View file

@ -9,51 +9,20 @@ use App\Util\ActivityPub\HttpSignature;
class ActivityPubFetchService
{
public $signed = true;
public $actor;
public $url;
public $headers = [
'Accept' => 'application/activity+json, application/json',
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'
];
public static function queue()
{
return new self;
}
public function signed($signed = true)
{
$this->signed = $signed;
return $this;
}
public function actor($profile)
{
$this->actor = $profile;
return $this;
}
public function url($url)
public static function get($url)
{
if(!Helpers::validateUrl($url)) {
throw new \Exception('Invalid URL');
return 0;
}
$this->url = $url;
return $this;
}
public function get()
{
if($this->signed == true && $this->actor == null) {
throw new \Exception('Cannot sign request without actor');
}
return $this->signedRequest();
}
$headers = HttpSignature::instanceActorSign($url, false, [
'Accept' => 'application/activity+json, application/json',
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'
]);
protected function signedRequest()
{
$this->headers = HttpSignature::sign($this->actor, $this->url, false, $this->headers);
return Zttp::withHeaders($this->headers)->get($this->url)->body();
return Zttp::withHeaders($headers)
->timeout(30)
->get($url)
->body();
}
}

File diff suppressed because one or more lines are too long

View file

@ -48,4 +48,30 @@ class MediaPathService {
return $path;
}
public static function story($account, $version = 1)
{
$mh = hash('sha256', date('Y').'-.-'.date('m'));
$monthHash = date('Y').date('m').substr($mh, 0, 6).substr($mh, 58, 6);
$random = '03'.Str::random(random_int(6,9)).'_'.Str::random(random_int(6,17));
if($account instanceOf User) {
switch ($version) {
case 1:
$userHash = $account->profile_id;
$path = "public/_esm.t3/{$monthHash}/{$userHash}/{$random}";
break;
default:
$userHash = $account->profile_id;
$path = "public/_esm.t3/{$monthHash}/{$userHash}/{$random}";
break;
}
}
if($account instanceOf Profile) {
$userHash = $account->id;
$path = "public/_esm.t3/{$monthHash}/{$userHash}/{$random}";
}
return $path;
}
}

View file

@ -0,0 +1,230 @@
<?php
namespace App\Services;
use App\Util\ActivityPub\Helpers;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Media;
use App\Profile;
use App\User;
use GuzzleHttp\Client;
use App\Http\Controllers\AvatarController;
use GuzzleHttp\Exception\RequestException;
class MediaStorageService {
public static function store(Media $media)
{
if(config('pixelfed.cloud_storage') == true) {
(new self())->cloudStore($media);
}
return;
}
public static function avatar($avatar)
{
return (new self())->fetchAvatar($avatar);
}
public static function head($url)
{
$c = new Client();
try {
$r = $c->request('HEAD', $url);
} catch (RequestException $e) {
return false;
}
$h = $r->getHeaders();
return [
'length' => $h['Content-Length'][0],
'mime' => $h['Content-Type'][0]
];
}
protected function cloudStore($media)
{
if($media->remote_media == true) {
(new self())->remoteToCloud($media);
} else {
(new self())->localToCloud($media);
}
}
protected function localToCloud($media)
{
$path = storage_path('app/'.$media->media_path);
$thumb = storage_path('app/'.$media->thumbnail_path);
$p = explode('/', $media->media_path);
$name = array_pop($p);
$pt = explode('/', $media->thumbnail_path);
$thumbname = array_pop($pt);
$storagePath = implode('/', $p);
$disk = Storage::disk(config('filesystems.cloud'));
$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
$url = $disk->url($file);
$thumbFile = $disk->putFileAs($storagePath, new File($thumb), $thumbname, 'public');
$thumbUrl = $disk->url($thumbFile);
$media->thumbnail_url = $thumbUrl;
$media->cdn_url = $url;
$media->optimized_url = $url;
$media->replicated_at = now();
$media->save();
if($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id);
}
}
protected function remoteToCloud($media)
{
$url = $media->remote_url;
if(!Helpers::validateUrl($url)) {
return;
}
$head = $this->head($media->remote_url);
if(!$head) {
return;
}
$mimes = [
'image/jpeg',
'image/png',
'video/mp4'
];
$mime = $head['mime'];
$max_size = (int) config('pixelfed.max_photo_size') * 1000;
$media->size = $head['length'];
$media->remote_media = true;
$media->save();
if(!in_array($mime, $mimes)) {
return;
}
if($head['length'] >= $max_size) {
return;
}
switch ($mime) {
case 'image/png':
$ext = '.png';
break;
case 'image/gif':
$ext = '.gif';
break;
case 'image/jpeg':
$ext = '.jpg';
break;
case 'video/mp4':
$ext = '.mp4';
break;
}
$base = MediaPathService::get($media->profile);
$path = Str::random(40) . $ext;
$tmpBase = storage_path('app/remcache/');
$tmpPath = $media->profile_id . '-' . $path;
$tmpName = $tmpBase . $tmpPath;
$data = file_get_contents($url, false, null, 0, $head['length']);
file_put_contents($tmpName, $data);
$hash = hash_file('sha256', $tmpName);
$disk = Storage::disk(config('filesystems.cloud'));
$file = $disk->putFileAs($base, new File($tmpName), $path, 'public');
$permalink = $disk->url($file);
$media->media_path = $base . $path;
$media->cdn_url = $permalink;
$media->original_sha256 = $hash;
$media->replicated_at = now();
$media->save();
if($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id);
}
unlink($tmpName);
}
protected function fetchAvatar($avatar)
{
$url = $avatar->remote_url;
if($url == null || Helpers::validateUrl($url) == false) {
return;
}
$head = $this->head($url);
if($head == false) {
return;
}
$mimes = [
'image/jpeg',
'image/png',
];
$mime = $head['mime'];
$max_size = (int) config('pixelfed.max_avatar_size') * 1000;
if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subDay())) {
return;
}
// handle pleroma edge case
if(Str::endsWith($mime, '; charset=utf-8')) {
$mime = str_replace('; charset=utf-8', '', $mime);
}
if(!in_array($mime, $mimes)) {
return;
}
if($head['length'] >= $max_size) {
return;
}
if($avatar->size && $head['length'] == $avatar->size) {
return;
}
$base = 'cache/avatars/' . $avatar->profile_id;
$ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png';
$path = Str::random(20) . '_avatar.' . $ext;
$tmpBase = storage_path('app/remcache/');
$tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path;
$tmpName = $tmpBase . $tmpPath;
$data = file_get_contents($url, false, null, 0, $head['length']);
file_put_contents($tmpName, $data);
$disk = Storage::disk(config('filesystems.cloud'));
$file = $disk->putFileAs($base, new File($tmpName), $path, 'public');
$permalink = $disk->url($file);
$avatar->media_path = $base . $path;
$avatar->is_remote = true;
$avatar->cdn_url = $permalink;
$avatar->size = $head['length'];
$avatar->change_count = $avatar->change_count + 1;
$avatar->last_fetched_at = now();
$avatar->save();
Cache::forget('avatar:' . $avatar->profile_id);
unlink($tmpName);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use App\Status;
use Illuminate\Support\Str;
class StatusLabelService
{
const CACHE_KEY = 'pf:services:status_label:_v0:';
public static function get(Status $status)
{
if(config('instance.label.covid.enabled') == false || !$status) {
return [
'covid' => false
];
}
return Cache::remember(self::CACHE_KEY . $status->id, now()->addDays(7), function() use($status) {
return [
'covid' => Str::of(strtolower($status->caption))->contains(['covid','corona', 'coronavirus', 'vaccine', 'vaxx', 'vaccination'])
];
});
}
}

View file

@ -9,7 +9,7 @@ class MediaTransformer extends Fractal\TransformerAbstract
{
public function transform(Media $media)
{
return [
$res = [
'id' => (string) $media->id,
'type' => lcfirst($media->activityVerb()),
'url' => $media->url(),
@ -17,7 +17,25 @@ class MediaTransformer extends Fractal\TransformerAbstract
'preview_url' => $media->thumbnailUrl(),
'text_url' => null,
'meta' => null,
'description' => $media->caption
'description' => $media->caption,
'blurhash' => $media->blurhash
];
if($media->width && $media->height) {
$res['meta'] = [
'focus' => [
'x' => 0,
'y' => 0
],
'original' => [
'width' => $media->width,
'height' => $media->height,
'size' => "{$media->width}x{$media->height}",
'aspect' => $media->width / $media->height
]
];
}
return $res;
}
}

View file

@ -9,7 +9,7 @@ class MediaTransformer extends Fractal\TransformerAbstract
{
public function transform(Media $media)
{
return [
$res = [
'id' => (string) $media->id,
'type' => $media->activityVerb(),
'url' => $media->url(),
@ -24,6 +24,24 @@ class MediaTransformer extends Fractal\TransformerAbstract
'filter_name' => $media->filter_name,
'filter_class' => $media->version == 1 ? $media->filter_class : null,
'mime' => $media->mime,
'blurhash' => $media->blurhash
];
if($media->width && $media->height) {
$res['meta'] = [
'focus' => [
'x' => 0,
'y' => 0
],
'original' => [
'width' => $media->width,
'height' => $media->height,
'size' => "{$media->width}x{$media->height}",
'aspect' => $media->width / $media->height
]
];
}
return $res;
}
}

View file

@ -7,6 +7,8 @@ use League\Fractal;
use Cache;
use App\Services\HashidService;
use App\Services\MediaTagService;
use App\Services\StatusLabelService;
use Illuminate\Support\Str;
class StatusTransformer extends Fractal\TransformerAbstract
{
@ -55,7 +57,8 @@ class StatusTransformer extends Fractal\TransformerAbstract
'parent' => [],
'place' => $status->place,
'local' => (bool) $status->local,
'taggedPeople' => $taggedPeople
'taggedPeople' => $taggedPeople,
'label' => StatusLabelService::get($status)
];
}

View file

@ -14,7 +14,7 @@ class StoryItemTransformer extends Fractal\TransformerAbstract
return [
'id' => (string) $item->id,
'type' => $item->type,
'length' => 5,
'length' => 10,
'src' => $item->url(),
'preview' => null,
'link' => null,

View file

@ -23,9 +23,12 @@ use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Util\ActivityPub\HttpSignature;
use Illuminate\Support\Str;
use App\Services\ActivityPubFetchService;
use App\Services\ActivityPubDeliveryService;
use App\Services\MediaPathService;
use App\Services\MediaStorageService;
use App\Jobs\MediaPipeline\MediaStoragePipeline;
use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
class Helpers {
@ -214,8 +217,8 @@ class Helpers {
$ttl = now()->addMinutes(5);
return Cache::remember($key, $ttl, function() use($url) {
$res = Zttp::withoutVerifying()->withHeaders(self::zttpUserAgent())->get($url);
$res = json_decode($res->body(), true, 8);
$res = ActivityPubFetchService::get($url);
$res = json_decode($res, true, 8);
if(json_last_error() == JSON_ERROR_NONE) {
return $res;
} else {
@ -242,129 +245,132 @@ class Helpers {
if($local) {
$id = (int) last(explode('/', $url));
return Status::whereNotIn('scope', ['draft','archived'])->findOrFail($id);
}
$cached = Status::whereNotIn('scope', ['draft','archived'])
->whereUri($url)
->orWhere('object_url', $url)
->first();
if($cached) {
return $cached;
}
$res = self::fetchFromUrl($url);
if(!$res || empty($res)) {
return;
}
if(isset($res['object'])) {
$activity = $res;
} else {
$cached = Status::whereNotIn('scope', ['draft','archived'])
->whereUri($url)
->orWhere('object_url', $url)
->first();
$activity = ['object' => $res];
}
if($cached) {
return $cached;
$scope = 'private';
$cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
if(isset($res['to']) == true) {
if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
$scope = 'public';
}
$res = self::fetchFromUrl($url);
if(!$res || empty($res)) {
return;
if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) {
$scope = 'public';
}
}
if(isset($res['object'])) {
$activity = $res;
} else {
$activity = ['object' => $res];
if(isset($res['cc']) == true) {
if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
$scope = 'unlisted';
}
$scope = 'private';
$cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
if(isset($res['to']) == true) {
if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
$scope = 'public';
}
if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) {
$scope = 'public';
}
if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) {
$scope = 'unlisted';
}
}
if(isset($res['cc']) == true) {
if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
$scope = 'unlisted';
}
if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) {
$scope = 'unlisted';
}
}
if(config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null) {
$keywords = config('costar.keyword.block');
foreach($keywords as $kw) {
if(Str::contains($res['content'], $kw) == true) {
abort(400, 'Invalid object');
}
if(config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null) {
$keywords = config('costar.keyword.block');
foreach($keywords as $kw) {
if(Str::contains($res['content'], $kw) == true) {
return;
}
}
$unlisted = config('costar.domain.unlisted');
if(in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
$unlisted = true;
$scope = 'unlisted';
} else {
$unlisted = false;
}
$cwDomains = config('costar.domain.cw');
if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
$cw = true;
}
}
$id = isset($res['id']) ? $res['id'] : $url;
if(!self::validateUrl($id) ||
!self::validateUrl($activity['object']['attributedTo'])
) {
return;
}
$idDomain = parse_url($id, PHP_URL_HOST);
$urlDomain = parse_url($url, PHP_URL_HOST);
$actorDomain = parse_url($activity['object']['attributedTo'], PHP_URL_HOST);
if(
$idDomain !== $urlDomain ||
$actorDomain !== $urlDomain ||
$idDomain !== $actorDomain
) {
return;
}
$profile = self::profileFirstOrNew($activity['object']['attributedTo']);
if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) && $replyTo == true) {
$reply_to = self::statusFirstOrFetch($activity['object']['inReplyTo'], false);
$reply_to = optional($reply_to)->id;
$unlisted = config('costar.domain.unlisted');
if(in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
$unlisted = true;
$scope = 'unlisted';
} else {
$reply_to = null;
$unlisted = false;
}
$ts = is_array($res['published']) ? $res['published'][0] : $res['published'];
$status = DB::transaction(function() use($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id) {
$status = new Status;
$status->profile_id = $profile->id;
$status->url = isset($res['url']) ? $res['url'] : $url;
$status->uri = isset($res['url']) ? $res['url'] : $url;
$status->object_url = $id;
$status->caption = strip_tags($res['content']);
$status->rendered = Purify::clean($res['content']);
$status->created_at = Carbon::parse($ts);
$status->in_reply_to_id = $reply_to;
$status->local = false;
$status->is_nsfw = $cw;
$status->scope = $scope;
$status->visibility = $scope;
$status->cw_summary = $cw == true && isset($res['summary']) ?
Purify::clean(strip_tags($res['summary'])) : null;
$status->save();
if($reply_to == null) {
self::importNoteAttachment($res, $status);
}
return $status;
});
return $status;
$cwDomains = config('costar.domain.cw');
if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
$cw = true;
}
}
$id = isset($res['id']) ? $res['id'] : $url;
$idDomain = parse_url($id, PHP_URL_HOST);
$urlDomain = parse_url($url, PHP_URL_HOST);
if(!self::validateUrl($id)) {
return;
}
if(isset($activity['object']['attributedTo'])) {
$actorDomain = parse_url($activity['object']['attributedTo'], PHP_URL_HOST);
if(!self::validateUrl($activity['object']['attributedTo']) ||
$idDomain !== $actorDomain)
{
return;
}
}
if(
$idDomain !== $urlDomain ||
$actorDomain !== $urlDomain
) {
return;
}
$profile = self::profileFirstOrNew($activity['object']['attributedTo']);
if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) && $replyTo == true) {
$reply_to = self::statusFirstOrFetch($activity['object']['inReplyTo'], false);
$reply_to = optional($reply_to)->id;
} else {
$reply_to = null;
}
$ts = is_array($res['published']) ? $res['published'][0] : $res['published'];
$status = DB::transaction(function() use($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id) {
$status = new Status;
$status->profile_id = $profile->id;
$status->url = isset($res['url']) ? $res['url'] : $url;
$status->uri = isset($res['url']) ? $res['url'] : $url;
$status->object_url = $id;
$status->caption = strip_tags($res['content']);
$status->rendered = Purify::clean($res['content']);
$status->created_at = Carbon::parse($ts);
$status->in_reply_to_id = $reply_to;
$status->local = false;
$status->is_nsfw = $cw;
$status->scope = $scope;
$status->visibility = $scope;
$status->cw_summary = $cw == true && isset($res['summary']) ?
Purify::clean(strip_tags($res['summary'])) : null;
$status->save();
if($reply_to == null) {
self::importNoteAttachment($res, $status);
}
return $status;
});
return $status;
}
public static function statusFetch($url)
@ -385,12 +391,14 @@ class Helpers {
foreach($attachments as $media) {
$type = $media['mediaType'];
$url = $media['url'];
$blurhash = isset($media['blurhash']) ? $media['blurhash'] : null;
$valid = self::validateUrl($url);
if(in_array($type, $allowed) == false || $valid == false) {
continue;
}
$media = new Media();
$media->blurhash = $blurhash;
$media->remote_media = true;
$media->status_id = $status->id;
$media->profile_id = $status->profile_id;
@ -398,7 +406,12 @@ class Helpers {
$media->media_path = $url;
$media->remote_url = $url;
$media->mime = $type;
$media->version = 3;
$media->save();
if(config('pixelfed.cloud_storage') == true) {
MediaStoragePipeline::dispatch($media);
}
}
$status->viewType();
@ -425,6 +438,7 @@ class Helpers {
->whereUsername($id)
->firstOrFail();
}
$res = self::fetchProfileFromUrl($url);
if(isset($res['id']) == false) {
return;
@ -460,10 +474,7 @@ class Helpers {
$profile->webfinger = strtolower(Purify::clean($webfinger));
$profile->last_fetched_at = now();
$profile->save();
if($runJobs == true) {
// RemoteFollowImportRecent::dispatch($res, $profile);
CreateAvatar::dispatch($profile);
}
RemoteAvatarFetch::dispatch($profile);
return $profile;
});
} else {
@ -477,6 +488,7 @@ class Helpers {
$profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) && Helpers::validateUrl($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null;
$profile->save();
}
RemoteAvatarFetch::dispatch($profile);
}
return $profile;
});

View file

@ -43,7 +43,7 @@ class HttpSignature {
$digest = self::_digest($body);
}
$headers = self::_headersToSign($url, $body ? $digest : false);
$headers = array_merge($headers, $addlHeaders);
$headers = array_unique(array_merge($headers, $addlHeaders));
$stringToSign = self::_headersToSigningString($headers);
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
$key = openssl_pkey_get_private($privateKey);
@ -53,7 +53,7 @@ class HttpSignature {
unset($headers['(request-target)']);
$headers['Signature'] = $signatureHeader;
return self::_headersToCurlArray($headers);
return $headers;
}
public static function parseSignatureHeader($signature) {

View file

@ -87,6 +87,14 @@ class RestrictedNames
'assets',
'public',
'storage',
'htaccess',
'.htaccess',
'favicon.ico',
'embed.js',
'index.php',
'manifest.json',
'mix-manifest.json',
'robots.txt',
// Laravel Horizon
'horizon',
@ -147,7 +155,6 @@ class RestrictedNames
'driver',
'e',
'embed',
'embed.js',
'email',
'emails',
'error',
@ -191,7 +198,6 @@ class RestrictedNames
'invites',
'import',
'imports',
'index.php',
'j',
'js',
'k',
@ -329,6 +335,7 @@ class RestrictedNames
$reserved = self::$reserved;
$res = array_merge($additional, $reserved, $banned);
$res = array_unique($res);
sort($res);
return $res;

View file

@ -4,7 +4,7 @@ namespace App\Util\Media;
use App\Media;
use Image as Intervention;
use Cache, Storage;
use Cache, Log, Storage;
class Image
{
@ -165,30 +165,32 @@ class Image
$quality = config('pixelfed.image_quality');
$img->save($newPath, $quality);
$media->width = $img->width();
$media->height = $img->height();
$img->destroy();
if (!$thumbnail) {
$media->orientation = $orientation;
}
if ($thumbnail == true) {
$media->thumbnail_path = $converted['path'];
$media->thumbnail_url = url(Storage::url($converted['path']));
} else {
$media->width = $img->width();
$media->height = $img->height();
$media->orientation = $orientation;
$media->media_path = $converted['path'];
$media->mime = $img->mime;
}
$img->destroy();
$media->save();
if($thumbnail) {
$this->generateBlurhash($media);
}
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
Cache::forget('status:thumb:'.$media->status_id);
} catch (Exception $e) {
$media->processed_at = now();
$media->save();
Log::info('MediaResizeException: Could not process media id: ' . $media->id);
}
}

View file

@ -15,12 +15,50 @@ class Bouncer {
return;
}
$recentKey = 'pf:bouncer:recent_by_pid:' . $status->profile_id;
$recentTtl = now()->addMinutes(5);
$recent = Cache::remember($recentKey, $recentTtl, function() use($status) {
return $status->profile->created_at->gt(now()->subMonths(2)) || $status->profile->statuses()->count() == 0;
$exemptionKey = 'pf:bouncer_v0:exemption_by_pid:' . $status->profile_id;
$exemptionTtl = now()->addDays(12);
$exemption = Cache::remember($exemptionKey, $exemptionTtl, function() use($status) {
$uid = $status->profile->user_id;
$ids = AccountInterstitial::whereUserId($uid)
->whereType('post.autospam')
->whereItemType('App\Status')
->whereNotNull('appeal_handled_at')
->latest()
->take(5)
->pluck('item_id');
if($ids->count() == 0) {
return false;
}
$count = Status::select('id', 'scope')
->whereScope('public')
->find($ids)
->count();
return $count >= 1 ? true : false;
});
if($exemption == true) {
return;
}
$recentKey = 'pf:bouncer_v0:recent_by_pid:' . $status->profile_id;
$recentTtl = now()->addHours(28);
$recent = Cache::remember($recentKey, $recentTtl, function() use($status) {
return $status
->profile
->created_at
->gt(now()->subMonths(6)) ||
$status
->profile
->statuses()
->whereScope('public')
->count() == 0;
});
if(!$recent) {
return;
}
@ -29,7 +67,16 @@ class Bouncer {
return;
}
if(!Str::contains($status->caption, ['https://', 'http://', 'hxxps://', 'hxxp://', 'www.', '.com', '.net', '.org'])) {
if(!Str::contains($status->caption, [
'https://',
'http://',
'hxxps://',
'hxxp://',
'www.',
'.com',
'.net',
'.org'
])) {
return;
}
@ -74,6 +121,8 @@ class Bouncer {
$status->is_nsfw = true;
$status->save();
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $status->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $status->profile_id);
}
}

View file

@ -8,7 +8,7 @@ use Illuminate\Support\Str;
class Config {
public static function get() {
return Cache::remember('api:site:configuration:_v0.1', now()->addHours(30), function() {
return Cache::remember('api:site:configuration:_v0.2', now()->addHours(30), function() {
return [
'open_registration' => config('pixelfed.open_registration'),
'uploader' => [
@ -62,6 +62,13 @@ class Config {
'instagram' => config('pixelfed.import.instagram.enabled'),
'mastodon' => false,
'pixelfed' => false
],
'label' => [
'covid' => [
'enabled' => config('instance.label.covid.enabled'),
'org' => config('instance.label.covid.org'),
'url' => config('instance.label.covid.url'),
]
]
]
];

View file

@ -44,6 +44,7 @@
"symfony/http-kernel": "5.1.5"
},
"require-dev": {
"brianium/paratest": "^6.1",
"facade/ignition": "^2.3.6",
"fzaninotto/faker": "^1.4",
"mockery/mockery": "^1.0",

296
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b4d25a7ba9e07f08e9ddacc2ddf5cfc1",
"content-hash": "eab416feda81875b20d5df2399f9ed86",
"packages": [
{
"name": "alchemy/binary-driver",
@ -130,16 +130,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.171.16",
"version": "3.172.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "216ff33ce238c30cf793973262ea727f2ce41224"
"reference": "5a5e66c4d54c392042820703eeb8a6bd3d222924"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/216ff33ce238c30cf793973262ea727f2ce41224",
"reference": "216ff33ce238c30cf793973262ea727f2ce41224",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5a5e66c4d54c392042820703eeb8a6bd3d222924",
"reference": "5a5e66c4d54c392042820703eeb8a6bd3d222924",
"shasum": ""
},
"require": {
@ -214,9 +214,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.171.16"
"source": "https://github.com/aws/aws-sdk-php/tree/3.172.0"
},
"time": "2021-01-12T19:12:49+00:00"
"time": "2021-01-22T19:21:38+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -1971,23 +1971,23 @@
},
{
"name": "jaybizzle/crawler-detect",
"version": "v1.2.103",
"version": "v1.2.104",
"source": {
"type": "git",
"url": "https://github.com/JayBizzle/Crawler-Detect.git",
"reference": "3efa2860959cc971f17624b40bf0699823f9d0f3"
"reference": "a581e89a9212c4e9d18049666dc735718c29de9c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/3efa2860959cc971f17624b40bf0699823f9d0f3",
"reference": "3efa2860959cc971f17624b40bf0699823f9d0f3",
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/a581e89a9212c4e9d18049666dc735718c29de9c",
"reference": "a581e89a9212c4e9d18049666dc735718c29de9c",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8|^5.5|^6.5"
"phpunit/phpunit": "^4.8|^5.5|^6.5|^9.4"
},
"type": "library",
"autoload": {
@ -2017,9 +2017,9 @@
],
"support": {
"issues": "https://github.com/JayBizzle/Crawler-Detect/issues",
"source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.103"
"source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.104"
},
"time": "2020-11-23T19:49:25+00:00"
"time": "2021-01-13T15:25:20+00:00"
},
{
"name": "jenssegers/agent",
@ -2106,16 +2106,16 @@
},
{
"name": "laravel/framework",
"version": "v8.22.1",
"version": "v8.25.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "5c70991b96c5722afed541a996479b5112654c8b"
"reference": "05da44d6823c2923597519ac10151f5827a24f80"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/5c70991b96c5722afed541a996479b5112654c8b",
"reference": "5c70991b96c5722afed541a996479b5112654c8b",
"url": "https://api.github.com/repos/laravel/framework/zipball/05da44d6823c2923597519ac10151f5827a24f80",
"reference": "05da44d6823c2923597519ac10151f5827a24f80",
"shasum": ""
},
"require": {
@ -2202,6 +2202,7 @@
},
"suggest": {
"aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).",
"brianium/paratest": "Required to run tests in parallel (^6.0).",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).",
"ext-ftp": "Required to use the Flysystem FTP driver.",
"ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
@ -2269,7 +2270,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2021-01-13T13:37:56+00:00"
"time": "2021-01-26T14:40:21+00:00"
},
{
"name": "laravel/helpers",
@ -3097,16 +3098,16 @@
},
{
"name": "league/mime-type-detection",
"version": "1.5.1",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/mime-type-detection.git",
"reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa"
"reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa",
"reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa",
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
"reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
"shasum": ""
},
"require": {
@ -3114,8 +3115,9 @@
"php": "^7.2 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.36",
"phpunit/phpunit": "^8.5.8"
"friendsofphp/php-cs-fixer": "^2.18",
"phpstan/phpstan": "^0.12.68",
"phpunit/phpunit": "^8.5.8 || ^9.3"
},
"type": "library",
"autoload": {
@ -3136,7 +3138,7 @@
"description": "Mime-type detection for Flysystem",
"support": {
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.5.1"
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0"
},
"funding": [
{
@ -3148,7 +3150,7 @@
"type": "tidelift"
}
],
"time": "2020-10-18T11:50:25+00:00"
"time": "2021-01-18T20:58:21+00:00"
},
{
"name": "league/oauth2-server",
@ -3239,16 +3241,16 @@
},
{
"name": "mobiledetect/mobiledetectlib",
"version": "2.8.34",
"version": "2.8.35",
"source": {
"type": "git",
"url": "https://github.com/serbanghita/Mobile-Detect.git",
"reference": "6f8113f57a508494ca36acbcfa2dc2d923c7ed5b"
"reference": "68a35170fdf36e7b35f9c125e5102338dbc3ff65"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/6f8113f57a508494ca36acbcfa2dc2d923c7ed5b",
"reference": "6f8113f57a508494ca36acbcfa2dc2d923c7ed5b",
"url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/68a35170fdf36e7b35f9c125e5102338dbc3ff65",
"reference": "68a35170fdf36e7b35f9c125e5102338dbc3ff65",
"shasum": ""
},
"require": {
@ -3289,9 +3291,9 @@
],
"support": {
"issues": "https://github.com/serbanghita/Mobile-Detect/issues",
"source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.34"
"source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.35"
},
"time": "2019-09-18T18:44:20+00:00"
"time": "2021-01-25T19:09:34+00:00"
},
{
"name": "monolog/monolog",
@ -3904,16 +3906,16 @@
},
{
"name": "pbmedia/laravel-ffmpeg",
"version": "7.5.4",
"version": "7.5.5",
"source": {
"type": "git",
"url": "https://github.com/protonemedia/laravel-ffmpeg.git",
"reference": "72bb005b4be13710663e7de9077d32c7a76158a3"
"reference": "460b879f7b1b6333ee02fe1fa35d6ff5bc4c0ea0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/72bb005b4be13710663e7de9077d32c7a76158a3",
"reference": "72bb005b4be13710663e7de9077d32c7a76158a3",
"url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/460b879f7b1b6333ee02fe1fa35d6ff5bc4c0ea0",
"reference": "460b879f7b1b6333ee02fe1fa35d6ff5bc4c0ea0",
"shasum": ""
},
"require": {
@ -3977,7 +3979,7 @@
],
"support": {
"issues": "https://github.com/protonemedia/laravel-ffmpeg/issues",
"source": "https://github.com/protonemedia/laravel-ffmpeg/tree/7.5.4"
"source": "https://github.com/protonemedia/laravel-ffmpeg/tree/7.5.5"
},
"funding": [
{
@ -3985,7 +3987,7 @@
"type": "github"
}
],
"time": "2021-01-07T08:06:09+00:00"
"time": "2021-01-18T14:48:50+00:00"
},
{
"name": "php-ffmpeg/php-ffmpeg",
@ -4976,16 +4978,16 @@
},
{
"name": "psy/psysh",
"version": "v0.10.5",
"version": "v0.10.6",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "7c710551d4a2653afa259c544508dc18a9098956"
"reference": "6f990c19f91729de8b31e639d6e204ea59f19cf3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/7c710551d4a2653afa259c544508dc18a9098956",
"reference": "7c710551d4a2653afa259c544508dc18a9098956",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/6f990c19f91729de8b31e639d6e204ea59f19cf3",
"reference": "6f990c19f91729de8b31e639d6e204ea59f19cf3",
"shasum": ""
},
"require": {
@ -5014,7 +5016,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.10.x-dev"
"dev-main": "0.10.x-dev"
}
},
"autoload": {
@ -5046,9 +5048,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.10.5"
"source": "https://github.com/bobthecow/psysh/tree/v0.10.6"
},
"time": "2020-12-04T02:51:30+00:00"
"time": "2021-01-18T15:53:43+00:00"
},
{
"name": "ralouphie/getallheaders",
@ -5096,16 +5098,16 @@
},
{
"name": "ramsey/collection",
"version": "1.1.1",
"version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/ramsey/collection.git",
"reference": "24d93aefb2cd786b7edd9f45b554aea20b28b9b1"
"reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/collection/zipball/24d93aefb2cd786b7edd9f45b554aea20b28b9b1",
"reference": "24d93aefb2cd786b7edd9f45b554aea20b28b9b1",
"url": "https://api.github.com/repos/ramsey/collection/zipball/28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1",
"reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1",
"shasum": ""
},
"require": {
@ -5115,19 +5117,19 @@
"captainhook/captainhook": "^5.3",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"ergebnis/composer-normalize": "^2.6",
"fzaninotto/faker": "^1.5",
"fakerphp/faker": "^1.5",
"hamcrest/hamcrest-php": "^2",
"jangregor/phpstan-prophecy": "^0.6",
"jangregor/phpstan-prophecy": "^0.8",
"mockery/mockery": "^1.3",
"phpstan/extension-installer": "^1",
"phpstan/phpstan": "^0.12.32",
"phpstan/phpstan-mockery": "^0.12.5",
"phpstan/phpstan-phpunit": "^0.12.11",
"phpunit/phpunit": "^8.5",
"phpunit/phpunit": "^8.5 || ^9",
"psy/psysh": "^0.10.4",
"slevomat/coding-standard": "^6.3",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^3.12.2"
"vimeo/psalm": "^4.4"
},
"type": "library",
"autoload": {
@ -5157,15 +5159,19 @@
],
"support": {
"issues": "https://github.com/ramsey/collection/issues",
"source": "https://github.com/ramsey/collection/tree/1.1.1"
"source": "https://github.com/ramsey/collection/tree/1.1.3"
},
"funding": [
{
"url": "https://github.com/ramsey",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/ramsey/collection",
"type": "tidelift"
}
],
"time": "2020-09-10T20:58:17+00:00"
"time": "2021-01-21T17:40:04+00:00"
},
{
"name": "ramsey/uuid",
@ -5261,16 +5267,16 @@
},
{
"name": "spatie/db-dumper",
"version": "2.18.0",
"version": "2.20.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/db-dumper.git",
"reference": "eddb2b7c6877817d97bbdc1c60d1a800bf5a267a"
"reference": "6a9004885b6de8417c2a5e1aa9e3712b49c1c59d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/db-dumper/zipball/eddb2b7c6877817d97bbdc1c60d1a800bf5a267a",
"reference": "eddb2b7c6877817d97bbdc1c60d1a800bf5a267a",
"url": "https://api.github.com/repos/spatie/db-dumper/zipball/6a9004885b6de8417c2a5e1aa9e3712b49c1c59d",
"reference": "6a9004885b6de8417c2a5e1aa9e3712b49c1c59d",
"shasum": ""
},
"require": {
@ -5309,7 +5315,7 @@
],
"support": {
"issues": "https://github.com/spatie/db-dumper/issues",
"source": "https://github.com/spatie/db-dumper/tree/2.18.0"
"source": "https://github.com/spatie/db-dumper/tree/2.20.0"
},
"funding": [
{
@ -5317,7 +5323,7 @@
"type": "github"
}
],
"time": "2020-11-10T09:20:18+00:00"
"time": "2021-01-26T07:44:13+00:00"
},
{
"name": "spatie/image-optimizer",
@ -5375,16 +5381,16 @@
},
{
"name": "spatie/laravel-backup",
"version": "6.14.2",
"version": "6.14.3",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-backup.git",
"reference": "3374e1eeb09ef32c6bfd495ae1f2f4de4b594922"
"reference": "8a4c95bffffde831edaca64bdef55aac213d0eef"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-backup/zipball/3374e1eeb09ef32c6bfd495ae1f2f4de4b594922",
"reference": "3374e1eeb09ef32c6bfd495ae1f2f4de4b594922",
"url": "https://api.github.com/repos/spatie/laravel-backup/zipball/8a4c95bffffde831edaca64bdef55aac213d0eef",
"reference": "8a4c95bffffde831edaca64bdef55aac213d0eef",
"shasum": ""
},
"require": {
@ -5448,7 +5454,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-backup/issues",
"source": "https://github.com/spatie/laravel-backup/tree/6.14.2"
"source": "https://github.com/spatie/laravel-backup/tree/6.14.3"
},
"funding": [
{
@ -5460,7 +5466,7 @@
"type": "other"
}
],
"time": "2020-12-23T10:13:12+00:00"
"time": "2021-01-15T13:25:43+00:00"
},
{
"name": "spatie/laravel-image-optimizer",
@ -8069,16 +8075,16 @@
},
{
"name": "vlucas/phpdotenv",
"version": "v5.2.0",
"version": "v5.3.0",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "fba64139db67123c7a57072e5f8d3db10d160b66"
"reference": "b3eac5c7ac896e52deab4a99068e3f4ab12d9e56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/fba64139db67123c7a57072e5f8d3db10d160b66",
"reference": "fba64139db67123c7a57072e5f8d3db10d160b66",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/b3eac5c7ac896e52deab4a99068e3f4ab12d9e56",
"reference": "b3eac5c7ac896e52deab4a99068e3f4ab12d9e56",
"shasum": ""
},
"require": {
@ -8093,7 +8099,7 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"ext-filter": "*",
"phpunit/phpunit": "^7.5.20 || ^8.5.2 || ^9.0"
"phpunit/phpunit": "^7.5.20 || ^8.5.14 || ^9.5.1"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
@ -8101,7 +8107,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.2-dev"
"dev-master": "5.3-dev"
}
},
"autoload": {
@ -8133,7 +8139,7 @@
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.2.0"
"source": "https://github.com/vlucas/phpdotenv/tree/v5.3.0"
},
"funding": [
{
@ -8145,7 +8151,7 @@
"type": "tidelift"
}
],
"time": "2020-09-14T15:57:31+00:00"
"time": "2021-01-20T15:23:13+00:00"
},
{
"name": "voku/portable-ascii",
@ -8226,12 +8232,12 @@
"version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
"reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
"shasum": ""
},
@ -8269,13 +8275,93 @@
"validate"
],
"support": {
"issues": "https://github.com/webmozart/assert/issues",
"source": "https://github.com/webmozart/assert/tree/master"
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.9.1"
},
"time": "2020-07-08T17:02:28+00:00"
}
],
"packages-dev": [
{
"name": "brianium/paratest",
"version": "v6.1.2",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "235db99a43401d68fdc4495b20b49291ea2e767d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/235db99a43401d68fdc4495b20b49291ea2e767d",
"reference": "235db99a43401d68fdc4495b20b49291ea2e767d",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-simplexml": "*",
"php": "^7.3 || ^8.0",
"phpunit/php-code-coverage": "^9.2.5",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-timer": "^5.0.3",
"phpunit/phpunit": "^9.5.0",
"sebastian/environment": "^5.1.3",
"symfony/console": "^4.4 || ^5.2",
"symfony/process": "^4.4 || ^5.2"
},
"require-dev": {
"doctrine/coding-standard": "^8.2.0",
"ekino/phpstan-banned-code": "^0.3.1",
"ergebnis/phpstan-rules": "^0.15.3",
"ext-posix": "*",
"infection/infection": "^0.18.2",
"phpstan/phpstan": "^0.12.58",
"phpstan/phpstan-deprecation-rules": "^0.12.5",
"phpstan/phpstan-phpunit": "^0.12.16",
"phpstan/phpstan-strict-rules": "^0.12.5",
"squizlabs/php_codesniffer": "^3.5.8",
"symfony/filesystem": "^5.2.0",
"thecodingmachine/phpstan-strict-rules": "^0.12.1",
"vimeo/psalm": "^4.3.1"
},
"bin": [
"bin/paratest"
],
"type": "library",
"autoload": {
"psr-4": {
"ParaTest\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Scaturro",
"email": "scaturrob@gmail.com",
"homepage": "http://brianscaturro.com",
"role": "Lead"
}
],
"description": "Parallel testing for PHP",
"homepage": "https://github.com/paratestphp/paratest",
"keywords": [
"concurrent",
"parallel",
"phpunit",
"testing"
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v6.1.2"
},
"time": "2020-12-15T11:41:54+00:00"
},
{
"name": "doctrine/instantiator",
"version": "1.4.0",
@ -8412,16 +8498,16 @@
},
{
"name": "facade/ignition",
"version": "2.5.8",
"version": "2.5.9",
"source": {
"type": "git",
"url": "https://github.com/facade/ignition.git",
"reference": "8e907d81244649c5ea746e2ec30c32c5f59df472"
"reference": "66b3138ecce38024723fb3bfc66ef8852a779ea9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/facade/ignition/zipball/8e907d81244649c5ea746e2ec30c32c5f59df472",
"reference": "8e907d81244649c5ea746e2ec30c32c5f59df472",
"url": "https://api.github.com/repos/facade/ignition/zipball/66b3138ecce38024723fb3bfc66ef8852a779ea9",
"reference": "66b3138ecce38024723fb3bfc66ef8852a779ea9",
"shasum": ""
},
"require": {
@ -8485,7 +8571,7 @@
"issues": "https://github.com/facade/ignition/issues",
"source": "https://github.com/facade/ignition"
},
"time": "2020-12-29T09:12:55+00:00"
"time": "2021-01-26T14:45:19+00:00"
},
{
"name": "facade/ignition-contracts",
@ -8542,16 +8628,16 @@
},
{
"name": "filp/whoops",
"version": "2.9.1",
"version": "2.9.2",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
"reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771"
"reference": "df7933820090489623ce0be5e85c7e693638e536"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/307fb34a5ab697461ec4c9db865b20ff2fd40771",
"reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771",
"url": "https://api.github.com/repos/filp/whoops/zipball/df7933820090489623ce0be5e85c7e693638e536",
"reference": "df7933820090489623ce0be5e85c7e693638e536",
"shasum": ""
},
"require": {
@ -8601,9 +8687,15 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
"source": "https://github.com/filp/whoops/tree/2.9.1"
"source": "https://github.com/filp/whoops/tree/2.9.2"
},
"time": "2020-11-01T12:00:00+00:00"
"funding": [
{
"url": "https://github.com/denis-sokolov",
"type": "github"
}
],
"time": "2021-01-24T12:00:00+00:00"
},
{
"name": "fzaninotto/faker",
@ -8843,16 +8935,16 @@
},
{
"name": "nunomaduro/collision",
"version": "v5.2.0",
"version": "v5.3.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
"reference": "aca954fd03414ba0dd85d7d8e42ba9b251893d1f"
"reference": "aca63581f380f63a492b1e3114604e411e39133a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/aca954fd03414ba0dd85d7d8e42ba9b251893d1f",
"reference": "aca954fd03414ba0dd85d7d8e42ba9b251893d1f",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/aca63581f380f63a492b1e3114604e411e39133a",
"reference": "aca63581f380f63a492b1e3114604e411e39133a",
"shasum": ""
},
"require": {
@ -8927,7 +9019,7 @@
"type": "patreon"
}
],
"time": "2021-01-13T10:00:08+00:00"
"time": "2021-01-25T15:34:13+00:00"
},
{
"name": "phar-io/manifest",
@ -9585,16 +9677,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.5.0",
"version": "9.5.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe"
"reference": "e7bdf4085de85a825f4424eae52c99a1cec2f360"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e16c225d57c3d6808014df6b1dd7598d0a5bbbe",
"reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e7bdf4085de85a825f4424eae52c99a1cec2f360",
"reference": "e7bdf4085de85a825f4424eae52c99a1cec2f360",
"shasum": ""
},
"require": {
@ -9672,7 +9764,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.0"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.1"
},
"funding": [
{
@ -9684,7 +9776,7 @@
"type": "github"
}
],
"time": "2020-12-04T05:05:53+00:00"
"time": "2021-01-17T07:42:25+00:00"
},
{
"name": "sebastian/cli-parser",

View file

@ -37,6 +37,13 @@ return [
'followLinks' => false,
],
'mysql' => [
'dump' => [
'useSingleTransaction' => true,
'useQuick' => true,
],
],
/*
* The names of the connections to the databases that should be backed up
* MySQL, PostgreSQL, SQLite and Mongo databases are supported.
@ -49,7 +56,7 @@ return [
/*
* The database dump can be gzipped to decrease diskspace usage.
*/
'gzip_database_dump' => false,
'gzip_database_dump' => true,
'destination' => [
@ -62,7 +69,7 @@ return [
* The disk names on which the backups will be stored.
*/
'disks' => [
'local',
'local'
],
],
],

View file

@ -70,7 +70,7 @@ return [
'redis' => [
'driver' => 'redis',
'client' => env('REDIS_CLIENT', 'predis'),
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),

View file

@ -36,6 +36,7 @@ return [
'body' => env('PAGE_503_BODY', 'Our service is in maintenance mode, please try again later.')
]
],
'username' => [
'banned' => env('BANNED_USERNAMES'),
'remote' => [
@ -61,5 +62,13 @@ return [
'enabled' => env('OAUTH_PAT_ENABLED', false),
'id' => env('OAUTH_PAT_ID'),
]
]
],
'label' => [
'covid' => [
'enabled' => env('ENABLE_COVID_LABEL', true),
'url' => env('COVID_LABEL_URL', 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/advice-for-public'),
'org' => env('COVID_LABEL_ORG', 'visit the WHO website')
]
],
];

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your Pixelfed instance.
|
*/
'version' => '0.10.9',
'version' => '0.10.10',
/*
|--------------------------------------------------------------------------

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddCdnUrlToAvatarsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('avatars', function (Blueprint $table) {
$table->string('cdn_url')->unique()->index()->nullable()->after('remote_url');
$table->unsignedInteger('size')->nullable()->after('cdn_url');
$table->boolean('is_remote')->nullable()->index()->after('cdn_url');
$table->dropColumn('thumb_path');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('avatars', function (Blueprint $table) {
$table->dropColumn('cdn_url');
$table->dropColumn('size');
$table->dropColumn('is_remote');
$table->string('thumb_path')->nullable();
});
}
}

12255
package-lock.json generated

File diff suppressed because it is too large Load diff

BIN
public/js/app.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/my2020.js vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/quill.js vendored

Binary file not shown.

BIN
public/js/rempos.js vendored

Binary file not shown.

BIN
public/js/rempro.js vendored

Binary file not shown.

BIN
public/js/search.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

BIN
public/js/vendor.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -96,6 +96,24 @@ window.App.util = {
return interval + "m";
}
return Math.floor(seconds) + "s";
}),
rewriteLinks: (function(i) {
let tag = i.innerText;
if(i.href.startsWith(window.location.origin)) {
return i.href;
}
if(tag.startsWith('#') == true) {
tag = '/discover/tags/' + tag.substr(1) +'?src=rph';
} else if(tag.startsWith('@') == true) {
tag = '/' + i.innerText + '?src=rpp';
} else {
tag = '/i/redirect?url=' + encodeURIComponent(tag);
}
return tag;
})
},
filters: [

View file

@ -4,7 +4,10 @@ import InfiniteLoading from 'vue-infinite-loading';
import Loading from 'vue-loading-overlay';
import VueTimeago from 'vue-timeago';
import VueCarousel from 'vue-carousel';
import VueBlurHash from 'vue-blurhash'
import 'vue-blurhash/dist/vue-blurhash.css'
Vue.use(VueBlurHash);
Vue.use(VueCarousel);
Vue.use(BootstrapVue);
Vue.use(InfiniteLoading);

View file

@ -86,10 +86,46 @@
<span v-else>
<a v-if="!pageLoading && (page > 1 && page <= 2) || (page == 1 && ids.length != 0) || page == 'cropPhoto'" class="font-weight-bold text-decoration-none" href="#" @click.prevent="nextPage">Next</a>
<a v-if="!pageLoading && page == 3" class="font-weight-bold text-decoration-none" href="#" @click.prevent="compose()">Post</a>
<a v-if="!pageLoading && page == 'addText'" class="font-weight-bold text-decoration-none" href="#" @click.prevent="composeTextPost()">Post</a>
</span>
</div>
</div>
<div class="card-body p-0 border-top">
<div v-if="page == 'textOptions'" class="w-100 h-100" style="min-height: 280px;">
test
</div>
<div v-if="page == 'addText'" class="w-100 h-100" style="min-height: 280px;">
<div class="mt-2">
<div class="media px-3">
<div class="media-body">
<div class="form-group">
<label class="font-weight-bold text-muted small d-none">Body</label>
<textarea class="form-control border-0 rounded-0 no-focus" rows="7" placeholder="What's happening?" style="font-size:18px;resize:none" v-model="composeText" v-on:keyup="composeTextLength = composeText.length"></textarea>
<div class="border-bottom"></div>
<p class="help-text small text-right text-muted mb-0 font-weight-bold">{{composeTextLength}}/{{config.uploader.max_caption_length}}</p>
<p class="mb-0 mt-2">
<a class="btn btn-primary rounded-pill mr-2" href="#" style="height: 37px;" @click.prevent="showTextOptions()">
<i class="fas fa-palette px-3 text-white"></i>
</a>
<!-- <a class="btn btn-outline-lighter rounded-pill ml-3" href="#" @click.prevent="showLocationCard()">
<i class="fas fa-map-marker-alt px-3"></i>
</a>
<a class="btn btn-outline-lighter rounded-pill mx-3" href="#" @click.prevent="showTagCard()">
<i class="fas fa-user-plus px-3"></i>
</a> -->
<a class="btn rounded-pill mx-3 d-inline-flex align-items-center" href="#" :class="[nsfw ? 'btn-danger' : 'btn-outline-lighter']" style="height: 37px;" @click.prevent="nsfw = !nsfw" title="Mark as sensitive/not safe for work">
<i class="far fa-flag px-3"></i> <span class="text-muted small font-weight-bold"></span>
</a>
<a class="btn btn-outline-lighter rounded-pill d-inline-flex align-items-center" href="#" style="height: 37px;" @click.prevent="showVisibilityCard()">
<i class="fas fa-eye mr-2"></i> <span class="text-muted small font-weight-bold">{{visibilityTag}}</span>
</a>
</p>
</div>
</div>
</div>
</div>
</div>
<div v-if="page == 1" class="w-100 h-100 d-flex justify-content-center align-items-center" style="min-height: 400px;">
<div class="text-center">
<div v-if="media.length == 0" class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark">
@ -107,6 +143,26 @@
</div>
</div>
</div>
<div v-if="config.ab.top == true && media.length == 0" class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark">
<div @click.prevent="addText" class="card-body">
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;border: 2px solid #008DF5">
<i class="far fa-edit text-primary fa-lg"></i>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">New Text Post</span>
<sup class="float-right mt-2">
<span class="btn btn-outline-lighter p-1 btn-sm font-weight-bold py-0" style="font-size:10px;line-height: 0.6">BETA</span>
</sup>
</p>
<p class="mb-0 text-muted">Share a text only post</p>
</div>
</div>
</div>
</div>
<a v-if="config.features.stories == true" class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/stories/new">
<div class="card-body">
<div class="media">
@ -349,6 +405,19 @@
<div v-if="page == 'advancedSettings'" class="w-100 h-100">
<div class="list-group list-group-flush">
<!-- <div class="d-none list-group-item d-flex justify-content-between">
<div>
<div class="text-dark ">Optimize Media</div>
<p v-if="mediaCropped" class="text-muted small mb-0">Media was cropped or filtered, it must be optimized.</p>
<p v-else class="text-muted small mb-0">Compress media for smaller file size.</p>
</div>
<div>
<div class="custom-control custom-switch" style="z-index: 9999;">
<input type="checkbox" class="custom-control-input" id="asoptimizemedia" v-model="optimizeMedia" :disabled="mediaCropped">
<label class="custom-control-label" for="asoptimizemedia"></label>
</div>
</div>
</div> -->
<div class="list-group-item d-flex justify-content-between">
<div>
<div class="text-dark ">Turn off commenting</div>
@ -591,6 +660,8 @@ export default {
nsfw: false,
place: false,
commentsDisabled: false,
optimizeMedia: true,
mediaCropped: false,
pageTitle: '',
cropper: {
@ -613,11 +684,13 @@ export default {
'addToStory',
'editMedia',
'cameraRoll',
'tagPeopleHelp'
'tagPeopleHelp',
'textOptions'
],
cameraRollMedia: [],
taggedUsernames: [],
taggedPeopleSearch: null
taggedPeopleSearch: null,
textMode: false
}
},
@ -664,6 +737,12 @@ export default {
el.removeAttr('disabled');
},
addText(event) {
this.pageTitle = 'New Text Post';
this.page = 'addText';
this.textMode = true;
},
mediaWatcher() {
let self = this;
$(document).on('change', '#pf-dz', function(e) {
@ -705,7 +784,7 @@ export default {
}
};
axios.post('/api/pixelfed/v1/media', form, xhrConfig)
axios.post('/api/compose/v0/media/upload', form, xhrConfig)
.then(function(e) {
self.uploadProgress = 100;
self.ids.push(e.data.id);
@ -747,7 +826,7 @@ export default {
}
let id = this.media[this.carouselCursor].id;
axios.delete('/api/pixelfed/v1/media', {
axios.delete('/api/compose/v0/media/delete', {
params: {
id: id
}
@ -794,9 +873,51 @@ export default {
cw: this.nsfw,
comments_disabled: this.commentsDisabled,
place: this.place,
tagged: this.taggedUsernames
tagged: this.taggedUsernames,
optimize_media: this.optimizeMedia
};
axios.post('/api/local/status/compose', data)
axios.post('/api/compose/v0/publish', data)
.then(res => {
let data = res.data;
window.location.href = data;
}).catch(err => {
let msg = err.response.data.message ? err.response.data.message : 'An unexpected error occured.'
swal('Oops, something went wrong!', msg, 'error');
});
return;
break;
case 'delete' :
this.ids = [];
this.media = [];
this.carouselCursor = 0;
this.composeText = '';
this.composeTextLength = 0;
$('#composeModal').modal('hide');
return;
break;
}
},
composeTextPost() {
let state = this.composeState;
if(this.composeText.length > this.config.uploader.max_caption_length) {
swal('Error', 'Caption is too long', 'error');
return;
}
switch(state) {
case 'publish' :
let data = {
caption: this.composeText,
visibility: this.visibility,
cw: this.nsfw,
comments_disabled: this.commentsDisabled,
place: this.place,
tagged: this.taggedUsernames,
};
axios.post('/api/compose/v0/publish/text', data)
.then(res => {
let data = res.data;
window.location.href = data;
@ -828,6 +949,14 @@ export default {
this.pageTitle = '';
switch(this.page) {
case 'addText':
this.page = 1;
break;
case 'textOptions':
this.page = 'addText';
break;
case 'cropPhoto':
case 'editMedia':
this.page = 2;
@ -838,7 +967,9 @@ export default {
break;
default:
this.namedPages.indexOf(this.page) != -1 ? this.page = 3 : this.page--;
this.namedPages.indexOf(this.page) != -1 ?
this.page = (this.textMode ? 'addText' : 3) :
(this.textMode ? 'addText' : this.page--);
break;
}
},
@ -860,10 +991,11 @@ export default {
imageSmoothingEnabled: false,
imageSmoothingQuality: 'high',
}).toBlob(function(blob) {
self.mediaCropped = true;
let data = new FormData();
data.append('file', blob);
let url = '/api/local/compose/media/update/' + self.ids[self.carouselCursor];
data.append('id', self.ids[self.carouselCursor]);
let url = '/api/compose/v0/media/update';
axios.post(url, data).then(res => {
self.media[self.carouselCursor].url = res.data.url;
self.pageLoading = false;
@ -921,7 +1053,7 @@ export default {
locationSearch(input) {
if (input.length < 1) { return []; };
let results = [];
return axios.get('/api/local/compose/location/search', {
return axios.get('/api/compose/v0/search/location', {
params: {
q: input
}
@ -936,8 +1068,8 @@ export default {
onSubmitLocation(result) {
this.place = result;
this.pageTitle = '';
this.page = 3;
this.pageTitle = this.textMode ? 'New Text Post' : '';
this.page = (this.textMode ? 'addText' : 3);
return;
},
@ -965,7 +1097,7 @@ export default {
this.visibility = state;
this.visibilityTag = tags[state];
this.pageTitle = '';
this.page = 3;
this.page = this.textMode ? 'addText' : 3;
},
showMediaDescriptionsCard() {
@ -1024,7 +1156,8 @@ export default {
canvas.toBlob(function(blob) {
data = new FormData();
data.append('file', blob);
axios.post('/api/local/compose/media/update/'+media.id, data).then(res => {
data.append('id', media.id);
axios.post('/api/compose/v0/media/update', data).then(res => {
}).catch(err => {
});
});
@ -1039,7 +1172,7 @@ export default {
if (input.length < 1) { return []; };
let self = this;
let results = [];
return axios.get('/api/local/compose/tag/search', {
return axios.get('/api/compose/v0/search/tag', {
params: {
q: input
}
@ -1070,6 +1203,11 @@ export default {
untagUsername(index) {
this.taggedUsernames.splice(index, 1);
},
showTextOptions() {
this.page = 'textOptions';
this.pageTitle = 'Text Post Options';
}
}
}

View file

@ -0,0 +1,239 @@
<template>
<div class="bg-dark text-white">
<div v-if="!loaded" style="height: 100vh;" class="d-flex justify-content-center align-items-center">
<div class="text-center">
<div class="spinner-border text-light" role="status">
<span class="sr-only">Loading...</span>
</div>
<p class="mb-0 lead mt-2">Loading</p>
</div>
</div>
<div v-if="loaded && notEnoughData" style="height: 100vh;" class="d-flex justify-content-center align-items-center">
<div class="text-center">
<p class="display-4">Oops!</p>
<p class="h3 font-weight-light py-3">We don't have enough data to display your <span class="font-weight-bold">#my2020</span>.</p>
<p class="mb-0 h5 font-weight-light">We hope to see you next year!</p>
</div>
</div>
<div v-if="loaded && !notEnoughData" class="d-flex justify-content-center align-items-center" style="width:100%;height:100vh;min-height:500px; padding: 0 15px;">
<div v-if="page == 1" class="text-center">
<p class="h1 font-weight-light">Hello {{user.username}}!</p>
<p class="h1 py-4">Your 2020 on Pixelfed.</p>
<p class="h4 font-weight-light mb-0 animate__animated animate__bounceInDown">Use the buttons below to navigate.</p>
</div>
<div v-if="page == 2" class="text-center mw-500">
<p class="display-4">User #<span class="font-weight-bold">{{stats.account.user_id}}</span></p>
<p class="h3 font-weight-light mb-0">You joined Pixelfed on {{stats.account.created_at}}</p>
</div>
<div v-if="page == 3" class="text-center mw-500">
<p class="display-4">You created <span class="font-weight-bold">{{stats.account.posts_count}}</span> posts</p>
<p class="h3 font-weight-light mb-0">The average user created <span class="font-weight-bold">{{stats.average.posts}}</span> posts this year.</p>
</div>
<div v-if="page == 4" class="text-center mw-500">
<p class="display-4">You liked <span class="font-weight-bold">{{stats.account.likes_count}}</span> posts</p>
<p class="h3 font-weight-light mb-0">The average user liked <span class="font-weight-bold">{{stats.average.likes}}</span> posts this year.</p>
</div>
<div v-if="page == 5" class="text-center mw-500">
<div v-if="stats.account.most_popular">
<p class="h1 font-weight-light mb-0 text-break md-line-height">Your most popular post of 2020 was created on <span class="font-weight-bold">{{stats.account.most_popular.created_at}}</span> with <span class="font-weight-bold">{{stats.account.most_popular.likes_count}}</span> likes.</p>
<p class="mt-4 mb-0">
<a class="btn btn-outline-light btn-lg btn-block rounded-pill" :href="stats.account.most_popular.url">View Post</a>
</p>
</div>
<div v-else>
<p class="h1 font-weight-light mb-0 text-break md-line-height">The most popular post of 2020 was created by <span class="font-weight-bold">{{stats.popular.post.username}}</span> on <span class="font-weight-bold">{{stats.popular.post.created_at}}</span> with <span class="font-weight-bold">{{stats.popular.post.likes_count}}</span> likes.</p>
<p class="mt-4 mb-0">
<a class="btn btn-outline-light btn-lg btn-block rounded-pill" :href="stats.popular.post.url">View Post</a>
</p>
</div>
</div>
<div v-if="page == 6" class="text-center mw-500">
<p class="display-4"><span class="font-weight-bold">{{stats.account.followers_this_year}}</span> New Followers</p>
<p class="h3 font-weight-light mb-0">You followed <span class="font-weight-bold">{{stats.account.followed_this_year}}</span> accounts this year!</p>
</div>
<div v-if="page == 7" class="text-center mw-500">
<div v-if="stats.account.hashtag">
<p class="h1 text-break">Your favourite hashtag was <span class="font-weight-bold">#{{stats.account.hashtag.name}}</span>.</p>
<p class="h3 font-weight-light mb-0">You used it <span class="font-weight-bold">{{stats.account.hashtag.count}}</span> times!</p>
</div>
<div v-else>
<p class="h1 text-break">The most popular hashtag was <span class="font-weight-bold">#{{stats.popular.hashtag.name}}</span></p>
<p class="h3 font-weight-light mb-0">It was used <span class="font-weight-bold">{{stats.popular.hashtag.count}}</span> times!</p>
</div>
</div>
<div v-if="page == 8" class="text-center mw-500">
<p class="display-4">You tagged <span class="font-weight-bold">{{stats.account.places_total}}</span> places.</p>
<p v-if="stats.account.places_total" class="h3 font-weight-light mb-0">You tagged <span class="font-weight-bold">{{stats.account.places.name}}</span> a total of <span class="font-weight-bold">{{stats.account.places.count}}</span> times!</p>
<p v-else class="h3 font-weight-light mb-0">The most tagged place was <span class="font-weight-bold">{{stats.popular.places.name}}</span> that was tagged a total of <span class="font-weight-bold">{{stats.popular.places.count}}</span> times!</p>
</div>
<div v-if="page == 9" class="text-center">
<p class="display-4">Happy 2021!</p>
<p class="h3 font-weight-light mb-0">We wish you the best in the new year.</p>
</div>
</div>
<div v-if="loaded" class="fixed-top">
<p class="text-center mt-3 d-flex justify-content-center align-items-center mb-0">
<img src="/img/pixelfed-icon-grey.svg" width="60" height="60">
<span class="text-light font-weight-bold ml-3" style="font-size: 22px;">#my2020</span>
</p>
</div>
<div v-if="loaded" class="fixed-bottom">
<p class="text-center">
<a v-if="!notEnoughData" :class="prevClass()" href="#" @click.prevent="prevPage()" :disabled="page == 1"><i class="fas fa-chevron-left"></i> Back</a>
<a class="btn btn-outline-light rounded-pill mx-3" href="/">Back to Pixelfed</a>
<a v-if="!notEnoughData" :class="nextClass()" href="#" @click.prevent="nextPage()">Next <i class="fas fa-chevron-right"></i></a>
</p>
</div>
</div>
</template>
<style type="text/css" scoped>
.md-line-height {
line-height: 1.65 !important;
}
.mw-500 {
max-width: 500px;
}
</style>
<script type="text/javascript">
export default {
data() {
return {
config: window.App.config,
user: {},
loggedIn: false,
loaded: false,
page: 1,
stats: [],
notEnoughData: false,
reportedView: false
}
},
mounted() {
let u = new URLSearchParams(window.location.search);
if( u.has('v') &&
u.has('ned') &&
u.has('sl') &&
u.get('v') == 20 &&
u.get('sl') >= 1 &&
u.get('sl') <= 9
) {
if(u.get('ned') == 0) {
this.page = u.get('sl');
} else {
this.notEnoughData = true;
}
}
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
.then(res => {
this.user = res.data;
window._sharedData.curUser = res.data;
});
this.fetchData();
},
updated() {
},
methods: {
fetchData() {
axios.get('/api/pixelfed/v2/seasonal/yir')
.then(res => {
this.stats = res.data;
this.loaded = true;
this.shortcuts();
})
},
nextPage() {
if(this.page == 9) {
return;
}
if(this.page == 7 && this.stats.popular.places == null) {
this.page = 9;
window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=9');
return;
}
if(this.page == 8) {
axios.post('/api/pixelfed/v2/seasonal/yir', {
'profile_id' : this.user.profile_id
})
}
++this.page;
window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=' + this.page);
},
prevPage() {
if(this.page == 1) {
return;
}
if(this.page == 9 && this.stats.popular.places == null) {
this.page = 7;
window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=7');
return;
}
--this.page;
if(this.page == 1) {
window.history.pushState({}, {}, '/i/my2020');
} else {
window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=' + this.page);
}
},
prevClass() {
return this.page == 1
? 'btn btn-outline-muted rounded-pill'
: 'btn btn-outline-light rounded-pill';
},
nextClass() {
return this.page == 9
? 'btn btn-outline-muted rounded-pill'
: 'btn btn-outline-light rounded-pill';
},
dateFormat(d) {
},
shortcuts() {
let self = this;
window.addEventListener("keydown", function(event) {
if (event.defaultPrevented) {
return;
}
switch(event.code) {
case "KeyA":
case "ArrowLeft":
self.prevPage();
break;
case "KeyD":
case "ArrowRight":
self.nextPage();
break;
}
event.preventDefault();
}, true);
}
}
}
</script>

View file

@ -45,7 +45,14 @@
</div>
<div class="col-12 col-md-8 px-0 mx-0">
<div class="postPresenterContainer d-none d-flex justify-content-center align-items-center" style="background: #000;">
<div v-if="status.pf_type === 'photo'" class="w-100">
<div v-if="status.pf_type === 'text'" class="w-100">
<div class="w-100 card-img-top border-bottom rounded-0" style="background-image: url(/storage/textimg/bg_1.jpg);background-size: cover;width: 100%;height: 540px;">
<div class="w-100 h-100 d-flex justify-content-center align-items-center">
<p class="text-center text-break h3 px-5 font-weight-bold" v-html="status.content"></p>
</div>
</div>
</div>
<div v-else-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
</div>
@ -104,7 +111,7 @@
</div>
<div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;">
<div class="card-body status-comments pt-0">
<div class="status-comment">
<div v-if="status.pf_type != 'text'" class="status-comment">
<div v-if="status.content.length" class="pt-3">
<div v-if="showCaption != true">
<span class="py-3">
@ -839,12 +846,13 @@ export default {
beforeMount() {
let u = new URLSearchParams(window.location.search);
let forceMetro = localStorage.getItem('pf_metro_ui.exp.forceMetro') == 'true';
if(this.statusTemplate == 'text') {
this.layout = 'metro';
return;
}
if(forceMetro == true || u.has('ui') && u.get('ui') == 'metro' && this.layout != 'metro') {
this.layout = 'metro';
}
if(u.has('ui') && u.get('ui') == 'moment' && this.layout != 'moment') {
this.layout = 'moment';
}
},
mounted() {
@ -897,15 +905,8 @@ export default {
}, 3000);
setTimeout(function() {
self.fetchState();
document.querySelectorAll('.status-comment .comment-text a').forEach(function(i, e) {
if(i.href.startsWith(window.location.origin)) {
return;
}
let tag = i.innerText;
if(tag.startsWith('#')) {
tag = tag.substr(1);
}
i.href = '/discover/tags/'+tag+'?src=rph';
document.querySelectorAll('.status-comment .postCommentsContainer .comment-body a').forEach(function(i, e) {
i.href = App.util.format.rewriteLinks(i);
});
}, 500);
}).catch(error => {
@ -1252,15 +1253,8 @@ export default {
$('.postCommentsLoader').addClass('d-none');
$('.postCommentsContainer').removeClass('d-none');
setTimeout(function() {
document.querySelectorAll('.comments .comment-body a').forEach(function(i, e) {
if(i.href.startsWith(window.location.origin)) {
return;
}
let tag = i.innerText;
if(tag.startsWith('#')) {
tag = tag.substr(1);
}
i.href = '/discover/tags/'+tag+'?src=rph';
document.querySelectorAll('.status-comment .postCommentsContainer .comment-body a').forEach(function(i, e) {
i.href = App.util.format.rewriteLinks(i);
});
}, 500);
}).catch(error => {

View file

@ -1,5 +1,12 @@
<template>
<div class="w-100 h-100">
<div v-if="owner && layout == 'moment'">
<div class="bg-primary shadow">
<p class="text-center text-white mb-0 py-3 font-weight-bold border-bottom border-info">
<i class="fas fa-exclamation-triangle fa-lg mr-2"></i> The Moment UI layout has been deprecated and will be removed in a future release.
</p>
</div>
</div>
<div v-if="isMobile" class="bg-white p-3 border-bottom">
<div class="d-flex justify-content-between align-items-center">
<div @click="goBack" class="cursor-pointer">
@ -679,10 +686,7 @@
if(forceMetro == true || u.has('ui') && u.get('ui') == 'metro' && this.layout != 'metro') {
this.layout = 'metro';
}
if(u.has('ui') && u.get('ui') == 'moment' && this.layout != 'moment') {
Vue.use(VueMasonry);
this.layout = 'moment';
}
if(this.layout == 'metro' && u.has('t')) {
if(this.modes.indexOf(u.get('t')) != -1) {
if(u.get('t') == 'bookmarks') {

View file

@ -627,15 +627,8 @@ export default {
}, 3000);
setTimeout(function() {
self.fetchState();
document.querySelectorAll('.status-comment .comment-text a').forEach(function(i, e) {
if(i.href.startsWith(window.location.origin)) {
return;
}
let tag = i.innerText;
if(tag.startsWith('#')) {
tag = tag.substr(1);
}
i.href = '/discover/tags/'+tag+'?src=rph';
document.querySelectorAll('.status-comment .postCommentsContainer .comment-body a').forEach(function(i, e) {
i.href = App.util.format.rewriteLinks(i);
});
}, 500);
}).catch(error => {
@ -977,15 +970,8 @@ export default {
$('.postCommentsLoader').addClass('d-none');
$('.postCommentsContainer').removeClass('d-none');
setTimeout(function() {
document.querySelectorAll('.comments .comment-body a').forEach(function(i, e) {
if(i.href.startsWith(window.location.origin)) {
return;
}
let tag = i.innerText;
if(tag.startsWith('#')) {
tag = tag.substr(1);
}
i.href = '/discover/tags/'+tag+'?src=rph';
document.querySelectorAll('.status-comment .postCommentsContainer .comment-body a').forEach(function(i, e) {
i.href = App.util.format.rewriteLinks(i);
});
}, 500);
}).catch(error => {

View file

@ -253,7 +253,7 @@
shares: status.reblogs_count,
comments: status.reply_count
},
thumb: status.media_attachments[0].preview_url,
thumb: status.media_attachments[0].url,
media: status.media_attachments,
timestamp: status.created_at,
type: status.pf_type,

View file

@ -33,7 +33,7 @@
<div class="pb-2">
<div class="media align-items-center py-2">
<div class="media-body text-truncate">
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
<i class="fas fa-map-marker-alt text-lighter mr-2"></i> {{hashtag.value}}
</p>
</div>
@ -74,7 +74,7 @@
<i class="fas fa-hashtag text-muted"></i>
</span>
<div class="media-body text-truncate">
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
#{{hashtag.value}}
</p>
<p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
@ -99,7 +99,7 @@
<div class="media align-items-center py-2 pr-3">
<img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px">
<div class="media-body">
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
{{profile.value}}
</p>
<p class="mb-0 small font-weight-bold text-muted text-uppercase">

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,26 @@
<template>
<div v-if="status.sensitive == true">
<details class="details-animated">
<summary>
<p class="mb-0 lead font-weight-bold">{{ status.spoiler_text ? status.spoiler_text : 'CW / NSFW / Hidden Media'}}</p>
<p class="font-weight-light">(click to show)</p>
</summary>
<div class="max-hide-overflow" :title="status.media_attachments[0].description">
<img :class="status.media_attachments[0].filter_class + ' card-img-top'" :src="status.media_attachments[0].url" loading="lazy" :alt="altText(status)" onerror="this.onerror=null;this.src='/storage/no-preview.png'">
</div>
</details>
<div class="text-light content-label">
<p class="text-center">
<i class="far fa-eye-slash fa-2x"></i>
</p>
<p class="h4 font-weight-bold text-center">
Sensitive Content
</p>
<p class="text-center py-2">
This photo contains sensitive content which <br/>
some people may find offsensive or disturbing.
</p>
<p class="mb-0">
<button @click="status.sensitive = false" class="btn btn-outline-light btn-block btn-sm font-weight-bold">See Photo</button>
</p>
</div>
<blur-hash-image
width="32"
height="32"
punch="1"
:hash="status.media_attachments[0].blurhash"
:alt="altText(status)"/>
</div>
<div v-else>
<div :title="status.media_attachments[0].description">
@ -22,6 +34,14 @@
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.content-label {
margin: 0;
position: absolute;
top:45%;
left:50%;
z-index: 999;
transform: translate(-50%, -50%);
}
</style>
<script type="text/javascript">

4
resources/assets/js/my2020.js vendored Normal file
View file

@ -0,0 +1,4 @@
Vue.component(
'my-yearreview',
require('./components/My2020.vue').default
);

View file

@ -0,0 +1,10 @@
@extends('layouts.blank')
@section('content')
<my-yearreview></my-yearreview>
@endsection
@push('scripts')
<script type="text/javascript" src="{{mix('js/my2020.js')}}"></script>
<script type="text/javascript">App.boot();</script>
@endpush

View file

@ -118,7 +118,6 @@
value: "unlisted",
},
cw: {
text: autocw == 0 ? "CW Media" : "Remove AutoCW",
text: autocw == 0 ? "CW Media" : "Remove AutoCW",
className: "bg-warning",
value: "autocw",

View file

@ -56,26 +56,24 @@
@stack('scripts')
<div class="d-block d-sm-none mt-5"></div>
<div class="d-block d-sm-none fixed-bottom">
<div class="card card-body rounded-0 py-2 d-flex align-items-middle box-shadow" style="border-top:1px solid #F1F5F8">
<ul class="nav nav-pills nav-fill">
<div class="card card-body rounded-0 py-2 box-shadow" style="border-top:1px solid #F1F5F8">
<ul class="nav nav-pills nav-fill d-flex align-items-middle">
<li class="nav-item">
<a class="nav-link {{request()->is('/')?'text-dark':'text-lighter'}}" href="/"><i class="fas fa-home fa-lg"></i></a>
<a class="nav-link text-dark" href="/"><i class="fas fa-home fa-lg"></i></a>
</li>
<li class="nav-item">
<a class="nav-link {{request()->is('discover')?'text-dark':'text-lighter'}}" href="/discover"><i class="fas fa-search fa-lg"></i></a>
<a class="nav-link text-dark" href="/discover"><i class="fas fa-search fa-lg"></i></a>
</li>
<li class="nav-item">
<div class="nav-link text-primary cursor-pointer" onclick="App.util.compose.post()">
<span class="border border-primary rounded p-2 bg-primary">
<i class="fas fa-camera fa-lg text-white" style="color:#fff !important;"></i>
</span>
<div class="nav-link cursor-pointer text-dark" onclick="App.util.compose.post()">
<i class="far fa-plus-square fa-2x"></i>
</div>
</li>
<li class="nav-item">
<a class="nav-link {{request()->is('account/activity')?'text-dark':'text-lighter'}}" href="/account/activity"><i class="far fa-heart fa-lg"></i></a>
<a class="nav-link text-dark" href="/account/activity"><i class="far fa-bell fa-lg"></i></a>
</li>
<li class="nav-item">
<a class="nav-link text-lighter" href="/i/me"><i class="far fa-user fa-lg"></i></a>
<a class="nav-link text-dark" href="/i/me"><i class="far fa-user fa-lg"></i></a>
</li>
</ul>
</div>

View file

@ -39,10 +39,10 @@
</a>
</li>
<li class="nav-item px-md-2 d-none d-md-block">
<a class="nav-link font-weight-bold text-dark" href="/?a=co" title="Compose" data-toggle="tooltip" data-placement="bottom">
<div class="nav-link font-weight-bold text-dark cursor-pointer" title="Compose" data-toggle="tooltip" data-placement="bottom" onclick="App.util.compose.post()">
<i class="far fa-plus-square fa-lg"></i>
<span class="sr-only">Compose</span>
</a>
</div>
</li>
<li class="nav-item px-md-2">
<a class="nav-link font-weight-bold text-dark" href="/account/direct" title="Direct" data-toggle="tooltip" data-placement="bottom">
@ -52,7 +52,7 @@
</li>
<li class="nav-item px-md-2 d-none d-md-block">
<a class="nav-link font-weight-bold text-dark" href="/account/activity" title="Notifications" data-toggle="tooltip" data-placement="bottom">
<i class="far fa-bell fa-lg"></i>
<i class="far fa-bell fa-lg" style="vertical-align: middle;"></i>
<span class="sr-only">Notifications</span>
</a>
</li>
@ -64,10 +64,6 @@
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="d-block d-md-none dropdown-item font-weight-bold" href="/">
<span class="fas fa-home pr-2 text-lighter"></span>
Home
</a>
<a class="dropdown-item font-weight-bold" href="{{route('discover')}}">
<span class="far fa-compass pr-2 text-lighter"></span>
{{__('navmenu.discover')}}

View file

@ -0,0 +1,26 @@
@extends('site.help.partial.template', ['breadcrumb'=>'Instance Actor'])
@section('section')
<div class="title">
<h3 class="font-weight-bold">Instance Actor</h3>
</div>
<hr>
<p class="lead">We use a special account type known as an Instance Actor to fetch content securely with other servers in the fediverse.</p>
<div class="py-4">
<p class="font-weight-bold h5 pb-3">For Instance Admins</p>
<p class="mb-0">If you are an instance admin that found this URL in a request or profile, this account is used to fetch content from remote instances using signed requests (HTTP Signatures) to enforce domain block compatibility with other instances.</p>
</div>
<hr>
<div class="card bg-primary border-primary" style="box-shadow: none !important;border: 3px solid #08d!important;">
<div class="card-header text-light font-weight-bold h4 p-4 bg-primary">Instance Actor Tips</div>
<div class="card-body bg-white p-3">
<ul class="pt-3">
<li class="lead mb-4">The Instance Actor will not appear in search results.</li>
<li class="lead mb-4">You cannot follow an Instance Actor.</li>
<li class="lead mb-4">The Instance Actor does not follow accounts.</li>
<li class="lead">The Instance Actor account does not post or share content from users.</li>
</ul>
</div>
</div>
@endsection

View file

@ -4,7 +4,7 @@
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 px-0">
<div class="card mt-md-5 px-0 mx-md-3">
<div class="card mt-md-5 px-0 mx-md-3 shadow-none border">
<div class="card-header font-weight-bold text-muted bg-white py-4">
<a href="{{route('site.help')}}" class="text-muted">{{__('helpcenter.helpcenter')}}</a>
<span class="px-2 font-weight-light">&mdash;</span>

View file

@ -2,12 +2,28 @@
@section('content')
<div class="alert alert-info text-center rounded-0">
<div class="container">
<span class="font-weight-bold">ComposeUI v3 is deprecated</span>
<br>
Please use the <a href="#" onclick="event.preventDefault();window.App.util.compose.post()" class="font-weight-bold">new UI</a> to compose a post.
</div>
<div class="row">
<div class="col-12 col-md-6 offset-md-3 mt-md-3 px-0">
<compose-modal></compose-modal>
</div>
</div>
</div>
@endsection
@endsection
@push('styles')
<style type="text/css">
.card {
box-shadow: none;
border: 1px solid #ddd;
}
.card .card-header .fas.fa-times {
color: #fff;
}
</style>
@endpush
@push('scripts')
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript">window.App.boot()</script>
@endpush

View file

@ -2,10 +2,13 @@
use Illuminate\Http\Request;
$middleware = ['auth:api','twofactor','validemail','localization', 'throttle:60,1'];
$middleware = ['auth:api','twofactor','validemail','throttle:60,1','interstitial'];
Route::post('/f/inbox', 'FederationController@sharedInbox');
Route::post('/users/{username}/inbox', 'FederationController@userInbox');
Route::get('i/actor', 'InstanceActorController@profile');
Route::post('i/actor/inbox', 'InstanceActorController@inbox');
Route::get('i/actor/outbox', 'InstanceActorController@outbox');
Route::group(['prefix' => 'api'], function() use($middleware) {

View file

@ -98,11 +98,28 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover/loops', 'DiscoverController@showLoops');
Route::get('discover/profiles', 'DiscoverController@profilesDirectory')->name('discover.profiles');
Route::group(['prefix' => 'api'], function () {
Route::get('search', 'SearchController@searchAPI');
Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
Route::group(['prefix' => 'compose'], function() {
Route::group(['prefix' => 'v0'], function() {
Route::post('/media/upload', 'ComposeController@mediaUpload');
Route::post('/media/update', 'ComposeController@mediaUpdate')
->middleware('throttle:maxComposeMediaUpdatesPerHour,60')
->middleware('throttle:maxComposeMediaUpdatesPerDay,1440')
->middleware('throttle:maxComposeMediaUpdatesPerMonth,43800');
Route::delete('/media/delete', 'ComposeController@mediaDelete');
Route::get('/search/tag', 'ComposeController@searchTag');
Route::get('/search/location', 'ComposeController@searchLocation');
Route::post('/publish', 'ComposeController@store')
->middleware('throttle:maxPostsPerHour,60')
->middleware('throttle:maxPostsPerDay,1440');
Route::post('/publish/text', 'ComposeController@storeText');
});
});
Route::group(['prefix' => 'direct'], function () {
Route::get('browse', 'DirectMessageController@browse');
Route::post('create', 'DirectMessageController@create');
@ -130,7 +147,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('loops', 'DiscoverController@loopsApi');
Route::post('loops/watch', 'DiscoverController@loopWatch');
Route::get('discover/tag', 'DiscoverController@getHashtags');
Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
});
Route::group(['prefix' => 'pixelfed'], function() {
@ -176,25 +192,13 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover/posts/trending', 'DiscoverController@trendingApi');
Route::get('discover/posts/hashtags', 'DiscoverController@trendingHashtags');
Route::get('discover/posts/places', 'DiscoverController@trendingPlaces');
Route::get('seasonal/yir', 'SeasonalController@getData');
Route::post('seasonal/yir', 'SeasonalController@store');
});
});
Route::group(['prefix' => 'local'], function () {
// Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials');
// Route::get('accounts/relationships', 'PublicApiController@relationships');
// Route::get('accounts/{id}/statuses', 'PublicApiController@accountStatuses');
// Route::get('accounts/{id}/following', 'PublicApiController@accountFollowing');
// Route::get('accounts/{id}/followers', 'PublicApiController@accountFollowers');
// Route::get('accounts/{id}', 'PublicApiController@account');
// Route::post('avatar/update', 'ApiController@avatarUpdate');
// Route::get('likes', 'ApiController@hydrateLikes');
// Route::post('media', 'ApiController@uploadMedia');
// Route::delete('media', 'ApiController@deleteMedia');
// Route::get('notifications', 'ApiController@notifications');
// Route::get('timelines/public', 'PublicApiController@publicTimelineApi');
// Route::get('timelines/home', 'PublicApiController@homeTimelineApi');
Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
// Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
Route::get('exp/rec', 'ApiController@userRecommendations');
Route::post('discover/tag/subscribe', 'HashtagFollowController@store')->middleware('throttle:maxHashtagFollowsPerHour,60')->middleware('throttle:maxHashtagFollowsPerDay,1440');
Route::get('discover/tag/list', 'HashtagFollowController@getTags');
@ -209,9 +213,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::post('collection/{id}/publish', 'CollectionController@publish')->middleware('throttle:maxCollectionsPerHour,60')->middleware('throttle:maxCollectionsPerDay,1440')->middleware('throttle:maxCollectionsPerMonth,43800');
Route::get('profile/collections/{id}', 'CollectionController@getUserCollections');
Route::post('compose/media/update/{id}', 'MediaController@composeUpdate')->middleware('throttle:maxComposeMediaUpdatesPerHour,60')->middleware('throttle:maxComposeMediaUpdatesPerDay,1440')->middleware('throttle:maxComposeMediaUpdatesPerMonth,43800');
Route::get('compose/location/search', 'ApiController@composeLocationSearch');
Route::get('compose/tag/search', 'MediaTagController@usernameLookup');
Route::post('compose/tag/untagme', 'MediaTagController@untagProfile');
});
Route::group(['prefix' => 'admin'], function () {
@ -308,6 +310,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('warning', 'AccountInterstitialController@get');
Route::post('warning', 'AccountInterstitialController@read');
Route::get('my2020', 'SeasonalController@yearInReview');
});
Route::group(['prefix' => 'account'], function () {
@ -440,6 +443,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::view('stories', 'site.help.stories')->name('help.stories');
Route::view('embed', 'site.help.embed')->name('help.embed');
Route::view('hashtags', 'site.help.hashtags')->name('help.hashtags');
Route::view('instance-actor', 'site.help.instance-actor')->name('help.instance-actor');
Route::view('discover', 'site.help.discover')->name('help.discover');
Route::view('direct-messages', 'site.help.dm')->name('help.dm');
Route::view('timelines', 'site.help.timelines')->name('help.timelines');

View file

@ -1,4 +1,5 @@
*
!public/
!remcache/
!cities.json
!.gitignore

View file

@ -1,4 +1,5 @@
*
!.gitignore
!no-preview.png
!m/
!m/
!textimg/

3
storage/app/public/textimg/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*
!.gitignore
!bg_1.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

2
storage/app/remcache/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore