1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/pixelfed_ynh.git synced 2024-09-03 20:06:04 +02:00

Merge pull request #1388 from pixelfed/frontend-ui-refactor

Add Remote Follows
This commit is contained in:
daniel 2019-06-16 16:32:38 -06:00 committed by GitHub
commit fe98b65d16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 691 additions and 186 deletions

View file

@ -15,9 +15,9 @@ LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
DB_DATABASE=pixelfed
DB_USERNAME=pixelfed
DB_PASSWORD=pixelfed
BROADCAST_DRIVER=log
CACHE_DRIVER=redis

View file

@ -71,7 +71,7 @@ class FixUsernames extends Command
foreach($affected as $u) {
$old = $u->username;
$this->info("Found user: {$old}");
$opt = $this->choice('Select fix method:', $opts, 0);
$opt = $this->choice('Select fix method:', $opts, 3);
switch ($opt) {
case $opts[0]:

View file

@ -83,6 +83,14 @@ class Installer extends Command
'mbstring',
'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...');
foreach($extensions as $ext) {
@ -90,9 +98,9 @@ class Installer extends Command
$this->error("- {$ext} extension not found, aborting installation");
exit;
} else {
$this->info("- {$ext} extension found!");
}
}
$this->info("- Required PHP extensions found!");
}
protected function checkPermissions()
@ -119,7 +127,7 @@ class Installer extends Command
protected function envCheck()
{
if(!file_exists(base_path('.env'))) {
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();
@ -148,18 +156,100 @@ class Installer extends Command
{
$this->line('');
// copy env
$name = $this->ask('Site name [ex: Pixelfed]');
$domain = $this->ask('Site Domain [ex: pixelfed.com]');
$tls = $this->choice('Use HTTPS/TLS?', ['https', 'http'], 0);
$dbDrive = $this->choice('Select database driver', ['mysql', 'pgsql'/*, 'sqlite', 'sqlsrv'*/], 0);
$ws = $this->choice('Select cache driver', ["apc", "array", "database", "file", "memcached", "redis"], 5);
if(!file_exists(app()->environmentFilePath())) {
exec('cp .env.example .env');
$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]');
$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');
$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_db = $this->ask('Select database', 'pixelfed');
$this->updateEnvFile('DB_DATABASE', $database_db ?? 'pixelfed');
$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);
break;
}
$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', $cache ?? 'redis');
$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_port = $this->ask('Set redis port', 6379);
$this->updateEnvFile('REDIS_PORT', $redis_port);
$open_registration = $this->choice('Allow new registrations?', ['true', 'false'], 1);
$this->updateEnvFile('OPEN_REGISTRATION', $open_registration);
$enforce_email_verification = $this->choice('Enforce email verification?', ['true', 'false'], 0);
$this->updateEnvFile('ENFORCE_EMAIL_VERIFICATION', $enforce_email_verification);
}
protected function updateEnvFile($key, $value)
{
$envPath = app()->environmentFilePath();
$payload = file_get_contents($envPath);
if ($existing = $this->existingEnv($key, $payload)) {
$payload = str_replace("{$key}={$existing}", "{$key}=\"{$value}\"", $payload);
$this->storeEnv($payload);
} else {
$payload = $payload . "\n{$key}=\"{$value}\"\n";
$this->storeEnv($payload);
}
}
protected function existingEnv($needle, $haystack)
{
preg_match("/^{$needle}=[^\r\n]*/m", $haystack, $matches);
if ($matches && count($matches)) {
return substr($matches[0], strlen($needle) + 1);
}
return false;
}
protected function storeEnv($payload)
{
$file = fopen(app()->environmentFilePath(), 'w');
fwrite($file, $payload);
fclose($file);
}
protected function postInstall()
{
$this->callSilent('config:cache');
//$this->call('route:cache');
//$this->callSilent('route:cache');
$this->info('Pixelfed has been successfully installed!');
}
}

View file

@ -7,7 +7,6 @@ use Illuminate\Http\Request;
use Carbon\Carbon;
use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
use App\Http\Controllers\Controller;
use Jackiedo\DotenvEditor\Facades\DotenvEditor;
use App\Util\Lexer\PrettyNumber;
trait AdminSettingsController
@ -24,9 +23,11 @@ trait AdminSettingsController
return view('admin.settings.backups', compact('files'));
}
public function settingsConfig(Request $request, DotenvEditor $editor)
public function settingsConfig(Request $request)
{
return view('admin.settings.config', compact('editor'));
$editor = [];
$config = file_get_contents(base_path('.env'));
return view('admin.settings.config', compact('editor', 'config'));
}
public function settingsMaintenance(Request $request)
@ -50,9 +51,7 @@ trait AdminSettingsController
$this->validate($request, [
'APP_NAME' => 'required|string',
]);
Artisan::call('config:clear');
DotenvEditor::setKey('APP_NAME', $request->input('APP_NAME'));
DotenvEditor::save();
// Artisan::call('config:clear');
return redirect()->back();
}

View file

@ -17,7 +17,6 @@ use App\{
use DB, Cache;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Jackiedo\DotenvEditor\DotenvEditor;
use App\Http\Controllers\Admin\{
AdminDiscoverController,
AdminInstanceController,

View file

@ -11,6 +11,7 @@ use App\{
use Auth, Cache;
use Illuminate\Http\Request;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\Util\ActivityPub\Helpers;
class FollowerController extends Controller
{
@ -55,12 +56,20 @@ class FollowerController extends Controller
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->count();
if($private == true && $isFollowing == 0 || $remote == true) {
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
}
if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
}
$follow = FollowRequest::firstOrCreate([
'follower_id' => $user->id,
'following_id' => $target->id
]);
if($remote == true) {
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
$this->sendFollow($user, $target);
}
} elseif ($isFollowing == 0) {
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
@ -77,6 +86,9 @@ class FollowerController extends Controller
FollowPipeline::dispatch($follower);
} else {
$follower = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->firstOrFail();
if($remote == true) {
$this->sendUndoFollow($user, $target);
}
$follower->delete();
}
@ -88,4 +100,46 @@ class FollowerController extends Controller
Cache::forget('user:account:id:'.$target->user_id);
Cache::forget('user:account:id:'.$user->user_id);
}
protected function sendFollow($user, $target)
{
if($target->domain == null || $user->domain != null) {
return;
}
$payload = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'type' => 'Follow',
'actor' => $user->permalink(),
'object' => $target->permalink()
];
$inbox = $target->sharedInbox ?? $target->inbox_url;
Helpers::sendSignedObject($user, $inbox, $payload);
}
protected function sendUndoFollow($user, $target)
{
if($target->domain == null || $user->domain != null) {
return;
}
$payload = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $user->permalink('#follow/'.$target->id.'/undo'),
'type' => 'Undo',
'actor' => $user->permalink(),
'object' => [
'id' => $user->permalink('#follows/'.$target->id),
'actor' => $user->permalink(),
'object' => $target->permalink(),
'type' => 'Follow'
]
];
$inbox = $target->sharedInbox ?? $target->inbox_url;
Helpers::sendSignedObject($user, $inbox, $payload);
}
}

View file

@ -134,16 +134,7 @@ trait PrivacySettings
public function blockedInstanceStore(Request $request)
{
$this->validate($request, [
'domain' => [
'required',
'min:3',
'max:100',
function($attribute, $value, $fail) {
if(!filter_var($value, FILTER_VALIDATE_DOMAIN)) {
$fail($attribute. 'is invalid');
}
}
]
'domain' => 'required|active_url'
]);
$domain = $request->input('domain');
$instance = Instance::firstOrCreate(['domain' => $domain]);

View file

@ -18,7 +18,11 @@ trait RelationshipSettings
public function relationshipsHome()
{
return view('settings.relationships.home');
$profile = Auth::user()->profile;
$following = $profile->following()->simplePaginate(10);
$followers = $profile->followers()->simplePaginate(10);
return view('settings.relationships.home', compact('profile', 'following', 'followers'));
}
}

View file

@ -14,6 +14,7 @@ use App\Http\Controllers\Settings\{
LabsSettings,
HomeSettings,
PrivacySettings,
RelationshipSettings,
SecuritySettings
};
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
@ -24,6 +25,7 @@ class SettingsController extends Controller
LabsSettings,
HomeSettings,
PrivacySettings,
RelationshipSettings,
SecuritySettings;
public function __construct()

View file

@ -56,7 +56,7 @@ class RemoteFollowImportRecent implements ShouldQueue
*/
public function handle()
{
$outbox = $this->fetchOutbox();
// $outbox = $this->fetchOutbox();
}
public function fetchOutbox($url = false)
@ -216,7 +216,7 @@ class RemoteFollowImportRecent implements ShouldQueue
$info = pathinfo($url);
$url = str_replace(' ', '%20', $url);
$img = file_get_contents($url);
$file = '/tmp/'.str_random(12).$info['basename'];
$file = '/tmp/'.str_random(64);
file_put_contents($file, $img);
$path = Storage::putFile($storagePath, new File($file), 'public');
@ -231,6 +231,8 @@ class RemoteFollowImportRecent implements ShouldQueue
ImageThumbnail::dispatch($media);
@unlink($file);
return true;
} catch (Exception $e) {
return false;

View file

@ -31,9 +31,39 @@ class AuthLogin
return;
}
$this->userProfile($user);
$this->userSettings($user);
$this->userState($user);
$this->userDevice($user);
$this->userProfileId($user);
}
protected function userProfile($user)
{
if (empty($user->profile)) {
DB::transaction(function() use($user) {
$profile = new Profile();
$profile->user_id = $user->id;
$profile->username = $user->username;
$profile->name = $user->name;
$pkiConfig = [
'digest_alg' => 'sha512',
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
$pki = openssl_pkey_new($pkiConfig);
openssl_pkey_export($pki, $pki_private);
$pki_public = openssl_pkey_get_details($pki);
$pki_public = $pki_public['key'];
$profile->private_key = $pki_private;
$profile->public_key = $pki_public;
$profile->save();
CreateAvatar::dispatch($profile);
});
}
}
protected function userSettings($user)
@ -88,4 +118,15 @@ class AuthLogin
]);
});
}
protected function userProfileId($user)
{
if($user->profile_id == null) {
DB::transaction(function() use($user) {
$profile = $user->profile;
$user->profile_id = $profile->id;
$user->save();
});
}
}
}

View file

@ -20,7 +20,7 @@ class UserObserver
public function saved(User $user)
{
if (empty($user->profile)) {
DB::transaction(function() use($user) {
$profile = DB::transaction(function() use($user) {
$profile = new Profile();
$profile->user_id = $user->id;
$profile->username = $user->username;
@ -38,9 +38,16 @@ class UserObserver
$profile->private_key = $pki_private;
$profile->public_key = $pki_public;
$profile->save();
return $profile;
});
DB::transaction(function() use($user, $profile) {
$user = User::findOrFail($user->id);
$user->profile_id = $profile->id;
$user->save();
CreateAvatar::dispatch($profile);
});
}
if (empty($user->settings)) {

View file

@ -65,7 +65,6 @@ class Status extends Model
return $this->hasMany(Media::class)->orderBy('order', 'asc')->first();
}
// todo: deprecate after 0.6.0
public function viewType()
{
if($this->type) {
@ -74,7 +73,6 @@ class Status extends Model
return $this->setType();
}
// todo: deprecate after 0.6.0
public function setType()
{
if(in_array($this->type, self::STATUS_TYPES)) {

View file

@ -55,8 +55,8 @@ class Helpers {
$activity = $data['object'];
$mediaTypes = ['Document', 'Image', 'Video'];
$mimeTypes = ['image/jpeg', 'image/png', 'video/mp4'];
$mimeTypes = explode(',', config('pixelfed.media_types'));
$mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image'];
if(!isset($activity['attachment']) || empty($activity['attachment'])) {
return false;
@ -249,7 +249,6 @@ class Helpers {
}
if(isset($res['cc']) == true) {
$scope = 'unlisted';
if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
$scope = 'unlisted';
}
@ -285,6 +284,12 @@ class Helpers {
}
}
if(!self::validateUrl($res['id']) ||
!self::validateUrl($activity['object']['attributedTo'])
) {
abort(400, 'Invalid object url');
}
$idDomain = parse_url($res['id'], PHP_URL_HOST);
$urlDomain = parse_url($url, PHP_URL_HOST);
$actorDomain = parse_url($activity['object']['attributedTo'], PHP_URL_HOST);
@ -339,6 +344,7 @@ class Helpers {
$userHash = hash('sha1', $user->id.(string) $user->created_at);
$storagePath = "public/m/{$monthHash}/{$userHash}";
$allowed = explode(',', config('pixelfed.media_types'));
foreach($attachments as $media) {
$type = $media['mediaType'];
$url = $media['url'];
@ -370,6 +376,8 @@ class Helpers {
ImageOptimize::dispatch($media);
unlink($file);
}
$status->viewType();
return;
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Util\ActivityPub\Validator;
use Validator;
use Illuminate\Validation\Rule;
class UndoFollow {
public static function validate($payload)
{
$valid = Validator::make($payload, [
'@context' => 'required',
'id' => 'required|string',
'type' => [
'required',
Rule::in(['Undo'])
],
'actor' => 'required|url',
'object.actor' => 'required|url',
'object.object' => 'required|url',
'object.type' => [
'required',
Rule::in(['Follow'])
],
])->passes();
return $valid;
}
}

View file

@ -17,7 +17,7 @@ return [
'inbox' => env('AP_INBOX', true),
'sharedInbox' => env('AP_SHAREDINBOX', false),
'remoteFollow' => env('AP_REMOTEFOLLOW', false),
'remoteFollow' => false,
'delivery' => [
'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0),

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddProfileIdsToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->bigInteger('profile_id')->unique()->unsigned()->nullable()->index()->after('id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('profile_id');
});
}
}

60
package-lock.json generated
View file

@ -783,6 +783,23 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@nuxt/opencollective": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.2.2.tgz",
"integrity": "sha512-ie50SpS47L+0gLsW4yP23zI/PtjsDRglyozX2G09jeiUazC1AJlGPZo0JUs9iuCDUoIgsDEf66y7/bSfig0BpA==",
"requires": {
"chalk": "^2.4.1",
"consola": "^2.3.0",
"node-fetch": "^2.3.0"
},
"dependencies": {
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
}
}
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@ -1684,37 +1701,22 @@
"integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag=="
},
"bootstrap-vue": {
"version": "2.0.0-rc.22",
"resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.0.0-rc.22.tgz",
"integrity": "sha512-QA363prZJZ5HFzAPAlj93yy7FLOwPU0P465kaz8l9COa5t5q5az2X+lMgmlp5c1Fe8yBUhc316KM1itbJqzVUg==",
"version": "2.0.0-rc.23",
"resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.0.0-rc.23.tgz",
"integrity": "sha512-N8D4yjTZ6nTBiw2mtv3xutg46V/eLK5VJpSuC/WJZmeGie34Qls3FtVv7QK5OH4nAG+H6O0qyz4mxOLC1C35Mw==",
"requires": {
"@nuxt/opencollective": "^0.2.2",
"bootstrap": "^4.3.1",
"core-js": ">=2.6.5 <3.0.0",
"popper.js": "^1.15.0",
"portal-vue": "^2.1.4",
"vue-functional-data-merge": "^2.0.7"
"portal-vue": "^2.1.5",
"vue-functional-data-merge": "^3.1.0"
},
"dependencies": {
"@nuxt/opencollective": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.2.2.tgz",
"integrity": "sha512-ie50SpS47L+0gLsW4yP23zI/PtjsDRglyozX2G09jeiUazC1AJlGPZo0JUs9iuCDUoIgsDEf66y7/bSfig0BpA==",
"requires": {
"chalk": "^2.4.1",
"consola": "^2.3.0",
"node-fetch": "^2.3.0"
}
},
"core-js": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
}
}
},
@ -5514,9 +5516,9 @@
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
},
"laravel-echo": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.5.3.tgz",
"integrity": "sha512-CFm0Kruz2zqAwTFSA5X9X5BmIvXYEmrHhcVp5nu4uIdhyObHohWAUBNUD34J4QsIR2J4nGSzB+wi/wsACwlPcg=="
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.5.4.tgz",
"integrity": "sha512-FTfgLQopGTPxIthYqFLbaZQ14kDuRH0AJa7N7HJNmWM5zJ4/qtZcP6zsfvATGazF+5Sr0M7IWgtF+OaNIUHqcw=="
},
"laravel-mix": {
"version": "4.0.16",
@ -9631,17 +9633,17 @@
"dev": true
},
"vue-content-loader": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/vue-content-loader/-/vue-content-loader-0.2.1.tgz",
"integrity": "sha1-DrMy4qcmQ9V/sgnXLWUmVzsZH1o=",
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/vue-content-loader/-/vue-content-loader-0.2.2.tgz",
"integrity": "sha512-8jcb0dJFiVAz7EPwpQjOd/GnswUiSDeKihEABkq/iAYxAI2MHSS7+VWlRblQDH3D1rm3Lewt7fDJoOpJKbYHjw==",
"requires": {
"babel-helper-vue-jsx-merge-props": "^2.0.3"
}
},
"vue-functional-data-merge": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-2.0.7.tgz",
"integrity": "sha512-pvLc+H+x2prwBj/uSEIITyxjz/7ZUVVK8uYbrYMmhDvMXnzh9OvQvVEwcOSBQjsubd4Eq41/CSJaWzy4hemMNQ=="
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
"integrity": "sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA=="
},
"vue-hot-reload-api": {
"version": "2.3.3",

View file

@ -17,8 +17,6 @@
"jquery": "^3.4.1",
"lodash": "^4.17.11",
"popper.js": "^1.15.0",
"purify-css": "^1.2.5",
"purifycss-webpack": "^0.7.0",
"resolve-url-loader": "^2.3.2",
"sass": "^1.21.0",
"sass-loader": "^7.1.0",
@ -26,12 +24,12 @@
"vue-template-compiler": "^2.6.10"
},
"dependencies": {
"bootstrap-vue": "^2.0.0-rc.22",
"bootstrap-vue": "^2.0.0-rc.23",
"emoji-mart-vue": "^2.6.6",
"filesize": "^3.6.1",
"howler": "^2.1.2",
"infinite-scroll": "^3.0.6",
"laravel-echo": "^1.5.3",
"laravel-echo": "^1.5.4",
"laravel-mix": "^4.0.16",
"node-sass": "^4.12.0",
"opencollective": "^1.0.3",
@ -44,7 +42,7 @@
"socket.io-client": "^2.2.0",
"sweetalert": "^2.1.2",
"twitter-text": "^2.0.5",
"vue-content-loader": "^0.2.1",
"vue-content-loader": "^0.2.2",
"vue-infinite-loading": "^2.4.4",
"vue-loading-overlay": "^3.2.0",
"vue-timeago": "^5.1.2"

8
public/css/app.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

View file

@ -3,16 +3,16 @@
"/js/vendor.js": "/js/vendor.js?id=cb03d3c4fd7d4093f5b1",
"/js/activity.js": "/js/activity.js?id=988d3df8e9dc2d16a43c",
"/js/app.js": "/js/app.js?id=025bc09bbc4e2d1898e3",
"/css/app.css": "/css/app.css?id=6dd2f075f0943c162eae",
"/css/appdark.css": "/css/appdark.css?id=04e696349aa09b6cd1c5",
"/css/landing.css": "/css/landing.css?id=44b9a4ffc24efc1ce8a9",
"/js/components.js": "/js/components.js?id=70b4d175e9291d20fd24",
"/js/compose.js": "/js/compose.js?id=0f2ecd714c3a9b07ebaa",
"/css/app.css": "/css/app.css?id=461b8782fc167f734ffa",
"/css/appdark.css": "/css/appdark.css?id=34cd81dd9c8bc68cfd51",
"/css/landing.css": "/css/landing.css?id=1ea686af4c64df5018fc",
"/js/components.js": "/js/components.js?id=97c7f6876bb4e5c9ffd8",
"/js/compose.js": "/js/compose.js?id=3e59092826085624eec5",
"/js/developers.js": "/js/developers.js?id=4dba6abdc3ae8ec8c222",
"/js/discover.js": "/js/discover.js?id=b9b5d3d054cadf63aed2",
"/js/loops.js": "/js/loops.js?id=d5c7e9d0d6a44553dd0d",
"/js/profile.js": "/js/profile.js?id=c58d6492959e657f197e",
"/js/profile.js": "/js/profile.js?id=def2cef049018dddfa02",
"/js/search.js": "/js/search.js?id=17415228e1fb52528d0c",
"/js/status.js": "/js/status.js?id=1e00f5bc1715025a07cb",
"/js/status.js": "/js/status.js?id=9cfc5c6a05070fad66e8",
"/js/timeline.js": "/js/timeline.js?id=c8346c0cf7265df6196e"
}

View file

@ -474,6 +474,10 @@ export default {
compose() {
let state = this.composeState;
if(this.uploadProgress != 100 || this.ids.length == 0) {
return;
}
switch(state) {
case 'publish' :
if(this.media.length == 0) {

View file

@ -104,7 +104,7 @@
</div>
</div>
</div>
<div class="d-flex flex-md-column flex-column-reverse h-100">
<div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;">
<div class="card-body status-comments pb-5">
<div class="status-comment">
<p :class="[status.content.length > 420 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;">
@ -433,7 +433,7 @@
background: transparent;
}
</style>
<style type="text/css">
<style type="text/css" scoped>
.momentui .bg-dark {
background: #000 !important;
}

View file

@ -579,7 +579,7 @@ export default {
}
})
.then(res => {
let data = res.data;
let data = res.data.filter(status => status.media_attachments.length > 0);
let ids = data.map(status => status.id);
this.ids = ids;
this.min_id = Math.max(...ids);

View file

@ -48,7 +48,7 @@
<label class="form-check-label font-weight-bold" for="video_autoplay">
{{__('Disable video autoplay')}}
</label>
<p class="text-muted small help-text">Prevent videos from autoplaying. <a href="#">Learn more</a>.</p>
<p class="text-muted small help-text">Prevent videos from autoplaying.</p>
</div>
<div class="form-group row mt-5 pt-5">
<div class="col-12 text-right">

View file

@ -6,8 +6,31 @@
<h3 class="font-weight-bold">Email Settings</h3>
</div>
<hr>
<div class="alert alert-danger">
Coming Soon
</div>
<form method="post" action="{{route('settings')}}">
@csrf
<input type="hidden" class="form-control" name="name" value="{{Auth::user()->profile->name}}">
<input type="hidden" class="form-control" name="username" value="{{Auth::user()->profile->username}}">
<input type="hidden" class="form-control" name="website" value="{{Auth::user()->profile->website}}">
<div class="form-group row">
<label for="email" class="col-sm-3 col-form-label font-weight-bold text-right">Email</label>
<div class="col-sm-9">
<input type="email" class="form-control" id="email" name="email" placeholder="Email Address" value="{{Auth::user()->email}}">
<p class="help-text small text-muted font-weight-bold">
@if(Auth::user()->email_verified_at)
<span class="text-success">Verified</span> {{Auth::user()->email_verified_at->diffForHumans()}}
@else
<span class="text-danger">Unverified</span> You need to <a href="/i/verify-email">verify your email</a>.
@endif
</p>
</div>
</div>
<hr>
<div class="form-group row">
<div class="col-12 text-right">
<button type="submit" class="btn btn-primary font-weight-bold float-right">Submit</button>
</div>
</div>
</form>
@endsection

View file

@ -63,23 +63,7 @@
</p>
</div>
</div>
<div class="pt-5">
<p class="font-weight-bold text-muted text-center">Private Information</p>
</div>
<div class="form-group row">
<label for="email" class="col-sm-3 col-form-label font-weight-bold text-right">Email</label>
<div class="col-sm-9">
<input type="email" class="form-control" id="email" name="email" placeholder="Email Address" value="{{Auth::user()->email}}">
<p class="help-text small text-muted font-weight-bold">
@if(Auth::user()->email_verified_at)
<span class="text-success">Verified</span> {{Auth::user()->email_verified_at->diffForHumans()}}
@else
<span class="text-danger">Unverified</span> You need to <a href="/i/verify-email">verify your email</a>.
@endif
</p>
</div>
</div>
<div class="pt-5">
<div class="pt-3">
<p class="font-weight-bold text-muted text-center">Storage Usage</p>
</div>
<div class="form-group row">
@ -98,30 +82,12 @@
</div>
</div>
</div>
<div class="pt-5">
<p class="font-weight-bold text-muted text-center">Layout</p>
</div>
<div class="alert alert-primary font-weight-bold text-center">Experimental features have been moved to the <a href="/settings/labs">Labs</a> settings page.</div>
<hr>
@if(config('pixelfed.account_deletion') == true)
<div class="form-group row py-3">
<div class="col-12 d-flex align-items-center justify-content-between">
<a class="font-weight-bold" href="{{route('settings.remove.temporary')}}">Temporarily Disable Account</a>
<button type="submit" class="btn btn-primary font-weight-bold float-right">Submit</button>
</div>
</div>
<hr>
<p class="mb-0 text-center pt-4">
<a class="font-weight-bold text-danger" href="{{route('settings.remove.permanent')}}">Delete Account</a>
</p>
@else
<div class="form-group row">
<div class="col-12 d-flex align-items-center justify-content-between">
<a class="font-weight-bold" href="{{route('settings.remove.temporary')}}">Temporarily Disable Account</a>
<div class="col-12 text-right">
<button type="submit" class="btn btn-primary font-weight-bold float-right">Submit</button>
</div>
</div>
@endif
</form>
@endsection

View file

@ -1,18 +1,17 @@
<div class="col-12 col-md-3 py-3" style="border-right:1px solid #ccc;">
<ul class="nav flex-column settings-nav">
<li class="nav-item pl-3 {{request()->is('settings/home')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings')}}">Profile</a>
<a class="nav-link font-weight-light text-muted" href="{{route('settings')}}">Account</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/password')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.password')}}">Password</a>
</li>
{{--
<li class="nav-item pl-3 {{request()->is('settings/accessibility')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.accessibility')}}">Accessibility</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/email')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.email')}}">Email</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/relationships*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.relationships')}}">Followers</a>
</li>
@if(config('pixelfed.user_invites.enabled'))
<li class="nav-item pl-3 {{request()->is('settings/invites*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.invites')}}">Invites</a>
@ -21,14 +20,16 @@
<li class="nav-item pl-3 {{request()->is('settings/notifications')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.notifications')}}">Notifications</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/reports*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.reports')}}">Reports</a>
<li class="nav-item pl-3 {{request()->is('settings/password')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.password')}}">Password</a>
</li>
--}}
<li class="nav-item pl-3 {{request()->is('settings/privacy*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.privacy')}}">Privacy</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/reports*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.reports')}}">Reports</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/security*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.security')}}">Security</a>
</li>
@ -41,16 +42,16 @@
<li class="nav-item pl-3 {{request()->is('settings/data-export')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.dataexport')}}">Data Export</a>
</li>
{{--
<li class="nav-item">
<hr>
</li>
<li class="nav-item pl-3 {{request()->is('settings/applications')?'active':''}}">
{{-- <li class="nav-item pl-3 {{request()->is('settings/applications')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.applications')}}">Applications</a>
</li>
</li> --}}
<li class="nav-item pl-3 {{request()->is('settings/developers')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.developers')}}">Developers</a>
</li> --}}
</li>
<li class="nav-item">
<hr>
</li>

View file

@ -0,0 +1,117 @@
@extends('settings.template')
@section('section')
<div class="title">
<h3 class="font-weight-bold">Followers & Following</h3>
</div>
<hr>
@if(empty($following) && empty($followers))
<p class="text-center lead pt-5 mt-5">You are not following anyone, or followed by anyone.</p>
@else
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="pt-0 pb-1 mt-0">
<input type="checkbox" name="check" class="form-control check-all">
</th>
<th scope="col">Username</th>
<th scope="col">Relationship</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
@foreach($followers as $follower)
<tr>
<th scope="row" class="pb-0 pt-1 my-0">
{{-- <input type="checkbox" class="form-control mr-1 check-row"> --}}
</th>
<td class="font-weight-bold">
<img src="{{$follower->avatarUrl()}}" width="20px" height="20px" class="rounded-circle border mr-2">{{$follower->username}}
</td>
<td class="text-center">Follower</td>
<td class="text-center">
<a class="btn btn-outline-primary btn-sm py-0 action-btn" href="#" data-id="{{$follower->id}}" data-action="mute">Mute</a>
<a class="btn btn-outline-danger btn-sm py-0 action-btn" href="#" data-id="{{$follower->id}}" data-action="block">Block</a>
</td>
</tr>
@endforeach
@foreach($following as $follower)
<tr>
<th scope="row" class="pb-0 pt-1 my-0">
<input type="checkbox" class="form-control mr-1 check-row">
</th>
<td class="font-weight-bold">
<img src="{{$follower->avatarUrl()}}" width="20px" height="20px" class="rounded-circle border mr-2">{{$follower->username}}
</td>
<td class="text-success text-center">Following</td>
<td class="text-center">
<a class="btn btn-outline-danger btn-sm py-0 action-btn" href="#" data-id="{{$follower->id}}" data-action="unfollow">Unfollow</a>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="d-flex justify-content-center">{{$following->links() ?? $followers->links()}}</div>
@endif
@endsection
@push('scripts')
<script type="text/javascript">
$(document).ready(() => {
$('.action-btn').on('click', e => {
e.preventDefault();
let action = e.target.getAttribute('data-action');
let id = e.target.getAttribute('data-id');
switch(action) {
case 'mute':
axios.post('/i/mute', {
type: 'user',
item: id
}).then(res => {
swal(
'Mute Successful',
'You have successfully muted that user',
'success'
);
});
break;
case 'block':
axios.post('/i/block', {
type: 'user',
item: id
}).then(res => {
swal(
'Block Successful',
'You have successfully blocked that user',
'success'
);
});
break;
case 'unfollow':
axios.post('/i/follow', {
item: id
}).then(res => {
swal(
'Unfollow Successful',
'You have successfully unfollowed that user',
'success'
);
});
break;
}
setTimeout(function() {
window.location.href = window.location.href;
}, 3000);
});
$('.check-all').on('click', e => {
})
});
</script>
@endpush

View file

@ -26,6 +26,32 @@
@include('settings.security.log-panel')
@include('settings.security.device-panel')
@if(config('pixelfed.account_deletion') == true)
<h4 class="font-weight-bold pt-3">Danger Zone</h4>
<div class="mb-4 border rounded border-danger">
<ul class="list-group mb-0 pb-0">
<li class="list-group-item border-left-0 border-right-0 py-3 d-flex justify-content-between">
<div>
<p class="font-weight-bold mb-1">Temporarily Disable Account</p>
<p class="mb-0 small">Disable your account to hide your posts until next log in.</p>
</div>
<div>
<a class="btn btn-outline-danger font-weight-bold py-1" href="{{route('settings.remove.temporary')}}">Disable</a>
</div>
</li>
<li class="list-group-item border-left-0 border-right-0 py-3 d-flex justify-content-between">
<div>
<p class="font-weight-bold mb-1">Delete this Account</p>
<p class="mb-0 small">Once you delete your account, there is no going back. Please be certain.</p>
</div>
<div>
<a class="btn btn-outline-danger font-weight-bold py-1" href="{{route('settings.remove.permanent')}}">Delete</a>
</div>
</li>
</ul>
</div>
@endif
</section>
@endsection

View file

@ -153,7 +153,9 @@
<ol class="">
<li>Log into <a href="{{config('app.url')}}">{{config('pixelfed.domain.app')}}</a></li>
<li>Tap or click the <i class="far fa-user text-dark"></i> menu and select <span class="font-weight-bold text-dark"><i class="fas fa-cog pr-1"></i> Settings</span></li>
<li>Scroll down and click on the <span class="font-weight-bold">Temporarily Disable Account</span> link.</li>
<li>Navigate to the <a href="{{route('settings.security')}}">Security Settings</a></li>
<li>Confirm your account password.</li>
<li>Scroll down to the Danger Zone section and click on the <span class="btn btn-sm btn-outline-danger py-1 font-weight-bold">Disable</span> button.</li>
<li>Follow the instructions on the next page.</li>
</ol>
</div>
@ -180,8 +182,10 @@
<p>To permanently delete your account:</p>
<ol class="">
<li>Go to <a href="{{route('settings.remove.permanent')}}">the <span class="font-weight-bold">Delete Your Account</span> page</a>. If you're not logged into pixelfed on the web, you'll be asked to log in first. You can't delete your account from within a mobile app.</li>
<li>Navigate to the <a href="{{route('settings.security')}}">Security Settings</a></li>
<li>Confirm your account password.</li>
<li>On the <span class="font-weight-bold">Delete Your Account</span> page click or tap on the <span>Permanently Delete My Account</span> button.</li>
<li>Scroll down to the Danger Zone section and click on the <span class="btn btn-sm btn-outline-danger py-1 font-weight-bold">Delete</span> button.</li>
<li>Follow the instructions on the next page.</li>
</ol>
</div>
</div>

View file

@ -237,6 +237,11 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('developers', 'SettingsController@developers')->name('settings.developers')->middleware('dangerzone');
Route::get('labs', 'SettingsController@labs')->name('settings.labs');
Route::post('labs', 'SettingsController@labsStore');
Route::group(['prefix' => 'relationships'], function() {
Route::redirect('/', '/settings/relationships/home');
Route::get('home', 'SettingsController@relationshipsHome')->name('settings.relationships');
});
});
Route::group(['prefix' => 'site'], function () {

View file

@ -0,0 +1,27 @@
<?php
namespace Tests\Unit;
use App\Util\ActivityPub\Helpers;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class FollowTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
$this->mastodon = '{"type":"Follow","signature":{"type":"RsaSignature2017","signatureValue":"Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==","creator":"http://mastodon.example.org/users/admin#main-key","created":"2018-02-17T13:29:31Z"},"object":"http://localtesting.pleroma.lol/users/lain","nickname":"lain","id":"http://mastodon.example.org/users/admin#follows/2","actor":"http://mastodon.example.org/users/admin","@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"toot":"http://joinmastodon.org/ns#","sensitive":"as:sensitive","ostatus":"http://ostatus.org#","movedTo":"as:movedTo","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","atomUri":"ostatus:atomUri","Hashtag":"as:Hashtag","Emoji":"toot:Emoji"}]}';
}
/** @test */
public function validateMastodonFollowObject()
{
$mastodon = json_decode($this->mastodon, true);
$mastodon = Helpers::validateObject($mastodon);
$this->assertTrue($mastodon);
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Tests\Unit\ActivityPub\Verb;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Util\ActivityPub\Validator\Accept;
class AcceptVerbTest extends TestCase
{
protected $validAccept;
protected $invalidAccept;
public function setUp(): void
{
parent::setUp();
$this->validAccept = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://example.org/og/b3e4a40b-0b26-4c5a-9079-094bd633fab7',
'type' => 'Accept',
'actor' => 'https://example.org/u/alice',
'object' => [
'id' => 'https://example.net/u/bob#follows/bb27f601-ddb9-4567-8f16-023d90605ca9',
'type' => 'Follow',
'actor' => 'https://example.net/u/bob',
'object' => 'https://example.org/u/alice'
]
];
$this->invalidAccept = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://example.org/og/b3e4a40b-0b26-4c5a-9079-094bd633fab7',
'type' => 'Accept2',
'actor' => 'https://example.org/u/alice',
'object' => [
'id' => 'https://example.net/u/bob#follows/bb27f601-ddb9-4567-8f16-023d90605ca9',
'type' => 'Follow',
'actor' => 'https://example.net/u/bob',
'object' => 'https://example.org/u/alice'
]
];
}
/** @test */
public function basic_accept()
{
$this->assertTrue(Accept::validate($this->validAccept));
}
/** @test */
public function invalid_accept()
{
$this->assertFalse(Accept::validate($this->invalidAccept));
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Tests\Unit\ActivityPub\Verb;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Util\ActivityPub\Validator\UndoFollow;
class UndoFollowTest extends TestCase
{
protected $validUndo;
protected $invalidUndo;
public function setUp(): void
{
parent::setUp();
$this->validUndo = json_decode('{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"toot":"http://joinmastodon.org/ns#","sensitive":"as:sensitive","ostatus":"http://ostatus.org#","movedTo":"as:movedTo","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","atomUri":"ostatus:atomUri","Hashtag":"as:Hashtag","Emoji":"toot:Emoji"}],"signature":{"type":"RsaSignature2017","signatureValue":"Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==","creator":"http://mastodon.example.org/users/admin#main-key","created":"2018-02-17T13:29:31Z"},"type":"Undo","object":{"type":"Follow","object":"http://localtesting.pleroma.lol/users/lain","nickname":"lain","id":"http://mastodon.example.org/users/admin#follows/2","actor":"http://mastodon.example.org/users/admin"},"actor":"http://mastodon.example.org/users/admin","id":"http://mastodon.example.org/users/admin#follow/2/undo"}', true, 8);
$this->invalidUndo = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://example.org/og/b3e4a40b-0b26-4c5a-9079-094bd633fab7',
'type' => 'Undo',
'actor' => 'https://example.org/u/alice',
'object' => [
'id' => 'https://example.net/u/bob#follows/bb27f601-ddb9-4567-8f16-023d90605ca9',
'type' => 'Follow',
]
];
}
/** @test */
public function valid_undo_follow()
{
$this->assertTrue(UndoFollow::validate($this->validUndo));
}
/** @test */
public function invalid_undo_follow()
{
$this->assertFalse(UndoFollow::validate($this->invalidUndo));
}
}

38
webpack.mix.js vendored
View file

@ -1,20 +1,5 @@
let mix = require('laravel-mix');
mix.options({
purifyCss: true,
});
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.sass('resources/assets/sass/app.scss', 'public/css', {
implementation: require('node-sass')
})
@ -28,35 +13,16 @@ mix.sass('resources/assets/sass/app.scss', 'public/css', {
mix.js('resources/assets/js/app.js', 'public/js')
.js('resources/assets/js/activity.js', 'public/js')
.js('resources/assets/js/components.js', 'public/js')
//.js('resources/assets/js/embed.js', 'public')
// Discover component
.js('resources/assets/js/discover.js', 'public/js')
// Profile component
.js('resources/assets/js/profile.js', 'public/js')
// Status component
.js('resources/assets/js/status.js', 'public/js')
// Timeline component
.js('resources/assets/js/timeline.js', 'public/js')
// ComposeModal component
.js('resources/assets/js/compose.js', 'public/js')
// SearchResults component
.js('resources/assets/js/search.js', 'public/js')
// Developer Components
.js('resources/assets/js/developers.js', 'public/js')
// // Direct Component
// .js('resources/assets/js/direct.js', 'public/js')
// Loops Component
.js('resources/assets/js/loops.js', 'public/js')
// .js('resources/assets/js/embed.js', 'public')
// .js('resources/assets/js/direct.js', 'public/js')
.extract([
'lodash',
'popper.js',