mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-25 15:55:22 +00:00
commit
e4b47d001c
18 changed files with 825 additions and 435 deletions
|
@ -56,6 +56,9 @@
|
||||||
- Updated NotificationCard, fix typo in mention, share and comments. Fixes #2848. ([b37bb426](https://github.com/pixelfed/pixelfed/commit/b37bb426))
|
- Updated NotificationCard, fix typo in mention, share and comments. Fixes #2848. ([b37bb426](https://github.com/pixelfed/pixelfed/commit/b37bb426))
|
||||||
- Updated StatusCard.vue, add togglecw events to other presenters. ([9607243f](https://github.com/pixelfed/pixelfed/commit/9607243f))
|
- Updated StatusCard.vue, add togglecw events to other presenters. ([9607243f](https://github.com/pixelfed/pixelfed/commit/9607243f))
|
||||||
- Updated presenters, fix content warning layout. ([fc56acb8](https://github.com/pixelfed/pixelfed/commit/fc56acb8))
|
- Updated presenters, fix content warning layout. ([fc56acb8](https://github.com/pixelfed/pixelfed/commit/fc56acb8))
|
||||||
|
- Updated reply blade view, fix missing avatar and media images. ([5fb33772](https://github.com/pixelfed/pixelfed/commit/5fb33772))
|
||||||
|
- Updated components, add fallback default avatar. ([726553f5](https://github.com/pixelfed/pixelfed/commit/726553f5))
|
||||||
|
- Updated job queue, separate deletes into their own queue. ([7f421392](https://github.com/pixelfed/pixelfed/commit/7f421392))
|
||||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||||
|
|
||||||
## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
|
## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
|
||||||
|
|
|
@ -3,16 +3,17 @@
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Jobs\InboxPipeline\{
|
use App\Jobs\InboxPipeline\{
|
||||||
InboxWorker,
|
DeleteWorker,
|
||||||
InboxValidator
|
InboxWorker,
|
||||||
|
InboxValidator
|
||||||
};
|
};
|
||||||
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
||||||
use App\{
|
use App\{
|
||||||
AccountLog,
|
AccountLog,
|
||||||
Like,
|
Like,
|
||||||
Profile,
|
Profile,
|
||||||
Status,
|
Status,
|
||||||
User
|
User
|
||||||
};
|
};
|
||||||
use App\Util\Lexer\Nickname;
|
use App\Util\Lexer\Nickname;
|
||||||
use App\Util\Webfinger\Webfinger;
|
use App\Util\Webfinger\Webfinger;
|
||||||
|
@ -23,146 +24,158 @@ use Illuminate\Http\Request;
|
||||||
use League\Fractal;
|
use League\Fractal;
|
||||||
use App\Util\Site\Nodeinfo;
|
use App\Util\Site\Nodeinfo;
|
||||||
use App\Util\ActivityPub\{
|
use App\Util\ActivityPub\{
|
||||||
Helpers,
|
Helpers,
|
||||||
HttpSignature,
|
HttpSignature,
|
||||||
Outbox
|
Outbox
|
||||||
};
|
};
|
||||||
use Zttp\Zttp;
|
use Zttp\Zttp;
|
||||||
|
|
||||||
class FederationController extends Controller
|
class FederationController extends Controller
|
||||||
{
|
{
|
||||||
public function nodeinfoWellKnown()
|
public function nodeinfoWellKnown()
|
||||||
{
|
{
|
||||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||||
return response()->json(Nodeinfo::wellKnown())
|
return response()->json(Nodeinfo::wellKnown())
|
||||||
->header('Access-Control-Allow-Origin','*');
|
->header('Access-Control-Allow-Origin','*');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function nodeinfo()
|
public function nodeinfo()
|
||||||
{
|
{
|
||||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||||
return response()->json(Nodeinfo::get())
|
return response()->json(Nodeinfo::get())
|
||||||
->header('Access-Control-Allow-Origin','*');
|
->header('Access-Control-Allow-Origin','*');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function webfinger(Request $request)
|
public function webfinger(Request $request)
|
||||||
{
|
{
|
||||||
abort_if(!config('federation.webfinger.enabled'), 400);
|
abort_if(!config('federation.webfinger.enabled'), 400);
|
||||||
|
|
||||||
abort_if(!$request->filled('resource'), 400);
|
abort_if(!$request->filled('resource'), 400);
|
||||||
|
|
||||||
$resource = $request->input('resource');
|
$resource = $request->input('resource');
|
||||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||||
if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
|
if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
$username = $parsed['username'];
|
$username = $parsed['username'];
|
||||||
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||||
if($profile->status != null) {
|
if($profile->status != null) {
|
||||||
return ProfileController::accountCheck($profile);
|
return ProfileController::accountCheck($profile);
|
||||||
}
|
}
|
||||||
$webfinger = (new Webfinger($profile))->generate();
|
$webfinger = (new Webfinger($profile))->generate();
|
||||||
|
|
||||||
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
|
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
|
||||||
->header('Access-Control-Allow-Origin','*');
|
->header('Access-Control-Allow-Origin','*');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hostMeta(Request $request)
|
public function hostMeta(Request $request)
|
||||||
{
|
{
|
||||||
abort_if(!config('federation.webfinger.enabled'), 404);
|
abort_if(!config('federation.webfinger.enabled'), 404);
|
||||||
|
|
||||||
$path = route('well-known.webfinger');
|
$path = route('well-known.webfinger');
|
||||||
$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 = '<?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>';
|
||||||
|
|
||||||
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function userOutbox(Request $request, $username)
|
public function userOutbox(Request $request, $username)
|
||||||
{
|
{
|
||||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||||
abort_if(!config('federation.activitypub.outbox'), 404);
|
abort_if(!config('federation.activitypub.outbox'), 404);
|
||||||
|
|
||||||
$profile = Profile::whereNull('domain')
|
$profile = Profile::whereNull('domain')
|
||||||
->whereNull('status')
|
->whereNull('status')
|
||||||
->whereIsPrivate(false)
|
->whereIsPrivate(false)
|
||||||
->whereUsername($username)
|
->whereUsername($username)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$key = 'ap:outbox:latest_10:pid:' . $profile->id;
|
$key = 'ap:outbox:latest_10:pid:' . $profile->id;
|
||||||
$ttl = now()->addMinutes(15);
|
$ttl = now()->addMinutes(15);
|
||||||
$res = Cache::remember($key, $ttl, function() use($profile) {
|
$res = Cache::remember($key, $ttl, function() use($profile) {
|
||||||
return Outbox::get($profile);
|
return Outbox::get($profile);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function userInbox(Request $request, $username)
|
public function userInbox(Request $request, $username)
|
||||||
{
|
{
|
||||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||||
abort_if(!config('federation.activitypub.inbox'), 404);
|
abort_if(!config('federation.activitypub.inbox'), 404);
|
||||||
|
|
||||||
$headers = $request->headers->all();
|
$headers = $request->headers->all();
|
||||||
$payload = $request->getContent();
|
$payload = $request->getContent();
|
||||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
$obj = json_decode($payload, true, 8);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sharedInbox(Request $request)
|
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||||
{
|
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
} else {
|
||||||
abort_if(!config('federation.activitypub.sharedInbox'), 404);
|
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$headers = $request->headers->all();
|
public function sharedInbox(Request $request)
|
||||||
$payload = $request->getContent();
|
{
|
||||||
dispatch(new InboxWorker($headers, $payload))->onQueue('high');
|
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||||
return;
|
abort_if(!config('federation.activitypub.sharedInbox'), 404);
|
||||||
}
|
|
||||||
|
|
||||||
public function userFollowing(Request $request, $username)
|
$headers = $request->headers->all();
|
||||||
{
|
$payload = $request->getContent();
|
||||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
$obj = json_decode($payload, true, 8);
|
||||||
|
|
||||||
$profile = Profile::whereNull('remote_url')
|
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||||
->whereUsername($username)
|
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||||
->whereIsPrivate(false)
|
} else {
|
||||||
->firstOrFail();
|
dispatch(new InboxWorker($headers, $payload))->onQueue('high');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if($profile->status != null) {
|
public function userFollowing(Request $request, $username)
|
||||||
abort(404);
|
{
|
||||||
}
|
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||||
|
|
||||||
$obj = [
|
$profile = Profile::whereNull('remote_url')
|
||||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
->whereUsername($username)
|
||||||
'id' => $request->getUri(),
|
->whereIsPrivate(false)
|
||||||
'type' => 'OrderedCollectionPage',
|
->firstOrFail();
|
||||||
'totalItems' => 0,
|
|
||||||
'orderedItems' => []
|
|
||||||
];
|
|
||||||
return response()->json($obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function userFollowers(Request $request, $username)
|
if($profile->status != null) {
|
||||||
{
|
abort(404);
|
||||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
}
|
||||||
|
|
||||||
$profile = Profile::whereNull('remote_url')
|
$obj = [
|
||||||
->whereUsername($username)
|
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||||
->whereIsPrivate(false)
|
'id' => $request->getUri(),
|
||||||
->firstOrFail();
|
'type' => 'OrderedCollectionPage',
|
||||||
|
'totalItems' => 0,
|
||||||
|
'orderedItems' => []
|
||||||
|
];
|
||||||
|
return response()->json($obj);
|
||||||
|
}
|
||||||
|
|
||||||
if($profile->status != null) {
|
public function userFollowers(Request $request, $username)
|
||||||
abort(404);
|
{
|
||||||
}
|
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||||
|
|
||||||
$obj = [
|
$profile = Profile::whereNull('remote_url')
|
||||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
->whereUsername($username)
|
||||||
'id' => $request->getUri(),
|
->whereIsPrivate(false)
|
||||||
'type' => 'OrderedCollectionPage',
|
->firstOrFail();
|
||||||
'totalItems' => 0,
|
|
||||||
'orderedItems' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
return response()->json($obj);
|
if($profile->status != null) {
|
||||||
}
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$obj = [
|
||||||
|
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id' => $request->getUri(),
|
||||||
|
'type' => 'OrderedCollectionPage',
|
||||||
|
'totalItems' => 0,
|
||||||
|
'orderedItems' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -573,9 +573,13 @@ class PublicApiController extends Controller
|
||||||
{
|
{
|
||||||
abort_unless(Auth::check(), 403);
|
abort_unless(Auth::check(), 403);
|
||||||
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
|
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
|
||||||
|
$owner = Auth::id() == $profile->user_id;
|
||||||
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
|
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
|
||||||
return response()->json([]);
|
return response()->json([]);
|
||||||
}
|
}
|
||||||
|
if(!$owner && $request->page > 5) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
|
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
|
||||||
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
|
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
|
||||||
$res = $this->fractal->createData($resource)->toArray();
|
$res = $this->fractal->createData($resource)->toArray();
|
||||||
|
@ -600,6 +604,10 @@ class PublicApiController extends Controller
|
||||||
abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
|
abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
|
||||||
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
|
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
|
||||||
|
|
||||||
|
if(!$owner && $request->page > 5) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if($search) {
|
if($search) {
|
||||||
abort_if(!$owner, 404);
|
abort_if(!$owner, 404);
|
||||||
$following = $profile->following()
|
$following = $profile->following()
|
||||||
|
|
93
app/Jobs/DeletePipeline/FanoutDeletePipeline.php
Normal file
93
app/Jobs/DeletePipeline/FanoutDeletePipeline.php
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\DeletePipeline;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Cache;
|
||||||
|
use DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use App\Profile;
|
||||||
|
use App\Util\ActivityPub\Helpers;
|
||||||
|
use GuzzleHttp\Pool;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Promise;
|
||||||
|
use App\Util\ActivityPub\HttpSignature;
|
||||||
|
|
||||||
|
class FanoutDeletePipeline implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $profile;
|
||||||
|
|
||||||
|
public $timeout = 300;
|
||||||
|
public $tries = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($profile)
|
||||||
|
{
|
||||||
|
$this->profile = $profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$profile = $this->profile;
|
||||||
|
|
||||||
|
$client = new Client([
|
||||||
|
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$audience = Cache::remember('pf:ap:known_instances', now()->addHours(6), function() {
|
||||||
|
return Profile::whereNotNull('sharedInbox')->groupBy('sharedInbox')->pluck('sharedInbox')->toArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
$activity = [
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id" => $profile->permalink('#delete'),
|
||||||
|
"type" => "Delete",
|
||||||
|
"actor" => $profile->permalink(),
|
||||||
|
"to" => [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public",
|
||||||
|
],
|
||||||
|
"object" => $profile->permalink(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$payload = json_encode($activity);
|
||||||
|
|
||||||
|
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||||
|
foreach($audience as $url) {
|
||||||
|
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||||
|
yield function() use ($client, $url, $headers, $payload) {
|
||||||
|
return $client->postAsync($url, [
|
||||||
|
'curl' => [
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
CURLOPT_POSTFIELDS => $payload,
|
||||||
|
CURLOPT_HEADER => true
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$pool = new Pool($client, $requests($audience), [
|
||||||
|
'concurrency' => config('federation.activitypub.delivery.concurrency'),
|
||||||
|
'fulfilled' => function ($response, $index) {
|
||||||
|
},
|
||||||
|
'rejected' => function ($reason, $index) {
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
$promise = $pool->promise();
|
||||||
|
|
||||||
|
$promise->wait();
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
223
app/Jobs/InboxPipeline/DeleteWorker.php
Normal file
223
app/Jobs/InboxPipeline/DeleteWorker.php
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\InboxPipeline;
|
||||||
|
|
||||||
|
use Cache;
|
||||||
|
use App\Profile;
|
||||||
|
use App\Util\ActivityPub\{
|
||||||
|
Helpers,
|
||||||
|
HttpSignature,
|
||||||
|
Inbox
|
||||||
|
};
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Zttp\Zttp;
|
||||||
|
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||||
|
|
||||||
|
class DeleteWorker implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $headers;
|
||||||
|
protected $payload;
|
||||||
|
|
||||||
|
public $timeout = 60;
|
||||||
|
public $tries = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($headers, $payload)
|
||||||
|
{
|
||||||
|
$this->headers = $headers;
|
||||||
|
$this->payload = $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$profile = null;
|
||||||
|
$headers = $this->headers;
|
||||||
|
$payload = json_decode($this->payload, true, 8);
|
||||||
|
|
||||||
|
if(isset($payload['id'])) {
|
||||||
|
$lockKey = 'pf:ap:del-lock:' . hash('sha256', $payload['id']);
|
||||||
|
if(Cache::get($lockKey) !== null) {
|
||||||
|
// Job processed already
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Cache::put($lockKey, 1, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($headers['signature']) || !isset($headers['date'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($headers) || empty($payload)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( $payload['type'] === 'Delete' &&
|
||||||
|
( ( is_string($payload['object']) &&
|
||||||
|
$payload['object'] === $payload['actor'] ) ||
|
||||||
|
( is_array($payload['object']) &&
|
||||||
|
isset($payload['object']['id'], $payload['object']['type']) &&
|
||||||
|
$payload['object']['type'] === 'Person' &&
|
||||||
|
$payload['actor'] === $payload['object']['id']
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
$actor = $payload['actor'];
|
||||||
|
$hash = strlen($actor) <= 48 ?
|
||||||
|
'b:' . base64_encode($actor) :
|
||||||
|
'h:' . hash('sha256', $actor);
|
||||||
|
|
||||||
|
$lockKey = 'ap:inbox:actor-delete-exists:lock:' . $hash;
|
||||||
|
Cache::lock($lockKey, 10)->block(5, function () use(
|
||||||
|
$headers,
|
||||||
|
$payload,
|
||||||
|
$actor,
|
||||||
|
$hash
|
||||||
|
) {
|
||||||
|
$key = 'ap:inbox:actor-delete-exists:' . $hash;
|
||||||
|
$actorDelete = Cache::remember($key, now()->addMinutes(15), function() use($actor) {
|
||||||
|
return Profile::whereRemoteUrl($actor)
|
||||||
|
->whereNotNull('domain')
|
||||||
|
->exists();
|
||||||
|
});
|
||||||
|
if($actorDelete) {
|
||||||
|
if($this->verifySignature($headers, $payload) == true) {
|
||||||
|
Cache::set($key, false);
|
||||||
|
$profile = Profile::whereNotNull('domain')
|
||||||
|
->whereNull('status')
|
||||||
|
->whereRemoteUrl($actor)
|
||||||
|
->first();
|
||||||
|
if($profile) {
|
||||||
|
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('delete');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Signature verification failed, exit.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remote user doesn't exist, exit early.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = null;
|
||||||
|
|
||||||
|
if($this->verifySignature($headers, $payload) == true) {
|
||||||
|
(new Inbox($headers, $profile, $payload))->handle();
|
||||||
|
return;
|
||||||
|
} else if($this->blindKeyRotation($headers, $payload) == true) {
|
||||||
|
(new Inbox($headers, $profile, $payload))->handle();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function verifySignature($headers, $payload)
|
||||||
|
{
|
||||||
|
$body = $this->payload;
|
||||||
|
$bodyDecoded = $payload;
|
||||||
|
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||||
|
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||||
|
if(!$signature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!$date) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||||
|
!now()->parse($date)->lt(now()->addDays(1))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||||
|
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||||
|
$id = Helpers::validateUrl($bodyDecoded['id']);
|
||||||
|
$keyDomain = parse_url($keyId, PHP_URL_HOST);
|
||||||
|
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||||
|
if(isset($bodyDecoded['object'])
|
||||||
|
&& is_array($bodyDecoded['object'])
|
||||||
|
&& isset($bodyDecoded['object']['attributedTo'])
|
||||||
|
) {
|
||||||
|
if(parse_url($bodyDecoded['object']['attributedTo'], PHP_URL_HOST) !== $keyDomain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$actor = Profile::whereKeyId($keyId)->first();
|
||||||
|
if(!$actor) {
|
||||||
|
$actorUrl = is_array($bodyDecoded['actor']) ? $bodyDecoded['actor'][0] : $bodyDecoded['actor'];
|
||||||
|
$actor = Helpers::profileFirstOrNew($actorUrl);
|
||||||
|
}
|
||||||
|
if(!$actor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$pkey = openssl_pkey_get_public($actor->public_key);
|
||||||
|
if(!$pkey) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$inboxPath = "/f/inbox";
|
||||||
|
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
|
||||||
|
if($verified == 1) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function blindKeyRotation($headers, $payload)
|
||||||
|
{
|
||||||
|
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||||
|
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||||
|
if(!$signature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!$date) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||||
|
!now()->parse($date)->lt(now()->addDays(1))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||||
|
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||||
|
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
|
||||||
|
if(!$actor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(Helpers::validateUrl($actor->remote_url) == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$res = Zttp::timeout(5)->withHeaders([
|
||||||
|
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
|
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
|
||||||
|
])->get($actor->remote_url);
|
||||||
|
$res = json_decode($res->body(), true, 8);
|
||||||
|
if($res['publicKey']['id'] !== $actor->key_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$actor->public_key = $res['publicKey']['publicKeyPem'];
|
||||||
|
$actor->save();
|
||||||
|
return $this->verifySignature($headers, $payload);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,66 +6,95 @@ use Illuminate\Support\Facades\Redis;
|
||||||
|
|
||||||
use App\{
|
use App\{
|
||||||
Follower,
|
Follower,
|
||||||
Profile
|
Profile,
|
||||||
|
User
|
||||||
};
|
};
|
||||||
|
|
||||||
class FollowerService {
|
class FollowerService
|
||||||
|
{
|
||||||
|
const FOLLOWING_KEY = 'pf:services:follow:following:id:';
|
||||||
|
const FOLLOWERS_KEY = 'pf:services:follow:followers:id:';
|
||||||
|
|
||||||
protected $profile;
|
public static function add($actor, $target)
|
||||||
public static $follower_prefix = 'px:profile:followers-v1.3:';
|
|
||||||
public static $following_prefix = 'px:profile:following-v1.3:';
|
|
||||||
|
|
||||||
public static function build()
|
|
||||||
{
|
{
|
||||||
return new self();
|
Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
|
||||||
|
Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function profile(Profile $profile)
|
public static function remove($actor, $target)
|
||||||
{
|
{
|
||||||
$this->profile = $profile;
|
Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
|
||||||
self::$follower_prefix .= $profile->id;
|
Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
|
||||||
self::$following_prefix .= $profile->id;
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function followers($limit = 100, $offset = 1)
|
public static function followers($id, $start = 0, $stop = 10)
|
||||||
{
|
{
|
||||||
if(Redis::zcard(self::$follower_prefix) == 0) {
|
return Redis::zrange(self::FOLLOWERS_KEY . $id, $start, $stop);
|
||||||
$followers = $this->profile->followers()->pluck('profile_id');
|
|
||||||
$followers->map(function($i) {
|
|
||||||
Redis::zadd(self::$follower_prefix, $i, $i);
|
|
||||||
});
|
|
||||||
return Redis::zrevrange(self::$follower_prefix, $offset, $limit);
|
|
||||||
} else {
|
|
||||||
return Redis::zrevrange(self::$follower_prefix, $offset, $limit);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function following($id, $start = 0, $stop = 10)
|
||||||
public function following($limit = 100, $offset = 1)
|
|
||||||
{
|
{
|
||||||
if(Redis::zcard(self::$following_prefix) == 0) {
|
return Redis::zrange(self::FOLLOWING_KEY . $id, $start, $stop);
|
||||||
$following = $this->profile->following()->pluck('following_id');
|
|
||||||
$following->map(function($i) {
|
|
||||||
Redis::zadd(self::$following_prefix, $i, $i);
|
|
||||||
});
|
|
||||||
return Redis::zrevrange(self::$following_prefix, $offset, $limit);
|
|
||||||
} else {
|
|
||||||
return Redis::zrevrange(self::$following_prefix, $offset, $limit);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function follows(string $actor, string $target)
|
public static function follows(string $actor, string $target)
|
||||||
{
|
{
|
||||||
$key = self::$follower_prefix . $target;
|
return Follower::whereProfileId($actor)->whereFollowingId($target)->exists();
|
||||||
if(Redis::zcard($key) == 0) {
|
}
|
||||||
$p = Profile::findOrFail($target);
|
|
||||||
self::build()->profile($p)->followers(1);
|
public static function audience($profile)
|
||||||
self::build()->profile($p)->following(1);
|
{
|
||||||
return (bool) Redis::zrank($key, $actor);
|
return (new self)->getAudienceInboxes($profile);
|
||||||
} else {
|
}
|
||||||
return (bool) Redis::zrank($key, $actor);
|
|
||||||
|
protected function getAudienceInboxes($profile)
|
||||||
|
{
|
||||||
|
if($profile instanceOf User) {
|
||||||
|
return $profile
|
||||||
|
->profile
|
||||||
|
->followers()
|
||||||
|
->whereLocalProfile(false)
|
||||||
|
->get()
|
||||||
|
->map(function($follow) {
|
||||||
|
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||||
|
})
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($profile instanceOf Profile) {
|
||||||
|
return $profile
|
||||||
|
->followers()
|
||||||
|
->whereLocalProfile(false)
|
||||||
|
->get()
|
||||||
|
->map(function($follow) {
|
||||||
|
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||||
|
})
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(is_string($profile) || is_integer($profile)) {
|
||||||
|
$profile = Profile::whereNull('domain')->find($profile);
|
||||||
|
if(!$profile) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $profile
|
||||||
|
->followers()
|
||||||
|
->whereLocalProfile(false)
|
||||||
|
->get()
|
||||||
|
->map(function($follow) {
|
||||||
|
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||||
|
})
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,190 +2,191 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Horizon Domain
|
| Horizon Domain
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This is the subdomain where Horizon will be accessible from. If this
|
| This is the subdomain where Horizon will be accessible from. If this
|
||||||
| setting is null, Horizon will reside under the same domain as the
|
| setting is null, Horizon will reside under the same domain as the
|
||||||
| application. Otherwise, this value will serve as the subdomain.
|
| application. Otherwise, this value will serve as the subdomain.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'domain' => null,
|
'domain' => null,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Horizon Path
|
| Horizon Path
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This is the URI path where Horizon will be accessible from. Feel free
|
| This is the URI path where Horizon will be accessible from. Feel free
|
||||||
| to change this path to anything you like. Note that the URI will not
|
| to change this path to anything you like. Note that the URI will not
|
||||||
| affect the paths of its internal API that aren't exposed to users.
|
| affect the paths of its internal API that aren't exposed to users.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'path' => 'horizon',
|
'path' => 'horizon',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Horizon Redis Connection
|
| Horizon Redis Connection
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This is the name of the Redis connection where Horizon will store the
|
| This is the name of the Redis connection where Horizon will store the
|
||||||
| meta information required for it to function. It includes the list
|
| meta information required for it to function. It includes the list
|
||||||
| of supervisors, failed jobs, job metrics, and other information.
|
| of supervisors, failed jobs, job metrics, and other information.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use' => 'default',
|
'use' => 'default',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Horizon Redis Prefix
|
| Horizon Redis Prefix
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This prefix will be used when storing all Horizon data in Redis. You
|
| This prefix will be used when storing all Horizon data in Redis. You
|
||||||
| may modify the prefix when you are running multiple installations
|
| may modify the prefix when you are running multiple installations
|
||||||
| of Horizon on the same server so that they don't have problems.
|
| of Horizon on the same server so that they don't have problems.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'prefix' => env('HORIZON_PREFIX', 'horizon-'.str_random(8).':'),
|
'prefix' => env('HORIZON_PREFIX', 'horizon-'.str_random(8).':'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Horizon Route Middleware
|
| Horizon Route Middleware
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| These middleware will get attached onto each Horizon route, giving you
|
| These middleware will get attached onto each Horizon route, giving you
|
||||||
| the chance to add your own middleware to this list or change any of
|
| the chance to add your own middleware to this list or change any of
|
||||||
| the existing middleware. Or, you can simply stick with this list.
|
| the existing middleware. Or, you can simply stick with this list.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'middleware' => ['web'],
|
'middleware' => ['web'],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Queue Wait Time Thresholds
|
| Queue Wait Time Thresholds
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This option allows you to configure when the LongWaitDetected event
|
| This option allows you to configure when the LongWaitDetected event
|
||||||
| will be fired. Every connection / queue combination may have its
|
| will be fired. Every connection / queue combination may have its
|
||||||
| own, unique threshold (in seconds) before this event is fired.
|
| own, unique threshold (in seconds) before this event is fired.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'waits' => [
|
'waits' => [
|
||||||
'redis:feed' => 30,
|
'redis:feed' => 30,
|
||||||
'redis:default' => 30,
|
'redis:default' => 30,
|
||||||
'redis:high' => 30,
|
'redis:high' => 30,
|
||||||
],
|
'redis:delete' => 30
|
||||||
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Job Trimming Times
|
| Job Trimming Times
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here you can configure for how long (in minutes) you desire Horizon to
|
| Here you can configure for how long (in minutes) you desire Horizon to
|
||||||
| persist the recent and failed jobs. Typically, recent jobs are kept
|
| persist the recent and failed jobs. Typically, recent jobs are kept
|
||||||
| for one hour while all failed jobs are stored for an entire week.
|
| for one hour while all failed jobs are stored for an entire week.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'trim' => [
|
'trim' => [
|
||||||
'recent' => 60,
|
'recent' => 60,
|
||||||
'pending' => 60,
|
'pending' => 60,
|
||||||
'completed' => 60,
|
'completed' => 60,
|
||||||
'recent_failed' => 10080,
|
'recent_failed' => 10080,
|
||||||
'failed' => 10080,
|
'failed' => 10080,
|
||||||
'monitored' => 10080,
|
'monitored' => 10080,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Metrics
|
| Metrics
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here you can configure how many snapshots should be kept to display in
|
| Here you can configure how many snapshots should be kept to display in
|
||||||
| the metrics graph. This will get used in combination with Horizon's
|
| the metrics graph. This will get used in combination with Horizon's
|
||||||
| `horizon:snapshot` schedule to define how long to retain metrics.
|
| `horizon:snapshot` schedule to define how long to retain metrics.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'metrics' => [
|
'metrics' => [
|
||||||
'trim_snapshots' => [
|
'trim_snapshots' => [
|
||||||
'job' => 24,
|
'job' => 24,
|
||||||
'queue' => 24,
|
'queue' => 24,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Fast Termination
|
| Fast Termination
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| When this option is enabled, Horizon's "terminate" command will not
|
| When this option is enabled, Horizon's "terminate" command will not
|
||||||
| wait on all of the workers to terminate unless the --wait option
|
| wait on all of the workers to terminate unless the --wait option
|
||||||
| is provided. Fast termination can shorten deployment delay by
|
| is provided. Fast termination can shorten deployment delay by
|
||||||
| allowing a new instance of Horizon to start while the last
|
| allowing a new instance of Horizon to start while the last
|
||||||
| instance will continue to terminate each of its workers.
|
| instance will continue to terminate each of its workers.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'fast_termination' => false,
|
'fast_termination' => false,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Memory Limit (MB)
|
| Memory Limit (MB)
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This value describes the maximum amount of memory the Horizon worker
|
| This value describes the maximum amount of memory the Horizon worker
|
||||||
| may consume before it is terminated and restarted. You should set
|
| may consume before it is terminated and restarted. You should set
|
||||||
| this value according to the resources available to your server.
|
| this value according to the resources available to your server.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'memory_limit' => 64,
|
'memory_limit' => 64,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Queue Worker Configuration
|
| Queue Worker Configuration
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here you may define the queue worker settings used by your application
|
| Here you may define the queue worker settings used by your application
|
||||||
| in all environments. These supervisors and settings handle all your
|
| in all environments. These supervisors and settings handle all your
|
||||||
| queued jobs and will be provisioned by Horizon during deployment.
|
| queued jobs and will be provisioned by Horizon during deployment.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'environments' => [
|
'environments' => [
|
||||||
'production' => [
|
'production' => [
|
||||||
'supervisor-1' => [
|
'supervisor-1' => [
|
||||||
'connection' => 'redis',
|
'connection' => 'redis',
|
||||||
'queue' => ['high', 'default', 'feed'],
|
'queue' => ['high', 'default', 'feed', 'delete'],
|
||||||
'balance' => 'auto',
|
'balance' => 'auto',
|
||||||
'maxProcesses' => 20,
|
'maxProcesses' => 20,
|
||||||
'memory' => 128,
|
'memory' => 128,
|
||||||
'tries' => 3,
|
'tries' => 3,
|
||||||
'nice' => 0,
|
'nice' => 0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'local' => [
|
'local' => [
|
||||||
'supervisor-1' => [
|
'supervisor-1' => [
|
||||||
'connection' => 'redis',
|
'connection' => 'redis',
|
||||||
'queue' => ['high', 'default', 'feed'],
|
'queue' => ['high', 'default', 'feed', 'delete'],
|
||||||
'balance' => 'auto',
|
'balance' => 'auto',
|
||||||
'maxProcesses' => 20,
|
'maxProcesses' => 20,
|
||||||
'memory' => 128,
|
'memory' => 128,
|
||||||
'tries' => 3,
|
'tries' => 3,
|
||||||
'nice' => 0,
|
'nice' => 0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'darkmode' => env('HORIZON_DARKMODE', false),
|
'darkmode' => env('HORIZON_DARKMODE', false),
|
||||||
];
|
];
|
||||||
|
|
BIN
public/js/direct.js
vendored
BIN
public/js/direct.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -26,10 +26,10 @@
|
||||||
<div v-if="!messages.inbox.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
|
<div v-if="!messages.inbox.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
|
||||||
<p class="lead mb-0">No messages found :(</p>
|
<p class="lead mb-0">No messages found :(</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-for="(thread, index) in messages.inbox">
|
<div v-else v-for="(thread, index) in messages.inbox" :key="'dm_inbox'+index">
|
||||||
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" :href="'/account/direct/t/'+thread.id">
|
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" :href="'/account/direct/t/'+thread.id">
|
||||||
<div class="media d-flex align-items-center">
|
<div class="media d-flex align-items-center">
|
||||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="32px">
|
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';" v-once>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
<span class="font-weight-bold text-truncate">
|
<span class="font-weight-bold text-truncate">
|
||||||
|
@ -62,10 +62,10 @@
|
||||||
<div v-if="!messages.sent.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
|
<div v-if="!messages.sent.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
|
||||||
<p class="lead mb-0">No messages found :(</p>
|
<p class="lead mb-0">No messages found :(</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-for="(thread, index) in messages.sent">
|
<div v-else v-for="(thread, index) in messages.sent" :key="'dm_sent'+index">
|
||||||
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" href="#" @click.prevent="loadMessage(thread.id)">
|
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" href="#" @click.prevent="loadMessage(thread.id)">
|
||||||
<div class="media d-flex align-items-center">
|
<div class="media d-flex align-items-center">
|
||||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="32px">
|
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';" v-once>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
<span class="font-weight-bold text-truncate">
|
<span class="font-weight-bold text-truncate">
|
||||||
|
@ -98,10 +98,10 @@
|
||||||
<div v-if="!messages.filtered.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
|
<div v-if="!messages.filtered.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
|
||||||
<p class="lead mb-0">No messages found :(</p>
|
<p class="lead mb-0">No messages found :(</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-for="(thread, index) in messages.filtered">
|
<div v-else v-for="(thread, index) in messages.filtered" :key="'dm_filtered'+index">
|
||||||
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" href="#" @click.prevent="loadMessage(thread.id)">
|
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" href="#" @click.prevent="loadMessage(thread.id)">
|
||||||
<div class="media d-flex align-items-center">
|
<div class="media d-flex align-items-center">
|
||||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="32px">
|
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';" v-once>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
<span class="font-weight-bold text-truncate">
|
<span class="font-weight-bold text-truncate">
|
||||||
|
@ -373,4 +373,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="40px">
|
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="40" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
<span class="font-weight-bold">{{thread.name}}</span>
|
<span class="font-weight-bold">{{thread.name}}</span>
|
||||||
|
@ -40,10 +40,10 @@
|
||||||
</li>
|
</li>
|
||||||
<li v-for="(convo, index) in thread.messages" class="list-group-item border-0 chat-msg cursor-pointer" @click="openCtxMenu(convo, index)">
|
<li v-for="(convo, index) in thread.messages" class="list-group-item border-0 chat-msg cursor-pointer" @click="openCtxMenu(convo, index)">
|
||||||
<div v-if="!convo.isAuthor" class="media d-inline-flex mb-0">
|
<div v-if="!convo.isAuthor" class="media d-inline-flex mb-0">
|
||||||
<img v-if="!hideAvatars" class="mr-3 mt-2 rounded-circle img-thumbnail" :src="thread.avatar" alt="avatar" width="32px">
|
<img v-if="!hideAvatars" class="mr-3 mt-2 rounded-circle img-thumbnail" :src="thread.avatar" alt="avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<p v-if="convo.type == 'photo'" class="pill-to p-0 shadow">
|
<p v-if="convo.type == 'photo'" class="pill-to p-0 shadow">
|
||||||
<img :src="convo.media" width="140px" style="border-radius:20px;">
|
<img :src="convo.media" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
|
||||||
</p>
|
</p>
|
||||||
<div v-else-if="convo.type == 'link'" class="media d-inline-flex mb-0 cursor-pointer">
|
<div v-else-if="convo.type == 'link'" class="media d-inline-flex mb-0 cursor-pointer">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
<div v-else class="media d-inline-flex float-right mb-0">
|
<div v-else class="media d-inline-flex float-right mb-0">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<p v-if="convo.type == 'photo'" class="pill-from p-0 shadow">
|
<p v-if="convo.type == 'photo'" class="pill-from p-0 shadow">
|
||||||
<img :src="convo.media" width="140px" style="border-radius:20px;">
|
<img :src="convo.media" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
|
||||||
</p>
|
</p>
|
||||||
<div v-else-if="convo.type == 'link'" class="media d-inline-flex float-right mb-0 cursor-pointer">
|
<div v-else-if="convo.type == 'link'" class="media d-inline-flex float-right mb-0 cursor-pointer">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
|
@ -134,7 +134,7 @@
|
||||||
</p>
|
</p>
|
||||||
<p v-else> </p>
|
<p v-else> </p>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="!hideAvatars" class="ml-3 mt-2 rounded-circle img-thumbnail" :src="profile.avatar" alt="avatar" width="32px">
|
<img v-if="!hideAvatars" class="ml-3 mt-2 rounded-circle img-thumbnail" :src="profile.avatar" alt="avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@ -682,4 +682,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -466,7 +466,7 @@
|
||||||
<div class="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
|
<div class="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<a :href="user.url">
|
<a :href="user.url">
|
||||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px">
|
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||||
</a>
|
</a>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<p class="mb-0" style="font-size: 14px">
|
<p class="mb-0" style="font-size: 14px">
|
||||||
|
|
|
@ -43,10 +43,10 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div v-if="hasStory" class="has-story cursor-pointer shadow-sm" @click="storyRedirect()">
|
<div v-if="hasStory" class="has-story cursor-pointer shadow-sm" @click="storyRedirect()">
|
||||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle" :src="profile.avatar" width="77px" height="77px">
|
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle" :src="profile.avatar" width="77px" height="77px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle border" :src="profile.avatar" width="77px" height="77px">
|
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle border" :src="profile.avatar" width="77px" height="77px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
|
@ -85,10 +85,10 @@
|
||||||
<!-- DESKTOP PROFILE PICTURE -->
|
<!-- DESKTOP PROFILE PICTURE -->
|
||||||
<div class="d-none d-md-block pb-3">
|
<div class="d-none d-md-block pb-3">
|
||||||
<div v-if="hasStory" class="has-story-lg cursor-pointer shadow-sm" @click="storyRedirect()">
|
<div v-if="hasStory" class="has-story-lg cursor-pointer shadow-sm" @click="storyRedirect()">
|
||||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow cursor-pointer" :src="profile.avatar" width="150px" height="150px">
|
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow cursor-pointer" :src="profile.avatar" width="150px" height="150px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow" :src="profile.avatar" width="150px" height="150px">
|
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow" :src="profile.avatar" width="150px" height="150px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
</div>
|
</div>
|
||||||
<p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3">
|
<p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3">
|
||||||
<button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0">
|
<button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0">
|
||||||
|
@ -404,53 +404,54 @@
|
||||||
title="Following"
|
title="Following"
|
||||||
body-class="list-group-flush py-3 px-0"
|
body-class="list-group-flush py-3 px-0"
|
||||||
dialog-class="follow-modal">
|
dialog-class="follow-modal">
|
||||||
<div v-if="!loading" class="list-group" style="min-height: 60vh;">
|
<div v-if="!followingLoading" class="list-group" style="max-height: 60vh;">
|
||||||
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
|
<div v-if="!following.length" class="list-group-item border-0">
|
||||||
<span class="d-flex px-4 pb-0 align-items-center">
|
<p class="text-center mb-0 font-weight-bold text-muted py-5">
|
||||||
<i class="fas fa-search text-lighter"></i>
|
<span class="text-dark">{{profileUsername}}</span> is not following yet</p>
|
||||||
<input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="owner == true" class="btn-group rounded-0 mt-n3 mb-3 border-top" role="group" aria-label="Following">
|
<div v-else>
|
||||||
<!-- <button type="button" :class="[followingModalTab == 'following' ? ' btn btn-light py-3 rounded-0 font-weight-bold modal-tab-active' : 'btn btn-light py-3 rounded-0 font-weight-bold']" style="font-size: 12px;">FOLLOWING</button> -->
|
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
|
||||||
<!-- <button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">MUTED</button>
|
<span class="d-flex px-4 pb-0 align-items-center">
|
||||||
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">BLOCKED</button> -->
|
<i class="fas fa-search text-lighter"></i>
|
||||||
</div>
|
<input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
|
||||||
<div v-else class="btn-group rounded-0 mt-n3 mb-3" role="group" aria-label="Following">
|
</span>
|
||||||
<!-- <button type="button" class="btn btn-light py-3 rounded-0 border-primary border-left-0 border-right-0 border-top-0 font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'following'">FOLLOWING</button>
|
</div>
|
||||||
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'mutual'">MUTUAL</button>
|
<div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in following" :key="'following_'+index">
|
||||||
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'blocked'">BLOCKED</button> -->
|
<div class="media">
|
||||||
</div>
|
<a :href="profileUrlRedirect(user)">
|
||||||
<div class="list-group-item border-0 py-1" v-for="(user, index) in following" :key="'following_'+index">
|
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||||
<div class="media">
|
</a>
|
||||||
<a :href="profileUrlRedirect(user)">
|
<div class="media-body text-truncate">
|
||||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
<p class="mb-0" style="font-size: 14px">
|
||||||
</a>
|
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
||||||
<div class="media-body text-truncate">
|
{{user.username}}
|
||||||
<p class="mb-0" style="font-size: 14px">
|
</a>
|
||||||
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
</p>
|
||||||
{{user.username}}
|
<p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
||||||
</a>
|
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
||||||
</p>
|
</p>
|
||||||
<p v-if="!user.local" class="text-muted mb-0 text-truncate mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||||
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
{{user.display_name ? user.display_name : user.username}}
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
</div>
|
||||||
{{user.display_name}}
|
<div v-if="owner">
|
||||||
</p>
|
<a class="btn btn-outline-dark btn-sm font-weight-bold" href="#" @click.prevent="followModalAction(user.id, index, 'following')">Following</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="owner">
|
|
||||||
<a class="btn btn-outline-dark btn-sm font-weight-bold" href="#" @click.prevent="followModalAction(user.id, index, 'following')">Following</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
|
||||||
<div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
|
<div class="list-group-item border-0 pt-5">
|
||||||
<div class="list-group-item border-0 pt-5">
|
<p class="p-3 text-center mb-0 lead">No Results Found</p>
|
||||||
<p class="p-3 text-center mb-0 lead">No Results Found</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="following.length > 0 && followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
|
||||||
|
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="following.length > 0 && followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
|
</div>
|
||||||
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
<div v-else class="text-center py-5">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
@ -463,31 +464,42 @@
|
||||||
body-class="list-group-flush py-3 px-0"
|
body-class="list-group-flush py-3 px-0"
|
||||||
dialog-class="follow-modal"
|
dialog-class="follow-modal"
|
||||||
>
|
>
|
||||||
<div class="list-group">
|
<div v-if="!followerLoading" class="list-group" style="max-height: 60vh;">
|
||||||
<div v-if="followers.length == 0" class="list-group-item border-0">
|
<div v-if="!followers.length" class="list-group-item border-0">
|
||||||
<p class="text-center mb-0 font-weight-bold text-muted py-5">
|
<p class="text-center mb-0 font-weight-bold text-muted py-5">
|
||||||
<span class="text-dark">{{profileUsername}}</span> has no followers yet</p>
|
<span class="text-dark">{{profileUsername}}</span> has no followers yet</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group-item border-0 py-1" v-for="(user, index) in followers" :key="'follower_'+index">
|
|
||||||
<div class="media mb-0">
|
<div v-else>
|
||||||
<a :href="profileUrlRedirect(user)">
|
<div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in followers" :key="'follower_'+index">
|
||||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" height="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
<div class="media mb-0">
|
||||||
</a>
|
<a :href="profileUrlRedirect(user)">
|
||||||
<div class="media-body mb-0">
|
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" height="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||||
<p class="mb-0" style="font-size: 14px">
|
</a>
|
||||||
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
<div class="media-body mb-0">
|
||||||
{{user.username}}
|
<p class="mb-0" style="font-size: 14px">
|
||||||
</a>
|
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
||||||
</p>
|
{{user.username}}
|
||||||
<p class="text-secondary mb-0" style="font-size: 13px">
|
</a>
|
||||||
{{user.display_name}}
|
</p>
|
||||||
</p>
|
<p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
||||||
|
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||||
|
{{user.display_name ? user.display_name : user.username}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> -->
|
||||||
</div>
|
</div>
|
||||||
<!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> -->
|
</div>
|
||||||
|
<div v-if="followers.length && followerMore" class="list-group-item text-center" v-on:click="followersLoadMore()">
|
||||||
|
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="followers.length && followerMore" class="list-group-item text-center" v-on:click="followersLoadMore()">
|
</div>
|
||||||
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
<div v-else class="text-center py-5">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
@ -558,20 +570,20 @@
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
<b-modal ref="embedModal"
|
<b-modal ref="embedModal"
|
||||||
id="ctx-embed-modal"
|
id="ctx-embed-modal"
|
||||||
hide-header
|
hide-header
|
||||||
hide-footer
|
hide-footer
|
||||||
centered
|
centered
|
||||||
rounded
|
rounded
|
||||||
size="md"
|
size="md"
|
||||||
body-class="p-2 rounded">
|
body-class="p-2 rounded">
|
||||||
<div>
|
<div>
|
||||||
<textarea class="form-control disabled text-monospace" rows="6" style="overflow-y:hidden;border: 1px solid #efefef; font-size: 12px; line-height: 18px; margin: 0 0 7px;resize:none;" v-model="ctxEmbedPayload" disabled=""></textarea>
|
<textarea class="form-control disabled text-monospace" rows="6" style="overflow-y:hidden;border: 1px solid #efefef; font-size: 12px; line-height: 18px; margin: 0 0 7px;resize:none;" v-model="ctxEmbedPayload" disabled=""></textarea>
|
||||||
<hr>
|
<hr>
|
||||||
<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
|
<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
|
||||||
<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p>
|
<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style type="text/css" scoped>
|
<style type="text/css" scoped>
|
||||||
|
@ -652,7 +664,6 @@
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
import VueMasonry from 'vue-masonry-css'
|
import VueMasonry from 'vue-masonry-css'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: [
|
||||||
'profile-id',
|
'profile-id',
|
||||||
|
@ -679,9 +690,11 @@
|
||||||
followers: [],
|
followers: [],
|
||||||
followerCursor: 1,
|
followerCursor: 1,
|
||||||
followerMore: true,
|
followerMore: true,
|
||||||
|
followerLoading: true,
|
||||||
following: [],
|
following: [],
|
||||||
followingCursor: 1,
|
followingCursor: 1,
|
||||||
followingMore: true,
|
followingMore: true,
|
||||||
|
followingLoading: true,
|
||||||
warning: false,
|
warning: false,
|
||||||
sponsorList: [],
|
sponsorList: [],
|
||||||
bookmarks: [],
|
bookmarks: [],
|
||||||
|
@ -1121,6 +1134,7 @@
|
||||||
if(res.data.length < 10) {
|
if(res.data.length < 10) {
|
||||||
this.followingMore = false;
|
this.followingMore = false;
|
||||||
}
|
}
|
||||||
|
this.followingLoading = false;
|
||||||
});
|
});
|
||||||
this.$refs.followingModal.show();
|
this.$refs.followingModal.show();
|
||||||
return;
|
return;
|
||||||
|
@ -1150,6 +1164,7 @@
|
||||||
if(res.data.length < 10) {
|
if(res.data.length < 10) {
|
||||||
this.followerMore = false;
|
this.followerMore = false;
|
||||||
}
|
}
|
||||||
|
this.followerLoading = false;
|
||||||
})
|
})
|
||||||
this.$refs.followerModal.show();
|
this.$refs.followerModal.show();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
<a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
|
<a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
|
||||||
<div class="pb-3">
|
<div class="pb-3">
|
||||||
<div class="media align-items-center py-2 pr-3">
|
<div class="media align-items-center py-2 pr-3">
|
||||||
<img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px">
|
<img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
|
<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
|
||||||
{{profile.value}}
|
{{profile.value}}
|
||||||
|
@ -123,8 +123,8 @@
|
||||||
<p class="text-secondary small font-weight-bold">STATUSES <span class="pl-1 text-lighter">({{results.statuses.length}})</span></p>
|
<p class="text-secondary small font-weight-bold">STATUSES <span class="pl-1 text-lighter">({{results.statuses.length}})</span></p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="results.statuses.length">
|
<div v-if="results.statuses.length">
|
||||||
<a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)">
|
<a v-for="(status, index) in results.statuses" :key="'srs:'+index" class="mr-2 result-card" :href="buildUrl('status', status)">
|
||||||
<img :src="status.thumb" width="90px" height="90px" class="mb-2">
|
<img :src="status.thumb" width="90px" height="90px" class="mb-2" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0';" v-once>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<announcements-card v-on:show-tips="showTips = $event"></announcements-card>
|
<announcements-card v-on:show-tips="showTips = $event"></announcements-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="index == 2 && showSuggestions == true && suggestions.length" class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border">
|
<div v-if="index == 2 && showSuggestions == true && suggestions.length" class="card status-card rounded-0 shadow-none border">
|
||||||
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
||||||
<h6 class="text-muted font-weight-bold mb-0">Suggestions For You</h6>
|
<h6 class="text-muted font-weight-bold mb-0">Suggestions For You</h6>
|
||||||
<span class="cursor-pointer text-muted" v-on:click="hideSuggestions"><i class="fas fa-times"></i></span>
|
<span class="cursor-pointer text-muted" v-on:click="hideSuggestions"><i class="fas fa-times"></i></span>
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="index == 4 && showHashtagPosts && hashtagPosts.length" class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border">
|
<div v-if="index == 4 && showHashtagPosts && hashtagPosts.length" class="card status-card rounded-0 shadow-none border">
|
||||||
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
||||||
<span></span>
|
<span></span>
|
||||||
<h6 class="text-muted font-weight-bold mb-0"><a :href="'/discover/tags/'+hashtagPostsName+'?src=tr'">#{{hashtagPostsName}}</a></h6>
|
<h6 class="text-muted font-weight-bold mb-0"><a :href="'/discover/tags/'+hashtagPostsName+'?src=tr'">#{{hashtagPostsName}}</a></h6>
|
||||||
|
@ -104,6 +104,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<status-card
|
<status-card
|
||||||
|
:class="{ 'border-top': index === 0 }"
|
||||||
:status="status"
|
:status="status"
|
||||||
:reaction-bar="reactionBar"
|
:reaction-bar="reactionBar"
|
||||||
v-on:status-delete="deleteStatus"
|
v-on:status-delete="deleteStatus"
|
||||||
|
@ -112,7 +113,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!loading && feed.length">
|
<div v-if="!loading && feed.length">
|
||||||
<div class="card rounded-0 border-top-0 status-card card-md-rounded-0 shadow-none border">
|
<div class="card rounded-0 border-top-0 status-card rounded-0 shadow-none border">
|
||||||
<div class="card-body py-5 my-5">
|
<div class="card-body py-5 my-5">
|
||||||
<infinite-loading @infinite="infiniteTimeline" :distance="800">
|
<infinite-loading @infinite="infiniteTimeline" :distance="800">
|
||||||
<div slot="no-more">
|
<div slot="no-more">
|
||||||
|
@ -157,8 +158,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!loading && scope == 'home' && feed.length == 0">
|
<div v-if="!loading && scope == 'home' && feed.length == 0">
|
||||||
<div class="card rounded-0 mt-4 status-card card-md-rounded-0 shadow-none border">
|
<div class="card rounded-0 mt-4 status-card rounded-0 shadow-none border">
|
||||||
<div v-if="profile.following_count != '0'" class="card-body py-5 my-5">
|
<div v-if="profile.following_count != '0'" class="card-body py-5 my-5">
|
||||||
<p class="text-center"><i class="far fa-check-circle fa-8x text-lighter"></i></p>
|
<p class="text-center"><i class="far fa-check-circle fa-8x text-lighter"></i></p>
|
||||||
<p class="text-center h3 font-weight-light">You're All Caught Up!</p>
|
<p class="text-center h3 font-weight-light">You're All Caught Up!</p>
|
||||||
|
@ -194,7 +196,7 @@
|
||||||
v-for="(status, index) in discover_feed"
|
v-for="(status, index) in discover_feed"
|
||||||
:key="`discover_feed-${index}-${status.id}`">
|
:key="`discover_feed-${index}-${status.id}`">
|
||||||
|
|
||||||
<div v-if="index == 2 && showSuggestions == true && suggestions.length" class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border">
|
<div v-if="index == 2 && showSuggestions == true && suggestions.length" class="card status-card rounded-0 shadow-none border">
|
||||||
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
||||||
<h6 class="text-muted font-weight-bold mb-0">Suggestions For You</h6>
|
<h6 class="text-muted font-weight-bold mb-0">Suggestions For You</h6>
|
||||||
<span class="cursor-pointer text-muted" v-on:click="hideSuggestions"><i class="fas fa-times"></i></span>
|
<span class="cursor-pointer text-muted" v-on:click="hideSuggestions"><i class="fas fa-times"></i></span>
|
||||||
|
@ -273,7 +275,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<status-card :status="status" :recommended="true" />
|
<status-card
|
||||||
|
:class="{'border-top': index === 0}"
|
||||||
|
:status="status"
|
||||||
|
:recommended="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
<div class="card-body p-0 m-0 bg-light border-bottom">
|
<div class="card-body p-0 m-0 bg-light border-bottom">
|
||||||
<div class="d-flex p-0 m-0 align-items-center">
|
<div class="d-flex p-0 m-0 align-items-center">
|
||||||
@if($status->parent()->parent()->media()->count())
|
@if($status->parent()->parent()->media()->count())
|
||||||
<img src="{{$status->parent()->parent()->thumb()}}" width="150px" height="150px" class="post-thumbnail">
|
<img src="{{$status->parent()->parent()->thumb()}}" width="150px" height="150px" class="post-thumbnail" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0';">
|
||||||
@endif
|
@endif
|
||||||
<div class="p-4 w-100">
|
<div class="p-4 w-100">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<img src="{{$status->parent()->parent()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px">
|
<img src="{{$status->parent()->parent()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<span class="font-weight-bold" v-pre>{{$status->parent()->parent()->profile->username}}</span>
|
<span class="font-weight-bold" v-pre>{{$status->parent()->parent()->profile->username}}</span>
|
||||||
<div class="">
|
<div class="">
|
||||||
|
@ -36,12 +36,12 @@
|
||||||
<div class="card-body p-0 m-0 bg-light border-bottom">
|
<div class="card-body p-0 m-0 bg-light border-bottom">
|
||||||
<div class="d-flex p-0 m-0 align-items-center">
|
<div class="d-flex p-0 m-0 align-items-center">
|
||||||
@if($status->parent()->media()->count())
|
@if($status->parent()->media()->count())
|
||||||
<img src="{{$status->parent()->thumb()}}" width="150px" height="150px" class="post-thumbnail">
|
<img src="{{$status->parent()->thumb()}}" width="150px" height="150px" class="post-thumbnail" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0';">
|
||||||
@endif
|
@endif
|
||||||
<div class="p-4 w-100">
|
<div class="p-4 w-100">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<img src="{{$status->parent()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px">
|
<img src="{{$status->parent()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<span class="font-weight-bold" v-pre>{{$status->parent()->profile->username}}</span>
|
<span class="font-weight-bold" v-pre>{{$status->parent()->profile->username}}</span>
|
||||||
<div class="">
|
<div class="">
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
<p class="py-5 mb-0 text-center">This comment may contain sensitive content. <span class="float-right font-weight-bold text-primary">Show</span></p>
|
<p class="py-5 mb-0 text-center">This comment may contain sensitive content. <span class="float-right font-weight-bold text-primary">Show</span></p>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="media py-5">
|
<div class="media py-5">
|
||||||
<img class="mr-3 rounded-circle img-thumbnail" src="{{$status->profile->avatarUrl()}}" width="60px">
|
<img class="mr-3 rounded-circle img-thumbnail" src="{{$status->profile->avatarUrl()}}" width="60px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5>
|
<h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5>
|
||||||
<p class="" v-pre>{!! $status->rendered !!}</p>
|
<p class="" v-pre>{!! $status->rendered !!}</p>
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
</details>
|
</details>
|
||||||
@else
|
@else
|
||||||
<div class="media py-5">
|
<div class="media py-5">
|
||||||
<img class="mr-3 rounded-circle img-thumbnail" src="{{$status->profile->avatarUrl()}}" width="60px">
|
<img class="mr-3 rounded-circle img-thumbnail" src="{{$status->profile->avatarUrl()}}" width="60px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5>
|
<h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5>
|
||||||
<p class="" v-pre>{!! $status->rendered !!}</p>
|
<p class="" v-pre>{!! $status->rendered !!}</p>
|
||||||
|
@ -105,12 +105,12 @@
|
||||||
<div class="card-body p-0 m-0 bg-light border-bottom">
|
<div class="card-body p-0 m-0 bg-light border-bottom">
|
||||||
<div class="d-flex p-0 m-0 align-items-center">
|
<div class="d-flex p-0 m-0 align-items-center">
|
||||||
@if($status->comments()->first()->media()->count())
|
@if($status->comments()->first()->media()->count())
|
||||||
<img src="{{$status->comments()->first()->thumb()}}" width="150px" height="150px" class="post-thumbnail">
|
<img src="{{$status->comments()->first()->thumb()}}" width="150px" height="150px" class="post-thumbnail" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0';">
|
||||||
@endif
|
@endif
|
||||||
<div class="p-4 w-100">
|
<div class="p-4 w-100">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<img src="{{$status->comments()->first()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px">
|
<img src="{{$status->comments()->first()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<span class="font-weight-bold" v-pre>{{$status->comments()->first()->profile->username}}</span>
|
<span class="font-weight-bold" v-pre>{{$status->comments()->first()->profile->username}}</span>
|
||||||
<div class="">
|
<div class="">
|
||||||
|
|
Loading…
Reference in a new issue