mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-09 16:24:51 +00:00
commit
255c41fb83
47 changed files with 1103 additions and 436 deletions
|
@ -26,6 +26,7 @@
|
|||
- Add ffmpeg config, disable logging by default ([108e3803](https://github.com/pixelfed/pixelfed/commit/108e3803))
|
||||
- Refactor AP profileFetch logic to fix race conditions and improve updating fields and avatars ([505261da](https://github.com/pixelfed/pixelfed/commit/505261da))
|
||||
- Update network timeline api, limit falloff to 2 days ([13a66303](https://github.com/pixelfed/pixelfed/commit/13a66303))
|
||||
- Update Inbox, store follow request activity ([c82f2085](https://github.com/pixelfed/pixelfed/commit/c82f2085))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.3 (2022-05-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.2...v0.11.3)
|
||||
|
|
|
@ -29,9 +29,6 @@ class GenerateInstanceActor extends Command
|
|||
}
|
||||
|
||||
if(InstanceActor::exists()) {
|
||||
$this->line(' ');
|
||||
$this->error('Instance actor already exists!');
|
||||
$this->line(' ');
|
||||
$actor = InstanceActor::whereNotNull('public_key')
|
||||
->whereNotNull('private_key')
|
||||
->firstOrFail();
|
||||
|
@ -42,7 +39,8 @@ class GenerateInstanceActor extends Command
|
|||
Cache::rememberForever(InstanceActor::PKI_PRIVATE, function() use($actor) {
|
||||
return $actor->private_key;
|
||||
});
|
||||
exit;
|
||||
$this->info('Instance actor succesfully generated. You do not need to run this command again.');
|
||||
return;
|
||||
}
|
||||
|
||||
$pkiConfig = [
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Console\Commands;
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use \PDO;
|
||||
|
||||
class Installer extends Command
|
||||
{
|
||||
|
@ -12,7 +13,7 @@ class Installer extends Command
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'install';
|
||||
protected $signature = 'install {--dangerously-overwrite-env : Re-run installation and overwrite current .env }';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
|
@ -21,6 +22,8 @@ class Installer extends Command
|
|||
*/
|
||||
protected $description = 'CLI Installer';
|
||||
|
||||
public $installType = 'Simple';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
|
@ -54,23 +57,48 @@ class Installer extends Command
|
|||
$this->info(' ');
|
||||
$this->info('Pixelfed version: ' . config('pixelfed.version'));
|
||||
$this->line(' ');
|
||||
$this->info('Scanning system...');
|
||||
$this->preflightCheck();
|
||||
$this->envCheck();
|
||||
}
|
||||
protected function preflightCheck()
|
||||
|
||||
protected function envCheck()
|
||||
{
|
||||
$this->line(' ');
|
||||
$this->info('Checking for installed dependencies...');
|
||||
$redis = Redis::connection();
|
||||
if($redis->ping()) {
|
||||
$this->info('- Found redis!');
|
||||
} else {
|
||||
$this->error('- Redis not found, aborting installation');
|
||||
if( file_exists(base_path('.env')) &&
|
||||
filesize(base_path('.env')) !== 0 &&
|
||||
!$this->option('dangerously-overwrite-env')
|
||||
) {
|
||||
$this->line('');
|
||||
$this->error('Installation aborted, found existing .env file');
|
||||
$this->line('Run the following command to re-run the installer:');
|
||||
$this->line('');
|
||||
$this->info('php artisan install --dangerously-overwrite-env');
|
||||
$this->line('');
|
||||
exit;
|
||||
}
|
||||
$this->installType();
|
||||
}
|
||||
|
||||
protected function installType()
|
||||
{
|
||||
$type = $this->choice('Select installation type', ['Simple', 'Advanced'], 0);
|
||||
$this->installType = $type;
|
||||
$this->preflightCheck();
|
||||
}
|
||||
|
||||
protected function preflightCheck()
|
||||
{
|
||||
if($this->installType === 'Advanced') {
|
||||
$this->info('Scanning system...');
|
||||
$this->line(' ');
|
||||
$this->info('Checking for installed dependencies...');
|
||||
$redis = Redis::connection();
|
||||
if($redis->ping()) {
|
||||
$this->info('- Found redis!');
|
||||
} else {
|
||||
$this->error('- Redis not found, aborting installation');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$this->checkPhpDependencies();
|
||||
$this->checkPermissions();
|
||||
$this->envCheck();
|
||||
}
|
||||
|
||||
protected function checkPhpDependencies()
|
||||
|
@ -81,31 +109,39 @@ class Installer extends Command
|
|||
'curl',
|
||||
'json',
|
||||
'mbstring',
|
||||
'openssl'
|
||||
'openssl',
|
||||
];
|
||||
$ffmpeg = exec('which ffmpeg');
|
||||
if(empty($ffmpeg)) {
|
||||
$this->error('FFmpeg not found, please install it.');
|
||||
$this->error('Cancelling installation.');
|
||||
exit;
|
||||
} else {
|
||||
$this->info('- Found FFmpeg!');
|
||||
}
|
||||
$this->line('');
|
||||
$this->info('Checking for required php extensions...');
|
||||
if($this->installType === 'Advanced') {
|
||||
$ffmpeg = exec('which ffmpeg');
|
||||
if(empty($ffmpeg)) {
|
||||
$this->error('FFmpeg not found, please install it.');
|
||||
$this->error('Cancelling installation.');
|
||||
exit;
|
||||
} else {
|
||||
$this->info('- Found FFmpeg!');
|
||||
}
|
||||
$this->line('');
|
||||
$this->info('Checking for required php extensions...');
|
||||
}
|
||||
foreach($extensions as $ext) {
|
||||
if(extension_loaded($ext) == false) {
|
||||
$this->error("- {$ext} extension not found, aborting installation");
|
||||
$this->error("\"{$ext}\" PHP extension not found, aborting installation");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$this->info("- Required PHP extensions found!");
|
||||
if($this->installType === 'Advanced') {
|
||||
$this->info("- Required PHP extensions found!");
|
||||
}
|
||||
|
||||
$this->checkPermissions();
|
||||
}
|
||||
|
||||
protected function checkPermissions()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Checking for proper filesystem permissions...');
|
||||
if($this->installType === 'Advanced') {
|
||||
$this->line('');
|
||||
$this->info('Checking for proper filesystem permissions...');
|
||||
}
|
||||
|
||||
$paths = [
|
||||
base_path('bootstrap'),
|
||||
|
@ -119,100 +155,152 @@ class Installer extends Command
|
|||
$this->error(" $path");
|
||||
exit;
|
||||
} else {
|
||||
$this->info("- Found valid permissions for {$path}");
|
||||
if($this->installType === 'Advanced') {
|
||||
$this->info("- Found valid permissions for {$path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function envCheck()
|
||||
{
|
||||
if(!file_exists(base_path('.env')) || filesize(base_path('.env')) == 0) {
|
||||
$this->line('');
|
||||
$this->info('No .env configuration file found. We will create one now!');
|
||||
$this->createEnv();
|
||||
} else {
|
||||
$confirm = $this->confirm('Found .env file, do you want to overwrite it?');
|
||||
if(!$confirm) {
|
||||
$this->info('Cancelling installation.');
|
||||
exit;
|
||||
}
|
||||
$confirm = $this->confirm('Are you really sure you want to overwrite it?');
|
||||
if(!$confirm) {
|
||||
$this->info('Cancelling installation.');
|
||||
exit;
|
||||
}
|
||||
$this->error('Warning ... if you did not backup your .env before its overwritten it will be permanently deleted.');
|
||||
$confirm = $this->confirm('The application may be installed already, are you really sure you want to overwrite it?');
|
||||
if(!$confirm) {
|
||||
$this->info('Cancelling installation.');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$this->postInstall();
|
||||
$this->createEnv();
|
||||
}
|
||||
|
||||
protected function createEnv()
|
||||
{
|
||||
$this->line('');
|
||||
// copy env
|
||||
if(!file_exists(app()->environmentFilePath())) {
|
||||
exec('cp .env.example .env');
|
||||
$this->call('key:generate');
|
||||
$this->updateEnvFile('APP_ENV', 'setup');
|
||||
$this->call('key:generate');
|
||||
}
|
||||
|
||||
$name = $this->ask('Site name [ex: Pixelfed]');
|
||||
$this->updateEnvFile('APP_NAME', $name ?? 'pixelfed');
|
||||
|
||||
$domain = $this->ask('Site Domain [ex: pixelfed.com]');
|
||||
if(empty($domain)) {
|
||||
$this->error('You must set the site domain');
|
||||
exit;
|
||||
}
|
||||
if(starts_with($domain, 'http')) {
|
||||
$this->error('The site domain cannot start with https://, you must use the FQDN (eg: example.org)');
|
||||
exit;
|
||||
}
|
||||
if(strpos($domain, '.') == false) {
|
||||
$this->error('You must enter a valid site domain');
|
||||
exit;
|
||||
}
|
||||
$this->updateEnvFile('APP_DOMAIN', $domain ?? 'example.org');
|
||||
$this->updateEnvFile('ADMIN_DOMAIN', $domain ?? 'example.org');
|
||||
$this->updateEnvFile('SESSION_DOMAIN', $domain ?? 'example.org');
|
||||
$this->updateEnvFile('APP_URL', 'https://' . $domain ?? 'https://example.org');
|
||||
$this->updateEnvFile('APP_URL', 'https://' . $domain);
|
||||
|
||||
$database = $this->choice('Select database driver', ['mysql', 'pgsql'], 0);
|
||||
$this->updateEnvFile('DB_CONNECTION', $database ?? 'mysql');
|
||||
switch ($database) {
|
||||
case 'mysql':
|
||||
$database_host = $this->ask('Select database host', '127.0.0.1');
|
||||
$this->updateEnvFile('DB_HOST', $database_host ?? 'mysql');
|
||||
|
||||
$database_port = $this->ask('Select database port', 3306);
|
||||
$this->updateEnvFile('DB_PORT', $database_port ?? 3306);
|
||||
$database_host = $this->ask('Select database host', '127.0.0.1');
|
||||
$this->updateEnvFile('DB_HOST', $database_host ?? 'mysql');
|
||||
|
||||
$database_db = $this->ask('Select database', 'pixelfed');
|
||||
$this->updateEnvFile('DB_DATABASE', $database_db ?? 'pixelfed');
|
||||
$database_port_default = $database === 'mysql' ? 3306 : 5432;
|
||||
$database_port = $this->ask('Select database port', $database_port_default);
|
||||
$this->updateEnvFile('DB_PORT', $database_port ?? $database_port_default);
|
||||
|
||||
$database_username = $this->ask('Select database username', 'pixelfed');
|
||||
$this->updateEnvFile('DB_USERNAME', $database_username ?? 'pixelfed');
|
||||
$database_db = $this->ask('Select database', 'pixelfed');
|
||||
$this->updateEnvFile('DB_DATABASE', $database_db ?? 'pixelfed');
|
||||
|
||||
$db_pass = str_random(64);
|
||||
$database_password = $this->secret('Select database password', $db_pass);
|
||||
$this->updateEnvFile('DB_PASSWORD', $database_password);
|
||||
break;
|
||||
|
||||
$database_username = $this->ask('Select database username', 'pixelfed');
|
||||
$this->updateEnvFile('DB_USERNAME', $database_username ?? 'pixelfed');
|
||||
|
||||
$db_pass = str_random(64);
|
||||
$database_password = $this->secret('Select database password', $db_pass);
|
||||
$this->updateEnvFile('DB_PASSWORD', $database_password);
|
||||
|
||||
$dsn = "{$database}:dbname={$database_db};host={$database_host};port={$database_port};";
|
||||
try {
|
||||
$dbh = new PDO($dsn, $database_username, $database_password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
} catch (\PDOException $e) {
|
||||
$this->error('Cannot connect to database, check your credentials and try again');
|
||||
exit;
|
||||
}
|
||||
|
||||
$cache = $this->choice('Select cache driver', ["redis", "apc", "array", "database", "file", "memcached"], 0);
|
||||
$this->updateEnvFile('CACHE_DRIVER', $cache ?? 'redis');
|
||||
if($this->installType === 'Advanced') {
|
||||
$cache = $this->choice('Select cache driver', ["redis", "apc", "array", "database", "file", "memcached"], 0);
|
||||
$this->updateEnvFile('CACHE_DRIVER', $cache ?? 'redis');
|
||||
|
||||
$session = $this->choice('Select session driver', ["redis", "file", "cookie", "database", "apc", "memcached", "array"], 0);
|
||||
$this->updateEnvFile('SESSION_DRIVER', $session ?? 'redis');
|
||||
$session = $this->choice('Select session driver', ["redis", "file", "cookie", "database", "apc", "memcached", "array"], 0);
|
||||
$this->updateEnvFile('SESSION_DRIVER', $session ?? 'redis');
|
||||
|
||||
$redis_host = $this->ask('Set redis host', 'localhost');
|
||||
$this->updateEnvFile('REDIS_HOST', $redis_host);
|
||||
$redis_host = $this->ask('Set redis host', 'localhost');
|
||||
$this->updateEnvFile('REDIS_HOST', $redis_host);
|
||||
|
||||
$redis_password = $this->ask('Set redis password', 'null');
|
||||
$this->updateEnvFile('REDIS_PASSWORD', $redis_password);
|
||||
$redis_password = $this->ask('Set redis password', 'null');
|
||||
$this->updateEnvFile('REDIS_PASSWORD', $redis_password);
|
||||
|
||||
$redis_port = $this->ask('Set redis port', 6379);
|
||||
$this->updateEnvFile('REDIS_PORT', $redis_port);
|
||||
$redis_port = $this->ask('Set redis port', 6379);
|
||||
$this->updateEnvFile('REDIS_PORT', $redis_port);
|
||||
}
|
||||
|
||||
$open_registration = $this->choice('Allow new registrations?', ['true', 'false'], 1);
|
||||
$open_registration = $this->choice('Allow new registrations?', ['false', 'true'], 0);
|
||||
$this->updateEnvFile('OPEN_REGISTRATION', $open_registration);
|
||||
|
||||
$enforce_email_verification = $this->choice('Enforce email verification?', ['true', 'false'], 0);
|
||||
$activitypub_federation = $this->choice('Enable ActivityPub federation?', ['false', 'true'], 1);
|
||||
$this->updateEnvFile('ACTIVITY_PUB', $activitypub_federation);
|
||||
$this->updateEnvFile('AP_INBOX', $activitypub_federation);
|
||||
$this->updateEnvFile('AP_SHAREDINBOX', $activitypub_federation);
|
||||
$this->updateEnvFile('AP_REMOTE_FOLLOW', $activitypub_federation);
|
||||
|
||||
$enforce_email_verification = $this->choice('Enforce email verification?', ['false', 'true'], 1);
|
||||
$this->updateEnvFile('ENFORCE_EMAIL_VERIFICATION', $enforce_email_verification);
|
||||
|
||||
$enable_mobile_apis = $this->choice('Enable mobile app/apis support?', ['false', 'true'], 1);
|
||||
$this->updateEnvFile('OAUTH_ENABLED', $enable_mobile_apis);
|
||||
$this->updateEnvFile('EXP_EMC', $enable_mobile_apis);
|
||||
|
||||
$optimize_media = $this->choice('Optimize media uploads? Requires jpegoptim and other dependencies!', ['false', 'true'], 0);
|
||||
$this->updateEnvFile('PF_OPTIMIZE_IMAGES', $optimize_media);
|
||||
|
||||
if($this->installType === 'Advanced') {
|
||||
|
||||
if($optimize_media === 'true') {
|
||||
$image_quality = $this->ask('Set image optimization quality between 1-100. Default is 80%, lower values use less disk space at the expense of image quality.', '80');
|
||||
if($image_quality < 1) {
|
||||
$this->error('Min image quality is 1. You should avoid such a low value, 60 at minimum is recommended.');
|
||||
exit;
|
||||
}
|
||||
if($image_quality > 100) {
|
||||
$this->error('Max image quality is 100');
|
||||
exit;
|
||||
}
|
||||
$this->updateEnvFile('IMAGE_QUALITY', $image_quality);
|
||||
}
|
||||
|
||||
$max_photo_size = $this->ask('Max photo upload size in kilobytes. Default 15000 which is equal to 15MB', '15000');
|
||||
if($max_photo_size * 1024 > $this->parseSize(ini_get('post_max_size'))) {
|
||||
$this->error('Max photo size (' . (round($max_photo_size / 1000)) . 'M) cannot exceed php.ini `post_max_size` of ' . ini_get('post_max_size'));
|
||||
exit;
|
||||
}
|
||||
$this->updateEnvFile('MAX_PHOTO_SIZE', $max_photo_size);
|
||||
|
||||
$max_caption_length = $this->ask('Max caption limit. Default to 500, max 5000.', '500');
|
||||
if($max_caption_length > 5000) {
|
||||
$this->error('Max caption length is 5000 characters.');
|
||||
exit;
|
||||
}
|
||||
$this->updateEnvFile('MAX_CAPTION_LENGTH', $max_caption_length);
|
||||
|
||||
$max_album_length = $this->ask('Max photos allowed per album. Choose a value between 1 and 10.', '4');
|
||||
if($max_album_length < 1) {
|
||||
$this->error('Min album length is 1 photos per album.');
|
||||
exit;
|
||||
}
|
||||
if($max_album_length > 10) {
|
||||
$this->error('Max album length is 10 photos per album.');
|
||||
exit;
|
||||
}
|
||||
$this->updateEnvFile('MAX_ALBUM_LENGTH', $max_album_length);
|
||||
}
|
||||
|
||||
$this->updateEnvFile('APP_ENV', 'production');
|
||||
$this->postInstall();
|
||||
}
|
||||
|
||||
protected function updateEnvFile($key, $value)
|
||||
|
@ -247,8 +335,35 @@ class Installer extends Command
|
|||
|
||||
protected function postInstall()
|
||||
{
|
||||
$this->callSilent('config:cache');
|
||||
//$this->callSilent('route:cache');
|
||||
$this->line('');
|
||||
$this->info('We recommend running database migrations now, or you can do it manually later.');
|
||||
$confirm = $this->choice('Do you want to run the database migrations?', ['No', 'Yes'], 0);
|
||||
if($confirm === 'Yes') {
|
||||
$this->callSilently('config:clear');
|
||||
sleep(3);
|
||||
$this->call('migrate', ['--force' => true]);
|
||||
$this->callSilently('instance:actor');
|
||||
$this->callSilently('passport:install');
|
||||
|
||||
$confirm = $this->choice('Do you want to create an admin account?', ['No', 'Yes'], 0);
|
||||
if($confirm === 'Yes') {
|
||||
$this->call('user:create');
|
||||
}
|
||||
} else {
|
||||
$this->callSilently('config:cache');
|
||||
}
|
||||
|
||||
$this->info('Pixelfed has been successfully installed!');
|
||||
}
|
||||
|
||||
protected function parseSize($size) {
|
||||
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
|
||||
$size = preg_replace('/[^0-9\.]/', '', $size);
|
||||
if ($unit) {
|
||||
return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
|
||||
}
|
||||
else {
|
||||
return round($size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,16 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class FollowRequest extends Model
|
||||
{
|
||||
protected $fillable = ['follower_id', 'following_id'];
|
||||
protected $fillable = ['follower_id', 'following_id', 'activity', 'handled_at'];
|
||||
|
||||
protected $casts = [
|
||||
'activity' => 'array',
|
||||
];
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'follower_id', 'id');
|
||||
}
|
||||
|
||||
public function follower()
|
||||
{
|
||||
|
@ -18,13 +27,14 @@ class FollowRequest extends Model
|
|||
return $this->belongsTo(Profile::class, 'following_id', 'id');
|
||||
}
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'follower_id', 'id');
|
||||
}
|
||||
|
||||
public function target()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'following_id', 'id');
|
||||
}
|
||||
|
||||
public function permalink($append = null, $namespace = '#accepts')
|
||||
{
|
||||
$path = $this->target->permalink("{$namespace}/follows/{$this->id}{$append}");
|
||||
return url($path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,8 @@ use App\Transformer\Api\Mastodon\v1\AccountTransformer;
|
|||
use App\Services\AccountService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\RelationshipService;
|
||||
use App\Jobs\FollowPipeline\FollowAcceptPipeline;
|
||||
use App\Jobs\FollowPipeline\FollowRejectPipeline;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
|
@ -363,12 +365,13 @@ class AccountController extends Controller
|
|||
'accounts' => $followers->take(10)->map(function($a) {
|
||||
$actor = $a->actor;
|
||||
return [
|
||||
'id' => $actor->id,
|
||||
'rid' => (string) $a->id,
|
||||
'id' => (string) $actor->id,
|
||||
'username' => $actor->username,
|
||||
'avatar' => $actor->avatarUrl(),
|
||||
'url' => $actor->url(),
|
||||
'local' => $actor->domain == null,
|
||||
'following' => $actor->followedBy(Auth::user()->profile)
|
||||
'account' => AccountService::get($actor->id)
|
||||
];
|
||||
})
|
||||
];
|
||||
|
@ -390,17 +393,35 @@ class AccountController extends Controller
|
|||
|
||||
switch ($action) {
|
||||
case 'accept':
|
||||
$follow = new Follower();
|
||||
$follow->profile_id = $follower->id;
|
||||
$follow->following_id = $pid;
|
||||
$follow->save();
|
||||
FollowPipeline::dispatch($follow);
|
||||
$followRequest->delete();
|
||||
$follow = new Follower();
|
||||
$follow->profile_id = $follower->id;
|
||||
$follow->following_id = $pid;
|
||||
$follow->save();
|
||||
|
||||
$profile = Profile::findOrFail($pid);
|
||||
$profile->followers_count++;
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
|
||||
$profile = Profile::findOrFail($follower->id);
|
||||
$profile->following_count++;
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
|
||||
if($follower->domain != null && $follower->private_key === null) {
|
||||
FollowAcceptPipeline::dispatch($followRequest);
|
||||
} else {
|
||||
FollowPipeline::dispatch($follow);
|
||||
$followRequest->delete();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'reject':
|
||||
$followRequest->is_rejected = true;
|
||||
$followRequest->save();
|
||||
if($follower->domain != null && $follower->private_key === null) {
|
||||
FollowRejectPipeline::dispatch($followRequest);
|
||||
} else {
|
||||
$followRequest->delete();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ use App\Services\{
|
|||
FollowerService,
|
||||
InstanceService,
|
||||
LikeService,
|
||||
NetworkTimelineService,
|
||||
NotificationService,
|
||||
MediaPathService,
|
||||
PublicTimelineService,
|
||||
|
@ -82,6 +83,8 @@ use App\Services\DiscoverService;
|
|||
use App\Services\CustomEmojiService;
|
||||
use App\Services\MarkerService;
|
||||
use App\Models\Conversation;
|
||||
use App\Jobs\FollowPipeline\FollowAcceptPipeline;
|
||||
use App\Jobs\FollowPipeline\FollowRejectPipeline;
|
||||
|
||||
class ApiV1Controller extends Controller
|
||||
{
|
||||
|
@ -441,7 +444,7 @@ class ApiV1Controller extends Controller
|
|||
|
||||
if($pid != $account['id']) {
|
||||
if($account['locked']) {
|
||||
if(FollowerService::follows($pid, $account['id'])) {
|
||||
if(!FollowerService::follows($pid, $account['id'])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -488,7 +491,7 @@ class ApiV1Controller extends Controller
|
|||
|
||||
if($pid != $account['id']) {
|
||||
if($account['locked']) {
|
||||
if(FollowerService::follows($pid, $account['id'])) {
|
||||
if(!FollowerService::follows($pid, $account['id'])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -722,6 +725,13 @@ class ApiV1Controller extends Controller
|
|||
->exists();
|
||||
|
||||
if($isFollowing == false) {
|
||||
$followRequest = FollowRequest::whereFollowerId($user->profile_id)
|
||||
->whereFollowingId($target->id)
|
||||
->first();
|
||||
if($followRequest) {
|
||||
$followRequest->delete();
|
||||
RelationshipService::refresh($target->id, $user->profile_id);
|
||||
}
|
||||
$resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
|
@ -1149,15 +1159,22 @@ class ApiV1Controller extends Controller
|
|||
public function accountFollowRequests(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'limit' => 'sometimes|integer|min:1|max:100'
|
||||
]);
|
||||
$user = $request->user();
|
||||
|
||||
$followRequests = FollowRequest::whereFollowingId($user->profile->id)->pluck('follower_id');
|
||||
$res = FollowRequest::whereFollowingId($user->profile->id)
|
||||
->limit($request->input('limit', 40))
|
||||
->pluck('follower_id')
|
||||
->map(function($id) {
|
||||
return AccountService::getMastodon($id, true);
|
||||
})
|
||||
->filter(function($acct) {
|
||||
return $acct && isset($acct['id']);
|
||||
})
|
||||
->values();
|
||||
|
||||
$profiles = Profile::find($followRequests);
|
||||
|
||||
$resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
|
@ -1171,10 +1188,46 @@ class ApiV1Controller extends Controller
|
|||
public function accountFollowRequestAccept(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$pid = $request->user()->profile_id;
|
||||
$target = AccountService::getMastodon($id);
|
||||
|
||||
// todo
|
||||
if(!$target) {
|
||||
return response()->json(['error' => 'Record not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json([]);
|
||||
$followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first();
|
||||
|
||||
if(!$followRequest) {
|
||||
return response()->json(['error' => 'Record not found'], 404);
|
||||
}
|
||||
|
||||
$follower = $followRequest->follower;
|
||||
$follow = new Follower();
|
||||
$follow->profile_id = $follower->id;
|
||||
$follow->following_id = $pid;
|
||||
$follow->save();
|
||||
|
||||
$profile = Profile::findOrFail($pid);
|
||||
$profile->followers_count++;
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
|
||||
$profile = Profile::findOrFail($follower->id);
|
||||
$profile->following_count++;
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
|
||||
if($follower->domain != null && $follower->private_key === null) {
|
||||
FollowAcceptPipeline::dispatch($followRequest);
|
||||
} else {
|
||||
FollowPipeline::dispatch($follow);
|
||||
$followRequest->delete();
|
||||
}
|
||||
|
||||
RelationshipService::refresh($pid, $id);
|
||||
$res = RelationshipService::get($pid, $id);
|
||||
$res['followed_by'] = true;
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1187,10 +1240,30 @@ class ApiV1Controller extends Controller
|
|||
public function accountFollowRequestReject(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$pid = $request->user()->profile_id;
|
||||
$target = AccountService::getMastodon($id);
|
||||
|
||||
// todo
|
||||
if(!$target) {
|
||||
return response()->json(['error' => 'Record not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json([]);
|
||||
$followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first();
|
||||
|
||||
if(!$followRequest) {
|
||||
return response()->json(['error' => 'Record not found'], 404);
|
||||
}
|
||||
|
||||
$follower = $followRequest->follower;
|
||||
|
||||
if($follower->domain != null && $follower->private_key === null) {
|
||||
FollowRejectPipeline::dispatch($followRequest);
|
||||
} else {
|
||||
$followRequest->delete();
|
||||
}
|
||||
|
||||
RelationshipService::refresh($pid, $id);
|
||||
$res = RelationshipService::get($pid, $id);
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1811,7 +1884,7 @@ class ApiV1Controller extends Controller
|
|||
->take(($limit * 2))
|
||||
->get()
|
||||
->map(function($s) use($pid) {
|
||||
$status = StatusService::getMastodon($s['id']);
|
||||
$status = StatusService::getMastodon($s['id'], false);
|
||||
if(!$status || !isset($status['account']) || !isset($status['account']['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1842,7 +1915,7 @@ class ApiV1Controller extends Controller
|
|||
->take(($limit * 2))
|
||||
->get()
|
||||
->map(function($s) use($pid) {
|
||||
$status = StatusService::getMastodon($s['id']);
|
||||
$status = StatusService::getMastodon($s['id'], false);
|
||||
if(!$status || !isset($status['account']) || !isset($status['account']['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1899,28 +1972,46 @@ class ApiV1Controller extends Controller
|
|||
$this->validate($request,[
|
||||
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||
'limit' => 'nullable|integer|max:100'
|
||||
'limit' => 'nullable|integer|max:100',
|
||||
'remote' => 'sometimes'
|
||||
]);
|
||||
|
||||
$min = $request->input('min_id');
|
||||
$max = $request->input('max_id');
|
||||
$limit = $request->input('limit') ?? 20;
|
||||
$user = $request->user();
|
||||
$remote = $request->has('remote');
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
|
||||
Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() {
|
||||
if(PublicTimelineService::count() == 0) {
|
||||
PublicTimelineService::warmCache(true, 400);
|
||||
}
|
||||
});
|
||||
if($remote && config('instance.timeline.network.cached')) {
|
||||
Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() {
|
||||
if(NetworkTimelineService::count() == 0) {
|
||||
NetworkTimelineService::warmCache(true, config('instance.timeline.network.cache_dropoff'));
|
||||
}
|
||||
});
|
||||
|
||||
if ($max) {
|
||||
$feed = PublicTimelineService::getRankedMaxId($max, $limit + 5);
|
||||
} else if ($min) {
|
||||
$feed = PublicTimelineService::getRankedMinId($min, $limit + 5);
|
||||
} else {
|
||||
$feed = PublicTimelineService::get(0, $limit + 5);
|
||||
}
|
||||
if ($max) {
|
||||
$feed = NetworkTimelineService::getRankedMaxId($max, $limit + 5);
|
||||
} else if ($min) {
|
||||
$feed = NetworkTimelineService::getRankedMinId($min, $limit + 5);
|
||||
} else {
|
||||
$feed = NetworkTimelineService::get(0, $limit + 5);
|
||||
}
|
||||
} else {
|
||||
Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() {
|
||||
if(PublicTimelineService::count() == 0) {
|
||||
PublicTimelineService::warmCache(true, 400);
|
||||
}
|
||||
});
|
||||
|
||||
if ($max) {
|
||||
$feed = PublicTimelineService::getRankedMaxId($max, $limit + 5);
|
||||
} else if ($min) {
|
||||
$feed = PublicTimelineService::getRankedMinId($min, $limit + 5);
|
||||
} else {
|
||||
$feed = PublicTimelineService::get(0, $limit + 5);
|
||||
}
|
||||
}
|
||||
|
||||
$res = collect($feed)
|
||||
->map(function($k) use($user) {
|
||||
|
@ -1943,6 +2034,9 @@ class ApiV1Controller extends Controller
|
|||
// ->toArray();
|
||||
|
||||
$baseUrl = config('app.url') . '/api/v1/timelines/public?limit=' . $limit . '&';
|
||||
if($remote) {
|
||||
$baseUrl .= 'remote=1&';
|
||||
}
|
||||
$minId = $res->map(function($s) {
|
||||
return ['id' => $s['id']];
|
||||
})->min('id');
|
||||
|
|
|
@ -217,4 +217,20 @@ class LiveStreamController extends Controller
|
|||
|
||||
return;
|
||||
}
|
||||
|
||||
public function getConfig(Request $request)
|
||||
{
|
||||
abort_if(!config('livestreaming.enabled'), 400);
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$res = [
|
||||
'enabled' => config('livestreaming.enabled'),
|
||||
'broadcast' => [
|
||||
'sources' => config('livestreaming.broadcast.sources'),
|
||||
'limits' => config('livestreaming.broadcast.limits')
|
||||
],
|
||||
];
|
||||
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ use App\Services\{
|
|||
LikeService,
|
||||
PublicTimelineService,
|
||||
ProfileService,
|
||||
NetworkTimelineService,
|
||||
ReblogService,
|
||||
RelationshipService,
|
||||
StatusService,
|
||||
|
@ -521,7 +522,7 @@ class PublicApiController extends Controller
|
|||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
$status = StatusService::get($s->id, false);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
|
@ -567,7 +568,7 @@ class PublicApiController extends Controller
|
|||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
$status = StatusService::get($s->id, false);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
|
@ -608,59 +609,92 @@ class PublicApiController extends Controller
|
|||
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
|
||||
if($min || $max) {
|
||||
$dir = $min ? '>' : '<';
|
||||
$id = $min ?? $max;
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'type',
|
||||
'scope',
|
||||
'created_at',
|
||||
)
|
||||
->where('id', $dir, $id)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNotNull('uri')
|
||||
->whereScope('public')
|
||||
->where('id', '>', $amin)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
|
||||
$status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
|
||||
$status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
|
||||
return $status;
|
||||
});
|
||||
$res = $timeline->toArray();
|
||||
} else {
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'type',
|
||||
'scope',
|
||||
'created_at',
|
||||
)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNotNull('uri')
|
||||
->whereScope('public')
|
||||
->where('id', '>', $amin)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
|
||||
$status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
|
||||
$status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
|
||||
return $status;
|
||||
});
|
||||
$res = $timeline->toArray();
|
||||
if(config('instance.timeline.network.cached') == false) {
|
||||
if($min || $max) {
|
||||
$dir = $min ? '>' : '<';
|
||||
$id = $min ?? $max;
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'type',
|
||||
'scope',
|
||||
'created_at',
|
||||
)
|
||||
->where('id', $dir, $id)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNotNull('uri')
|
||||
->whereScope('public')
|
||||
->where('id', '>', $amin)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
|
||||
$status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
|
||||
$status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
|
||||
return $status;
|
||||
});
|
||||
$res = $timeline->toArray();
|
||||
} else {
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'type',
|
||||
'scope',
|
||||
'created_at',
|
||||
)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNotNull('uri')
|
||||
->whereScope('public')
|
||||
->where('id', '>', $amin)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
|
||||
$status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
|
||||
$status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
|
||||
return $status;
|
||||
});
|
||||
$res = $timeline->toArray();
|
||||
}
|
||||
} else {
|
||||
Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() {
|
||||
if(NetworkTimelineService::count() == 0) {
|
||||
NetworkTimelineService::warmCache(true, 400);
|
||||
}
|
||||
});
|
||||
|
||||
if ($max) {
|
||||
$feed = NetworkTimelineService::getRankedMaxId($max, $limit);
|
||||
} else if ($min) {
|
||||
$feed = NetworkTimelineService::getRankedMinId($min, $limit);
|
||||
} else {
|
||||
$feed = NetworkTimelineService::get(0, $limit);
|
||||
}
|
||||
|
||||
$res = collect($feed)
|
||||
->map(function($k) use($user) {
|
||||
$status = StatusService::get($k);
|
||||
if($status && isset($status['account']) && $user) {
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $k);
|
||||
$status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $k);
|
||||
$status['reblogged'] = (bool) ReblogService::get($user->profile_id, $k);
|
||||
$status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']);
|
||||
}
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($filtered) {
|
||||
return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return response()->json($res);
|
||||
|
@ -704,7 +738,7 @@ class PublicApiController extends Controller
|
|||
|
||||
if($pid != $account['id']) {
|
||||
if($account['locked']) {
|
||||
if(FollowerService::follows($pid, $account['id'])) {
|
||||
if(!FollowerService::follows($pid, $account['id'])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -744,7 +778,7 @@ class PublicApiController extends Controller
|
|||
|
||||
if($pid != $account['id']) {
|
||||
if($account['locked']) {
|
||||
if(FollowerService::follows($pid, $account['id'])) {
|
||||
if(!FollowerService::follows($pid, $account['id'])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,5 +19,9 @@ class TrustProxies extends Middleware
|
|||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $headers = Request::HEADER_X_FORWARDED_ALL;
|
||||
protected $headers = Request::HEADER_X_FORWARDED_FOR |
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO |
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB;
|
||||
}
|
||||
|
|
69
app/Jobs/FollowPipeline/FollowAcceptPipeline.php
Normal file
69
app/Jobs/FollowPipeline/FollowAcceptPipeline.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\FollowPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
use Cache, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\FollowRequest;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Transformer\ActivityPub\Verb\AcceptFollow;
|
||||
|
||||
class FollowAcceptPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $followRequest;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(FollowRequest $followRequest)
|
||||
{
|
||||
$this->followRequest = $followRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$follow = $this->followRequest;
|
||||
$actor = $follow->actor;
|
||||
$target = $follow->target;
|
||||
|
||||
if($actor->domain == null || $actor->inbox_url == null || !$target->private_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($follow, new AcceptFollow());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
$url = $actor->sharedInbox ?? $actor->inbox_url;
|
||||
|
||||
Helpers::sendSignedObject($target, $url, $activity);
|
||||
|
||||
$follow->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -63,11 +63,6 @@ class FollowPipeline implements ShouldQueue
|
|||
$notification->item_id = $target->id;
|
||||
$notification->item_type = "App\Profile";
|
||||
$notification->save();
|
||||
|
||||
$redis = Redis::connection();
|
||||
|
||||
$nkey = config('cache.prefix').':user.'.$target->id.'.notifications';
|
||||
$redis->lpush($nkey, $notification->id);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
|
|
69
app/Jobs/FollowPipeline/FollowRejectPipeline.php
Normal file
69
app/Jobs/FollowPipeline/FollowRejectPipeline.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\FollowPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
use Cache, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\FollowRequest;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Transformer\ActivityPub\Verb\RejectFollow;
|
||||
|
||||
class FollowRejectPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $followRequest;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(FollowRequest $followRequest)
|
||||
{
|
||||
$this->followRequest = $followRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$follow = $this->followRequest;
|
||||
$actor = $follow->actor;
|
||||
$target = $follow->target;
|
||||
|
||||
if($actor->domain == null || $actor->inbox_url == null || !$target->private_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($follow, new RejectFollow());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
$url = $actor->sharedInbox ?? $actor->inbox_url;
|
||||
|
||||
Helpers::sendSignedObject($target, $url, $activity);
|
||||
|
||||
$follow->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,21 @@ class LiveStream extends Model
|
|||
$host = config('livestreaming.server.host');
|
||||
$port = ':' . config('livestreaming.server.port');
|
||||
$path = '/' . config('livestreaming.server.path') . '?';
|
||||
$query = http_build_query([
|
||||
'name' => $this->stream_id,
|
||||
'key' => $this->stream_key,
|
||||
'ts' => time()
|
||||
]);
|
||||
|
||||
return $proto . $host . $port . $path . $query;
|
||||
}
|
||||
|
||||
public function getStreamRtmpUrl()
|
||||
{
|
||||
$proto = 'rtmp://';
|
||||
$host = config('livestreaming.server.host');
|
||||
$port = ':' . config('livestreaming.server.port');
|
||||
$path = '/' . config('livestreaming.server.path') . '/'. $this->stream_id . '?';
|
||||
$query = http_build_query([
|
||||
'key' => $this->stream_key,
|
||||
'ts' => time()
|
||||
|
|
|
@ -271,7 +271,28 @@ class Profile extends Model
|
|||
$this->permalink('/followers')
|
||||
]
|
||||
];
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'unlisted':
|
||||
$audience = [
|
||||
'to' => [
|
||||
],
|
||||
'cc' => [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
$this->permalink('/followers')
|
||||
]
|
||||
];
|
||||
break;
|
||||
|
||||
case 'private':
|
||||
$audience = [
|
||||
'to' => [
|
||||
$this->permalink('/followers')
|
||||
],
|
||||
'cc' => [
|
||||
]
|
||||
];
|
||||
break;
|
||||
}
|
||||
return $audience;
|
||||
}
|
||||
|
|
|
@ -78,11 +78,11 @@ class FollowerService
|
|||
}
|
||||
return $profile
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
->get()
|
||||
->map(function($follow) {
|
||||
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||
})
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
|
95
app/Services/NetworkTimelineService.php
Normal file
95
app/Services/NetworkTimelineService.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{
|
||||
Profile,
|
||||
Status,
|
||||
UserFilter
|
||||
};
|
||||
|
||||
class NetworkTimelineService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:timeline:network';
|
||||
|
||||
public static function get($start = 0, $stop = 10)
|
||||
{
|
||||
if($stop > 100) {
|
||||
$stop = 100;
|
||||
}
|
||||
|
||||
return Redis::zrevrange(self::CACHE_KEY, $start, $stop);
|
||||
}
|
||||
|
||||
public static function getRankedMaxId($start = null, $limit = 10)
|
||||
{
|
||||
if(!$start) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY, $start, '-inf', [
|
||||
'withscores' => true,
|
||||
'limit' => [1, $limit]
|
||||
]));
|
||||
}
|
||||
|
||||
public static function getRankedMinId($end = null, $limit = 10)
|
||||
{
|
||||
if(!$end) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY, '+inf', $end, [
|
||||
'withscores' => true,
|
||||
'limit' => [0, $limit]
|
||||
]));
|
||||
}
|
||||
|
||||
public static function add($val)
|
||||
{
|
||||
if(self::count() > config('instance.timeline.network.cache_dropoff')) {
|
||||
if(config('database.redis.client') === 'phpredis') {
|
||||
Redis::zpopmin(self::CACHE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
return Redis::zadd(self::CACHE_KEY, $val, $val);
|
||||
}
|
||||
|
||||
public static function rem($val)
|
||||
{
|
||||
return Redis::zrem(self::CACHE_KEY, $val);
|
||||
}
|
||||
|
||||
public static function del($val)
|
||||
{
|
||||
return self::rem($val);
|
||||
}
|
||||
|
||||
public static function count()
|
||||
{
|
||||
return Redis::zcard(self::CACHE_KEY);
|
||||
}
|
||||
|
||||
public static function warmCache($force = false, $limit = 100)
|
||||
{
|
||||
if(self::count() == 0 || $force == true) {
|
||||
Redis::del(self::CACHE_KEY);
|
||||
$ids = Status::whereNotNull('uri')
|
||||
->whereScope('public')
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->where('created_at', '>', now()->subHours(config('instance.timeline.network.max_hours_old')))
|
||||
->orderByDesc('created_at')
|
||||
->limit($limit)
|
||||
->pluck('id');
|
||||
foreach($ids as $id) {
|
||||
self::add($id);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -57,6 +57,8 @@ class RelationshipService
|
|||
|
||||
public static function refresh($aid, $tid)
|
||||
{
|
||||
Cache::forget('pf:services:follow:audience:' . $aid);
|
||||
Cache::forget('pf:services:follow:audience:' . $tid);
|
||||
self::delete($tid, $aid);
|
||||
self::delete($aid, $tid);
|
||||
self::get($tid, $aid);
|
||||
|
|
|
@ -3,26 +3,24 @@
|
|||
namespace App\Services;
|
||||
|
||||
use Cache;
|
||||
use App\UserFilter;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
use App\{
|
||||
Follower,
|
||||
Profile,
|
||||
UserFilter
|
||||
};
|
||||
|
||||
class UserFilterService {
|
||||
|
||||
class UserFilterService
|
||||
{
|
||||
const USER_MUTES_KEY = 'pf:services:mutes:ids:';
|
||||
const USER_BLOCKS_KEY = 'pf:services:blocks:ids:';
|
||||
|
||||
public static function mutes(int $profile_id) : array
|
||||
public static function mutes(int $profile_id)
|
||||
{
|
||||
$key = self::USER_MUTES_KEY . $profile_id;
|
||||
$cached = Redis::zrevrange($key, 0, -1);
|
||||
if($cached) {
|
||||
return $cached;
|
||||
$warm = Cache::has($key . ':cached');
|
||||
if($warm) {
|
||||
return Redis::zrevrange($key, 0, -1) ?? [];
|
||||
} else {
|
||||
if(Redis::zrevrange($key, 0, -1)) {
|
||||
return Redis::zrevrange($key, 0, -1);
|
||||
}
|
||||
$ids = UserFilter::whereFilterType('mute')
|
||||
->whereUserId($profile_id)
|
||||
->pluck('filterable_id')
|
||||
|
@ -30,29 +28,34 @@ class UserFilterService {
|
|||
foreach ($ids as $muted_id) {
|
||||
Redis::zadd($key, (int) $muted_id, (int) $muted_id);
|
||||
}
|
||||
Cache::set($key . ':cached', 1, 7776000);
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
|
||||
public static function blocks(int $profile_id) : array
|
||||
public static function blocks(int $profile_id)
|
||||
{
|
||||
$key = self::USER_BLOCKS_KEY . $profile_id;
|
||||
$cached = Redis::zrevrange($key, 0, -1);
|
||||
if($cached) {
|
||||
return $cached;
|
||||
$warm = Cache::has($key . ':cached');
|
||||
if($warm) {
|
||||
return Redis::zrevrange($key, 0, -1) ?? [];
|
||||
} else {
|
||||
if(Redis::zrevrange($key, 0, -1)) {
|
||||
return Redis::zrevrange($key, 0, -1);
|
||||
}
|
||||
$ids = UserFilter::whereFilterType('block')
|
||||
->whereUserId($profile_id)
|
||||
->pluck('filterable_id')
|
||||
->toArray();
|
||||
foreach ($ids as $blocked_id) {
|
||||
Redis::zadd($key, $blocked_id, $blocked_id);
|
||||
Redis::zadd($key, (int) $blocked_id, (int) $blocked_id);
|
||||
}
|
||||
Cache::set($key . ':cached', 1, 7776000);
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
|
||||
public static function filters(int $profile_id) : array
|
||||
public static function filters(int $profile_id)
|
||||
{
|
||||
return array_unique(array_merge(self::mutes($profile_id), self::blocks($profile_id)));
|
||||
}
|
||||
|
|
25
app/Transformer/ActivityPub/Verb/AcceptFollow.php
Normal file
25
app/Transformer/ActivityPub/Verb/AcceptFollow.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\FollowRequest;
|
||||
use League\Fractal;
|
||||
|
||||
class AcceptFollow extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(FollowRequest $follow)
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Accept',
|
||||
'id' => $follow->permalink(),
|
||||
'actor' => $follow->target->permalink(),
|
||||
'object' => [
|
||||
'type' => 'Follow',
|
||||
'id' => $follow->activity && isset($follow->activity['id']) ? $follow->activity['id'] : null,
|
||||
'actor' => $follow->actor->permalink(),
|
||||
'object' => $follow->target->permalink()
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
25
app/Transformer/ActivityPub/Verb/RejectFollow.php
Normal file
25
app/Transformer/ActivityPub/Verb/RejectFollow.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\FollowRequest;
|
||||
use League\Fractal;
|
||||
|
||||
class RejectFollow extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(FollowRequest $follow)
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Reject',
|
||||
'id' => $follow->permalink(null, '#rejects'),
|
||||
'actor' => $follow->target->permalink(),
|
||||
'object' => [
|
||||
'type' => 'Follow',
|
||||
'id' => $follow->activity && isset($follow->activity['id']) ? $follow->activity['id'] : null,
|
||||
'actor' => $follow->actor->permalink(),
|
||||
'object' => $follow->target->permalink()
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ use App\Services\CustomEmojiService;
|
|||
use App\Services\InstanceService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\NetworkTimelineService;
|
||||
use App\Jobs\MediaPipeline\MediaStoragePipeline;
|
||||
use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
|
||||
use App\Util\Media\License;
|
||||
|
@ -490,6 +491,16 @@ class Helpers {
|
|||
if(isset($activity['tag']) && is_array($activity['tag']) && !empty($activity['tag'])) {
|
||||
StatusTagsPipeline::dispatch($activity, $status);
|
||||
}
|
||||
|
||||
if( config('instance.timeline.network.cached') &&
|
||||
$status->in_reply_to_id === null &&
|
||||
$status->reblog_of_id === null &&
|
||||
in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) &&
|
||||
$status->created_at->gt(now()->subHours(config('instance.timeline.network.max_hours_old')))
|
||||
) {
|
||||
NetworkTimelineService::add($status->id);
|
||||
}
|
||||
|
||||
return $status;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -473,17 +473,12 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
if($target->is_private == true) {
|
||||
FollowRequest::firstOrCreate([
|
||||
FollowRequest::updateOrCreate([
|
||||
'follower_id' => $actor->id,
|
||||
'following_id' => $target->id
|
||||
'following_id' => $target->id,
|
||||
],[
|
||||
'activity' => collect($this->payload)->only(['id','actor','object','type'])->toArray()
|
||||
]);
|
||||
|
||||
Cache::forget('profile:follower_count:'.$target->id);
|
||||
Cache::forget('profile:follower_count:'.$actor->id);
|
||||
Cache::forget('profile:following_count:'.$target->id);
|
||||
Cache::forget('profile:following_count:'.$actor->id);
|
||||
FollowerService::add($actor->id, $target->id);
|
||||
|
||||
} else {
|
||||
$follower = new Follower;
|
||||
$follower->profile_id = $actor->id;
|
||||
|
|
406
composer.lock
generated
406
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -24,6 +24,12 @@ return [
|
|||
'timeline' => [
|
||||
'local' => [
|
||||
'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false)
|
||||
],
|
||||
|
||||
'network' => [
|
||||
'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', false) : false,
|
||||
'cache_dropoff' => env('INSTANCE_NETWORK_TIMELINE_CACHE_DROPOFF', 100),
|
||||
'max_hours_old' => env('INSTANCE_NETWORK_TIMELINE_CACHE_MAX_HOUR_INGEST', 6)
|
||||
]
|
||||
],
|
||||
|
||||
|
|
|
@ -10,14 +10,20 @@ return [
|
|||
],
|
||||
|
||||
'broadcast' => [
|
||||
'max_duration' => env('HLS_LIVE_BROADCAST_MAX_DURATION', 60),
|
||||
'max_active' => env('HLS_LIVE_BROADCAST_MAX_ACTIVE', 10),
|
||||
'delete_token_after_finished' => (bool) env('HLS_LIVE_BROADCAST_DELETE_TOKEN_AFTER', true),
|
||||
'max_duration' => (int) env('HLS_LIVE_BROADCAST_MAX_DURATION', 60),
|
||||
'max_active' => (int) env('HLS_LIVE_BROADCAST_MAX_ACTIVE', 10),
|
||||
|
||||
'limits' => [
|
||||
'enabled' => env('HLS_LIVE_BROADCAST_LIMITS', true),
|
||||
'min_follower_count' => env('HLS_LIVE_BROADCAST_LIMITS_MIN_FOLLOWERS', 100),
|
||||
'min_account_age' => env('HLS_LIVE_BROADCAST_LIMITS_MIN_ACCOUNT_AGE', 14),
|
||||
'admins_only' => env('HLS_LIVE_BROADCAST_LIMITS_ADMINS_ONLY', true)
|
||||
'enabled' => (bool) env('HLS_LIVE_BROADCAST_LIMITS', true),
|
||||
'min_follower_count' => (int) env('HLS_LIVE_BROADCAST_LIMITS_MIN_FOLLOWERS', 100),
|
||||
'min_account_age' => (int) env('HLS_LIVE_BROADCAST_LIMITS_MIN_ACCOUNT_AGE', 14),
|
||||
'admins_only' => (bool) env('HLS_LIVE_BROADCAST_LIMITS_ADMINS_ONLY', true)
|
||||
],
|
||||
|
||||
'sources' => [
|
||||
'app' => (bool) env('HLS_LIVE_BROADCAST_SOURCE_APP', false),
|
||||
'web' => (bool) env('HLS_LIVE_BROADCAST_SOURCE_WEB', false)
|
||||
]
|
||||
],
|
||||
|
||||
|
|
|
@ -239,7 +239,7 @@ return [
|
|||
]
|
||||
],
|
||||
|
||||
'max_collection_length' => (int) env('PF_MAX_COLLECTION_LENGTH', 18),
|
||||
'max_collection_length' => (int) env('PF_MAX_COLLECTION_LENGTH', 100),
|
||||
|
||||
'media_types' => env('MEDIA_TYPES', 'image/jpeg,image/png,image/gif'),
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddObjectColumnToFollowRequestsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('follow_requests', function (Blueprint $table) {
|
||||
$table->json('activity')->nullable()->after('following_id');
|
||||
$table->timestamp('handled_at')->nullable()->after('is_local');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('follow_requests', function (Blueprint $table) {
|
||||
$table->dropColumn('activity');
|
||||
$table->dropColumn('handled_at');
|
||||
});
|
||||
}
|
||||
}
|
BIN
public/css/app.css
vendored
BIN
public/css/app.css
vendored
Binary file not shown.
BIN
public/css/appdark.css
vendored
BIN
public/css/appdark.css
vendored
Binary file not shown.
BIN
public/js/compose-ojtjadoml.js
vendored
BIN
public/js/compose-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/daci-ojtjadoml.js
vendored
BIN
public/js/daci-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/dffc-ojtjadoml.js
vendored
BIN
public/js/dffc-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/dmsg-ojtjadoml.js
vendored
BIN
public/js/dmsg-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/dmyh-ojtjadoml.js
vendored
BIN
public/js/dmyh-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/dmym-ojtjadoml.js
vendored
BIN
public/js/dmym-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/dsfc-ojtjadoml.js
vendored
BIN
public/js/dsfc-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/dssc-ojtjadoml.js
vendored
BIN
public/js/dssc-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/home-ojtjadoml.js
vendored
BIN
public/js/home-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/manifest.js
vendored
BIN
public/js/manifest.js
vendored
Binary file not shown.
BIN
public/js/notifications-ojtjadoml.js
vendored
BIN
public/js/notifications-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/post-ojtjadoml.js
vendored
BIN
public/js/post-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/profile-ojtjadoml.js
vendored
BIN
public/js/profile-ojtjadoml.js
vendored
Binary file not shown.
BIN
public/js/spa.js
vendored
BIN
public/js/spa.js
vendored
Binary file not shown.
BIN
public/js/vendor.js
vendored
BIN
public/js/vendor.js
vendored
Binary file not shown.
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
|
||||
/*!
|
||||
* BootstrapVue Icons, generated from Bootstrap Icons 1.2.2
|
||||
* BootstrapVue Icons, generated from Bootstrap Icons 1.5.0
|
||||
*
|
||||
* @link https://icons.getbootstrap.com/
|
||||
* @license MIT
|
||||
|
|
Binary file not shown.
|
@ -105,5 +105,6 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
|
|||
Route::get('chat/latest', 'LiveStreamController@getLatestChat')->middleware($middleware);
|
||||
Route::post('chat/message', 'LiveStreamController@addChatComment')->middleware($middleware);
|
||||
Route::post('chat/delete', 'LiveStreamController@deleteChatComment')->middleware($middleware);
|
||||
Route::get('config', 'LiveStreamController@getConfig')->middleware($middleware);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue