Merge branch 'frontend-ui-refactor' into fix-startup-script

pull/553/head
Brad Koehn 2018-11-25 15:14:56 -06:00 zatwierdzone przez GitHub
commit bd822cd5dc
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
159 zmienionych plików z 6179 dodań i 1088 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -33,6 +33,7 @@ class UpdateCommand extends Command
'0.1.7',
'0.1.8',
'0.1.9',
'0.2.1',
];
protected $version;
@ -76,10 +77,15 @@ class UpdateCommand extends Command
case '0.1.8':
$this->info('You are running an older version that doesn\'t require any updates!');
break;
case '0.1.9':
$this->update019();
break;
case '0.2.1':
$this->update021();
break;
default:
# code...
break;
@ -127,6 +133,34 @@ class UpdateCommand extends Command
$bar->finish();
}
public function update021()
{
$this->buildVersionFile();
$v = $this->getVersionFile();
if($v['updated'] == true) {
$this->info('Already up to date!');
exit;
}
$statusCount = Status::count();
$this->info('Running updates ...');
$bar = $this->output->createProgressBar($statusCount);
Status::has('media')->chunk(200, function($statuses) use ($bar) {
foreach($statuses as $status) {
if($status->firstMedia()) {
$media = $status->firstMedia();
if(in_array($media->mime, ['image/jpeg', 'image/png'])) {
ImageThumbnail::dispatch($media);
}
}
$bar->advance();
}
});
$this->updateVersionFile('0.2.1', true);
$bar->finish();
}
protected function buildVersionFile()
{
$path = storage_path('app/version.json');

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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);
@ -200,6 +203,11 @@ class AccountController extends Controller
'filter_type' => 'mute',
]);
$pid = $user->id;
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:people:$pid");
Cache::forget("feature:discover:posts:$pid");
return redirect()->back();
}
@ -221,6 +229,9 @@ class AccountController extends Controller
switch ($type) {
case 'user':
$profile = Profile::findOrFail($item);
if ($profile->id == $user->id) {
return abort(403);
}
$class = get_class($profile);
$filterable['id'] = $profile->id;
$filterable['type'] = $class;
@ -241,6 +252,10 @@ class AccountController extends Controller
'filter_type' => 'block',
]);
$pid = $user->id;
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:people:$pid");
Cache::forget("feature:discover:posts:$pid");
return redirect()->back();
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class CollectionController extends Controller
{
//
}

Wyświetl plik

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class CollectionItemController extends Controller
{
//
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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'));
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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');
}

Wyświetl plik

@ -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'));
}
}

Wyświetl plik

@ -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');
}
}

Wyświetl plik

@ -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');
}
}

Wyświetl plik

@ -4,12 +4,19 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\{
DirectMessage,
Hashtag,
Follower,
Like,
Media,
Notification,
Profile,
StatusHashtag,
Status,
UserFilter,
};
use Auth,Cache;
use Carbon\Carbon;
use League\Fractal;
use App\Transformer\Api\{
AccountTransformer,
@ -30,60 +37,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 +54,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 +90,127 @@ 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;
$pid = $profile->id;
$following = Cache::remember('feature:discover:following:'.$pid, 60, function() use ($pid) {
return Follower::whereProfileId($pid)->pluck('following_id')->toArray();
});
$filters = Cache::remember("user:filter:list:$pid", 60, function() use($pid) {
return UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id')->toArray();
});
$following = array_merge($following, $filters);
$people = Cache::remember('feature:discover:people:'.$pid, 15, function() use ($following) {
return Profile::select('id', 'name', 'username')
->with('avatar')
->inRandomOrder()
->whereHas('statuses')
->whereNull('domain')
->whereNotIn('id', $following)
->whereIsPrivate(false)
->take(3)
->get();
});
$posts = Cache::remember('feature:discover:posts:'.$pid, 60, function() use ($following) {
return Status::select('id', 'caption', 'profile_id')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereIsNsfw(false)
->whereVisibility('public')
->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;
}
}

Wyświetl plik

@ -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;
}
@ -150,7 +152,7 @@ class ProfileController extends Controller
$blocked = $this->blockedProfileCheck($profile);
$check = $this->privateProfileCheck($profile, null);
if($check || $blocked) {
return view('profile.private', compact('user'));
return view('profile.private', compact('user', 'is_following'));
}
}
$followers = $profile->followers()->orderBy('created_at', 'desc')->simplePaginate(12);
@ -174,7 +176,7 @@ class ProfileController extends Controller
$blocked = $this->blockedProfileCheck($profile);
$check = $this->privateProfileCheck($profile, null);
if($check || $blocked) {
return view('profile.private', compact('user'));
return view('profile.private', compact('user', 'is_following'));
}
}
$following = $profile->following()->orderBy('created_at', 'desc')->simplePaginate(12);

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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(),

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -33,12 +33,15 @@ 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);
$following = Follower::whereProfileId($pid)->pluck('following_id');
$following->push($pid)->toArray();
$filtered = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id');
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id')->toArray();
$timeline = Status::whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered)
->whereHas('media')
@ -46,6 +49,7 @@ class SiteController extends Controller
->orderBy('created_at', 'desc')
->withCount(['comments', 'likes', 'shares'])
->simplePaginate(20);
$type = 'personal';
return view('timeline.template', compact('timeline', 'type'));
@ -53,29 +57,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');
}
}

Wyświetl plik

@ -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',
@ -162,12 +133,17 @@ class StatusController extends Controller
if ($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
StatusDelete::dispatch($status);
}
return redirect(Auth::user()->url());
if($request->wantsJson()) {
return response()->json(['Status successfully deleted.']);
} else {
return redirect(Auth::user()->url());
}
}
public function storeShare(Request $request)
{
$this->authCheck();
$this->validate($request, [
'item' => 'required|integer',
]);

Wyświetl plik

@ -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,22 @@ 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)
$private = Profile::whereIsPrivate(true)->where('id', '!=', $pid)->pluck('id');
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id');
$private = Profile::whereIsPrivate(true)->pluck('id');
$filtered = $filtered->merge($private);
->pluck('filterable_id')->toArray();
$filtered = array_merge($private->toArray(), $filters);
$timeline = Status::whereHas('media')
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')
@ -57,7 +41,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'));

Wyświetl plik

@ -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,

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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();

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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;
@ -35,15 +38,12 @@ class StatusDelete implements ShouldQueue
public function handle()
{
$status = $this->status;
$this->unlinkRemoveMedia($status);
}
public function unlinkRemoveMedia($status)
{
if ($status->media()->count() == 0) {
return;
}
foreach ($status->media as $media) {
$thumbnail = storage_path("app/{$media->thumbnail_path}");
$photo = storage_path("app/{$media->media_path}");
@ -73,6 +73,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;

Wyświetl plik

@ -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();
});

Wyświetl plik

@ -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()
{
//
}
}

Wyświetl plik

@ -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()
{
//
}
}

Wyświetl plik

@ -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) {
}
}
}

Wyświetl plik

@ -15,6 +15,7 @@ class Like extends Model
* @var array
*/
protected $dates = ['deleted_at'];
protected $fillable = ['profile_id', 'status_id'];
public function actor()
{

Wyświetl plik

@ -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'];
}
}
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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'));
}

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -2,7 +2,7 @@
namespace App;
use Auth;
use Auth, Cache;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Storage;
@ -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()
{
@ -37,26 +37,29 @@ class Status extends Model
public function viewType()
{
$media = $this->firstMedia();
$mime = explode('/', $media->mime)[0];
$count = $this->media()->count();
$type = ($mime == 'image') ? 'image' : 'video';
if($count > 1) {
$type = ($type == 'image') ? 'album' : 'video-album';
}
return $type;
return Cache::remember('status:view-type:'.$this->id, 40320, function() {
$media = $this->firstMedia();
$mime = explode('/', $media->mime)[0];
$count = $this->media()->count();
$type = ($mime == 'image') ? 'image' : 'video';
if($count > 1) {
$type = ($type == 'image') ? 'album' : 'video-album';
}
return $type;
});
}
public function thumb($showNsfw = false)
{
$type = $this->viewType();
$is_nsfw = !$showNsfw ? $this->is_nsfw : false;
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['image', 'album'])) {
return 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
}
return Cache::remember('status:thumb:'.$this->id, 40320, function() use ($showNsfw) {
$type = $this->viewType();
$is_nsfw = !$showNsfw ? $this->is_nsfw : false;
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['image', 'album', 'video'])) {
return 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
}
return url(Storage::url($this->firstMedia()->thumbnail_path));
return url(Storage::url($this->firstMedia()->thumbnail_path));
});
}
public function url()
@ -64,11 +67,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 +101,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 +116,7 @@ class Status extends Model
public function bookmarked()
{
if (!Auth::check()) {
return 0;
return false;
}
$profile = Auth::user()->profile;
@ -130,6 +130,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 +142,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 +257,7 @@ class Status extends Model
'url' => $media->url(),
'name' => null
];
})
})->toArray()
]
];
}
@ -268,18 +271,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 100644
Wyświetl plik

@ -0,0 +1,10 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Story extends Model
{
//
}

Wyświetl plik

@ -0,0 +1,10 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class StoryReaction extends Model
{
//
}

Wyświetl plik

@ -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()
];
}
}

Wyświetl plik

@ -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,
]
];
}
}

Wyświetl plik

@ -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()
];
}
}

Wyświetl plik

@ -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()
];
}
}

Wyświetl plik

@ -16,8 +16,13 @@ class MediaTransformer extends Fractal\TransformerAbstract
'remote_url' => null,
'preview_url' => $media->thumbnailUrl(),
'text_url' => null,
'meta' => $media->metadata,
'description' => null,
'meta' => null,
'description' => $media->caption,
'license' => $media->license,
'is_nsfw' => $media->is_nsfw,
'orientation' => $media->orientation,
'filter_name' => $media->filter_name,
'filter_class' => $media->filter_class,
];
}
}

Wyświetl plik

@ -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
];
}
}

Wyświetl plik

@ -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());
}

Wyświetl plik

@ -20,7 +20,7 @@ use App\Jobs\AvatarPipeline\CreateAvatar;
use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
use App\Jobs\StatusPipeline\NewStatusPipeline;
use HttpSignatures\{GuzzleHttpSignatures, KeyStore, Context, Verifier};
use App\Util\HttpSignatures\{GuzzleHttpSignatures, KeyStore, Context, Verifier};
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
class Helpers {
@ -309,31 +309,27 @@ class Helpers {
{
$profile = $senderProfile;
$keyId = $profile->keyId();
$privateKey = openssl_pkey_get_private($profile->private_key);
$date = date('D, d M Y h:i:s').' GMT';
$date = new \DateTime('UTC');
$date = $date->format('D, d M Y H:i:s \G\M\T');
$host = parse_url($url, PHP_URL_HOST);
$path = parse_url($url, PHP_URL_PATH);
$headers = [
'(request-target)' => 'post '.parse_url($url, PHP_URL_PATH),
'Date' => $date,
'Host' => $host,
'Content-Type' => 'application/activity+json',
'date' => $date,
'host' => $host,
'content-type' => 'application/activity+json',
];
if($body) {
$payload = is_string($body) ? $body : json_encode($body);
$digest = base64_encode(hash('sha256', $payload, true));
$headers['Digest'] = 'SHA-256=' . $digest;
}
$stringToSign = self::_headersToSigningString($headers);
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
openssl_sign($stringToSign, $signature, $privateKey, OPENSSL_ALGO_SHA256);
openssl_free_key($privateKey);
$signature = base64_encode($signature);
$signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
unset($headers['(request-target)']);
$headers['Signature'] = $signatureHeader;
Zttp::withHeaders($headers)->post($url, $body);
$context = new Context([
'keys' => [$profile->keyId() => $profile->private_key],
'algorithm' => 'rsa-sha256',
'headers' => ['(request-target)', 'date', 'host', 'content-type'],
]);
$handlerStack = GuzzleHttpSignatures::defaultHandlerFromContext($context);
$client = new Client(['handler' => $handlerStack]);
$response = $client->request('POST', $url, ['headers' => $headers, 'json' => $body]);
return;
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -0,0 +1,7 @@
<?php
namespace App\Util\HttpSignatures;
class AlgorithmException extends Exception
{
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -0,0 +1,7 @@
<?php
namespace App\Util\HttpSignatures;
class Exception extends \Exception
{
}

Wyświetl plik

@ -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);
};
};
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -0,0 +1,7 @@
<?php
namespace App\Util\HttpSignatures;
class KeyException extends Exception
{
}

Wyświetl plik

@ -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");
}
}
}

Wyświetl plik

@ -0,0 +1,7 @@
<?php
namespace App\Util\HttpSignatures;
class KeyStoreException extends Exception
{
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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');
}
}
}

Wyświetl plik

@ -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()
);
}
}

Wyświetl plik

@ -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());
}
}

Wyświetl plik

@ -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");
}
}
}

Wyświetl plik

@ -0,0 +1,7 @@
<?php
namespace App\Util\HttpSignatures;
class SignatureParseException extends Exception
{
}

Wyświetl plik

@ -0,0 +1,7 @@
<?php
namespace App\Util\HttpSignatures;
class SignedHeaderNotPresentException extends Exception
{
}

Wyświetl plik

@ -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
);
}
}

Wyświetl plik

@ -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()
);
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -98,7 +98,6 @@ class Extractor extends Regex
$entities = array_merge($entities, $this->extractURLsWithIndices($tweet));
$entities = array_merge($entities, $this->extractHashtagsWithIndices($tweet, false));
$entities = array_merge($entities, $this->extractMentionsOrListsWithIndices($tweet));
$entities = array_merge($entities, $this->extractCashtagsWithIndices($tweet));
$entities = $this->removeOverlappingEntities($entities);
return $entities;
@ -303,33 +302,6 @@ class Extractor extends Regex
*/
public function extractCashtagsWithIndices($tweet = null)
{
if (is_null($tweet)) {
$tweet = $this->tweet;
}
if (!preg_match('/\$/iu', $tweet)) {
return [];
}
preg_match_all(self::$patterns['valid_cashtag'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
$tags = [];
foreach ($matches as $match) {
list($all, $before, $dollar, $cash_text, $outer) = array_pad($match, 3, ['', 0]);
$start_position = $dollar[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $dollar[1])) : $dollar[1];
$end_position = $start_position + StringUtils::strlen($dollar[0].$cash_text[0]);
if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
continue;
}
$tags[] = [
'cashtag' => $cash_text[0],
'indices' => [$start_position, $end_position],
];
}
return $tags;
}
/**

Wyświetl plik

@ -15,7 +15,7 @@ class Image
public $orientation;
public $acceptedMimes = [
'image/png',
'image/jpeg',
'image/jpeg'
];
public function __construct()
@ -112,11 +112,15 @@ class Image
try {
$img = Intervention::make($file)->orientate();
if($thumbnail) {
$img->crop($aspect['width'], $aspect['height']);
} else {
$img->resize($aspect['width'], $aspect['height'], function ($constraint) {
$constraint->aspectRatio();
});
} 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']);

Wyświetl plik

@ -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",

Wyświetl plik

@ -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,8 @@ 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,
'FFMpeg' => Pbmedia\LaravelFFMpeg\FFMpegFacade::class,
],
];

Wyświetl plik

@ -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)

Wyświetl plik

@ -56,12 +56,13 @@ return [
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
],
],

Wyświetl plik

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.1.9',
'version' => '0.2.1',
/*
|--------------------------------------------------------------------------

141
config/purify.php 100644
Wyświetl plik

@ -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'
],
],
];

Wyświetl plik

@ -61,7 +61,7 @@ return [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'retry_after' => 1800,
'block_for' => null,
],

Wyświetl plik

@ -4,16 +4,16 @@ ARG COMPOSER_VERSION="1.6.5"
ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& apt-get install -y --no-install-recommends git gosu \
optipng pngquant jpegoptim gifsicle \
libfreetype6 libjpeg62-turbo libpng16-16 libxpm4 libvpx4 libmagickwand-6.q16-3 \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
libfreetype6 libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-3 \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libwebp-dev libmagickwand-dev \
&& docker-php-source extract \
&& docker-php-ext-configure gd \
--with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
--with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
--with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
--with-vpx-dir=/usr/lib/x86_64-linux-gnu/ \
--with-webp-dir=/usr/lib/x86_64-linux-gnu/ \
&& docker-php-ext-install pdo_mysql pcntl gd exif bcmath \
&& pecl install imagick \
&& docker-php-ext-enable imagick pcntl imagick gd exif \

Wyświetl plik

@ -1,31 +1,64 @@
FROM php:7.2.6-fpm-alpine
FROM php:7-fpm
ARG COMPOSER_VERSION="1.6.5"
ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
RUN apk add --no-cache --virtual .build build-base autoconf imagemagick-dev libtool && \
apk --no-cache add imagemagick git && \
docker-php-ext-install pdo_mysql pcntl && \
pecl install imagick && \
docker-php-ext-enable imagick pcntl imagick && \
curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /tmp/composer.phar && \
echo "${COMPOSER_CHECKSUM} /tmp/composer.phar" | sha256sum -c - && \
install -m0755 -o root -g root /tmp/composer.phar /usr/bin/composer.phar && \
ln -sf /usr/bin/composer.phar /usr/bin/composer && \
rm /tmp/composer.phar && \
apk --no-cache del --purge .build
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
optipng pngquant jpegoptim gifsicle \
libfreetype6 libjpeg62-turbo libpng16-16 libxpm4 libvpx4 libmagickwand-6.q16-3 \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
&& docker-php-source extract \
&& docker-php-ext-configure gd \
--with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
--with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
--with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
--with-vpx-dir=/usr/lib/x86_64-linux-gnu/ \
&& docker-php-ext-install pdo_mysql pcntl gd exif bcmath \
&& pecl install imagick \
&& docker-php-ext-enable imagick pcntl imagick gd exif \
&& curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /usr/bin/composer \
&& echo "${COMPOSER_CHECKSUM} /usr/bin/composer" | sha256sum -c - \
&& chmod 755 /usr/bin/composer \
&& apt-get autoremove --purge -y \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
&& rm -rf /var/cache/apt \
&& docker-php-source delete
COPY . /var/www/html/
WORKDIR /var/www/html
RUN install -d -m0755 -o www-data -g www-data \
/var/www/html/storage \
/var/www/html/storage/framework \
/var/www/html/storage/logs \
/var/www/html/storage/framework/sessions \
/var/www/html/storage/framework/views \
/var/www/html/storage/framework/cache && \
composer install --prefer-source --no-interaction
VOLUME ["/var/www/html"]
ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"
COPY . /var/www/
WORKDIR /var/www/
RUN cp -r storage storage.skel \
&& cp contrib/docker/php.ini /usr/local/etc/php/conf.d/pixelfed.ini \
&& composer install --prefer-source --no-interaction \
&& rm -rf html && ln -s public html
VOLUME ["/var/www/storage", "/var/www/public.ext"]
ENV APP_ENV=production \
APP_DEBUG=false \
LOG_CHANNEL=stderr \
DB_CONNECTION=mysql \
DB_PORT=3306 \
DB_HOST=db \
BROADCAST_DRIVER=log \
QUEUE_DRIVER=redis \
HORIZON_PREFIX=horizon-pixelfed \
REDIS_HOST=redis \
SESSION_SECURE_COOKIE=true \
API_BASE="/api/1/" \
API_SEARCH="/api/search" \
OPEN_REGISTRATION=true \
ENFORCE_EMAIL_VERIFICATION=true \
REMOTE_FOLLOW=false \
ACTIVITY_PUB=false
CMD cp -r storage.skel/* storage/ \
&& cp -r public/* public.ext/ \
&& chown -R www-data:www-data storage/ \
&& php artisan storage:link \
&& php artisan migrate --force \
&& php artisan update \
&& exec php-fpm

Wyświetl plik

@ -2,18 +2,18 @@
# Create the storage tree if needed and fix permissions
cp -r storage.skel/* storage/
chown -R www-data:www-data storage/
chown -R www-data:www-data storage/ bootstrap/cache/
php artisan storage:link
# Migrate database if the app was upgraded
php artisan migrate --force
gosu www-data:www-data php artisan migrate --force
# Run other specific migratins if required
php artisan update
gosu www-data:www-data php artisan update
# Run a worker if it is set as embedded
if [ "$HORIZON_EMBED" = "true" ]; then
php artisan horizon &
gosu www-data:www-data php artisan horizon &
fi
# Finally run Apache

Wyświetl plik

@ -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),
];
});

Wyświetl plik

@ -38,7 +38,7 @@ services:
# - "app-storage:/var/www/storage"
# networks:
# - internal
# command: php artisan horizon
# command: gosu www-data php artisan horizon
db:
image: mysql:5.7

194
package-lock.json wygenerowano
Wyświetl plik

@ -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",

Wyświetl plik

@ -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"
}
}

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 90 KiB

Wyświetl plik

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>04/icon/black/svg/pixelfed-icon-black</title>
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>icon/black/svg/pixelfed-icon-black</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon-copy-6" fill="#000000">
<path d="M26.1989752,16.1654164 C26.1191375,15.6717246 25.9970712,15.1834955 25.8327784,14.7064947 C25.4032698,13.4577487 24.7071549,12.3267513 23.7688167,11.3882121 L18.8200906,6.43842552 C18.6286052,6.24689918 18.4253978,6.0651186 18.2059577,5.88739016 C18.9457319,2.7779446 21.7493666,0.5 25.0056862,0.5 C28.4355286,0.5 31.3631553,3.02718742 31.9058524,6.39134335 C31.9709938,6.7962157 32.0040616,7.15536036 32.0040616,7.499875 L32.0040616,14.49975 C32.0040616,15.3777768 31.8350727,16.2367131 31.511582,17.0488633 C30.4438253,16.4852131 29.2225744,16.1654164 27.9252137,16.1654164 L26.1989752,16.1654164 Z M23.8073996,31.046185 C23.5834161,32.4635947 23.7071641,33.9247757 24.178594,35.2935053 C24.6081026,36.5422513 25.3042175,37.6732487 26.2425556,38.6117879 L31.1921829,43.5624744 C31.3833579,43.7531345 31.5862081,43.9343531 31.8055418,44.112075 C31.065962,47.2217942 28.2621925,49.5 25.0056862,49.5 C21.5757888,49.5 18.6481233,46.9727317 18.1054939,43.6084951 C18.0403874,43.2038799 18.0073108,42.8446855 18.0073108,42.500125 L18.0073108,35.50025 C18.0073108,35.4938387 18.0073198,35.4874284 18.0073378,35.4810192 L22.6056081,31.046185 L23.8073996,31.046185 Z M35.1468104,25.8777977 C35.1968412,25.8615612 35.2467562,25.8448643 35.2965488,25.8277069 C36.5465838,25.396568 37.6766493,24.7009751 38.6141205,23.763303 L43.5628467,18.8135164 C43.7543612,18.6219608 43.9361291,18.4186771 44.1138454,18.1991515 C47.2222774,18.9383789 49.5,21.7431301 49.5,25.0004375 C49.5,28.4310698 46.9732731,31.3593627 43.6097572,31.9021084 C43.2052287,31.9672288 42.8461113,32.0003125 42.5016246,32.0003125 L35.5032492,32.0003125 C34.4465015,32.0003125 33.4174129,31.75537 32.4642879,31.2907626 C31.9585449,31.0442436 31.4866999,30.7388098 31.0577027,30.3834131 C32.9917905,29.5211241 34.4916783,27.8849489 35.1468104,25.8777977 Z M32.9818651,18.0619641 C33.4869192,16.9416136 33.7536554,15.736333 33.7536554,14.49975 L33.7536554,7.499875 C33.7536554,7.22965928 33.7384248,6.95788037 33.7088174,6.67629632 C36.4300225,5.000963 40.0234106,5.37310623 42.325648,7.67579129 C44.7509518,10.1024315 45.0347176,13.9597464 43.0399586,16.7217074 C42.8019022,17.0521235 42.5708139,17.3300572 42.325697,17.5752267 L37.3769708,22.5250133 C36.8285828,23.0735188 36.1967915,23.5184213 35.5026804,23.8530318 C35.505411,23.7709507 35.506787,23.6885334 35.506787,23.6058007 C35.506787,21.4032822 34.5316098,19.4242869 32.9818651,18.0619641 Z M29.1180777,30.9546188 C29.8495788,31.7387598 30.723016,32.3887119 31.6978107,32.8638651 C32.8860975,33.4431037 34.176657,33.7502812 35.5032492,33.7502812 L42.5016246,33.7502812 C42.7718182,33.7502812 43.043575,33.7350433 43.3251385,33.7054219 C45.0005544,36.4268542 44.6282644,40.021099 42.325697,42.3241597 C39.9008053,44.7495711 36.0434832,45.0332243 33.2816352,43.0384785 C32.9514226,42.8004669 32.6735484,42.5693291 32.4284315,42.3241597 L27.4797053,37.3743731 C26.7330644,36.6275722 26.1784347,35.7261305 25.8326956,34.723272 C25.4247205,33.5387745 25.3418769,32.266466 25.5842066,31.046185 L27.9252137,31.046185 C28.3310733,31.046185 28.7294844,31.0148879 29.1180777,30.9546188 Z M18.0361092,17.005126 C16.9498565,16.5196511 15.7507833,16.2505937 14.4967508,16.2505937 L7.49837538,16.2505937 C7.232029,16.2505937 6.96418507,16.2653384 6.68632324,16.2940088 C5.0111787,13.5720997 5.38321091,9.97879809 7.68567533,7.67584029 C10.1105671,5.25042891 13.9678891,4.96677571 16.7297371,6.96152155 C17.0599498,7.19953306 17.337824,7.43067088 17.5829409,7.67584029 L22.5316124,12.6264472 C23.2782947,13.3732895 23.8329179,14.2747125 24.1786584,15.2775495 C24.2790442,15.5688604 24.3597451,15.8655119 24.4207608,16.1654164 L20.6352393,16.1654164 C19.6618729,16.1654164 18.7627665,16.4774398 18.0361092,17.005126 Z M16.2612547,35.2537259 C16.258899,35.3357513 16.257717,35.4179313 16.257717,35.50025 L16.257717,42.500125 C16.257717,42.7703407 16.2729476,43.0421196 16.3025549,43.3237037 C13.5813306,44.9990489 9.98791079,44.6268885 7.68567533,42.3241597 C5.26078356,39.8987483 4.97719112,36.0405997 6.97150962,33.2781599 C7.20947014,32.9478765 7.44055844,32.6699428 7.68567533,32.4247733 L12.6344015,27.4749867 C13.3810424,26.7281858 14.282291,26.1734373 15.2849346,25.8276241 C15.6048854,25.7173766 15.9312445,25.6308666 16.2612547,25.5680951 L16.2612547,35.2537259 Z M16.2612547,23.7931301 C15.7377606,23.8724201 15.2199939,23.9990983 14.7148236,24.1731681 C13.4647886,24.604307 12.334723,25.2998999 11.3972518,26.237572 L6.44852566,31.1873586 C6.25392808,31.3819979 6.06939348,31.588746 5.88894736,31.812338 C2.78032488,31.0741675 0.5,28.2622334 0.5,25.0004375 C0.5,21.5662743 3.03179045,18.6360332 6.40961211,18.0974198 C6.55156249,18.0732434 7.23969074,18.0005625 7.49837538,18.0005625 L14.4967508,18.0005625 C15.2999569,18.0005625 16.0753514,18.1390919 16.7989258,18.3943464 C16.4561913,19.0066903 16.2612547,19.7099771 16.2612547,20.4579458 L16.2612547,23.7931301 Z" id="Combined-Shape"></path>
<g id="icon-copy-11" fill="#000000">
<g id="Group-12">
<path d="M25,50 C11.1928813,50 0,38.8071187 0,25 C0,11.1928813 11.1928813,0 25,0 C38.8071187,0 50,11.1928813 50,25 C50,38.8071187 38.8071187,50 25,50 Z M23.0153159,30.4580153 L27.6014641,30.4580153 C31.9217989,30.4580153 35.4241222,27.0484974 35.4241222,22.842644 C35.4241222,18.6367906 31.9217989,15.2272727 27.6014641,15.2272727 L20.9822918,15.2272727 C18.4897909,15.2272727 16.4692198,17.1943023 16.4692198,19.6207562 L16.4692198,36.7207782 L23.0153159,30.4580153 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 5.3 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.1 KiB

Wyświetl plik

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>icon/grey/svg/pixelfed-icon-grey</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon-copy-10" fill="#D8D8D8">
<g id="Group-12">
<path d="M25,50 C11.1928813,50 0,38.8071187 0,25 C0,11.1928813 11.1928813,0 25,0 C38.8071187,0 50,11.1928813 50,25 C50,38.8071187 38.8071187,50 25,50 Z M23.0153159,30.4580153 L27.6014641,30.4580153 C31.9217989,30.4580153 35.4241222,27.0484974 35.4241222,22.842644 C35.4241222,18.6367906 31.9217989,15.2272727 27.6014641,15.2272727 L20.9822918,15.2272727 C18.4897909,15.2272727 16.4692198,17.1943023 16.4692198,19.6207562 L16.4692198,36.7207782 L23.0153159,30.4580153 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.1 KiB

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

1
public/js/components.js vendored 100644

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More