mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-20 20:13:17 +00:00
Merge pull request #547 from pixelfed/frontend-ui-refactor
[WIP] The big one.
This commit is contained in:
commit
a3d80971bc
150 changed files with 5890 additions and 919 deletions
|
@ -22,7 +22,7 @@ jobs:
|
|||
- checkout
|
||||
|
||||
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
|
||||
- run: sudo docker-php-ext-install bcmath pcntl zip
|
||||
- run: sudo docker-php-ext-install pcntl
|
||||
|
||||
# Download and cache dependencies
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class Follower extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['profile_id', 'following_id', 'local_profile'];
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
|
@ -21,9 +24,9 @@ class Follower extends Model
|
|||
return $this->belongsTo(Profile::class, 'following_id', 'id');
|
||||
}
|
||||
|
||||
public function permalink()
|
||||
public function permalink($append = null)
|
||||
{
|
||||
$path = $this->actor->permalink("/follow/{$this->id}");
|
||||
$path = $this->actor->permalink("/follow/{$this->id}{$append}");
|
||||
return url($path);
|
||||
}
|
||||
|
||||
|
|
|
@ -64,10 +64,13 @@ class AccountController extends Controller
|
|||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$action = $request->input('a');
|
||||
$allowed = ['like', 'follow'];
|
||||
$timeago = Carbon::now()->subMonths(3);
|
||||
$following = $profile->following->pluck('id');
|
||||
$notifications = Notification::whereIn('actor_id', $following)
|
||||
->where('profile_id', '!=', $profile->id)
|
||||
->whereIn('action', $allowed)
|
||||
->where('actor_id', '<>', $profile->id)
|
||||
->where('profile_id', '<>', $profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('notifications.created_at', 'desc')
|
||||
->simplePaginate(30);
|
||||
|
|
|
@ -2,22 +2,27 @@
|
|||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Avatar;
|
||||
use App\Http\Controllers\AvatarController;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\AvatarPipeline\AvatarOptimize;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\Transformer\Api\AccountTransformer;
|
||||
use App\Transformer\Api\MediaTransformer;
|
||||
use App\Transformer\Api\StatusTransformer;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use App\Http\Controllers\{
|
||||
Controller,
|
||||
AvatarController
|
||||
};
|
||||
use Auth, Cache, URL;
|
||||
use App\{Avatar,Media,Profile};
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
MediaTransformer,
|
||||
StatusTransformer
|
||||
};
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Jobs\AvatarPipeline\AvatarOptimize;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\VideoPipeline\{
|
||||
VideoOptimize,
|
||||
VideoPostProcess,
|
||||
VideoThumbnail
|
||||
};
|
||||
|
||||
class BaseApiController extends Controller
|
||||
{
|
||||
|
@ -187,7 +192,20 @@ class BaseApiController extends Controller
|
|||
$url = URL::temporarySignedRoute(
|
||||
'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id]
|
||||
);
|
||||
ImageOptimize::dispatch($media);
|
||||
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
ImageOptimize::dispatch($media);
|
||||
break;
|
||||
|
||||
case 'video/mp4':
|
||||
VideoThumbnail::dispatch($media);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$res = [
|
||||
'id' => $media->id,
|
||||
|
|
10
app/Http/Controllers/CollectionController.php
Normal file
10
app/Http/Controllers/CollectionController.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CollectionController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
10
app/Http/Controllers/CollectionItemController.php
Normal file
10
app/Http/Controllers/CollectionItemController.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CollectionItemController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
55
app/Http/Controllers/DirectMessageController.php
Normal file
55
app/Http/Controllers/DirectMessageController.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use App\{
|
||||
DirectMessage,
|
||||
Profile,
|
||||
Status
|
||||
};
|
||||
|
||||
class DirectMessageController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function inbox(Request $request)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
$inbox = DirectMessage::whereToId($profile->id)
|
||||
->with(['author','status'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->groupBy('from_id')
|
||||
->paginate(10);
|
||||
return view('account.messages', compact('inbox'));
|
||||
|
||||
}
|
||||
|
||||
public function show(Request $request, int $pid, $mid)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
if($pid !== $profile->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$msg = DirectMessage::whereToId($profile->id)
|
||||
->findOrFail($mid);
|
||||
|
||||
$thread = DirectMessage::whereToId($profile->id)
|
||||
->orWhere([['from_id', $profile->id],['to_id', $msg->from_id]])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(10);
|
||||
|
||||
return view('account.message', compact('msg', 'profile', 'thread'));
|
||||
}
|
||||
|
||||
public function compose(Request $request)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
}
|
||||
}
|
|
@ -21,53 +21,7 @@ class DiscoverController extends Controller
|
|||
|
||||
public function home(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|integer|max:50'
|
||||
]);
|
||||
|
||||
$pid = Auth::user()->profile->id;
|
||||
|
||||
$following = Cache::remember('feature:discover:following:'.$pid, 720, function() use($pid) {
|
||||
$following = Follower::select('following_id')
|
||||
->whereProfileId($pid)
|
||||
->pluck('following_id');
|
||||
$filtered = UserFilter::select('filterable_id')
|
||||
->whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id');
|
||||
$following->push($pid);
|
||||
|
||||
if($filtered->count() > 0) {
|
||||
$following->push($filtered);
|
||||
}
|
||||
return $following;
|
||||
});
|
||||
|
||||
$people = Cache::remember('feature:discover:people:'.$pid, 15, function() use($following) {
|
||||
return Profile::select('id', 'name', 'username')->inRandomOrder()
|
||||
->whereHas('statuses')
|
||||
->whereNull('domain')
|
||||
->whereNotIn('id', $following)
|
||||
->whereIsPrivate(false)
|
||||
->take(3)
|
||||
->get();
|
||||
});
|
||||
|
||||
$posts = Status::select('id', 'caption', 'profile_id')
|
||||
->whereHas('media')
|
||||
->whereHas('profile', function($q) {
|
||||
$q->where('is_private', false);
|
||||
})
|
||||
->whereIsNsfw(false)
|
||||
->whereVisibility('public')
|
||||
->where('profile_id', '<>', $pid)
|
||||
->whereNotIn('profile_id', $following)
|
||||
->withCount(['comments', 'likes'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->simplePaginate(21);
|
||||
|
||||
return view('discover.home', compact('people', 'posts'));
|
||||
return view('discover.home');
|
||||
}
|
||||
|
||||
public function showTags(Request $request, $hashtag)
|
||||
|
@ -82,13 +36,17 @@ class DiscoverController extends Controller
|
|||
->firstOrFail();
|
||||
|
||||
$posts = $tag->posts()
|
||||
->whereHas('media')
|
||||
->withCount(['likes', 'comments'])
|
||||
->whereIsNsfw(false)
|
||||
->whereVisibility('public')
|
||||
->has('media')
|
||||
->orderBy('id', 'desc')
|
||||
->simplePaginate(12);
|
||||
|
||||
if($posts->count() == 0) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('discover.tags.show', compact('tag', 'posts'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ use Cache;
|
|||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Fractal;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
|
||||
class FederationController extends Controller
|
||||
{
|
||||
|
@ -133,6 +134,19 @@ class FederationController extends Controller
|
|||
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function hostMeta(Request $request)
|
||||
{
|
||||
$path = route('well-known.webfinger');
|
||||
$xml = <<<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
<Link rel="lrdd" type="application/xrd+xml" template="{$path}?resource={uri}"/>
|
||||
</XRD>
|
||||
XML;
|
||||
|
||||
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
||||
}
|
||||
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
if (config('pixelfed.activitypub_enabled') == false) {
|
||||
|
@ -153,6 +167,42 @@ class FederationController extends Controller
|
|||
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
return;
|
||||
// todo
|
||||
}
|
||||
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
if (config('pixelfed.activitypub_enabled') == false) {
|
||||
abort(403);
|
||||
}
|
||||
$profile = Profile::whereNull('remote_url')->whereUsername($username)->firstOrFail();
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => $profile->following()->count(),
|
||||
'orderedItems' => $profile->following->map(function($f) {
|
||||
return $f->permalink();
|
||||
})
|
||||
];
|
||||
return response()->json($obj);
|
||||
}
|
||||
|
||||
public function userFollowers(Request $request, $username)
|
||||
{
|
||||
if (config('pixelfed.activitypub_enabled') == false) {
|
||||
abort(403);
|
||||
}
|
||||
$profile = Profile::whereNull('remote_url')->whereUsername($username)->firstOrFail();
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => $profile->followers()->count(),
|
||||
'orderedItems' => $profile->followers->map(function($f) {
|
||||
return $f->permalink();
|
||||
})
|
||||
];
|
||||
return response()->json($obj);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
|
@ -19,7 +21,7 @@ class HomeController extends Controller
|
|||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('home');
|
||||
}
|
||||
|
|
151
app/Http/Controllers/Import/Instagram.php
Normal file
151
app/Http/Controllers/Import/Instagram.php
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Import;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Auth, DB;
|
||||
use App\{
|
||||
ImportData,
|
||||
ImportJob,
|
||||
Profile,
|
||||
User
|
||||
};
|
||||
|
||||
trait Instagram
|
||||
{
|
||||
public function instagram()
|
||||
{
|
||||
return view('settings.import.instagram.home');
|
||||
}
|
||||
|
||||
public function instagramStart(Request $request)
|
||||
{
|
||||
$job = $this->instagramRedirectOrNew();
|
||||
return redirect($job->url());
|
||||
}
|
||||
|
||||
protected function instagramRedirectOrNew()
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
$exists = ImportJob::whereProfileId($profile->id)
|
||||
->whereService('instagram')
|
||||
->whereNull('completed_at')
|
||||
->exists();
|
||||
if($exists) {
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereService('instagram')
|
||||
->whereNull('completed_at')
|
||||
->first();
|
||||
} else {
|
||||
$job = new ImportJob;
|
||||
$job->profile_id = $profile->id;
|
||||
$job->service = 'instagram';
|
||||
$job->uuid = (string) Str::uuid();
|
||||
$job->stage = 1;
|
||||
$job->save();
|
||||
}
|
||||
return $job;
|
||||
}
|
||||
|
||||
public function instagramStepOne(Request $request, $uuid)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereNull('completed_at')
|
||||
->whereUuid($uuid)
|
||||
->whereStage(1)
|
||||
->firstOrFail();
|
||||
return view('settings.import.instagram.step-one', compact('profile', 'job'));
|
||||
}
|
||||
|
||||
public function instagramStepOneStore(Request $request, $uuid)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'media.*' => 'required|mimes:bin,jpeg,png,gif|max:500',
|
||||
//'mediajson' => 'required|file|mimes:json'
|
||||
]);
|
||||
$media = $request->file('media');
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereNull('completed_at')
|
||||
->whereUuid($uuid)
|
||||
->whereStage(1)
|
||||
->firstOrFail();
|
||||
|
||||
foreach ($media as $k => $v) {
|
||||
$original = $v->getClientOriginalName();
|
||||
if(strlen($original) < 32 || $k > 100) {
|
||||
continue;
|
||||
}
|
||||
$storagePath = "import/{$job->uuid}";
|
||||
$path = $v->store($storagePath);
|
||||
DB::transaction(function() use ($profile, $job, $path, $original) {
|
||||
$data = new ImportData;
|
||||
$data->profile_id = $profile->id;
|
||||
$data->job_id = $job->id;
|
||||
$data->service = 'instagram';
|
||||
$data->path = $path;
|
||||
$data->stage = $job->stage;
|
||||
$data->original_name = $original;
|
||||
$data->save();
|
||||
});
|
||||
}
|
||||
DB::transaction(function() use ($profile, $job) {
|
||||
$job->stage = 2;
|
||||
$job->save();
|
||||
});
|
||||
return redirect($job->url());
|
||||
return view('settings.import.instagram.step-one', compact('profile', 'job'));
|
||||
}
|
||||
|
||||
public function instagramStepTwo(Request $request, $uuid)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereNull('completed_at')
|
||||
->whereUuid($uuid)
|
||||
->whereStage(2)
|
||||
->firstOrFail();
|
||||
return view('settings.import.instagram.step-two', compact('profile', 'job'));
|
||||
}
|
||||
|
||||
public function instagramStepTwoStore(Request $request, $uuid)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'media' => 'required|file|max:1000'
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereNull('completed_at')
|
||||
->whereUuid($uuid)
|
||||
->whereStage(2)
|
||||
->firstOrFail();
|
||||
$media = $request->file('media');
|
||||
$file = file_get_contents($media);
|
||||
$json = json_decode($file, true);
|
||||
if(!$json || !isset($json['photos'])) {
|
||||
return abort(500);
|
||||
}
|
||||
$storagePath = "import/{$job->uuid}";
|
||||
$path = $media->store($storagePath);
|
||||
$job->media_json = $path;
|
||||
$job->stage = 3;
|
||||
$job->save();
|
||||
return redirect($job->url());
|
||||
return $json;
|
||||
|
||||
}
|
||||
|
||||
public function instagramStepThree(Request $request, $uuid)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereNull('completed_at')
|
||||
->whereUuid($uuid)
|
||||
->whereStage(3)
|
||||
->firstOrFail();
|
||||
return view('settings.import.instagram.step-three', compact('profile', 'job'));
|
||||
}
|
||||
}
|
13
app/Http/Controllers/Import/Mastodon.php
Normal file
13
app/Http/Controllers/Import/Mastodon.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Import;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait Mastodon
|
||||
{
|
||||
public function mastodon()
|
||||
{
|
||||
return view('settings.import.mastodon.home');
|
||||
}
|
||||
}
|
16
app/Http/Controllers/ImportController.php
Normal file
16
app/Http/Controllers/ImportController.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ImportController extends Controller
|
||||
{
|
||||
use Import\Instagram, Import\Mastodon;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
}
|
|
@ -4,12 +4,17 @@ namespace App\Http\Controllers;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{
|
||||
DirectMessage,
|
||||
Hashtag,
|
||||
Like,
|
||||
Media,
|
||||
Notification,
|
||||
Profile,
|
||||
StatusHashtag,
|
||||
Status,
|
||||
};
|
||||
use Auth,Cache;
|
||||
use Carbon\Carbon;
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
|
@ -30,60 +35,6 @@ class InternalApiController extends Controller
|
|||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
}
|
||||
|
||||
public function status(Request $request, $username, int $postid)
|
||||
{
|
||||
$auth = Auth::user()->profile;
|
||||
$profile = Profile::whereUsername($username)->first();
|
||||
$status = Status::whereProfileId($profile->id)->find($postid);
|
||||
$status = new Fractal\Resource\Item($status, new StatusTransformer());
|
||||
$user = new Fractal\Resource\Item($auth, new AccountTransformer());
|
||||
$res = [];
|
||||
$res['status'] = $this->fractal->createData($status)->toArray();
|
||||
$res['user'] = $this->fractal->createData($user)->toArray();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function statusComments(Request $request, $username, int $postId)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'min_id' => 'nullable|integer|min:1',
|
||||
'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
|
||||
'limit' => 'nullable|integer|min:5|max:50'
|
||||
]);
|
||||
$limit = $request->limit ?? 10;
|
||||
$auth = Auth::user()->profile;
|
||||
$profile = Profile::whereUsername($username)->first();
|
||||
$status = Status::whereProfileId($profile->id)->find($postId);
|
||||
if($request->filled('min_id') || $request->filled('max_id')) {
|
||||
$q = false;
|
||||
$limit = 50;
|
||||
if($request->filled('min_id')) {
|
||||
$replies = $status->comments()
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'created_at')
|
||||
->where('id', '>=', $request->min_id)
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
}
|
||||
if($request->filled('max_id')) {
|
||||
$replies = $status->comments()
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'created_at')
|
||||
->where('id', '<=', $request->max_id)
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
}
|
||||
} else {
|
||||
$replies = $status->comments()
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'created_at')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
}
|
||||
|
||||
$resource = new Fractal\Resource\Collection($replies, new StatusTransformer(), 'data');
|
||||
$resource->setPaginator(new IlluminatePaginatorAdapter($replies));
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function compose(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
|
@ -101,13 +52,15 @@ class InternalApiController extends Controller
|
|||
$attachments = [];
|
||||
$status = new Status;
|
||||
|
||||
foreach($medias as $media) {
|
||||
foreach($medias as $k => $media) {
|
||||
$m = Media::findOrFail($media['id']);
|
||||
if($m->profile_id !== $profile->id || $m->status_id) {
|
||||
abort(403, 'Invalid media id');
|
||||
}
|
||||
$m->filter_class = $media['filter'];
|
||||
$m->license = $media['license'];
|
||||
$m->caption = strip_tags($media['alt']);
|
||||
$m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
|
||||
if($media['cw'] == true) {
|
||||
$m->is_nsfw = true;
|
||||
$status->is_nsfw = true;
|
||||
|
@ -135,4 +88,116 @@ class InternalApiController extends Controller
|
|||
|
||||
return $status->url();
|
||||
}
|
||||
|
||||
public function notifications(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|min:1|max:3',
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$timeago = Carbon::now()->subMonths(6);
|
||||
$notifications = Notification::with('actor')
|
||||
->whereProfileId($profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id', 'desc')
|
||||
->simplePaginate(30);
|
||||
$notifications = $notifications->map(function($k, $v) {
|
||||
return [
|
||||
'id' => $k->id,
|
||||
'action' => $k->action,
|
||||
'message' => $k->message,
|
||||
'rendered' => $k->rendered,
|
||||
'actor' => [
|
||||
'avatar' => $k->actor->avatarUrl(),
|
||||
'username' => $k->actor->username,
|
||||
'url' => $k->actor->url(),
|
||||
],
|
||||
'url' => $k->item->url(),
|
||||
'read_at' => $k->read_at,
|
||||
];
|
||||
});
|
||||
return response()->json($notifications, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function discover(Request $request)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$following = Cache::get('feature:discover:following:'.$profile->id, []);
|
||||
$people = Profile::select('id', 'name', 'username')
|
||||
->with('avatar')
|
||||
->inRandomOrder()
|
||||
->whereHas('statuses')
|
||||
->whereNull('domain')
|
||||
->whereNotIn('id', $following)
|
||||
->whereIsPrivate(false)
|
||||
->take(3)
|
||||
->get();
|
||||
|
||||
$posts = Status::select('id', 'caption', 'profile_id')
|
||||
->whereHas('media')
|
||||
->whereHas('profile', function($q) {
|
||||
$q->where('is_private', false);
|
||||
})
|
||||
->whereIsNsfw(false)
|
||||
->whereVisibility('public')
|
||||
->where('profile_id', '<>', $profile->id)
|
||||
->whereNotIn('profile_id', $following)
|
||||
->withCount(['comments', 'likes'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->take(21)
|
||||
->get();
|
||||
|
||||
$res = [
|
||||
'people' => $people->map(function($profile) {
|
||||
return [
|
||||
'avatar' => $profile->avatarUrl(),
|
||||
'name' => $profile->name,
|
||||
'username' => $profile->username,
|
||||
'url' => $profile->url(),
|
||||
];
|
||||
}),
|
||||
'posts' => $posts->map(function($post) {
|
||||
return [
|
||||
'url' => $post->url(),
|
||||
'thumb' => $post->thumb(),
|
||||
'comments_count' => $post->comments_count,
|
||||
'likes_count' => $post->likes_count,
|
||||
];
|
||||
})
|
||||
];
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
public function directMessage(Request $request, $profileId, $threadId)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
if($profileId != $profile->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$msg = DirectMessage::whereToId($profile->id)
|
||||
->orWhere('from_id',$profile->id)
|
||||
->findOrFail($threadId);
|
||||
|
||||
$thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id])
|
||||
->whereIn('from_id', [$profile->id,$msg->from_id])
|
||||
->orderBy('created_at', 'asc')
|
||||
->paginate(30);
|
||||
|
||||
return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function notificationMarkAllRead(Request $request)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$notifications = Notification::whereProfileId($profile->id)->get();
|
||||
foreach($notifications as $n) {
|
||||
$n->read_at = Carbon::now();
|
||||
$n->save();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ class ProfileController extends Controller
|
|||
if ($user->remote_url) {
|
||||
$settings = new \StdClass;
|
||||
$settings->crawlable = false;
|
||||
$settings->show_profile_follower_count = true;
|
||||
$settings->show_profile_following_count = true;
|
||||
} else {
|
||||
$settings = User::whereUsername($username)->firstOrFail()->settings;
|
||||
}
|
||||
|
|
103
app/Http/Controllers/PublicApiController.php
Normal file
103
app/Http/Controllers/PublicApiController.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{
|
||||
Hashtag,
|
||||
Like,
|
||||
Media,
|
||||
Notification,
|
||||
Profile,
|
||||
StatusHashtag,
|
||||
Status,
|
||||
};
|
||||
use Auth,Cache;
|
||||
use Carbon\Carbon;
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
StatusTransformer,
|
||||
};
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
|
||||
|
||||
class PublicApiController extends Controller
|
||||
{
|
||||
protected $fractal;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('throttle:200, 15');
|
||||
$this->fractal = new Fractal\Manager();
|
||||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
}
|
||||
|
||||
protected function getUserData()
|
||||
{
|
||||
if(false == Auth::check()) {
|
||||
return [];
|
||||
} else {
|
||||
$profile = Auth::user()->profile;
|
||||
$user = new Fractal\Resource\Item($profile, new AccountTransformer());
|
||||
return $this->fractal->createData($user)->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function status(Request $request, $username, int $postid)
|
||||
{
|
||||
$profile = Profile::whereUsername($username)->first();
|
||||
$status = Status::whereProfileId($profile->id)->find($postid);
|
||||
$item = new Fractal\Resource\Item($status, new StatusTransformer());
|
||||
$res = [
|
||||
'status' => $this->fractal->createData($item)->toArray(),
|
||||
'user' => $this->getUserData(),
|
||||
'reactions' => [
|
||||
'liked' => $status->liked(),
|
||||
'shared' => $status->shared(),
|
||||
'bookmarked' => $status->bookmarked(),
|
||||
],
|
||||
];
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function statusComments(Request $request, $username, int $postId)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'min_id' => 'nullable|integer|min:1',
|
||||
'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
|
||||
'limit' => 'nullable|integer|min:5|max:50'
|
||||
]);
|
||||
$limit = $request->limit ?? 10;
|
||||
$profile = Profile::whereUsername($username)->first();
|
||||
$status = Status::whereProfileId($profile->id)->find($postId);
|
||||
if($request->filled('min_id') || $request->filled('max_id')) {
|
||||
if($request->filled('min_id')) {
|
||||
$replies = $status->comments()
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at')
|
||||
->where('id', '>=', $request->min_id)
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
}
|
||||
if($request->filled('max_id')) {
|
||||
$replies = $status->comments()
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at')
|
||||
->where('id', '<=', $request->max_id)
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
}
|
||||
} else {
|
||||
$replies = $status->comments()
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
}
|
||||
|
||||
$resource = new Fractal\Resource\Collection($replies, new StatusTransformer(), 'data');
|
||||
$resource->setPaginator(new IlluminatePaginatorAdapter($replies));
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
|
@ -38,7 +38,10 @@ class SearchController extends Controller
|
|||
});
|
||||
$tokens->push($tags);
|
||||
}
|
||||
$users = Profile::select('username', 'name', 'id')->where('username', 'like', '%'.$tag.'%')->limit(20)->get();
|
||||
$users = Profile::select('username', 'name', 'id')
|
||||
->where('username', 'like', '%'.$tag.'%')
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
if($users->count() > 0) {
|
||||
$profiles = $users->map(function ($item, $key) {
|
||||
|
@ -71,7 +74,7 @@ class SearchController extends Controller
|
|||
'count' => 0,
|
||||
'url' => $item->url(),
|
||||
'type' => 'status',
|
||||
'value' => 'Posted '.$item->created_at->diffForHumans(),
|
||||
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
|
||||
'tokens' => [$item->caption],
|
||||
'name' => $item->caption,
|
||||
'thumb' => $item->thumb(),
|
||||
|
|
|
@ -11,6 +11,7 @@ use App\UserFilter;
|
|||
use App\Util\Lexer\PrettyNumber;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Purify;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait HomeSettings
|
||||
|
@ -40,8 +41,8 @@ trait HomeSettings
|
|||
]);
|
||||
|
||||
$changes = false;
|
||||
$name = $request->input('name');
|
||||
$bio = $request->input('bio');
|
||||
$name = strip_tags($request->input('name'));
|
||||
$bio = $request->filled('bio') ? Purify::clean($request->input('bio')) : null;
|
||||
$website = $request->input('website');
|
||||
$email = $request->input('email');
|
||||
$user = Auth::user();
|
||||
|
@ -79,12 +80,12 @@ trait HomeSettings
|
|||
$profile->name = $name;
|
||||
}
|
||||
|
||||
if (!$profile->website || $profile->website != $website) {
|
||||
if ($profile->website != $website) {
|
||||
$changes = true;
|
||||
$profile->website = $website;
|
||||
}
|
||||
|
||||
if (!$profile->bio || !$profile->bio != $bio) {
|
||||
if ($profile->bio != $bio) {
|
||||
$changes = true;
|
||||
$profile->bio = $bio;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use App\UserFilter;
|
|||
use Auth;
|
||||
use DB;
|
||||
use Cache;
|
||||
use Purify;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Settings\{
|
||||
HomeSettings,
|
||||
|
|
|
@ -33,12 +33,19 @@ class SiteController extends Controller
|
|||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
// TODO: Use redis for timelines
|
||||
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
||||
$following->push(Auth::user()->profile->id);
|
||||
$filtered = UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id');
|
||||
$following = Cache::rememberForever("user:following:list:$pid", function() use($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
$following->push($pid);
|
||||
return $following->toArray();
|
||||
});
|
||||
|
||||
$filtered = Cache::rememberForever("user:filter:list:$pid", function() use($pid) {
|
||||
return UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id')->toArray();
|
||||
});
|
||||
|
||||
$timeline = Status::whereIn('profile_id', $following)
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereHas('media')
|
||||
|
@ -53,29 +60,22 @@ class SiteController extends Controller
|
|||
|
||||
public function changeLocale(Request $request, $locale)
|
||||
{
|
||||
if (!App::isLocale($locale)) {
|
||||
return redirect()->back();
|
||||
// todo: add other locales after pushing new l10n strings
|
||||
$locales = ['en'];
|
||||
if(in_array($locale, $locales)) {
|
||||
session()->put('locale', $locale);
|
||||
}
|
||||
App::setLocale($locale);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function about()
|
||||
{
|
||||
$res = Cache::remember('site:page:about', 15, function () {
|
||||
$statuses = Status::whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->count();
|
||||
$statusCount = PrettyNumber::convert($statuses);
|
||||
$userCount = PrettyNumber::convert(User::count());
|
||||
$remoteCount = PrettyNumber::convert(Profile::whereNotNull('remote_url')->count());
|
||||
$adminContact = User::whereIsAdmin(true)->first();
|
||||
return view('site.about');
|
||||
}
|
||||
|
||||
return view('site.about')->with(compact('statusCount', 'userCount', 'remoteCount', 'adminContact'))->render();
|
||||
});
|
||||
|
||||
return $res;
|
||||
public function language()
|
||||
{
|
||||
return view('site.language');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,7 @@ class StatusController extends Controller
|
|||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->where('visibility', '!=', 'draft')
|
||||
->withCount(['likes', 'comments', 'media'])
|
||||
->whereNotIn('visibility',['draft','direct'])
|
||||
->findOrFail($id);
|
||||
|
||||
if($status->visibility == 'private' || $user->is_private) {
|
||||
|
@ -40,34 +39,8 @@ class StatusController extends Controller
|
|||
return $this->showActivityPub($request, $status);
|
||||
}
|
||||
|
||||
$template = $this->detectTemplate($status);
|
||||
|
||||
$replies = Status::whereInReplyToId($status->id)->orderBy('created_at', 'desc')->simplePaginate(30);
|
||||
|
||||
return view($template, compact('user', 'status', 'replies'));
|
||||
}
|
||||
|
||||
protected function detectTemplate($status)
|
||||
{
|
||||
$template = Cache::rememberForever('template:status:type:'.$status->id, function () use ($status) {
|
||||
$template = 'status.show.photo';
|
||||
if (!$status->media_path && $status->in_reply_to_id) {
|
||||
$template = 'status.reply';
|
||||
}
|
||||
if ($status->media->count() > 1) {
|
||||
$template = 'status.show.album';
|
||||
}
|
||||
if ($status->viewType() == 'video') {
|
||||
$template = 'status.show.video';
|
||||
}
|
||||
if ($status->viewType() == 'video-album') {
|
||||
$template = 'status.show.video-album';
|
||||
}
|
||||
|
||||
return $template;
|
||||
});
|
||||
|
||||
return $template;
|
||||
$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
|
||||
return view($template, compact('user', 'status'));
|
||||
}
|
||||
|
||||
public function compose()
|
||||
|
@ -148,9 +121,7 @@ class StatusController extends Controller
|
|||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
abort(403);
|
||||
}
|
||||
$this->authCheck();
|
||||
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string',
|
||||
|
@ -168,6 +139,8 @@ class StatusController extends Controller
|
|||
|
||||
public function storeShare(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer',
|
||||
]);
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth, Cache;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TimelineController extends Controller
|
||||
{
|
||||
|
@ -17,39 +18,23 @@ class TimelineController extends Controller
|
|||
$this->middleware('twofactor');
|
||||
}
|
||||
|
||||
public function personal()
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
// TODO: Use redis for timelines
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
$following->push($pid);
|
||||
$filtered = UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id');
|
||||
$timeline = Status::whereIn('profile_id', $following)
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereVisibility('public')
|
||||
->orderBy('created_at', 'desc')
|
||||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(20);
|
||||
$type = 'personal';
|
||||
|
||||
return view('timeline.template', compact('timeline', 'type'));
|
||||
}
|
||||
|
||||
public function local()
|
||||
public function local(Request $request)
|
||||
{
|
||||
$this->validate($request,[
|
||||
'page' => 'nullable|integer|max:20'
|
||||
]);
|
||||
// TODO: Use redis for timelines
|
||||
// $timeline = Timeline::build()->local();
|
||||
$pid = Auth::user()->profile->id;
|
||||
|
||||
$filtered = UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id');
|
||||
$filtered = Cache::rememberForever("user:filter:list:$pid", function() use($pid) {
|
||||
return UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id')->toArray();
|
||||
});
|
||||
$private = Profile::whereIsPrivate(true)->pluck('id');
|
||||
$filtered = $filtered->merge($private);
|
||||
$filtered = array_merge($private->toArray(), $filtered);
|
||||
$timeline = Status::whereHas('media')
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereNull('in_reply_to_id')
|
||||
|
@ -57,7 +42,7 @@ class TimelineController extends Controller
|
|||
->whereVisibility('public')
|
||||
->withCount(['comments', 'likes'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->simplePaginate(20);
|
||||
->simplePaginate(10);
|
||||
$type = 'local';
|
||||
|
||||
return view('timeline.template', compact('timeline', 'type'));
|
||||
|
|
|
@ -58,6 +58,7 @@ class Kernel extends HttpKernel
|
|||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'dangerzone' => \App\Http\Middleware\DangerZone::class,
|
||||
'localization' => \App\Http\Middleware\Localization::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
|
|
23
app/Http/Middleware/Localization.php
Normal file
23
app/Http/Middleware/Localization.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure, Session;
|
||||
|
||||
class Localization
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if(Session::has('locale')) {
|
||||
app()->setLocale(Session::get('locale'));
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
115
app/Jobs/ImportPipeline/ImportInstagram.php
Normal file
115
app/Jobs/ImportPipeline/ImportInstagram.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\ImportPipeline;
|
||||
|
||||
use DB;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\{
|
||||
ImportJob,
|
||||
ImportData,
|
||||
Media,
|
||||
Profile,
|
||||
Status,
|
||||
};
|
||||
|
||||
class ImportInstagram implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $job;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ImportJob $job)
|
||||
{
|
||||
$this->job = $job;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$job = $this->job;
|
||||
$profile = $this->job->profile;
|
||||
$json = $job->mediaJson();
|
||||
$collection = $json['photos'];
|
||||
$files = $job->files;
|
||||
$monthHash = hash('sha1', date('Y').date('m'));
|
||||
$userHash = hash('sha1', $profile->id . (string) $profile->created_at);
|
||||
$fs = new Filesystem;
|
||||
|
||||
foreach($collection as $import)
|
||||
{
|
||||
$caption = $import['caption'];
|
||||
try {
|
||||
$min = Carbon::create(2010, 10, 6, 0, 0, 0);
|
||||
$taken_at = Carbon::parse($import['taken_at']);
|
||||
if(!$min->lt($taken_at)) {
|
||||
$taken_at = Carbon::now();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
|
||||
}
|
||||
$filename = last( explode('/', $import['path']) );
|
||||
$importData = ImportData::whereJobId($job->id)
|
||||
->whereOriginalName($filename)
|
||||
->firstOrFail();
|
||||
|
||||
if(is_file(storage_path("app/$importData->path")) == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::transaction(function() use(
|
||||
$fs, $job, $profile, $caption, $taken_at, $filename,
|
||||
$monthHash, $userHash, $importData
|
||||
) {
|
||||
$status = new Status();
|
||||
$status->profile_id = $profile->id;
|
||||
$status->caption = strip_tags($caption);
|
||||
$status->is_nsfw = false;
|
||||
$status->visibility = 'public';
|
||||
$status->created_at = $taken_at;
|
||||
$status->save();
|
||||
|
||||
|
||||
$path = storage_path("app/$importData->path");
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$newPath = "app/$storagePath/$filename";
|
||||
$fs->move($path,storage_path($newPath));
|
||||
$path = $newPath;
|
||||
$hash = \hash_file('sha256', storage_path($path));
|
||||
$media = new Media();
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $profile->user->id;
|
||||
$media->media_path = "$storagePath/$filename";
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $fs->size(storage_path($path));
|
||||
$media->mime = $fs->mimeType(storage_path($path));
|
||||
$media->filter_class = null;
|
||||
$media->filter_name = null;
|
||||
$media->order = 1;
|
||||
$media->save();
|
||||
ImageOptimize::dispatch($media);
|
||||
NewStatusPipeline::dispatch($status);
|
||||
});
|
||||
}
|
||||
|
||||
$job->completed_at = Carbon::now();
|
||||
$job->save();
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ class InboxWorker implements ShouldQueue
|
|||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $request;
|
||||
protected $headers;
|
||||
protected $profile;
|
||||
protected $payload;
|
||||
|
||||
|
@ -23,9 +23,9 @@ class InboxWorker implements ShouldQueue
|
|||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($request, Profile $profile, $payload)
|
||||
public function __construct($headers, $profile, $payload)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->headers = $headers;
|
||||
$this->profile = $profile;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
@ -37,6 +37,6 @@ class InboxWorker implements ShouldQueue
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
(new Inbox($this->request, $this->profile, $this->payload))->handle();
|
||||
(new Inbox($this->headers, $this->profile, $this->payload))->handle();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ class LikePipeline implements ShouldQueue
|
|||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if ($status->url !== null) {
|
||||
// Ignore notifications to remote statuses
|
||||
if (!$status || $status->url !== null) {
|
||||
// Ignore notifications to remote statuses, or deleted statuses
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -207,6 +207,7 @@ class RemoteFollowImportRecent implements ShouldQueue
|
|||
|
||||
try {
|
||||
$info = pathinfo($url);
|
||||
$url = str_replace(' ', '%20', $url);
|
||||
$img = file_get_contents($url);
|
||||
$file = '/tmp/'.str_random(12).$info['basename'];
|
||||
file_put_contents($file, $img);
|
||||
|
|
|
@ -83,7 +83,7 @@ class RemoteFollowPipeline implements ShouldQueue
|
|||
$profile->domain = $domain;
|
||||
$profile->username = $remoteUsername;
|
||||
$profile->name = $res['name'];
|
||||
$profile->bio = str_limit($res['summary'], 125);
|
||||
$profile->bio = Purify::clean($res['summary']);
|
||||
$profile->sharedInbox = $res['endpoints']['sharedInbox'];
|
||||
$profile->remote_url = $res['url'];
|
||||
$profile->save();
|
||||
|
|
79
app/Jobs/SharePipeline/SharePipeline.php
Normal file
79
app/Jobs/SharePipeline/SharePipeline.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\SharePipeline;
|
||||
|
||||
use App\Status;
|
||||
use App\Notification;
|
||||
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 Log;
|
||||
use Redis;
|
||||
|
||||
class SharePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $like;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$actor = $this->status->profile;
|
||||
$target = $this->status->parent()->profile;
|
||||
|
||||
if ($status->url !== null) {
|
||||
// Ignore notifications to remote statuses
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('like')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->count();
|
||||
|
||||
if ($actor->id === $status->profile_id || $exists !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$notification = new Notification();
|
||||
$notification->profile_id = $status->profile_id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'like';
|
||||
$notification->message = $like->toText();
|
||||
$notification->rendered = $like->toHtml();
|
||||
$notification->item_id = $status->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
|
||||
Cache::forever('notification.'.$notification->id, $notification);
|
||||
|
||||
$redis = Redis::connection();
|
||||
$key = config('cache.prefix').':user.'.$status->profile_id.'.notifications';
|
||||
$redis->lpush($key, $notification->id);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ class NewStatusPipeline implements ShouldQueue
|
|||
$status = $this->status;
|
||||
|
||||
StatusEntityLexer::dispatch($status);
|
||||
//StatusActivityPubDeliver::dispatch($status);
|
||||
StatusActivityPubDeliver::dispatch($status);
|
||||
|
||||
Cache::forever('post.'.$status->id, $status);
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\CreateNote;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
|
||||
class StatusActivityPubDeliver implements ShouldQueue
|
||||
{
|
||||
|
@ -34,6 +38,18 @@ class StatusActivityPubDeliver implements ShouldQueue
|
|||
{
|
||||
$status = $this->status;
|
||||
|
||||
$audience = $status->profile->getAudienceInbox();
|
||||
$profile = $status->profile;
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($status, new CreateNote());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
foreach($audience as $url) {
|
||||
Helpers::sendSignedObject($profile, $url, $activity);
|
||||
}
|
||||
|
||||
// todo: fanout on write
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use App\Notification;
|
||||
use App\Status;
|
||||
use App\StatusHashtag;
|
||||
use App\{
|
||||
Notification,
|
||||
Report,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
@ -73,6 +76,9 @@ class StatusDelete implements ShouldQueue
|
|||
->whereItemId($status->id)
|
||||
->delete();
|
||||
StatusHashtag::whereStatusId($status->id)->delete();
|
||||
Report::whereObjectType('App\Status')
|
||||
->whereObjectId($status->id)
|
||||
->delete();
|
||||
$status->delete();
|
||||
|
||||
return true;
|
||||
|
|
|
@ -69,7 +69,7 @@ class StatusEntityLexer implements ShouldQueue
|
|||
$this->storeMentions();
|
||||
DB::transaction(function () {
|
||||
$status = $this->status;
|
||||
$status->rendered = $this->autolink;
|
||||
$status->rendered = nl2br($this->autolink);
|
||||
$status->entities = json_encode($this->entities);
|
||||
$status->save();
|
||||
});
|
||||
|
|
34
app/Jobs/VideoPipeline/VideoOptimize.php
Normal file
34
app/Jobs/VideoPipeline/VideoOptimize.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\VideoPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class VideoOptimize implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
34
app/Jobs/VideoPipeline/VideoPostProcess.php
Normal file
34
app/Jobs/VideoPipeline/VideoPostProcess.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\VideoPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class VideoPostProcess implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
62
app/Jobs/VideoPipeline/VideoThumbnail.php
Normal file
62
app/Jobs/VideoPipeline/VideoThumbnail.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\VideoPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use FFMpeg;
|
||||
use App\Media;
|
||||
|
||||
class VideoThumbnail implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Media $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$media = $this->media;
|
||||
$base = $media->media_path;
|
||||
$path = explode('/', $base);
|
||||
$name = last($path);
|
||||
try {
|
||||
$t = explode('.', $name);
|
||||
$t = $t[0].'_thumb.png';
|
||||
$i = count($path) - 1;
|
||||
$path[$i] = $t;
|
||||
$save = implode('/', $path);
|
||||
$video = FFMpeg::open($base);
|
||||
if($video->getDurationInSeconds() < 1) {
|
||||
$video->getFrameFromSeconds(0);
|
||||
} elseif($video->getDurationInSeconds() < 5) {
|
||||
$video->getFrameFromSeconds(4);
|
||||
}
|
||||
$video->export()
|
||||
->save($save);
|
||||
|
||||
$media->thumbnail_path = $save;
|
||||
$media->save();
|
||||
|
||||
} catch (Exception $e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ class Like extends Model
|
|||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
protected $fillable = ['profile_id', 'status_id'];
|
||||
|
||||
public function actor()
|
||||
{
|
||||
|
|
|
@ -19,8 +19,12 @@ class Media extends Model
|
|||
|
||||
public function url()
|
||||
{
|
||||
$path = $this->media_path;
|
||||
$url = Storage::url($path);
|
||||
if(!empty($this->remote_media) && $this->remote_url) {
|
||||
$url = $this->remote_url;
|
||||
} else {
|
||||
$path = $this->media_path;
|
||||
$url = Storage::url($path);
|
||||
}
|
||||
|
||||
return url($url);
|
||||
}
|
||||
|
@ -60,4 +64,15 @@ class Media extends Model
|
|||
{
|
||||
return json_decode($this->metadata, true, 3);
|
||||
}
|
||||
|
||||
public function getModel()
|
||||
{
|
||||
if(empty($this->metadata)) {
|
||||
return false;
|
||||
}
|
||||
$meta = $this->getMetadata();
|
||||
if($meta && isset($meta['Model'])) {
|
||||
return $meta['Model'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,9 @@ class Profile extends Model
|
|||
|
||||
public function permalink($suffix = '')
|
||||
{
|
||||
if($this->remote_url) {
|
||||
return $this->remote_url;
|
||||
}
|
||||
return url('users/'.$this->username.$suffix);
|
||||
}
|
||||
|
||||
|
@ -248,4 +251,44 @@ class Profile extends Model
|
|||
{
|
||||
return $this->sharedInbox ?? $this->inboxUrl();
|
||||
}
|
||||
|
||||
public function getDefaultScope()
|
||||
{
|
||||
return $this->is_private == true ? 'private' : 'public';
|
||||
}
|
||||
|
||||
public function getAudience($scope = false)
|
||||
{
|
||||
if($this->remote_url) {
|
||||
return [];
|
||||
}
|
||||
$scope = $scope ?? $this->getDefaultScope();
|
||||
$audience = [];
|
||||
switch ($scope) {
|
||||
case 'public':
|
||||
$audience = [
|
||||
'to' => [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc' => [
|
||||
$this->permalink('/followers')
|
||||
]
|
||||
];
|
||||
break;
|
||||
}
|
||||
return $audience;
|
||||
}
|
||||
|
||||
public function getAudienceInbox($scope = 'public')
|
||||
{
|
||||
return $this
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
->get()
|
||||
->map(function($follow) {
|
||||
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||
})
|
||||
->unique()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,8 +65,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
*/
|
||||
protected function mapApiRoutes()
|
||||
{
|
||||
Route::prefix('api')
|
||||
->middleware('api')
|
||||
Route::middleware('api')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/api.php'));
|
||||
}
|
||||
|
|
|
@ -22,14 +22,15 @@ class Report extends Model
|
|||
{
|
||||
$class = $this->object_type;
|
||||
switch ($class) {
|
||||
case 'App\Status':
|
||||
$column = 'id';
|
||||
break;
|
||||
case 'App\Status':
|
||||
$column = 'id';
|
||||
break;
|
||||
|
||||
default:
|
||||
$column = 'id';
|
||||
break;
|
||||
}
|
||||
default:
|
||||
$class = 'App\Status';
|
||||
$column = 'id';
|
||||
break;
|
||||
}
|
||||
|
||||
return (new $class())->where($column, $this->object_id)->firstOrFail();
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ class Status extends Model
|
|||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
protected $fillable = ['profile_id', 'visibility'];
|
||||
protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id'];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
|
@ -52,7 +52,7 @@ class Status extends Model
|
|||
{
|
||||
$type = $this->viewType();
|
||||
$is_nsfw = !$showNsfw ? $this->is_nsfw : false;
|
||||
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['image', 'album'])) {
|
||||
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['image', 'album', 'video'])) {
|
||||
return 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
|
||||
}
|
||||
|
||||
|
@ -64,11 +64,6 @@ class Status extends Model
|
|||
$id = $this->id;
|
||||
$username = $this->profile->username;
|
||||
$path = config('app.url')."/p/{$username}/{$id}";
|
||||
if (!is_null($this->in_reply_to_id)) {
|
||||
$pid = $this->in_reply_to_id;
|
||||
$path = config('app.url')."/p/{$username}/{$pid}/c/{$id}";
|
||||
}
|
||||
|
||||
return url($path);
|
||||
}
|
||||
|
||||
|
@ -103,8 +98,10 @@ class Status extends Model
|
|||
|
||||
public function liked() : bool
|
||||
{
|
||||
if(Auth::check() == false) {
|
||||
return false;
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||
}
|
||||
|
||||
|
@ -116,7 +113,7 @@ class Status extends Model
|
|||
public function bookmarked()
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
|
@ -130,6 +127,9 @@ class Status extends Model
|
|||
|
||||
public function shared() : bool
|
||||
{
|
||||
if(Auth::check() == false) {
|
||||
return false;
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
return self::whereProfileId($profile->id)->whereReblogOfId($this->id)->count();
|
||||
|
@ -139,7 +139,7 @@ class Status extends Model
|
|||
{
|
||||
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
|
||||
if (!empty($parent)) {
|
||||
return self::findOrFail($parent);
|
||||
return $this->findOrFail($parent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,7 +254,7 @@ class Status extends Model
|
|||
'url' => $media->url(),
|
||||
'name' => null
|
||||
];
|
||||
})
|
||||
})->toArray()
|
||||
]
|
||||
];
|
||||
}
|
||||
|
@ -268,18 +268,25 @@ class Status extends Model
|
|||
$res['to'] = [];
|
||||
$res['cc'] = [];
|
||||
$scope = $this->scope;
|
||||
$mentions = $this->mentions->map(function ($mention) {
|
||||
return $mention->permalink();
|
||||
})->toArray();
|
||||
|
||||
switch ($scope) {
|
||||
case 'public':
|
||||
$res['to'] = [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
];
|
||||
$res['cc'] = [
|
||||
$this->profile->permalink('/followers')
|
||||
];
|
||||
$res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions);
|
||||
break;
|
||||
|
||||
default:
|
||||
# code...
|
||||
|
||||
case 'unlisted':
|
||||
break;
|
||||
|
||||
case 'private':
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
break;
|
||||
}
|
||||
return $res[$audience];
|
||||
|
|
10
app/Story.php
Normal file
10
app/Story.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Story extends Model
|
||||
{
|
||||
//
|
||||
}
|
10
app/StoryReaction.php
Normal file
10
app/StoryReaction.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class StoryReaction extends Model
|
||||
{
|
||||
//
|
||||
}
|
19
app/Transformer/ActivityPub/Verb/Announce.php
Normal file
19
app/Transformer/ActivityPub/Verb/Announce.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
|
||||
class Announce extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Announce',
|
||||
'actor' => $status->profile->permalink(),
|
||||
'object' => $status->parent()->url()
|
||||
];
|
||||
}
|
||||
}
|
70
app/Transformer/ActivityPub/Verb/CreateNote.php
Normal file
70
app/Transformer/ActivityPub/Verb/CreateNote.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
|
||||
class CreateNote extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Status $status)
|
||||
{
|
||||
|
||||
$mentions = $status->mentions->map(function ($mention) {
|
||||
return [
|
||||
'type' => 'Mention',
|
||||
'href' => $mention->permalink(),
|
||||
'name' => $mention->emailUrl()
|
||||
];
|
||||
})->toArray();
|
||||
$hashtags = $status->hashtags->map(function ($hashtag) {
|
||||
return [
|
||||
'type' => 'Hashtag',
|
||||
'href' => $hashtag->url(),
|
||||
'name' => "#{$hashtag->name}",
|
||||
];
|
||||
})->toArray();
|
||||
$tags = array_merge($mentions, $hashtags);
|
||||
|
||||
return [
|
||||
'@context' => [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
[
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
'featured' => [
|
||||
'https://pixelfed.org/ns#featured' => ['@type' => '@id'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'id' => $status->permalink(),
|
||||
'type' => 'Create',
|
||||
'actor' => $status->profile->permalink(),
|
||||
'published' => $status->created_at->toAtomString(),
|
||||
'to' => $status->scopeToAudience('to'),
|
||||
'cc' => $status->scopeToAudience('cc'),
|
||||
'object' => [
|
||||
'id' => $status->url(),
|
||||
'type' => 'Note',
|
||||
'summary' => null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
|
||||
'published' => $status->created_at->toAtomString(),
|
||||
'url' => $status->url(),
|
||||
'attributedTo' => $status->profile->permalink(),
|
||||
'to' => $status->scopeToAudience('to'),
|
||||
'cc' => $status->scopeToAudience('cc'),
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'attachment' => $status->media->map(function ($media) {
|
||||
return [
|
||||
'type' => 'Document',
|
||||
'mediaType' => $media->mime,
|
||||
'url' => $media->url(),
|
||||
'name' => null,
|
||||
];
|
||||
})->toArray(),
|
||||
'tag' => $tags,
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
19
app/Transformer/ActivityPub/Verb/Follow.php
Normal file
19
app/Transformer/ActivityPub/Verb/Follow.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\Follower;
|
||||
use League\Fractal;
|
||||
|
||||
class Follow extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Follower $follower)
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Follow',
|
||||
'actor' => $follower->actor->permalink(),
|
||||
'object' => $follower->target->permalink()
|
||||
];
|
||||
}
|
||||
}
|
19
app/Transformer/ActivityPub/Verb/Like.php
Normal file
19
app/Transformer/ActivityPub/Verb/Like.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\Like as LikeModel;
|
||||
use League\Fractal;
|
||||
|
||||
class Like extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(LikeModel $like)
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Like',
|
||||
'actor' => $like->actor->permalink(),
|
||||
'object' => $like->status->url()
|
||||
];
|
||||
}
|
||||
}
|
|
@ -17,7 +17,12 @@ class MediaTransformer extends Fractal\TransformerAbstract
|
|||
'preview_url' => $media->thumbnailUrl(),
|
||||
'text_url' => null,
|
||||
'meta' => $media->metadata,
|
||||
'description' => null,
|
||||
'description' => $media->caption,
|
||||
'license' => $media->license,
|
||||
'is_nsfw' => $media->is_nsfw,
|
||||
'orientation' => $media->orientation,
|
||||
'filter_name' => $media->filter_name,
|
||||
'filter_class' => $media->filter_class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
25
app/Transformer/Api/RelationshipTransformer.php
Normal file
25
app/Transformer/Api/RelationshipTransformer.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Profile;
|
||||
use League\Fractal;
|
||||
|
||||
class RelationshipTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
return [
|
||||
'id' => $profile->id,
|
||||
'following' => null,
|
||||
'followed_by' => null,
|
||||
'blocking' => null,
|
||||
'muting' => null,
|
||||
'muting_notifications' => null,
|
||||
'requested' => null,
|
||||
'domain_blocking' => null,
|
||||
'showing_reblogs' => null,
|
||||
'endorsed' => null
|
||||
];
|
||||
}
|
||||
}
|
|
@ -59,7 +59,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
|
||||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
$media = $status->media;
|
||||
$media = $status->media()->orderBy('order')->get();
|
||||
|
||||
return $this->collection($media, new MediaTransformer());
|
||||
}
|
||||
|
|
|
@ -2,18 +2,30 @@
|
|||
|
||||
namespace App\Util\ActivityPub;
|
||||
|
||||
use App\Like;
|
||||
use App\Profile;
|
||||
use Cache, DB, Log, Redis, Validator;
|
||||
use App\{
|
||||
Activity,
|
||||
Follower,
|
||||
FollowRequest,
|
||||
Like,
|
||||
Notification,
|
||||
Profile,
|
||||
Status
|
||||
};
|
||||
use Carbon\Carbon;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Jobs\LikePipeline\LikePipeline;
|
||||
|
||||
class Inbox
|
||||
{
|
||||
protected $request;
|
||||
protected $headers;
|
||||
protected $profile;
|
||||
protected $payload;
|
||||
protected $logger;
|
||||
|
||||
public function __construct($request, Profile $profile, $payload)
|
||||
public function __construct($headers, $profile, $payload)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->headers = $headers;
|
||||
$this->profile = $profile;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
@ -25,15 +37,31 @@ class Inbox
|
|||
|
||||
public function authenticatePayload()
|
||||
{
|
||||
// todo
|
||||
try {
|
||||
$signature = Helpers::validateSignature($this->headers, $this->payload);
|
||||
$payload = Helpers::validateObject($this->payload);
|
||||
if($signature == false) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return;
|
||||
}
|
||||
$this->payloadLogger();
|
||||
}
|
||||
|
||||
public function payloadLogger()
|
||||
{
|
||||
$logger = new Activity;
|
||||
$logger->data = json_encode($this->payload);
|
||||
$logger->save();
|
||||
$this->logger = $logger;
|
||||
Log::info('AP:inbox:activity:new:'.$this->logger->id);
|
||||
$this->handleVerb();
|
||||
}
|
||||
|
||||
public function handleVerb()
|
||||
{
|
||||
$verb = $this->payload['type'];
|
||||
|
||||
switch ($verb) {
|
||||
case 'Create':
|
||||
$this->handleCreateActivity();
|
||||
|
@ -43,43 +71,254 @@ class Inbox
|
|||
$this->handleFollowActivity();
|
||||
break;
|
||||
|
||||
case 'Announce':
|
||||
$this->handleAnnounceActivity();
|
||||
break;
|
||||
|
||||
case 'Accept':
|
||||
$this->handleAcceptActivity();
|
||||
break;
|
||||
|
||||
case 'Delete':
|
||||
$this->handleDeleteActivity();
|
||||
break;
|
||||
|
||||
case 'Like':
|
||||
$this->handleLikeActivity();
|
||||
break;
|
||||
|
||||
case 'Reject':
|
||||
$this->handleRejectActivity();
|
||||
break;
|
||||
|
||||
case 'Undo':
|
||||
$this->handleUndoActivity();
|
||||
break;
|
||||
|
||||
default:
|
||||
// TODO: decide how to handle invalid verbs.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function handleCreateActivity()
|
||||
public function verifyNoteAttachment()
|
||||
{
|
||||
// todo
|
||||
}
|
||||
$activity = $this->payload['object'];
|
||||
|
||||
public function handleFollowActivity()
|
||||
{
|
||||
$actor = $this->payload['object'];
|
||||
$target = $this->profile;
|
||||
if(isset($activity['inReplyTo']) &&
|
||||
!empty($activity['inReplyTo']) &&
|
||||
Helpers::validateUrl($activity['inReplyTo'])
|
||||
) {
|
||||
// reply detected, skip attachment check
|
||||
return true;
|
||||
}
|
||||
|
||||
$valid = Helpers::verifyAttachments($activity);
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
public function actorFirstOrCreate($actorUrl)
|
||||
{
|
||||
if (Profile::whereRemoteUrl($actorUrl)->count() !== 0) {
|
||||
return Profile::whereRemoteUrl($actorUrl)->firstOrFail();
|
||||
return Helpers::profileFirstOrNew($actorUrl);
|
||||
}
|
||||
|
||||
public function handleCreateActivity()
|
||||
{
|
||||
$activity = $this->payload['object'];
|
||||
if(!$this->verifyNoteAttachment()) {
|
||||
return;
|
||||
}
|
||||
if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) {
|
||||
$this->handleNoteReply();
|
||||
|
||||
} elseif($activity['type'] == 'Note' && !empty($activity['attachment'])) {
|
||||
$this->handleNoteCreate();
|
||||
}
|
||||
}
|
||||
|
||||
public function handleNoteReply()
|
||||
{
|
||||
$activity = $this->payload['object'];
|
||||
$actor = $this->actorFirstOrCreate($this->payload['actor']);
|
||||
$inReplyTo = $activity['inReplyTo'];
|
||||
|
||||
if(!Helpers::statusFirstOrFetch($activity['url'], true)) {
|
||||
$this->logger->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
$res = (new DiscoverActor($url))->discover();
|
||||
$this->logger->to_id = $this->profile->id;
|
||||
$this->logger->from_id = $actor->id;
|
||||
$this->logger->processed_at = Carbon::now();
|
||||
$this->logger->save();
|
||||
}
|
||||
|
||||
$domain = parse_url($res['url'], PHP_URL_HOST);
|
||||
$username = $res['preferredUsername'];
|
||||
$remoteUsername = "@{$username}@{$domain}";
|
||||
public function handleNoteCreate()
|
||||
{
|
||||
$activity = $this->payload['object'];
|
||||
$actor = $this->actorFirstOrCreate($this->payload['actor']);
|
||||
if(!$actor || $actor->domain == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$profile = new Profile();
|
||||
$profile->user_id = null;
|
||||
$profile->domain = $domain;
|
||||
$profile->username = $remoteUsername;
|
||||
$profile->name = $res['name'];
|
||||
$profile->bio = str_limit($res['summary'], 125);
|
||||
$profile->sharedInbox = $res['endpoints']['sharedInbox'];
|
||||
$profile->remote_url = $res['url'];
|
||||
$profile->save();
|
||||
if(Helpers::userInAudience($this->profile, $this->payload) == false) {
|
||||
//Log::error('AP:inbox:userInAudience:false - Activity#'.$this->logger->id);
|
||||
$logger = Activity::find($this->logger->id);
|
||||
$logger->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
if(Status::whereUrl($activity['url'])->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$status = DB::transaction(function() use($activity, $actor) {
|
||||
$status = new Status;
|
||||
$status->profile_id = $actor->id;
|
||||
$status->caption = strip_tags($activity['content']);
|
||||
$status->visibility = $status->scope = 'public';
|
||||
$status->url = $activity['url'];
|
||||
$status->save();
|
||||
return $status;
|
||||
});
|
||||
|
||||
Helpers::importNoteAttachment($activity, $status);
|
||||
|
||||
$logger = Activity::find($this->logger->id);
|
||||
$logger->to_id = $this->profile->id;
|
||||
$logger->from_id = $actor->id;
|
||||
$logger->processed_at = Carbon::now();
|
||||
$logger->save();
|
||||
}
|
||||
|
||||
public function handleFollowActivity()
|
||||
{
|
||||
$actor = $this->actorFirstOrCreate($this->payload['actor']);
|
||||
if(!$actor || $actor->domain == null) {
|
||||
return;
|
||||
}
|
||||
$target = $this->profile;
|
||||
if($target->is_private == true) {
|
||||
// make follow request
|
||||
FollowRequest::firstOrCreate([
|
||||
'follower_id' => $actor->id,
|
||||
'following_id' => $target->id
|
||||
]);
|
||||
// todo: send notification
|
||||
} else {
|
||||
// store new follower
|
||||
$follower = Follower::firstOrCreate([
|
||||
'profile_id' => $actor->id,
|
||||
'following_id' => $target->id,
|
||||
'local_profile' => empty($actor->domain)
|
||||
]);
|
||||
if($follower->wasRecentlyCreated == false) {
|
||||
$this->logger->delete();
|
||||
return;
|
||||
}
|
||||
// send notification
|
||||
$notification = new Notification();
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'follow';
|
||||
$notification->message = $follower->toText();
|
||||
$notification->rendered = $follower->toHtml();
|
||||
$notification->item_id = $target->id;
|
||||
$notification->item_type = "App\Profile";
|
||||
$notification->save();
|
||||
|
||||
\Cache::forever('notification.'.$notification->id, $notification);
|
||||
|
||||
$redis = Redis::connection();
|
||||
|
||||
$nkey = config('cache.prefix').':user.'.$target->id.'.notifications';
|
||||
$redis->lpush($nkey, $notification->id);
|
||||
|
||||
// send Accept to remote profile
|
||||
$accept = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $follower->permalink('/accept'),
|
||||
'type' => 'Accept',
|
||||
'actor' => $target->permalink(),
|
||||
'object' => [
|
||||
'id' => $this->payload['id'],
|
||||
'type' => 'Follow',
|
||||
'actor' => $target->permalink(),
|
||||
'object' => $actor->permalink()
|
||||
]
|
||||
];
|
||||
Helpers::sendSignedObject($target, $actor->inbox_url, $accept);
|
||||
}
|
||||
$this->logger->to_id = $target->id;
|
||||
$this->logger->from_id = $actor->id;
|
||||
$this->logger->processed_at = Carbon::now();
|
||||
$this->logger->save();
|
||||
}
|
||||
|
||||
public function handleAnnounceActivity()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function handleAcceptActivity()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function handleDeleteActivity()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function handleLikeActivity()
|
||||
{
|
||||
$actor = $this->payload['actor'];
|
||||
$profile = self::actorFirstOrCreate($actor);
|
||||
$obj = $this->payload['object'];
|
||||
if(Helpers::validateLocalUrl($obj) == false) {
|
||||
return;
|
||||
}
|
||||
$status = Helpers::statusFirstOrFetch($obj);
|
||||
$like = Like::firstOrCreate([
|
||||
'profile_id' => $profile->id,
|
||||
'status_id' => $status->id
|
||||
]);
|
||||
|
||||
if($like->wasRecentlyCreated == false) {
|
||||
return;
|
||||
}
|
||||
LikePipeline::dispatch($like);
|
||||
$this->logger->to_id = $status->profile_id;
|
||||
$this->logger->from_id = $profile->id;
|
||||
$this->logger->processed_at = Carbon::now();
|
||||
$this->logger->save();
|
||||
}
|
||||
|
||||
|
||||
public function handleRejectActivity()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function handleUndoActivity()
|
||||
{
|
||||
$actor = $this->payload['actor'];
|
||||
$profile = self::actorFirstOrCreate($actor);
|
||||
$obj = $this->payload['object'];
|
||||
$status = Helpers::statusFirstOrFetch($obj['object']);
|
||||
|
||||
switch ($obj['type']) {
|
||||
case 'Like':
|
||||
Like::whereProfileId($profile->id)
|
||||
->whereStatusId($status->id)
|
||||
->delete();
|
||||
break;
|
||||
}
|
||||
|
||||
$this->logger->to_id = $status->profile_id;
|
||||
$this->logger->from_id = $profile->id;
|
||||
$this->logger->processed_at = Carbon::now();
|
||||
$this->logger->save();
|
||||
}
|
||||
}
|
||||
|
|
34
app/Util/HTTPSignatures/Algorithm.php
Executable file
34
app/Util/HTTPSignatures/Algorithm.php
Executable file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
abstract class Algorithm
|
||||
{
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return HmacAlgorithm
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function create($name)
|
||||
{
|
||||
switch ($name) {
|
||||
case 'hmac-sha1':
|
||||
return new HmacAlgorithm('sha1');
|
||||
break;
|
||||
case 'hmac-sha256':
|
||||
return new HmacAlgorithm('sha256');
|
||||
break;
|
||||
case 'rsa-sha1':
|
||||
return new RsaAlgorithm('sha1');
|
||||
break;
|
||||
case 'rsa-sha256':
|
||||
return new RsaAlgorithm('sha256');
|
||||
break;
|
||||
default:
|
||||
throw new AlgorithmException("No algorithm named '$name'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
7
app/Util/HTTPSignatures/AlgorithmException.php
Executable file
7
app/Util/HTTPSignatures/AlgorithmException.php
Executable file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class AlgorithmException extends Exception
|
||||
{
|
||||
}
|
19
app/Util/HTTPSignatures/AlgorithmInterface.php
Executable file
19
app/Util/HTTPSignatures/AlgorithmInterface.php
Executable file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
interface AlgorithmInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function name();
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string $data
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sign($key, $data);
|
||||
}
|
119
app/Util/HTTPSignatures/Context.php
Executable file
119
app/Util/HTTPSignatures/Context.php
Executable file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class Context
|
||||
{
|
||||
/** @var array */
|
||||
private $headers;
|
||||
|
||||
/** @var KeyStoreInterface */
|
||||
private $keyStore;
|
||||
|
||||
/** @var array */
|
||||
private $keys;
|
||||
|
||||
/** @var string */
|
||||
private $signingKeyId;
|
||||
|
||||
/** @var AlgorithmInterface */
|
||||
private $algorithm;
|
||||
|
||||
/**
|
||||
* @param array $args
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct($args)
|
||||
{
|
||||
if (isset($args['keys']) && isset($args['keyStore'])) {
|
||||
throw new Exception(__CLASS__.' accepts keys or keyStore but not both');
|
||||
} elseif (isset($args['keys'])) {
|
||||
// array of keyId => keySecret
|
||||
$this->keys = $args['keys'];
|
||||
} elseif (isset($args['keyStore'])) {
|
||||
$this->setKeyStore($args['keyStore']);
|
||||
}
|
||||
|
||||
// algorithm for signing; not necessary for verifying.
|
||||
if (isset($args['algorithm'])) {
|
||||
$this->algorithm = Algorithm::create($args['algorithm']);
|
||||
}
|
||||
// headers list for signing; not necessary for verifying.
|
||||
if (isset($args['headers'])) {
|
||||
$this->headers = $args['headers'];
|
||||
}
|
||||
|
||||
// signingKeyId specifies the key used for signing messages.
|
||||
if (isset($args['signingKeyId'])) {
|
||||
$this->signingKeyId = $args['signingKeyId'];
|
||||
} elseif (isset($args['keys']) && 1 === count($args['keys'])) {
|
||||
list($this->signingKeyId) = array_keys($args['keys']); // first key
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Signer
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function signer()
|
||||
{
|
||||
return new Signer(
|
||||
$this->signingKey(),
|
||||
$this->algorithm,
|
||||
$this->headerList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Verifier
|
||||
*/
|
||||
public function verifier()
|
||||
{
|
||||
return new Verifier($this->keyStore());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Key
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws KeyStoreException
|
||||
*/
|
||||
private function signingKey()
|
||||
{
|
||||
if (isset($this->signingKeyId)) {
|
||||
return $this->keyStore()->fetch($this->signingKeyId);
|
||||
} else {
|
||||
throw new Exception('no implicit or specified signing key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HeaderList
|
||||
*/
|
||||
private function headerList()
|
||||
{
|
||||
return new HeaderList($this->headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return KeyStore
|
||||
*/
|
||||
private function keyStore()
|
||||
{
|
||||
if (empty($this->keyStore)) {
|
||||
$this->keyStore = new KeyStore($this->keys);
|
||||
}
|
||||
|
||||
return $this->keyStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KeyStoreInterface $keyStore
|
||||
*/
|
||||
private function setKeyStore(KeyStoreInterface $keyStore)
|
||||
{
|
||||
$this->keyStore = $keyStore;
|
||||
}
|
||||
}
|
7
app/Util/HTTPSignatures/Exception.php
Executable file
7
app/Util/HTTPSignatures/Exception.php
Executable file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class Exception extends \Exception
|
||||
{
|
||||
}
|
41
app/Util/HTTPSignatures/GuzzleHttpSignatures.php
Normal file
41
app/Util/HTTPSignatures/GuzzleHttpSignatures.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use App\Util\HttpSignatures\Context;
|
||||
|
||||
class GuzzleHttpSignatures
|
||||
{
|
||||
/**
|
||||
* @param Context $context
|
||||
* @return HandlerStack
|
||||
*/
|
||||
public static function defaultHandlerFromContext(Context $context)
|
||||
{
|
||||
$stack = HandlerStack::create();
|
||||
$stack->push(self::middlewareFromContext($context));
|
||||
|
||||
return $stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $context
|
||||
* @return \Closure
|
||||
*/
|
||||
public static function middlewareFromContext(Context $context)
|
||||
{
|
||||
return function (callable $handler) use ($context)
|
||||
{
|
||||
return function (
|
||||
Request $request,
|
||||
array $options
|
||||
) use ($handler, $context)
|
||||
{
|
||||
$request = $context->signer()->sign($request);
|
||||
return $handler($request, $options);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
48
app/Util/HTTPSignatures/HeaderList.php
Executable file
48
app/Util/HTTPSignatures/HeaderList.php
Executable file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class HeaderList
|
||||
{
|
||||
/** @var array */
|
||||
public $names;
|
||||
|
||||
/**
|
||||
* @param array $names
|
||||
*/
|
||||
public function __construct(array $names)
|
||||
{
|
||||
$this->names = array_map(
|
||||
[$this, 'normalize'],
|
||||
$names
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
*
|
||||
* @return HeaderList
|
||||
*/
|
||||
public static function fromString($string)
|
||||
{
|
||||
return new static(explode(' ', $string));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function string()
|
||||
{
|
||||
return implode(' ', $this->names);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function normalize($name)
|
||||
{
|
||||
return strtolower($name);
|
||||
}
|
||||
}
|
36
app/Util/HTTPSignatures/HmacAlgorithm.php
Executable file
36
app/Util/HTTPSignatures/HmacAlgorithm.php
Executable file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class HmacAlgorithm implements AlgorithmInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $digestName;
|
||||
|
||||
/**
|
||||
* @param string $digestName
|
||||
*/
|
||||
public function __construct($digestName)
|
||||
{
|
||||
$this->digestName = $digestName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function name()
|
||||
{
|
||||
return sprintf('hmac-%s', $this->digestName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string $data
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sign($secret, $data)
|
||||
{
|
||||
return hash_hmac($this->digestName, $data, $secret, true);
|
||||
}
|
||||
}
|
260
app/Util/HTTPSignatures/Key.php
Executable file
260
app/Util/HTTPSignatures/Key.php
Executable file
|
@ -0,0 +1,260 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class Key
|
||||
{
|
||||
/** @var string */
|
||||
private $id;
|
||||
|
||||
/** @var string */
|
||||
private $secret;
|
||||
|
||||
/** @var resource */
|
||||
private $certificate;
|
||||
|
||||
/** @var resource */
|
||||
private $publicKey;
|
||||
|
||||
/** @var resource */
|
||||
private $privateKey;
|
||||
|
||||
/** @var string */
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param string|array $secret
|
||||
*/
|
||||
public function __construct($id, $item)
|
||||
{
|
||||
$this->id = $id;
|
||||
if (Key::hasX509Certificate($item) || Key::hasPublicKey($item)) {
|
||||
$publicKey = Key::getPublicKey($item);
|
||||
} else {
|
||||
$publicKey = null;
|
||||
}
|
||||
if (Key::hasPrivateKey($item)) {
|
||||
$privateKey = Key::getPrivateKey($item);
|
||||
} else {
|
||||
$privateKey = null;
|
||||
}
|
||||
if (($publicKey || $privateKey)) {
|
||||
$this->type = 'asymmetric';
|
||||
if ($publicKey && $privateKey) {
|
||||
$publicKeyPEM = openssl_pkey_get_details($publicKey)['key'];
|
||||
$privateKeyPublicPEM = openssl_pkey_get_details($privateKey)['key'];
|
||||
if ($privateKeyPublicPEM != $publicKeyPEM) {
|
||||
throw new KeyException('Supplied Certificate and Key are not related');
|
||||
}
|
||||
}
|
||||
$this->privateKey = $privateKey;
|
||||
$this->publicKey = $publicKey;
|
||||
$this->secret = null;
|
||||
} else {
|
||||
$this->type = 'secret';
|
||||
$this->secret = $item;
|
||||
$this->publicKey = null;
|
||||
$this->privateKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves private key resource from a input string or
|
||||
* array of strings.
|
||||
*
|
||||
* @param string|array $object PEM-format Private Key or file path to same
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
public static function getPrivateKey($object)
|
||||
{
|
||||
if (is_array($object)) {
|
||||
foreach ($object as $candidateKey) {
|
||||
$privateKey = Key::getPrivateKey($candidateKey);
|
||||
if ($privateKey) {
|
||||
return $privateKey;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// OpenSSL libraries don't have detection methods, so try..catch
|
||||
try {
|
||||
$privateKey = openssl_get_privatekey($object);
|
||||
|
||||
return $privateKey;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves public key resource from a input string or
|
||||
* array of strings.
|
||||
*
|
||||
* @param string|array $object PEM-format Public Key or file path to same
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
public static function getPublicKey($object)
|
||||
{
|
||||
if (is_array($object)) {
|
||||
// If we implement key rotation in future, this should add to a collection
|
||||
foreach ($object as $candidateKey) {
|
||||
$publicKey = Key::getPublicKey($candidateKey);
|
||||
if ($publicKey) {
|
||||
return $publicKey;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// OpenSSL libraries don't have detection methods, so try..catch
|
||||
try {
|
||||
$publicKey = openssl_get_publickey($object);
|
||||
|
||||
return $publicKey;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signing HTTP Messages 'keyId' field.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws KeyException
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve Verifying Key - Public Key for Asymmetric/PKI, or shared secret for HMAC.
|
||||
*
|
||||
* @return string Shared Secret or PEM-format Public Key
|
||||
*
|
||||
* @throws KeyException
|
||||
*/
|
||||
public function getVerifyingKey()
|
||||
{
|
||||
switch ($this->type) {
|
||||
case 'asymmetric':
|
||||
if ($this->publicKey) {
|
||||
return openssl_pkey_get_details($this->publicKey)['key'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case 'secret':
|
||||
return $this->secret;
|
||||
default:
|
||||
throw new KeyException("Unknown key type $this->type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve Signing Key - Private Key for Asymmetric/PKI, or shared secret for HMAC.
|
||||
*
|
||||
* @return string Shared Secret or PEM-format Private Key
|
||||
*
|
||||
* @throws KeyException
|
||||
*/
|
||||
public function getSigningKey()
|
||||
{
|
||||
switch ($this->type) {
|
||||
case 'asymmetric':
|
||||
if ($this->privateKey) {
|
||||
openssl_pkey_export($this->privateKey, $pem);
|
||||
|
||||
return $pem;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case 'secret':
|
||||
return $this->secret;
|
||||
default:
|
||||
throw new KeyException("Unknown key type $this->type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string 'secret' for HMAC or 'asymmetric'
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if $object is, points to or contains, X.509 PEM-format certificate.
|
||||
*
|
||||
* @param string|array $object PEM Format X.509 Certificate or file path to one
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasX509Certificate($object)
|
||||
{
|
||||
if (is_array($object)) {
|
||||
foreach ($object as $candidateCertificate) {
|
||||
$result = Key::hasX509Certificate($candidateCertificate);
|
||||
if ($result) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// OpenSSL libraries don't have detection methods, so try..catch
|
||||
try {
|
||||
openssl_x509_export($object, $null);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if $object is, points to or contains, PEM-format Public Key.
|
||||
*
|
||||
* @param string|array $object PEM-format Public Key or file path to one
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasPublicKey($object)
|
||||
{
|
||||
if (is_array($object)) {
|
||||
foreach ($object as $candidatePublicKey) {
|
||||
$result = Key::hasPublicKey($candidatePublicKey);
|
||||
if ($result) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false == !openssl_pkey_get_public($object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if $object is, points to or contains, PEM-format Private Key.
|
||||
*
|
||||
* @param string|array $object PEM-format Private Key or file path to one
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasPrivateKey($object)
|
||||
{
|
||||
if (is_array($object)) {
|
||||
foreach ($object as $candidatePrivateKey) {
|
||||
$result = Key::hasPrivateKey($candidatePrivateKey);
|
||||
if ($result) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false != openssl_pkey_get_private($object);
|
||||
}
|
||||
}
|
||||
}
|
7
app/Util/HTTPSignatures/KeyException.php
Executable file
7
app/Util/HTTPSignatures/KeyException.php
Executable file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class KeyException extends Exception
|
||||
{
|
||||
}
|
36
app/Util/HTTPSignatures/KeyStore.php
Executable file
36
app/Util/HTTPSignatures/KeyStore.php
Executable file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class KeyStore implements KeyStoreInterface
|
||||
{
|
||||
/** @var Key[] */
|
||||
private $keys;
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
*/
|
||||
public function __construct($keys)
|
||||
{
|
||||
$this->keys = [];
|
||||
foreach ($keys as $id => $key) {
|
||||
$this->keys[$id] = new Key($id, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $keyId
|
||||
*
|
||||
* @return Key
|
||||
*
|
||||
* @throws KeyStoreException
|
||||
*/
|
||||
public function fetch($keyId)
|
||||
{
|
||||
if (isset($this->keys[$keyId])) {
|
||||
return $this->keys[$keyId];
|
||||
} else {
|
||||
throw new KeyStoreException("Key '$keyId' not found");
|
||||
}
|
||||
}
|
||||
}
|
7
app/Util/HTTPSignatures/KeyStoreException.php
Executable file
7
app/Util/HTTPSignatures/KeyStoreException.php
Executable file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class KeyStoreException extends Exception
|
||||
{
|
||||
}
|
15
app/Util/HTTPSignatures/KeyStoreInterface.php
Executable file
15
app/Util/HTTPSignatures/KeyStoreInterface.php
Executable file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
interface KeyStoreInterface
|
||||
{
|
||||
/**
|
||||
* return the secret for the specified $keyId.
|
||||
*
|
||||
* @param string $keyId
|
||||
*
|
||||
* @return Key
|
||||
*/
|
||||
public function fetch($keyId);
|
||||
}
|
64
app/Util/HTTPSignatures/RsaAlgorithm.php
Executable file
64
app/Util/HTTPSignatures/RsaAlgorithm.php
Executable file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class RsaAlgorithm implements AlgorithmInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $digestName;
|
||||
|
||||
/**
|
||||
* @param string $digestName
|
||||
*/
|
||||
public function __construct($digestName)
|
||||
{
|
||||
$this->digestName = $digestName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function name()
|
||||
{
|
||||
return sprintf('rsa-%s', $this->digestName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string $data
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \HttpSignatures\AlgorithmException
|
||||
*/
|
||||
public function sign($signingKey, $data)
|
||||
{
|
||||
$algo = $this->getRsaHashAlgo($this->digestName);
|
||||
if (!openssl_get_privatekey($signingKey)) {
|
||||
throw new AlgorithmException("OpenSSL doesn't understand the supplied key (not valid or not found)");
|
||||
}
|
||||
$signature = '';
|
||||
openssl_sign($data, $signature, $signingKey, $algo);
|
||||
|
||||
return $signature;
|
||||
}
|
||||
|
||||
public function verify($message, $signature, $verifyingKey)
|
||||
{
|
||||
$algo = $this->getRsaHashAlgo($this->digestName);
|
||||
|
||||
return openssl_verify($message, base64_decode($signature), $verifyingKey, $algo);
|
||||
}
|
||||
|
||||
private function getRsaHashAlgo($digestName)
|
||||
{
|
||||
switch ($digestName) {
|
||||
case 'sha256':
|
||||
return OPENSSL_ALGO_SHA256;
|
||||
case 'sha1':
|
||||
return OPENSSL_ALGO_SHA1;
|
||||
default:
|
||||
throw new HttpSignatures\AlgorithmException($digestName.' is not a supported hash format');
|
||||
}
|
||||
}
|
||||
}
|
38
app/Util/HTTPSignatures/Signature.php
Executable file
38
app/Util/HTTPSignatures/Signature.php
Executable file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class Signature
|
||||
{
|
||||
/** @var Key */
|
||||
private $key;
|
||||
|
||||
/** @var AlgorithmInterface */
|
||||
private $algorithm;
|
||||
|
||||
/** @var SigningString */
|
||||
private $signingString;
|
||||
|
||||
/**
|
||||
* @param RequestInterface $message
|
||||
* @param Key $key
|
||||
* @param AlgorithmInterface $algorithm
|
||||
* @param HeaderList $headerList
|
||||
*/
|
||||
public function __construct($message, Key $key, AlgorithmInterface $algorithm, HeaderList $headerList)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->algorithm = $algorithm;
|
||||
$this->signingString = new SigningString($headerList, $message);
|
||||
}
|
||||
|
||||
public function string()
|
||||
{
|
||||
return $this->algorithm->sign(
|
||||
$this->key->getSigningKey(),
|
||||
$this->signingString->string()
|
||||
);
|
||||
}
|
||||
}
|
49
app/Util/HTTPSignatures/SignatureParameters.php
Executable file
49
app/Util/HTTPSignatures/SignatureParameters.php
Executable file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class SignatureParameters
|
||||
{
|
||||
/**
|
||||
* @param Key $key
|
||||
* @param AlgorithmInterface $algorithm
|
||||
* @param HeaderList $headerList
|
||||
* @param Signature $signature
|
||||
*/
|
||||
public function __construct($key, $algorithm, $headerList, $signature)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->algorithm = $algorithm;
|
||||
$this->headerList = $headerList;
|
||||
$this->signature = $signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function string()
|
||||
{
|
||||
return implode(',', $this->parameterComponents());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function parameterComponents()
|
||||
{
|
||||
return [
|
||||
sprintf('keyId="%s"', $this->key->getId()),
|
||||
sprintf('algorithm="%s"', $this->algorithm->name()),
|
||||
sprintf('headers="%s"', $this->headerList->string()),
|
||||
sprintf('signature="%s"', $this->signatureBase64()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function signatureBase64()
|
||||
{
|
||||
return base64_encode($this->signature->string());
|
||||
}
|
||||
}
|
111
app/Util/HTTPSignatures/SignatureParametersParser.php
Executable file
111
app/Util/HTTPSignatures/SignatureParametersParser.php
Executable file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class SignatureParametersParser
|
||||
{
|
||||
/** @var string */
|
||||
private $input;
|
||||
|
||||
/**
|
||||
* @param string $input
|
||||
*/
|
||||
public function __construct($input)
|
||||
{
|
||||
$this->input = $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function parse()
|
||||
{
|
||||
$result = $this->pairsToAssociative(
|
||||
$this->arrayOfPairs()
|
||||
);
|
||||
$this->validate($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $pairs
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function pairsToAssociative($pairs)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$result[$pair[0]] = $pair[1];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function arrayOfPairs()
|
||||
{
|
||||
return array_map(
|
||||
[$this, 'pair'],
|
||||
$this->segments()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function segments()
|
||||
{
|
||||
return explode(',', $this->input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $segment
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws SignatureParseException
|
||||
*/
|
||||
private function pair($segment)
|
||||
{
|
||||
$segmentPattern = '/\A(keyId|algorithm|headers|signature)="(.*)"\z/';
|
||||
$matches = [];
|
||||
$result = preg_match($segmentPattern, $segment, $matches);
|
||||
if (1 !== $result) {
|
||||
throw new SignatureParseException("Signature parameters segment '$segment' invalid");
|
||||
}
|
||||
array_shift($matches);
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $result
|
||||
*
|
||||
* @throws SignatureParseException
|
||||
*/
|
||||
private function validate($result)
|
||||
{
|
||||
$this->validateAllKeysArePresent($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $result
|
||||
*
|
||||
* @throws SignatureParseException
|
||||
*/
|
||||
private function validateAllKeysArePresent($result)
|
||||
{
|
||||
// Regexp in pair() ensures no unwanted keys exist.
|
||||
// Ensure that all wanted keys exist.
|
||||
$wanted = ['keyId', 'algorithm', 'headers', 'signature'];
|
||||
$missing = array_diff($wanted, array_keys($result));
|
||||
if (!empty($missing)) {
|
||||
$csv = implode(', ', $missing);
|
||||
throw new SignatureParseException("Missing keys $csv");
|
||||
}
|
||||
}
|
||||
}
|
7
app/Util/HTTPSignatures/SignatureParseException.php
Executable file
7
app/Util/HTTPSignatures/SignatureParseException.php
Executable file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class SignatureParseException extends Exception
|
||||
{
|
||||
}
|
7
app/Util/HTTPSignatures/SignedHeaderNotPresentException.php
Executable file
7
app/Util/HTTPSignatures/SignedHeaderNotPresentException.php
Executable file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
class SignedHeaderNotPresentException extends Exception
|
||||
{
|
||||
}
|
104
app/Util/HTTPSignatures/Signer.php
Executable file
104
app/Util/HTTPSignatures/Signer.php
Executable file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class Signer
|
||||
{
|
||||
/** @var Key */
|
||||
private $key;
|
||||
|
||||
/** @var HmacAlgorithm */
|
||||
private $algorithm;
|
||||
|
||||
/** @var HeaderList */
|
||||
private $headerList;
|
||||
|
||||
/**
|
||||
* @param Key $key
|
||||
* @param HmacAlgorithm $algorithm
|
||||
* @param HeaderList $headerList
|
||||
*/
|
||||
public function __construct($key, $algorithm, $headerList)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->algorithm = $algorithm;
|
||||
$this->headerList = $headerList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RequestInterface $message
|
||||
*
|
||||
* @return RequestInterface
|
||||
*/
|
||||
public function sign($message)
|
||||
{
|
||||
$signatureParameters = $this->signatureParameters($message);
|
||||
$message = $message->withAddedHeader('Signature', $signatureParameters->string());
|
||||
$message = $message->withAddedHeader('Authorization', 'Signature '.$signatureParameters->string());
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RequestInterface $message
|
||||
*
|
||||
* @return RequestInterface
|
||||
*/
|
||||
public function signWithDigest($message)
|
||||
{
|
||||
$message = $this->addDigest($message);
|
||||
|
||||
return $this->sign($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RequestInterface $message
|
||||
*
|
||||
* @return RequestInterface
|
||||
*/
|
||||
private function addDigest($message)
|
||||
{
|
||||
if (!array_search('digest', $this->headerList->names)) {
|
||||
$this->headerList->names[] = 'digest';
|
||||
}
|
||||
$message = $message->withoutHeader('Digest')
|
||||
->withHeader(
|
||||
'Digest',
|
||||
'SHA-256='.base64_encode(hash('sha256', $message->getBody(), true))
|
||||
);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RequestInterface $message
|
||||
*
|
||||
* @return SignatureParameters
|
||||
*/
|
||||
private function signatureParameters($message)
|
||||
{
|
||||
return new SignatureParameters(
|
||||
$this->key,
|
||||
$this->algorithm,
|
||||
$this->headerList,
|
||||
$this->signature($message)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RequestInterface $message
|
||||
*
|
||||
* @return Signature
|
||||
*/
|
||||
private function signature($message)
|
||||
{
|
||||
return new Signature(
|
||||
$message,
|
||||
$this->key,
|
||||
$this->algorithm,
|
||||
$this->headerList
|
||||
);
|
||||
}
|
||||
}
|
89
app/Util/HTTPSignatures/SigningString.php
Executable file
89
app/Util/HTTPSignatures/SigningString.php
Executable file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class SigningString
|
||||
{
|
||||
/** @var HeaderList */
|
||||
private $headerList;
|
||||
|
||||
/** @var RequestInterface */
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* @param HeaderList $headerList
|
||||
* @param RequestInterface $message
|
||||
*/
|
||||
public function __construct(HeaderList $headerList, $message)
|
||||
{
|
||||
$this->headerList = $headerList;
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function string()
|
||||
{
|
||||
return implode("\n", $this->lines());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function lines()
|
||||
{
|
||||
return array_map(
|
||||
[$this, 'line'],
|
||||
$this->headerList->names
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws SignedHeaderNotPresentException
|
||||
*/
|
||||
private function line($name)
|
||||
{
|
||||
if ('(request-target)' == $name) {
|
||||
return $this->requestTargetLine();
|
||||
} else {
|
||||
return sprintf('%s: %s', $name, $this->headerValue($name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws SignedHeaderNotPresentException
|
||||
*/
|
||||
private function headerValue($name)
|
||||
{
|
||||
if ($this->message->hasHeader($name)) {
|
||||
$header = $this->message->getHeader($name);
|
||||
|
||||
return end($header);
|
||||
} else {
|
||||
throw new SignedHeaderNotPresentException("Header '$name' not in message");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function requestTargetLine()
|
||||
{
|
||||
return sprintf(
|
||||
'(request-target): %s %s',
|
||||
strtolower($this->message->getMethod()),
|
||||
$this->message->getRequestTarget()
|
||||
);
|
||||
}
|
||||
}
|
202
app/Util/HTTPSignatures/Verification.php
Executable file
202
app/Util/HTTPSignatures/Verification.php
Executable file
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class Verification
|
||||
{
|
||||
/** @var RequestInterface */
|
||||
private $message;
|
||||
|
||||
/** @var KeyStoreInterface */
|
||||
private $keyStore;
|
||||
|
||||
/** @var array */
|
||||
private $_parameters;
|
||||
|
||||
/**
|
||||
* @param RequestInterface $message
|
||||
* @param KeyStoreInterface $keyStore
|
||||
*/
|
||||
public function __construct($message, KeyStoreInterface $keyStore)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->keyStore = $keyStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
return $this->hasSignatureHeader() && $this->signatureMatches();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function signatureMatches()
|
||||
{
|
||||
try {
|
||||
$key = $this->key();
|
||||
switch ($key->getType()) {
|
||||
case 'secret':
|
||||
$random = random_bytes(32);
|
||||
$expectedResult = hash_hmac(
|
||||
'sha256', $this->expectedSignatureBase64(),
|
||||
$random,
|
||||
true
|
||||
);
|
||||
$providedResult = hash_hmac(
|
||||
'sha256', $this->providedSignatureBase64(),
|
||||
$random,
|
||||
true
|
||||
);
|
||||
|
||||
return $expectedResult === $providedResult;
|
||||
case 'asymmetric':
|
||||
$signedString = new SigningString(
|
||||
$this->headerList(),
|
||||
$this->message
|
||||
);
|
||||
$hashAlgo = explode('-', $this->parameter('algorithm'))[1];
|
||||
$algorithm = new RsaAlgorithm($hashAlgo);
|
||||
$result = $algorithm->verify(
|
||||
$signedString->string(),
|
||||
$this->parameter('signature'),
|
||||
$key->getVerifyingKey());
|
||||
|
||||
return $result;
|
||||
default:
|
||||
throw new Exception("Unknown key type '".$key->getType()."', cannot verify");
|
||||
}
|
||||
} catch (SignatureParseException $e) {
|
||||
return false;
|
||||
} catch (KeyStoreException $e) {
|
||||
return false;
|
||||
} catch (SignedHeaderNotPresentException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function expectedSignatureBase64()
|
||||
{
|
||||
return base64_encode($this->expectedSignature()->string());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Signature
|
||||
*/
|
||||
private function expectedSignature()
|
||||
{
|
||||
return new Signature(
|
||||
$this->message,
|
||||
$this->key(),
|
||||
$this->algorithm(),
|
||||
$this->headerList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function providedSignatureBase64()
|
||||
{
|
||||
return $this->parameter('signature');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Key
|
||||
*/
|
||||
private function key()
|
||||
{
|
||||
return $this->keyStore->fetch($this->parameter('keyId'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HmacAlgorithm
|
||||
*/
|
||||
private function algorithm()
|
||||
{
|
||||
return Algorithm::create($this->parameter('algorithm'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HeaderList
|
||||
*/
|
||||
private function headerList()
|
||||
{
|
||||
return HeaderList::fromString($this->parameter('headers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parameter($name)
|
||||
{
|
||||
$parameters = $this->parameters();
|
||||
if (!isset($parameters[$name])) {
|
||||
throw new Exception("Signature parameters does not contain '$name'");
|
||||
}
|
||||
|
||||
return $parameters[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function parameters()
|
||||
{
|
||||
if (!isset($this->_parameters)) {
|
||||
$parser = new SignatureParametersParser($this->signatureHeader());
|
||||
$this->_parameters = $parser->parse();
|
||||
}
|
||||
|
||||
return $this->_parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function hasSignatureHeader()
|
||||
{
|
||||
return $this->message->hasHeader('Signature') || $this->message->hasHeader('Authorization');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function signatureHeader()
|
||||
{
|
||||
if ($signature = $this->fetchHeader('Signature')) {
|
||||
return $signature;
|
||||
} elseif ($authorization = $this->fetchHeader('Authorization')) {
|
||||
return substr($authorization, strlen('Signature '));
|
||||
} else {
|
||||
throw new Exception('HTTP message has no Signature or Authorization header');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function fetchHeader($name)
|
||||
{
|
||||
// grab the most recently set header.
|
||||
$header = $this->message->getHeader($name);
|
||||
|
||||
return end($header);
|
||||
}
|
||||
}
|
31
app/Util/HTTPSignatures/Verifier.php
Executable file
31
app/Util/HTTPSignatures/Verifier.php
Executable file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\HttpSignatures;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class Verifier
|
||||
{
|
||||
/** @var KeyStoreInterface */
|
||||
private $keyStore;
|
||||
|
||||
/**
|
||||
* @param KeyStoreInterface $keyStore
|
||||
*/
|
||||
public function __construct(KeyStoreInterface $keyStore)
|
||||
{
|
||||
$this->keyStore = $keyStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RequestInterface $message
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($message)
|
||||
{
|
||||
$verification = new Verification($message, $this->keyStore);
|
||||
|
||||
return $verification->isValid();
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ class Image
|
|||
public $orientation;
|
||||
public $acceptedMimes = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/jpeg'
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
|
@ -114,9 +114,11 @@ class Image
|
|||
if($thumbnail) {
|
||||
$img->crop($aspect['width'], $aspect['height']);
|
||||
} else {
|
||||
$metadata = $img->exif();
|
||||
$img->resize($aspect['width'], $aspect['height'], function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
$media->metadata = json_encode($metadata);
|
||||
}
|
||||
$converted = $this->setBaseName($path, $thumbnail, $img->extension);
|
||||
$newPath = storage_path('app/'.$converted['path']);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "laravel/laravel",
|
||||
"description": "The Laravel Framework.",
|
||||
"keywords": ["framework", "laravel"],
|
||||
"license": "MIT",
|
||||
"name": "pixelfed/pixelfed",
|
||||
"description": "Open and ethical photo sharing platform, powered by ActivityPub federation.",
|
||||
"keywords": ["framework", "laravel", "pixelfed", "activitypub", "social", "network", "federation"],
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
|
|
|
@ -13,7 +13,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
'name' => env('APP_NAME', 'Pixelfed'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -52,7 +52,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
'url' => env('APP_URL', 'https://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -214,6 +214,7 @@ return [
|
|||
'Recaptcha' => Greggilbert\Recaptcha\Facades\Recaptcha::class,
|
||||
'DotenvEditor' => Jackiedo\DotenvEditor\Facades\DotenvEditor::class,
|
||||
'PrettyNumber' => App\Util\Lexer\PrettyNumber::class,
|
||||
'Purify' => Stevebauman\Purify\Facades\Purify::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -14,7 +14,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'enabled' => env('DEBUGBAR_ENABLED', false),
|
||||
'enabled' => false,
|
||||
'except' => [
|
||||
//
|
||||
],
|
||||
|
@ -32,7 +32,7 @@ return [
|
|||
|
|
||||
*/
|
||||
'storage' => [
|
||||
'enabled' => true,
|
||||
'enabled' => false,
|
||||
'driver' => 'file', // redis, file, pdo, custom
|
||||
'path' => storage_path('debugbar'), // For file driver
|
||||
'connection' => null, // Leave null for default connection (Redis/PDO)
|
||||
|
|
|
@ -23,7 +23,7 @@ return [
|
|||
| This value is the version of your PixelFed instance.
|
||||
|
|
||||
*/
|
||||
'version' => '0.1.9',
|
||||
'version' => '0.2.0',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
141
config/purify.php
Normal file
141
config/purify.php
Normal file
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The configuration settings array is passed directly to HTMLPurifier.
|
||||
|
|
||||
| Feel free to add / remove / customize these attributes as you wish.
|
||||
|
|
||||
| Documentation: http://htmlpurifier.org/live/configdoc/plain.html
|
||||
|
|
||||
*/
|
||||
|
||||
'settings' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Core.Encoding
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The encoding to convert input to.
|
||||
|
|
||||
| http://htmlpurifier.org/live/configdoc/plain.html#Core.Encoding
|
||||
|
|
||||
*/
|
||||
|
||||
'Core.Encoding' => 'utf-8',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Core.SerializerPath
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The HTML purifier serializer cache path.
|
||||
|
|
||||
| http://htmlpurifier.org/live/configdoc/plain.html#Cache.SerializerPath
|
||||
|
|
||||
*/
|
||||
|
||||
'Cache.SerializerPath' => storage_path('purify'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTML.Doctype
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Doctype to use during filtering.
|
||||
|
|
||||
| http://htmlpurifier.org/live/configdoc/plain.html#HTML.Doctype
|
||||
|
|
||||
*/
|
||||
|
||||
'HTML.Doctype' => 'XHTML 1.0 Strict',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTML.Allowed
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The allowed HTML Elements with their allowed attributes.
|
||||
|
|
||||
| http://htmlpurifier.org/live/configdoc/plain.html#HTML.Allowed
|
||||
|
|
||||
*/
|
||||
|
||||
'HTML.Allowed' => 'a[href|title|rel],p',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTML.ForbiddenElements
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The forbidden HTML elements. Elements that are listed in
|
||||
| this string will be removed, however their content will remain.
|
||||
|
|
||||
| For example if 'p' is inside the string, the string: '<p>Test</p>',
|
||||
|
|
||||
| Will be cleaned to: 'Test'
|
||||
|
|
||||
| http://htmlpurifier.org/live/configdoc/plain.html#HTML.ForbiddenElements
|
||||
|
|
||||
*/
|
||||
|
||||
'HTML.ForbiddenElements' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CSS.AllowedProperties
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The Allowed CSS properties.
|
||||
|
|
||||
| http://htmlpurifier.org/live/configdoc/plain.html#CSS.AllowedProperties
|
||||
|
|
||||
*/
|
||||
|
||||
'CSS.AllowedProperties' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AutoFormat.AutoParagraph
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The Allowed CSS properties.
|
||||
|
|
||||
| This directive turns on auto-paragraphing, where double
|
||||
| newlines are converted in to paragraphs whenever possible.
|
||||
|
|
||||
| http://htmlpurifier.org/live/configdoc/plain.html#AutoFormat.AutoParagraph
|
||||
|
|
||||
*/
|
||||
|
||||
'AutoFormat.AutoParagraph' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AutoFormat.RemoveEmpty
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When enabled, HTML Purifier will attempt to remove empty
|
||||
| elements that contribute no semantic information to the document.
|
||||
|
|
||||
| http://htmlpurifier.org/live/configdoc/plain.html#AutoFormat.RemoveEmpty
|
||||
|
|
||||
*/
|
||||
|
||||
'AutoFormat.RemoveEmpty' => false,
|
||||
|
||||
'Attr.AllowedRel' => [
|
||||
'noreferrer',
|
||||
'noopener',
|
||||
'nofollow'
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
|
@ -61,7 +61,7 @@ return [
|
|||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
'queue' => 'default',
|
||||
'retry_after' => 90,
|
||||
'retry_after' => 1800,
|
||||
'block_for' => null,
|
||||
],
|
||||
|
||||
|
|
|
@ -15,9 +15,10 @@ use Faker\Generator as Faker;
|
|||
|
||||
$factory->define(App\User::class, function (Faker $faker) {
|
||||
return [
|
||||
'name' => $faker->name,
|
||||
'email' => $faker->unique()->safeEmail,
|
||||
'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
|
||||
'name' => $faker->name,
|
||||
'username' => str_replace('.', '', $faker->unique()->userName),
|
||||
'email' => str_random(8).$faker->unique()->safeEmail,
|
||||
'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
|
||||
'remember_token' => str_random(10),
|
||||
];
|
||||
});
|
||||
|
|
194
package-lock.json
generated
194
package-lock.json
generated
|
@ -2,6 +2,20 @@
|
|||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@videojs/http-streaming": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-1.2.5.tgz",
|
||||
"integrity": "sha512-kqjx9oc4NiiUwzqt8EI2PcuebC0WlnxsWydUoMSktLmXc/T6qVS0m8d1eyMA2tjlDILvKkjq2YPS7Jl81phbQQ==",
|
||||
"requires": {
|
||||
"aes-decrypter": "3.0.0",
|
||||
"global": "^4.3.0",
|
||||
"m3u8-parser": "4.2.0",
|
||||
"mpd-parser": "0.6.1",
|
||||
"mux.js": "4.5.1",
|
||||
"url-toolkit": "^2.1.3",
|
||||
"video.js": "^6.8.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
|
@ -87,6 +101,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"aes-decrypter": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.0.0.tgz",
|
||||
"integrity": "sha1-eEihwUW5/b9Xrj4rWxvHzwZEqPs=",
|
||||
"requires": {
|
||||
"commander": "^2.9.0",
|
||||
"global": "^4.3.2",
|
||||
"pkcs7": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"after": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
|
||||
|
@ -1933,8 +1957,7 @@
|
|||
"commander": {
|
||||
"version": "2.17.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
|
||||
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg=="
|
||||
},
|
||||
"commondir": {
|
||||
"version": "1.0.1",
|
||||
|
@ -2764,6 +2787,11 @@
|
|||
"buffer-indexof": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"dom-walk": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz",
|
||||
"integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg="
|
||||
},
|
||||
"domain-browser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
|
||||
|
@ -3596,6 +3624,14 @@
|
|||
"debug": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
|
||||
"requires": {
|
||||
"is-callable": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"for-in": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||
|
@ -4424,6 +4460,22 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz",
|
||||
"integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=",
|
||||
"requires": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "~0.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"process": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz",
|
||||
"integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8="
|
||||
}
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
|
||||
|
@ -4991,6 +5043,11 @@
|
|||
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
|
||||
"integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
|
||||
},
|
||||
"individual": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
|
||||
"integrity": "sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c="
|
||||
},
|
||||
"infinite-scroll": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/infinite-scroll/-/infinite-scroll-3.0.5.tgz",
|
||||
|
@ -5161,8 +5218,7 @@
|
|||
"is-callable": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
|
||||
"integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA=="
|
||||
},
|
||||
"is-data-descriptor": {
|
||||
"version": "0.1.4",
|
||||
|
@ -5256,6 +5312,11 @@
|
|||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
|
||||
},
|
||||
"is-function": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz",
|
||||
"integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU="
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
|
||||
|
@ -5421,8 +5482,7 @@
|
|||
"jquery": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
|
||||
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
|
||||
},
|
||||
"js-base64": {
|
||||
"version": "2.4.9",
|
||||
|
@ -5836,6 +5896,11 @@
|
|||
"yallist": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"m3u8-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-LVHw0U6IPJjwk9i9f7Xe26NqaUHTNlIt4SSWoEfYFROeVKHN6MIjOhbRheI3dg8Jbq5WCuMFQ0QU3EgZpmzFPg=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
|
@ -6019,6 +6084,14 @@
|
|||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
|
||||
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
|
||||
},
|
||||
"min-document": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
||||
"integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
|
||||
"requires": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
|
@ -6126,6 +6199,15 @@
|
|||
"run-queue": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"mpd-parser": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.6.1.tgz",
|
||||
"integrity": "sha512-3ucsY5NJMABltTLtYMSDfqZpvKV4yF8YvMx91hZFrHiblseuoKq4XUQ5IkcdtFAIRBAkPhXMU3/eunTFNCNsHw==",
|
||||
"requires": {
|
||||
"global": "^4.3.0",
|
||||
"url-toolkit": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
|
@ -6152,6 +6234,11 @@
|
|||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
|
||||
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
|
||||
},
|
||||
"mux.js": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-4.5.1.tgz",
|
||||
"integrity": "sha512-j4rEyZKCRinGaSiBxPx9YD9B782TMPHPOlKyaMY07vIGTNYg4ouCEBvL6zX9Hh1k1fKZ5ZF3S7c+XVk6PB+Igw=="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
|
||||
|
@ -6786,6 +6873,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"parse-headers": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.1.tgz",
|
||||
"integrity": "sha1-aug6eqJanZtwCswoaYzR8e1+lTY=",
|
||||
"requires": {
|
||||
"for-each": "^0.3.2",
|
||||
"trim": "0.0.1"
|
||||
}
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
|
||||
|
@ -6928,6 +7024,11 @@
|
|||
"pinkie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pkcs7": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.2.tgz",
|
||||
"integrity": "sha1-ttulJ1KMKUK/wSLOLa/NteWQdOc="
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
|
||||
|
@ -9329,6 +9430,14 @@
|
|||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"readmore-js": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/readmore-js/-/readmore-js-2.2.1.tgz",
|
||||
"integrity": "sha512-hbPP0nQpYYkAywCEZ8ozHivvhWyHic37KJ2IXrHES4qzjp0+nmw8R33MeyMAtXBZfXX4Es8cpd5JBVf9qj47+Q==",
|
||||
"requires": {
|
||||
"jquery": ">2.1.4"
|
||||
}
|
||||
},
|
||||
"recast": {
|
||||
"version": "0.11.23",
|
||||
"resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz",
|
||||
|
@ -9712,6 +9821,14 @@
|
|||
"aproba": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"rust-result": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz",
|
||||
"integrity": "sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I=",
|
||||
"requires": {
|
||||
"individual": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"rx": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz",
|
||||
|
@ -9722,6 +9839,14 @@
|
|||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safe-json-parse": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
|
||||
"integrity": "sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw=",
|
||||
"requires": {
|
||||
"rust-result": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"safe-regex": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
||||
|
@ -10755,6 +10880,11 @@
|
|||
"punycode": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"trim": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
|
||||
"integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0="
|
||||
},
|
||||
"trim-newlines": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
|
||||
|
@ -10776,6 +10906,11 @@
|
|||
"glob": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"tsml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tsml/-/tsml-1.0.1.tgz",
|
||||
"integrity": "sha1-ifghi52eJX9H1/a1bQHFpNLGj8M="
|
||||
},
|
||||
"tty-browserify": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
||||
|
@ -11135,6 +11270,11 @@
|
|||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"url-toolkit": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.1.6.tgz",
|
||||
"integrity": "sha512-UaZ2+50am4HwrV2crR/JAf63Q4VvPYphe63WGeoJxeu8gmOm0qxPt+KsukfakPNrX9aymGNEkkaoICwn+OuvBw=="
|
||||
},
|
||||
"use": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
|
@ -11209,6 +11349,34 @@
|
|||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"video.js": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.2.3.tgz",
|
||||
"integrity": "sha512-oiRGXew1yKk3ILh9+8cvnV0PQp8oqs/2XtkoO46j7BMsFvhgl9L+dy+hS//MUSh1JNgDGUkM/K+E6WTTLlwN7w==",
|
||||
"requires": {
|
||||
"@videojs/http-streaming": "1.2.5",
|
||||
"babel-runtime": "^6.9.2",
|
||||
"global": "4.3.2",
|
||||
"safe-json-parse": "4.0.0",
|
||||
"tsml": "1.0.1",
|
||||
"videojs-font": "3.0.0",
|
||||
"videojs-vtt.js": "0.14.1",
|
||||
"xhr": "2.4.0"
|
||||
}
|
||||
},
|
||||
"videojs-font": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.0.0.tgz",
|
||||
"integrity": "sha512-XS6agz2T7p2cFuuXulJD70md8XMlAN617SJkMWjoTPqZWv+RU8NcZCKsE3Tk73inzxnQdihOp0cvI7NGz2ngHg=="
|
||||
},
|
||||
"videojs-vtt.js": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.14.1.tgz",
|
||||
"integrity": "sha512-YxOiywx6N9t3J5nqsE5WN2Sw4CSqVe3zV+AZm2T4syOc2buNJaD6ZoexSdeszx2sHLU/RRo2r4BJAXFDQ7Qo2Q==",
|
||||
"requires": {
|
||||
"global": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"vm-browserify": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
|
||||
|
@ -11694,6 +11862,17 @@
|
|||
"ultron": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"xhr": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/xhr/-/xhr-2.4.0.tgz",
|
||||
"integrity": "sha1-4W5mpF+GmGHu76tBbV7/ci3ECZM=",
|
||||
"requires": {
|
||||
"global": "~4.3.0",
|
||||
"is-function": "^1.0.1",
|
||||
"parse-headers": "^2.0.0",
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"xmlhttprequest": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
|
||||
|
@ -11707,8 +11886,7 @@
|
|||
"xtend": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
|
||||
"dev": true
|
||||
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
|
||||
},
|
||||
"y18n": {
|
||||
"version": "3.2.1",
|
||||
|
|
|
@ -25,8 +25,10 @@
|
|||
"infinite-scroll": "^3.0.4",
|
||||
"laravel-echo": "^1.4.0",
|
||||
"pusher-js": "^4.2.2",
|
||||
"readmore-js": "^2.2.1",
|
||||
"socket.io-client": "^2.1.1",
|
||||
"sweetalert": "^2.1.0",
|
||||
"twitter-text": "^2.0.5"
|
||||
"twitter-text": "^2.0.5",
|
||||
"video.js": "^7.2.3"
|
||||
}
|
||||
}
|
||||
|
|
BIN
public/css/app.css
vendored
BIN
public/css/app.css
vendored
Binary file not shown.
1
public/img/help/what_is_the_fediverse.svg
Normal file
1
public/img/help/what_is_the_fediverse.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
public/img/pixelfed-icon-grey.svg
Normal file
BIN
public/img/pixelfed-icon-grey.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
public/js/app.js
vendored
BIN
public/js/app.js
vendored
Binary file not shown.
BIN
public/js/components.js
vendored
Normal file
BIN
public/js/components.js
vendored
Normal file
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
Binary file not shown.
BIN
public/static/beep.mp3
Normal file
BIN
public/static/beep.mp3
Normal file
Binary file not shown.
106
resources/assets/js/bootstrap.js
vendored
106
resources/assets/js/bootstrap.js
vendored
|
@ -1,113 +1,17 @@
|
|||
window._ = require('lodash');
|
||||
window.Popper = require('popper.js').default;
|
||||
import swal from 'sweetalert';
|
||||
|
||||
window.pixelfed = {};
|
||||
window.pixelfed = window.pixelfed || {};
|
||||
window.$ = window.jQuery = require('jquery');
|
||||
require('bootstrap');
|
||||
window.Vue = require('vue');
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
try {
|
||||
window.InfiniteScroll = require('infinite-scroll');
|
||||
window.filesize = require('filesize');
|
||||
window.typeahead = require('./lib/typeahead');
|
||||
window.Bloodhound = require('./lib/bloodhound');
|
||||
require('./components/localstorage');
|
||||
require('./components/likebutton');
|
||||
require('./components/commentform');
|
||||
require('./components/searchform');
|
||||
require('./components/bookmarkform');
|
||||
require('./components/statusform');
|
||||
// require('./components/embed');
|
||||
// require('./components/shortcuts');
|
||||
|
||||
Vue.component(
|
||||
'follow-suggestions',
|
||||
require('./components/FollowSuggestions.vue')
|
||||
);
|
||||
|
||||
// Vue.component(
|
||||
// 'circle-panel',
|
||||
// require('./components/CirclePanel.vue')
|
||||
// );
|
||||
|
||||
// Vue.component(
|
||||
// 'post-presenter',
|
||||
// require('./components/PostPresenter.vue')
|
||||
// );
|
||||
|
||||
// Vue.component(
|
||||
// 'post-comments',
|
||||
// require('./components/PostComments.vue')
|
||||
// );
|
||||
|
||||
Vue.component(
|
||||
'passport-clients',
|
||||
require('./components/passport/Clients.vue')
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'passport-authorized-clients',
|
||||
require('./components/passport/AuthorizedClients.vue')
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'passport-personal-access-tokens',
|
||||
require('./components/passport/PersonalAccessTokens.vue')
|
||||
);
|
||||
|
||||
} catch (e) {}
|
||||
|
||||
$(document).ready(function() {
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
});
|
||||
|
||||
window.typeahead = require('./lib/typeahead');
|
||||
window.Bloodhound = require('./lib/bloodhound');
|
||||
window.axios = require('axios');
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
require('readmore-js');
|
||||
|
||||
let token = document.head.querySelector('meta[name="csrf-token"]');
|
||||
if (token) {
|
||||
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
|
||||
} else {
|
||||
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
|
||||
}
|
||||
|
||||
// import Echo from "laravel-echo"
|
||||
|
||||
// window.io = require('socket.io-client');
|
||||
|
||||
// window.pixelfed.bootEcho = function() {
|
||||
// window.Echo = new Echo({
|
||||
// broadcaster: 'socket.io',
|
||||
// host: window.location.hostname + ':2096',
|
||||
// auth: {
|
||||
// headers: {
|
||||
// Authorization: 'Bearer ' + token.content,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
window.pixelfed.copyToClipboard = (str) => {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = str;
|
||||
el.setAttribute('readonly', '');
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
const selected =
|
||||
document.getSelection().rangeCount > 0
|
||||
? document.getSelection().getRangeAt(0)
|
||||
: false;
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(selected);
|
||||
}
|
||||
};
|
117
resources/assets/js/components.js
vendored
Normal file
117
resources/assets/js/components.js
vendored
Normal file
|
@ -0,0 +1,117 @@
|
|||
window.Vue = require('vue');
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
pixelfed.readmore = () => {
|
||||
$(document).find('.read-more').each(function(k,v) {
|
||||
let el = $(this);
|
||||
let attr = el.attr('data-readmore');
|
||||
if(typeof attr !== typeof undefined && attr !== false) {
|
||||
return;
|
||||
}
|
||||
el.readmore({
|
||||
collapsedHeight: 44,
|
||||
heightMargin: 20,
|
||||
moreLink: '<a href="#" class="font-weight-bold small">Read more</a>',
|
||||
lessLink: '<a href="#" class="font-weight-bold small">Hide</a>',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.InfiniteScroll = require('infinite-scroll');
|
||||
window.filesize = require('filesize');
|
||||
import swal from 'sweetalert';
|
||||
|
||||
require('./components/localstorage');
|
||||
require('./components/likebutton');
|
||||
require('./components/commentform');
|
||||
require('./components/searchform');
|
||||
require('./components/bookmarkform');
|
||||
require('./components/statusform');
|
||||
require('./components/embed');
|
||||
require('./components/notifications');
|
||||
|
||||
// import Echo from "laravel-echo"
|
||||
|
||||
// window.io = require('socket.io-client');
|
||||
|
||||
// window.pixelfed.bootEcho = function() {
|
||||
// window.Echo = new Echo({
|
||||
// broadcaster: 'socket.io',
|
||||
// host: window.location.hostname + ':2096',
|
||||
// auth: {
|
||||
// headers: {
|
||||
// Authorization: 'Bearer ' + token.content,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// Initalize Notification Helper
|
||||
window.pixelfed.n = {};
|
||||
|
||||
Vue.component(
|
||||
'follow-suggestions',
|
||||
require('./components/FollowSuggestions.vue')
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'discover-component',
|
||||
require('./components/DiscoverComponent.vue')
|
||||
);
|
||||
|
||||
// Vue.component(
|
||||
// 'circle-panel',
|
||||
// require('./components/CirclePanel.vue')
|
||||
// );
|
||||
|
||||
Vue.component(
|
||||
'post-component',
|
||||
require('./components/PostComponent.vue')
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'post-comments',
|
||||
require('./components/PostComments.vue')
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'passport-clients',
|
||||
require('./components/passport/Clients.vue')
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'passport-authorized-clients',
|
||||
require('./components/passport/AuthorizedClients.vue')
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'passport-personal-access-tokens',
|
||||
require('./components/passport/PersonalAccessTokens.vue')
|
||||
);
|
||||
|
||||
window.pixelfed.copyToClipboard = (str) => {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = str;
|
||||
el.setAttribute('readonly', '');
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
const selected =
|
||||
document.getSelection().rangeCount > 0
|
||||
? document.getSelection().getRangeAt(0)
|
||||
: false;
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(selected);
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
});
|
33
resources/assets/js/components/CirclePanel.vue
Normal file
33
resources/assets/js/components/CirclePanel.vue
Normal file
|
@ -0,0 +1,33 @@
|
|||
<style scoped>
|
||||
.b-dropdown {
|
||||
padding:0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header py-1 bg-white d-flex align-items-center justify-content-between">
|
||||
<span class="font-weight-bold h5 mb-0">Circles</span>
|
||||
|
||||
<b-dropdown variant="link" no-caret right>
|
||||
<template slot="button-content">
|
||||
<i class="fas fa-ellipsis-v text-muted"></i><span class="sr-only">Options</span>
|
||||
</template>
|
||||
<b-dropdown-item class="font-weight-bold">Create a circle</b-dropdown-item>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item class="font-weight-bold">Settings</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center p-3">
|
||||
<p class="mb-0"><a class="btn btn-sm btn-outline-primary" style="border-radius: 20px;" href="/i/circle/create">Create new circle</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
95
resources/assets/js/components/DiscoverComponent.vue
Normal file
95
resources/assets/js/components/DiscoverComponent.vue
Normal file
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<section class="mb-5 section-people">
|
||||
<p class="lead text-muted font-weight-bold mb-0">Discover People</p>
|
||||
<div class="loader text-center">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
</div>
|
||||
<div class="row d-none">
|
||||
<div class="col-4 p-0 p-sm-2 p-md-3" v-for="profile in people">
|
||||
<div class="card card-md-border-0">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="avatar pb-3">
|
||||
<a :href="profile.url">
|
||||
<img :src="profile.avatar" class="img-thumbnail rounded-circle" width="64px">
|
||||
</a>
|
||||
</div>
|
||||
<p class="lead font-weight-bold mb-0 text-truncate"><a :href="profile.url" class="text-dark">{{profile.username}}</a></p>
|
||||
<p class="text-muted text-truncate">{{profile.name}}</p>
|
||||
<form class="follow-form" method="post" action="/i/follow" data-id="#" data-action="follow">
|
||||
<input type="hidden" name="item" value="#">
|
||||
<button class="btn btn-primary font-weight-bold px-4 py-0" type="submit">Follow</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mb-5 section-explore">
|
||||
<p class="lead text-muted font-weight-bold mb-0">Explore</p>
|
||||
<div class="profile-timeline">
|
||||
<div class="loader text-center">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
</div>
|
||||
<div class="row d-none">
|
||||
<div class="col-4 p-0 p-sm-2 p-md-3" v-for="post in posts">
|
||||
<a class="card info-overlay card-md-border-0" :href="post.url">
|
||||
<div class="square filter_class">
|
||||
<div class="square-content" v-bind:style="{ 'background-image': 'url(' + post.thumb + ')' }"></div>
|
||||
<div class="info-overlay-text">
|
||||
<h5 class="text-white m-auto font-weight-bold">
|
||||
<span class="pr-4">
|
||||
<span class="far fa-heart fa-lg pr-1"></span> {{post.likes_count}}
|
||||
</span>
|
||||
<span>
|
||||
<span class="far fa-comment fa-lg pr-1"></span> {{post.comments_count}}
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mb-5">
|
||||
<p class="lead text-center">To view more posts, check the <a href="#" class="font-weight-bold">home</a>, <a href="#" class="font-weight-bold">local</a> or <a href="#" class="font-weight-bold">federated</a> timelines.</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
people: {},
|
||||
posts: {},
|
||||
trending: {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData() {
|
||||
axios.get('/api/v2/discover')
|
||||
.then((res) => {
|
||||
let data = res.data;
|
||||
this.people = data.people;
|
||||
this.posts = data.posts;
|
||||
|
||||
if(this.people.length > 1) {
|
||||
$('.section-people .lds-ring').hide();
|
||||
$('.section-people .row.d-none').removeClass('d-none');
|
||||
}
|
||||
|
||||
if(this.posts.length > 1) {
|
||||
$('.section-explore .lds-ring').hide();
|
||||
$('.section-explore .row.d-none').removeClass('d-none');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,82 +1,44 @@
|
|||
<style type="text/css">
|
||||
.b-dropdown > button {
|
||||
padding:0 !important;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
<style>
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
.comment-text {
|
||||
word-break: break-all;
|
||||
}
|
||||
.b-dropdown {
|
||||
padding:0 !important;
|
||||
.comment-text p {
|
||||
display: inline;
|
||||
}
|
||||
.b-dropdown < button {
|
||||
}
|
||||
.lds-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 51px;
|
||||
height: 51px;
|
||||
margin: 6px;
|
||||
border: 6px solid #6c757d;
|
||||
border-radius: 50%;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #6c757d transparent transparent transparent;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="lwrapper text-center">
|
||||
<div class="postCommentsLoader text-center">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
</div>
|
||||
<div class="cwrapper d-none">
|
||||
<div class="postCommentsContainer d-none">
|
||||
<p class="mb-1 text-center load-more-link"><a href="#" class="text-muted" v-on:click="loadMore">Load more comments</a></p>
|
||||
<div class="comments" data-min-id="0" data-max-id="0">
|
||||
<p class="mb-0 d-flex justify-content-between align-items-center" v-for="(comment, index) in results" :data-id="comment.id" v-bind:key="comment.id">
|
||||
<span class="pr-3">
|
||||
<span class="font-weight-bold pr-1"><bdi><a class="text-dark" :href="comment.account.url">{{comment.account.username}}</a></bdi></span>
|
||||
<span class="comment-text" v-html="comment.content"></span>
|
||||
<p class="mb-1" v-for="(comment, index) in results" :data-id="comment.id" v-bind:key="comment.id">
|
||||
<span class="d-flex justify-content-between align-items-center">
|
||||
<span class="pr-3" style="overflow: hidden;">
|
||||
<div class="font-weight-bold pr-1"><bdi><a class="text-dark" :href="comment.account.url" :title="comment.account.username">{{l(comment.account.username)}}</a></bdi>
|
||||
</div>
|
||||
<div class="read-more" style="overflow: hidden;">
|
||||
<span class="comment-text" v-html="comment.content" style="overflow: hidden;"></span>
|
||||
</div>
|
||||
</span>
|
||||
<b-dropdown :id="comment.uri" variant="link" no-caret class="float-right">
|
||||
<template slot="button-content">
|
||||
<i class="fas fa-ellipsis-v text-muted"></i><span class="sr-only">Options</span>
|
||||
</template>
|
||||
<b-dropdown-item class="font-weight-bold" v-on:click="reply(comment)">Reply</b-dropdown-item>
|
||||
<b-dropdown-item class="font-weight-bold" :href="comment.url">Permalink</b-dropdown-item>
|
||||
<b-dropdown-item class="font-weight-bold" v-on:click="embed(comment)">Embed</b-dropdown-item>
|
||||
<b-dropdown-item class="font-weight-bold" :href="comment.account.url">Profile</b-dropdown-item>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item class="font-weight-bold" :href="'/i/report?type=post&id='+comment.id">Report</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</span>
|
||||
<b-dropdown :id="comment.uri" variant="link" no-caret class="float-right">
|
||||
<template slot="button-content">
|
||||
<i class="fas fa-ellipsis-v text-muted"></i><span class="sr-only">Options</span>
|
||||
</template>
|
||||
<b-dropdown-item class="font-weight-bold" v-on:click="reply(comment)">Reply</b-dropdown-item>
|
||||
<b-dropdown-item class="font-weight-bold" :href="comment.url">Permalink</b-dropdown-item>
|
||||
<b-dropdown-item class="font-weight-bold" v-on:click="embed(comment)">Embed</b-dropdown-item>
|
||||
<b-dropdown-item class="font-weight-bold" :href="comment.account.url">Profile</b-dropdown-item>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item class="font-weight-bold" :href="'/i/report?type=post&id='+comment.id">Report</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -98,10 +60,18 @@ export default {
|
|||
mounted() {
|
||||
this.fetchData();
|
||||
},
|
||||
updated() {
|
||||
pixelfed.readmore();
|
||||
},
|
||||
methods: {
|
||||
embed(e) {
|
||||
pixelfed.embed.build(e);
|
||||
},
|
||||
l(e) {
|
||||
let len = e.length;
|
||||
if(len < 10) { return e; }
|
||||
return e.substr(0, 10)+'...';
|
||||
},
|
||||
reply(e) {
|
||||
this.reply_to_profile_id = e.account.id;
|
||||
$('.comment-form input[name=comment]').val('@'+e.account.username+' ');
|
||||
|
@ -114,11 +84,33 @@ export default {
|
|||
let self = this;
|
||||
this.results = response.data.data;
|
||||
this.pagination = response.data.meta.pagination;
|
||||
$('.lwrapper').addClass('d-none');
|
||||
$('.cwrapper').removeClass('d-none');
|
||||
$('.postCommentsLoader').addClass('d-none');
|
||||
$('.postCommentsContainer').removeClass('d-none');
|
||||
}).catch(error => {
|
||||
$('.lds-ring').attr('style','width:100%').addClass('pt-4 font-weight-bold text-muted').text('An error occured, cannot fetch comments. Please try again later.');
|
||||
if(!error.response) {
|
||||
$('.postCommentsLoader .lds-ring')
|
||||
.attr('style','width:100%')
|
||||
.addClass('pt-4 font-weight-bold text-muted')
|
||||
.text('An error occured, cannot fetch comments. Please try again later.');
|
||||
console.log(error);
|
||||
} else {
|
||||
switch(error.response.status) {
|
||||
case 401:
|
||||
$('.postCommentsLoader .lds-ring')
|
||||
.attr('style','width:100%')
|
||||
.addClass('pt-4 font-weight-bold text-muted')
|
||||
.text('Please login to view.');
|
||||
break;
|
||||
|
||||
default:
|
||||
$('.postCommentsLoader .lds-ring')
|
||||
.attr('style','width:100%')
|
||||
.addClass('pt-4 font-weight-bold text-muted')
|
||||
.text('An error occured, cannot fetch comments. Please try again later.');
|
||||
break;
|
||||
}
|
||||
console.log(error.response.status);
|
||||
}
|
||||
});
|
||||
},
|
||||
loadMore(e) {
|
||||
|
@ -127,18 +119,16 @@ export default {
|
|||
$('.load-more-link').addClass('d-none');
|
||||
return;
|
||||
}
|
||||
$('.cwrapper').addClass('d-none');
|
||||
$('.lwrapper').removeClass('d-none');
|
||||
$('.postCommentsLoader').removeClass('d-none');
|
||||
let next = this.pagination.links.next;
|
||||
axios.get(next)
|
||||
.then(response => {
|
||||
let self = this;
|
||||
let res = response.data.data;
|
||||
$('.lwrapper').addClass('d-none');
|
||||
$('.postCommentsLoader').addClass('d-none');
|
||||
for(let i=0; i < res.length; i++) {
|
||||
this.results.unshift(res[i]);
|
||||
}
|
||||
$('.cwrapper').removeClass('d-none');
|
||||
this.pagination = response.data.meta.pagination;
|
||||
});
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue