Merge pull request #3532 from pixelfed/staging

Staging
This commit is contained in:
daniel 2022-06-13 02:19:41 -06:00 committed by GitHub
commit 255c41fb83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1103 additions and 436 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

BIN
public/css/appdark.css vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/manifest.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/spa.js vendored

Binary file not shown.

BIN
public/js/vendor.js vendored

Binary file not shown.

View file

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

View file

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