mirror of
https://github.com/pixelfed/pixelfed.git
synced 2025-02-02 01:40:46 +00:00
commit
4deec1f6c1
92 changed files with 15130 additions and 1127 deletions
55
CHANGELOG.md
55
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
515
app/Http/Controllers/ComposeController.php
Normal file
515
app/Http/Controllers/ComposeController.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
98
app/Jobs/AvatarPipeline/RemoteAvatarFetch.php
Normal file
98
app/Jobs/AvatarPipeline/RemoteAvatarFetch.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
31
app/Jobs/MediaPipeline/MediaStoragePipeline.php
Normal file
31
app/Jobs/MediaPipeline/MediaStoragePipeline.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}";
|
||||
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
230
app/Services/MediaStorageService.php
Normal file
230
app/Services/MediaStorageService.php
Normal 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);
|
||||
}
|
||||
}
|
28
app/Services/StatusLabelService.php
Normal file
28
app/Services/StatusLabelService.php
Normal 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'])
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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'),
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
|
|
@ -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
296
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
],
|
||||
],
|
||||
],
|
||||
|
|
|
@ -70,7 +70,7 @@ return [
|
|||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'client' => env('REDIS_CLIENT', 'predis'),
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'default' => [
|
||||
'scheme' => env('REDIS_SCHEME', 'tcp'),
|
||||
|
|
|
@ -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')
|
||||
]
|
||||
],
|
||||
];
|
|
@ -23,7 +23,7 @@ return [
|
|||
| This value is the version of your Pixelfed instance.
|
||||
|
|
||||
*/
|
||||
'version' => '0.10.9',
|
||||
'version' => '0.10.10',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -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
12255
package-lock.json
generated
File diff suppressed because it is too large
Load diff
BIN
public/js/app.js
vendored
BIN
public/js/app.js
vendored
Binary file not shown.
BIN
public/js/components.js
vendored
BIN
public/js/components.js
vendored
Binary file not shown.
BIN
public/js/compose.js
vendored
BIN
public/js/compose.js
vendored
Binary file not shown.
BIN
public/js/my2020.js
vendored
Normal file
BIN
public/js/my2020.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile-directory.js
vendored
BIN
public/js/profile-directory.js
vendored
Binary file not shown.
BIN
public/js/profile.js
vendored
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/quill.js
vendored
BIN
public/js/quill.js
vendored
Binary file not shown.
BIN
public/js/rempos.js
vendored
BIN
public/js/rempos.js
vendored
Binary file not shown.
BIN
public/js/rempro.js
vendored
BIN
public/js/rempro.js
vendored
Binary file not shown.
BIN
public/js/search.js
vendored
BIN
public/js/search.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/story-compose.js
vendored
BIN
public/js/story-compose.js
vendored
Binary file not shown.
BIN
public/js/theme-monokai.js
vendored
BIN
public/js/theme-monokai.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
BIN
public/js/vendor.js
vendored
BIN
public/js/vendor.js
vendored
Binary file not shown.
Binary file not shown.
18
resources/assets/js/app.js
vendored
18
resources/assets/js/app.js
vendored
|
@ -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: [
|
||||
|
|
5
resources/assets/js/components.js
vendored
5
resources/assets/js/components.js
vendored
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
239
resources/assets/js/components/My2020.vue
Normal file
239
resources/assets/js/components/My2020.vue
Normal 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>
|
|
@ -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 => {
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
@ -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
4
resources/assets/js/my2020.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
Vue.component(
|
||||
'my-yearreview',
|
||||
require('./components/My2020.vue').default
|
||||
);
|
10
resources/views/account/yir.blade.php
Normal file
10
resources/views/account/yir.blade.php
Normal 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
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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')}}
|
||||
|
|
26
resources/views/site/help/instance-actor.blade.php
Normal file
26
resources/views/site/help/instance-actor.blade.php
Normal 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
|
|
@ -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">—</span>
|
||||
|
|
|
@ -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
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
1
storage/app/.gitignore
vendored
1
storage/app/.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
*
|
||||
!public/
|
||||
!remcache/
|
||||
!cities.json
|
||||
!.gitignore
|
||||
|
|
3
storage/app/public/.gitignore
vendored
3
storage/app/public/.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
*
|
||||
!.gitignore
|
||||
!no-preview.png
|
||||
!m/
|
||||
!m/
|
||||
!textimg/
|
3
storage/app/public/textimg/.gitignore
vendored
Normal file
3
storage/app/public/textimg/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!.gitignore
|
||||
!bg_1.jpg
|
BIN
storage/app/public/textimg/bg_1.jpg
Normal file
BIN
storage/app/public/textimg/bg_1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
2
storage/app/remcache/.gitignore
vendored
Normal file
2
storage/app/remcache/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
Loading…
Reference in a new issue