kopia lustrzana https://github.com/pixelfed/pixelfed
				
				
				
			Merge branch 'dev' of github.com:pixelfed/pixelfed into jippi-fork
						commit
						890827d60e
					
				| 
						 | 
				
			
			@ -84,6 +84,7 @@
 | 
			
		|||
- Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a))
 | 
			
		||||
- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc))
 | 
			
		||||
- Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1))
 | 
			
		||||
- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c))
 | 
			
		||||
-  ([](https://github.com/pixelfed/pixelfed/commit/))
 | 
			
		||||
 | 
			
		||||
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,118 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Console\Commands;
 | 
			
		||||
 | 
			
		||||
use App\Models\CustomEmoji;
 | 
			
		||||
use Illuminate\Console\Command;
 | 
			
		||||
use Illuminate\Support\Facades\Cache;
 | 
			
		||||
use Illuminate\Support\Facades\Storage;
 | 
			
		||||
 | 
			
		||||
class ImportEmojis extends Command
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * The name and signature of the console command.
 | 
			
		||||
     *
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    protected $signature = 'import:emojis
 | 
			
		||||
                            {path : Path to a tar.gz archive with the emojis}
 | 
			
		||||
                            {--prefix : Define a prefix for the emjoi shortcode}
 | 
			
		||||
                            {--suffix : Define a suffix for the emjoi shortcode}
 | 
			
		||||
                            {--overwrite : Overwrite existing emojis}
 | 
			
		||||
                            {--disabled : Import all emojis as disabled}';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The console command description.
 | 
			
		||||
     *
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    protected $description = 'Import emojis to the database';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute the console command.
 | 
			
		||||
     *
 | 
			
		||||
     * @return int
 | 
			
		||||
     */
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
        $path = $this->argument('path');
 | 
			
		||||
 | 
			
		||||
        if (!file_exists($path) || !mime_content_type($path) == 'application/x-tar') {
 | 
			
		||||
            $this->error('Path does not exist or is not a tarfile');
 | 
			
		||||
            return Command::FAILURE;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $imported = 0;
 | 
			
		||||
        $skipped = 0;
 | 
			
		||||
        $failed = 0;
 | 
			
		||||
 | 
			
		||||
        $tar = new \PharData($path);
 | 
			
		||||
        $tar->decompress();
 | 
			
		||||
 | 
			
		||||
        foreach (new \RecursiveIteratorIterator($tar) as $entry) {
 | 
			
		||||
            $this->line("Processing {$entry->getFilename()}");
 | 
			
		||||
            if (!$entry->isFile() || !$this->isImage($entry) || !$this->isEmoji($entry->getPathname())) {
 | 
			
		||||
                $failed++;
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $filename = pathinfo($entry->getFilename(), PATHINFO_FILENAME);
 | 
			
		||||
            $extension = pathinfo($entry->getFilename(), PATHINFO_EXTENSION);
 | 
			
		||||
 | 
			
		||||
            // Skip macOS shadow files
 | 
			
		||||
            if (str_starts_with($filename, '._')) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $shortcode = implode('', [
 | 
			
		||||
                $this->option('prefix'),
 | 
			
		||||
                $filename,
 | 
			
		||||
                $this->option('suffix'),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            $customEmoji = CustomEmoji::whereShortcode($shortcode)->first();
 | 
			
		||||
 | 
			
		||||
            if ($customEmoji && !$this->option('overwrite')) {
 | 
			
		||||
                $skipped++;
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $emoji = $customEmoji ?? new CustomEmoji();
 | 
			
		||||
            $emoji->shortcode = $shortcode;
 | 
			
		||||
            $emoji->domain = config('pixelfed.domain.app');
 | 
			
		||||
            $emoji->disabled = $this->option('disabled');
 | 
			
		||||
            $emoji->save();
 | 
			
		||||
 | 
			
		||||
            $fileName = $emoji->id . '.' . $extension;
 | 
			
		||||
            Storage::putFileAs('public/emoji', $entry->getPathname(), $fileName);
 | 
			
		||||
            $emoji->media_path = 'emoji/' . $fileName;
 | 
			
		||||
            $emoji->save();
 | 
			
		||||
            $imported++;
 | 
			
		||||
            Cache::forget('pf:custom_emoji');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->line("Imported: {$imported}");
 | 
			
		||||
        $this->line("Skipped: {$skipped}");
 | 
			
		||||
        $this->line("Failed: {$failed}");
 | 
			
		||||
 | 
			
		||||
        //delete file
 | 
			
		||||
        unlink(str_replace('.tar.gz', '.tar', $path));
 | 
			
		||||
 | 
			
		||||
        return Command::SUCCESS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function isImage($file)
 | 
			
		||||
    {
 | 
			
		||||
        $image = getimagesize($file->getPathname());
 | 
			
		||||
        return $image !== false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function isEmoji($filename)
 | 
			
		||||
    {
 | 
			
		||||
        $allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
 | 
			
		||||
        $mimeType = mime_content_type($filename);
 | 
			
		||||
 | 
			
		||||
        return in_array($mimeType, $allowedMimeTypes);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -98,6 +98,7 @@ use App\Jobs\MediaPipeline\MediaSyncLicensePipeline;
 | 
			
		|||
use App\Services\DiscoverService;
 | 
			
		||||
use App\Services\CustomEmojiService;
 | 
			
		||||
use App\Services\MarkerService;
 | 
			
		||||
use App\Services\UserRoleService;
 | 
			
		||||
use App\Models\Conversation;
 | 
			
		||||
use App\Jobs\FollowPipeline\FollowAcceptPipeline;
 | 
			
		||||
use App\Jobs\FollowPipeline\FollowRejectPipeline;
 | 
			
		||||
| 
						 | 
				
			
			@ -1244,6 +1245,7 @@ class ApiV1Controller extends Controller
 | 
			
		|||
        abort_if(!$request->user(), 403);
 | 
			
		||||
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
        abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action');
 | 
			
		||||
 | 
			
		||||
        AccountService::setLastActive($user->id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1305,6 +1307,7 @@ class ApiV1Controller extends Controller
 | 
			
		|||
        abort_if(!$request->user(), 403);
 | 
			
		||||
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
        abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action');
 | 
			
		||||
 | 
			
		||||
        AccountService::setLastActive($user->id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1623,6 +1626,8 @@ class ApiV1Controller extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
        abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
 | 
			
		||||
 | 
			
		||||
        AccountService::setLastActive($user->id);
 | 
			
		||||
 | 
			
		||||
        if($user->last_active_at == null) {
 | 
			
		||||
| 
						 | 
				
			
			@ -1792,6 +1797,7 @@ class ApiV1Controller extends Controller
 | 
			
		|||
        abort_if(!$request->user(), 403);
 | 
			
		||||
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
        abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
 | 
			
		||||
        AccountService::setLastActive($user->id);
 | 
			
		||||
 | 
			
		||||
        $media = Media::whereUserId($user->id)
 | 
			
		||||
| 
						 | 
				
			
			@ -1831,6 +1837,7 @@ class ApiV1Controller extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
        abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
 | 
			
		||||
 | 
			
		||||
        if($user->last_active_at == null) {
 | 
			
		||||
            return [];
 | 
			
		||||
| 
						 | 
				
			
			@ -2419,8 +2426,13 @@ class ApiV1Controller extends Controller
 | 
			
		|||
        $max = $request->input('max_id');
 | 
			
		||||
        $limit = $request->input('limit') ?? 20;
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
 | 
			
		||||
        $remote = $request->has('remote');
 | 
			
		||||
        $local = $request->has('local');
 | 
			
		||||
        $userRoleKey = $remote ? 'can-view-network-feed' : 'can-view-public-feed';
 | 
			
		||||
        if($user->has_roles && !UserRoleService::can($userRoleKey, $user->id)) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
 | 
			
		||||
        AccountService::setLastActive($user->id);
 | 
			
		||||
        $domainBlocks = UserFilterService::domainBlocks($user->profile_id);
 | 
			
		||||
| 
						 | 
				
			
			@ -3165,6 +3177,7 @@ class ApiV1Controller extends Controller
 | 
			
		|||
        abort_if(!$request->user(), 403);
 | 
			
		||||
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
        abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action');
 | 
			
		||||
        AccountService::setLastActive($user->id);
 | 
			
		||||
        $status = Status::whereScope('public')->findOrFail($id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3212,6 +3225,7 @@ class ApiV1Controller extends Controller
 | 
			
		|||
        abort_if(!$request->user(), 403);
 | 
			
		||||
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
        abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action');
 | 
			
		||||
        AccountService::setLastActive($user->id);
 | 
			
		||||
        $status = Status::whereScope('public')->findOrFail($id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3262,6 +3276,13 @@ class ApiV1Controller extends Controller
 | 
			
		|||
          '_pe'         => 'sometimes'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
        abort_if(
 | 
			
		||||
            $user->has_roles && !UserRoleService::can('can-view-hashtag-feed', $user->id),
 | 
			
		||||
            403,
 | 
			
		||||
            'Invalid permissions for this action'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if(config('database.default') === 'pgsql') {
 | 
			
		||||
            $tag = Hashtag::where('name', 'ilike', $hashtag)
 | 
			
		||||
                ->orWhere('slug', 'ilike', $hashtag)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,6 +54,7 @@ use App\Util\Lexer\Autolink;
 | 
			
		|||
use App\Util\Lexer\Extractor;
 | 
			
		||||
use App\Util\Media\License;
 | 
			
		||||
use Image;
 | 
			
		||||
use App\Services\UserRoleService;
 | 
			
		||||
 | 
			
		||||
class ComposeController extends Controller
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -92,6 +93,7 @@ class ComposeController extends Controller
 | 
			
		|||
 | 
			
		||||
		$user = Auth::user();
 | 
			
		||||
		$profile = $user->profile;
 | 
			
		||||
		abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
 | 
			
		||||
 | 
			
		||||
		$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
 | 
			
		||||
		$limitTtl = now()->addMinutes(15);
 | 
			
		||||
| 
						 | 
				
			
			@ -184,6 +186,7 @@ class ComposeController extends Controller
 | 
			
		|||
		]);
 | 
			
		||||
 | 
			
		||||
		$user = Auth::user();
 | 
			
		||||
		abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
 | 
			
		||||
 | 
			
		||||
		$limitKey = 'compose:rate-limit:media-updates:' . $user->id;
 | 
			
		||||
		$limitTtl = now()->addMinutes(15);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use App\Services\UserRoleService;
 | 
			
		||||
 | 
			
		||||
class UserRolesController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->middleware('auth');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRoles(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'id' => 'required'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return UserRoleService::getRoles($request->user()->id);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Models;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
use App\User;
 | 
			
		||||
 | 
			
		||||
class UserRoles extends Model
 | 
			
		||||
{
 | 
			
		||||
    use HasFactory;
 | 
			
		||||
 | 
			
		||||
    protected $guarded = [];
 | 
			
		||||
 | 
			
		||||
    protected $casts = [
 | 
			
		||||
        'roles' => 'array'
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    public function user()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->belongsTo(User::class);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ class AuthServiceProvider extends ServiceProvider
 | 
			
		|||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    protected $policies = [
 | 
			
		||||
        'App\Model' => 'App\Policies\ModelPolicy',
 | 
			
		||||
        // 'App\Model' => 'App\Policies\ModelPolicy',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ use League\Fractal;
 | 
			
		|||
use League\Fractal\Serializer\ArraySerializer;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use \NumberFormatter;
 | 
			
		||||
 | 
			
		||||
class AccountService
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -244,4 +245,38 @@ class AccountService
 | 
			
		|||
 | 
			
		||||
        return UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->exists();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function formatNumber($num) {
 | 
			
		||||
        if(!$num || $num < 1) {
 | 
			
		||||
            return "0";
 | 
			
		||||
        }
 | 
			
		||||
        $num = intval($num);
 | 
			
		||||
        $formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
 | 
			
		||||
        $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 1);
 | 
			
		||||
 | 
			
		||||
        if ($num >= 1000000000) {
 | 
			
		||||
            return $formatter->format($num / 1000000000) . 'B';
 | 
			
		||||
        } else if ($num >= 1000000) {
 | 
			
		||||
            return $formatter->format($num / 1000000) . 'M';
 | 
			
		||||
        } elseif ($num >= 1000) {
 | 
			
		||||
            return $formatter->format($num / 1000) . 'K';
 | 
			
		||||
        } else {
 | 
			
		||||
            return $formatter->format($num);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getMetaDescription($id)
 | 
			
		||||
    {
 | 
			
		||||
        $account = self::get($id, true);
 | 
			
		||||
 | 
			
		||||
        if(!$account) return "";
 | 
			
		||||
 | 
			
		||||
        $posts = self::formatNumber($account['statuses_count']) . ' Posts, ';
 | 
			
		||||
        $following = self::formatNumber($account['following_count']) . ' Following, ';
 | 
			
		||||
        $followers = self::formatNumber($account['followers_count']) . ' Followers';
 | 
			
		||||
        $note = $account['note'] && strlen($account['note']) ?
 | 
			
		||||
            ' · ' . \Purify::clean(strip_tags(str_replace("\n", '', str_replace("\r", '', $account['note'])))) :
 | 
			
		||||
            '';
 | 
			
		||||
        return $posts . $following . $followers . $note;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,119 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Services;
 | 
			
		||||
 | 
			
		||||
use App\Models\UserRoles;
 | 
			
		||||
 | 
			
		||||
class UserRoleService
 | 
			
		||||
{
 | 
			
		||||
    public static function can($action, $id, $useDefaultFallback = true)
 | 
			
		||||
    {
 | 
			
		||||
        $default = self::defaultRoles();
 | 
			
		||||
        $roles = self::get($id);
 | 
			
		||||
        return
 | 
			
		||||
            in_array($action, array_keys($roles)) ?
 | 
			
		||||
                $roles[$action] :
 | 
			
		||||
                (
 | 
			
		||||
                    $useDefaultFallback ?
 | 
			
		||||
                        $default[$action] :
 | 
			
		||||
                        false
 | 
			
		||||
                );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    public static function get($id)
 | 
			
		||||
    {
 | 
			
		||||
        if($roles = UserRoles::whereUserId($id)->first()) {
 | 
			
		||||
            return $roles->roles;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return self::defaultRoles();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function roleKeys()
 | 
			
		||||
    {
 | 
			
		||||
        return array_keys(self::defaultRoles());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function defaultRoles()
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'account-force-private' => true,
 | 
			
		||||
            'account-ignore-follow-requests' => true,
 | 
			
		||||
 | 
			
		||||
            'can-view-public-feed' => true,
 | 
			
		||||
            'can-view-network-feed' => true,
 | 
			
		||||
            'can-view-discover' => true,
 | 
			
		||||
            'can-view-hashtag-feed' => false,
 | 
			
		||||
 | 
			
		||||
            'can-post' => true,
 | 
			
		||||
            'can-comment' => true,
 | 
			
		||||
            'can-like' => true,
 | 
			
		||||
            'can-share' => true,
 | 
			
		||||
 | 
			
		||||
            'can-follow' => false,
 | 
			
		||||
            'can-make-public' => false,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getRoles($id)
 | 
			
		||||
    {
 | 
			
		||||
        $myRoles = self::get($id);
 | 
			
		||||
        $roleData = collect(self::roleData())
 | 
			
		||||
            ->map(function($role, $k) use($myRoles) {
 | 
			
		||||
                $role['value'] = $myRoles[$k];
 | 
			
		||||
                return $role;
 | 
			
		||||
            })
 | 
			
		||||
            ->toArray();
 | 
			
		||||
        return $roleData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function roleData()
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'account-force-private' => [
 | 
			
		||||
                'title' => 'Force Private Account',
 | 
			
		||||
                'action' => 'Prevent changing account from private'
 | 
			
		||||
            ],
 | 
			
		||||
            'account-ignore-follow-requests' => [
 | 
			
		||||
                'title' => 'Ignore Follow Requests',
 | 
			
		||||
                'action' => 'Hide follow requests and associated notifications'
 | 
			
		||||
            ],
 | 
			
		||||
            'can-view-public-feed' => [
 | 
			
		||||
                'title' => 'Hide Public Feed',
 | 
			
		||||
                'action' => 'Hide the public feed timeline'
 | 
			
		||||
            ],
 | 
			
		||||
            'can-view-network-feed' => [
 | 
			
		||||
                'title' => 'Hide Network Feed',
 | 
			
		||||
                'action' => 'Hide the network feed timeline'
 | 
			
		||||
            ],
 | 
			
		||||
            'can-view-discover' => [
 | 
			
		||||
                'title' => 'Hide Discover',
 | 
			
		||||
                'action' => 'Hide the discover feature'
 | 
			
		||||
            ],
 | 
			
		||||
            'can-post' => [
 | 
			
		||||
                'title' => 'Can post',
 | 
			
		||||
                'action' => 'Allows new posts to be shared'
 | 
			
		||||
            ],
 | 
			
		||||
            'can-comment' => [
 | 
			
		||||
                'title' => 'Can comment',
 | 
			
		||||
                'action' => 'Allows new comments to be posted'
 | 
			
		||||
            ],
 | 
			
		||||
            'can-like' => [
 | 
			
		||||
                'title' => 'Can Like',
 | 
			
		||||
                'action' => 'Allows the ability to like posts and comments'
 | 
			
		||||
            ],
 | 
			
		||||
            'can-share' => [
 | 
			
		||||
                'title' => 'Can Share',
 | 
			
		||||
                'action' => 'Allows the ability to share posts and comments'
 | 
			
		||||
            ],
 | 
			
		||||
            'can-follow' => [
 | 
			
		||||
                'title' => 'Can Follow',
 | 
			
		||||
                'action' => 'Allows the ability to follow accounts'
 | 
			
		||||
            ],
 | 
			
		||||
            'can-make-public' => [
 | 
			
		||||
                'title' => 'Can make account public',
 | 
			
		||||
                'action' => 'Allows the ability to make account public'
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +11,18 @@ class Webfinger
 | 
			
		|||
 | 
			
		||||
    public function __construct($user)
 | 
			
		||||
    {
 | 
			
		||||
        $avatar = $user ? $user->avatarUrl() : url('/storage/avatars/default.jpg');
 | 
			
		||||
        $avatarPath = parse_url($avatar, PHP_URL_PATH);
 | 
			
		||||
        $extension = pathinfo($avatarPath, PATHINFO_EXTENSION);
 | 
			
		||||
        $mimeTypes = [
 | 
			
		||||
            'jpg' => 'image/jpeg',
 | 
			
		||||
            'jpeg' => 'image/jpeg',
 | 
			
		||||
            'png' => 'image/png',
 | 
			
		||||
            'gif' => 'image/gif',
 | 
			
		||||
            'svg' => 'image/svg',
 | 
			
		||||
        ];
 | 
			
		||||
        $avatarType = $mimeTypes[$extension] ?? 'application/octet-stream';
 | 
			
		||||
 | 
			
		||||
        $this->subject = 'acct:'.$user->username.'@'.parse_url(config('app.url'), PHP_URL_HOST);
 | 
			
		||||
        $this->aliases = [
 | 
			
		||||
            $user->url(),
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +44,11 @@ class Webfinger
 | 
			
		|||
                'type' => 'application/activity+json',
 | 
			
		||||
                'href' => $user->permalink(),
 | 
			
		||||
            ],
 | 
			
		||||
            [
 | 
			
		||||
                'rel' => 'http://webfinger.net/rel/avatar',
 | 
			
		||||
                'type' => $avatarType,
 | 
			
		||||
                'href' => $avatar,
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
 | 
			
		||||
return new class extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function up(): void
 | 
			
		||||
    {
 | 
			
		||||
        Schema::create('user_roles', function (Blueprint $table) {
 | 
			
		||||
            $table->id();
 | 
			
		||||
            $table->unsignedBigInteger('profile_id')->unique()->index();
 | 
			
		||||
            $table->unsignedInteger('user_id')->unique()->index();
 | 
			
		||||
            $table->json('roles')->nullable();
 | 
			
		||||
            $table->json('meta')->nullable();
 | 
			
		||||
            $table->timestamps();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function down(): void
 | 
			
		||||
    {
 | 
			
		||||
        Schema::dropIfExists('user_roles');
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
 | 
			
		||||
return new class extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function up(): void
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('users', function (Blueprint $table) {
 | 
			
		||||
            $table->boolean('has_roles')->default(false);
 | 
			
		||||
            $table->unsignedInteger('parent_id')->nullable();
 | 
			
		||||
            $table->tinyInteger('role_id')->unsigned()->nullable()->index();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function down(): void
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('users', function (Blueprint $table) {
 | 
			
		||||
            $table->dropColumn('has_roles');
 | 
			
		||||
            $table->dropColumn('parent_id');
 | 
			
		||||
            $table->dropColumn('role_id');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +34,16 @@
 | 
			
		|||
						</div>
 | 
			
		||||
						<div v-else-if="n.type == 'comment'">
 | 
			
		||||
							<p class="my-0">
 | 
			
		||||
								<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 | 
			
		||||
								<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your
 | 
			
		||||
								<span v-if="n.status && n.status.hasOwnProperty('media_attachments')">
 | 
			
		||||
									<a class="font-weight-bold" v-bind:href="getPostUrl(n.status)" :id="'fvn-' + n.id">post</a>.
 | 
			
		||||
									<b-popover :target="'fvn-' + n.id" title="" triggers="hover" placement="top" boundary="window">
 | 
			
		||||
										<img :src="notificationPreview(n)" width="100px" height="100px" style="object-fit: cover;">
 | 
			
		||||
									</b-popover>
 | 
			
		||||
								</span>
 | 
			
		||||
								<span v-else>
 | 
			
		||||
									<a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 | 
			
		||||
								</span>
 | 
			
		||||
							</p>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div v-else-if="n.type == 'group:comment'">
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +73,16 @@
 | 
			
		|||
						</div>
 | 
			
		||||
						<div v-else-if="n.type == 'share'">
 | 
			
		||||
							<p class="my-0">
 | 
			
		||||
								<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 | 
			
		||||
								<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> shared your
 | 
			
		||||
								<span v-if="n.status && n.status.hasOwnProperty('media_attachments')">
 | 
			
		||||
									<a class="font-weight-bold" v-bind:href="getPostUrl(n.status)" :id="'fvn-' + n.id">post</a>.
 | 
			
		||||
									<b-popover :target="'fvn-' + n.id" title="" triggers="hover" placement="top" boundary="window">
 | 
			
		||||
										<img :src="notificationPreview(n)" width="100px" height="100px" style="object-fit: cover;">
 | 
			
		||||
									</b-popover>
 | 
			
		||||
								</span>
 | 
			
		||||
								<span v-else>
 | 
			
		||||
									<a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 | 
			
		||||
								</span>
 | 
			
		||||
							</p>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div v-else-if="n.type == 'modlog'">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,9 +12,9 @@
 | 
			
		|||
	<title>{{ $title ?? config_cache('app.name') }}</title>
 | 
			
		||||
	<link rel="manifest" href="{{url('/manifest.json')}}">
 | 
			
		||||
 | 
			
		||||
	<meta property="og:site_name" content="{{ config_cache('app.name') }}">
 | 
			
		||||
	<meta property="og:title" content="{{ $title ?? config_cache('app.name') }}">
 | 
			
		||||
	<meta property="og:type" content="article">
 | 
			
		||||
	<meta property="og:site_name" content="Pixelfed">
 | 
			
		||||
	<meta property="og:title" content="{{ $ogTitle ?? $title ?? config_cache('app.name') }}">
 | 
			
		||||
	<meta property="og:type" content="{{ $ogType ?? 'article' }}">
 | 
			
		||||
	<meta property="og:url" content="{{url(request()->url())}}">
 | 
			
		||||
	@stack('meta')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,9 +73,9 @@
 | 
			
		|||
	<title>{{ $title ?? config('app.name', 'Pixelfed') }}</title>
 | 
			
		||||
	<link rel="manifest" href="/manifest.json">
 | 
			
		||||
 | 
			
		||||
	<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
 | 
			
		||||
	<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
 | 
			
		||||
	<meta property="og:type" content="article">
 | 
			
		||||
	<meta property="og:site_name" content="Pixelfed">
 | 
			
		||||
	<meta property="og:title" content="{{ $ogTitle ?? $title ?? config('app.name', 'pixelfed') }}">
 | 
			
		||||
	<meta property="og:type" content="{{ $ogType ?? 'article' }}">
 | 
			
		||||
	<meta property="og:url" content="{{url(request()->url())}}">
 | 
			
		||||
	@stack('meta')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,13 @@
 | 
			
		|||
@extends('layouts.app',['title' => $profile->username . " on " . config('app.name')])
 | 
			
		||||
@extends('layouts.app', [
 | 
			
		||||
    'title' => $profile->name . ' (@' . $acct . ') - Pixelfed',
 | 
			
		||||
    'ogTitle' => $profile->name . ' (@' . $acct . ')',
 | 
			
		||||
    'ogType' => 'profile'
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
@php
 | 
			
		||||
$acct = $profile->username . '@' . config('pixelfed.domain.app');
 | 
			
		||||
$metaDescription = \App\Services\AccountService::getMetaDescription($profile->id);
 | 
			
		||||
@endphp
 | 
			
		||||
 | 
			
		||||
@section('content')
 | 
			
		||||
@if (session('error'))
 | 
			
		||||
| 
						 | 
				
			
			@ -8,9 +17,6 @@
 | 
			
		|||
@endif
 | 
			
		||||
 | 
			
		||||
<profile profile-id="{{$profile->id}}" profile-username="{{$profile->username}}" :profile-settings="{{json_encode($settings)}}" profile-layout="metro"></profile>
 | 
			
		||||
@if($profile->website)
 | 
			
		||||
<a class="d-none" href="{{$profile->website}}" rel="me external nofollow noopener">{{$profile->website}}</a>
 | 
			
		||||
@endif
 | 
			
		||||
 | 
			
		||||
<noscript>
 | 
			
		||||
	<div class="container">
 | 
			
		||||
| 
						 | 
				
			
			@ -20,13 +26,20 @@
 | 
			
		|||
 | 
			
		||||
@endsection
 | 
			
		||||
 | 
			
		||||
@push('meta')<meta property="og:description" content="{{strip_tags($profile->bio)}}">
 | 
			
		||||
	@if(false == $settings['crawlable'] || $profile->remote_url)
 | 
			
		||||
	<meta name="robots" content="noindex, nofollow">
 | 
			
		||||
	@else  <meta property="og:image" content="{{$profile->avatarUrl()}}">
 | 
			
		||||
@push('meta')<meta name="description" content="{{$metaDescription}}">
 | 
			
		||||
    <meta property="og:description" content="{{$metaDescription}}">
 | 
			
		||||
    <meta property="og:image" content="{{$profile->avatarUrl()}}">
 | 
			
		||||
    <meta property="og:image:width" content="200">
 | 
			
		||||
    <meta property="og:image:height" content="200">
 | 
			
		||||
    <meta property="twitter:card" content="summary">
 | 
			
		||||
    <meta property="profile:username" content="{{$acct}}">
 | 
			
		||||
	<link href="{{$profile->permalink('.atom')}}" rel="alternate" title="{{$profile->username}} on Pixelfed" type="application/atom+xml">
 | 
			
		||||
		<link href='{{$profile->permalink()}}' rel='alternate' type='application/activity+json'>
 | 
			
		||||
	@endif
 | 
			
		||||
	<link href="{{$profile->permalink()}}" rel="alternate" type="application/activity+json">
 | 
			
		||||
    <meta name="application-name" content="Pixelfed">
 | 
			
		||||
    <meta name="generator" content="pixelfed">
 | 
			
		||||
    @if($profile->website)<link href="{{$profile->website}}" rel="me" type="text/html">
 | 
			
		||||
@endif
 | 
			
		||||
	@if(false == $settings['crawlable'] || $profile->remote_url)<meta name="robots" content="noindex, nofollow">@endif
 | 
			
		||||
@endpush
 | 
			
		||||
 | 
			
		||||
@push('scripts')<script type="text/javascript" src="{{ mix('js/profile.js') }}"></script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,30 @@
 | 
			
		|||
@extends('layouts.app',['title' => "{$user->username} shared a post"])
 | 
			
		||||
@extends('layouts.app', [
 | 
			
		||||
    'title' => $desc ?? "{$user->username} shared a post",
 | 
			
		||||
    'ogTitle' => $ogTitle
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
@php
 | 
			
		||||
$s = \App\Services\StatusService::get($status->id);
 | 
			
		||||
$displayName = $s && $s['account'] ? $s['account']['display_name'] : false;
 | 
			
		||||
$captionPreview = false;
 | 
			
		||||
$domain = $displayName ? '@' . parse_url($s['account']['url'], PHP_URL_HOST) : '';
 | 
			
		||||
$wf = $displayName ? $s['account']['username'] . $domain : '';
 | 
			
		||||
$ogTitle = $displayName ? $displayName . ' (@' . $s['account']['username'] . $domain . ')' : '';
 | 
			
		||||
$mediaCount = $s['media_attachments'] && count($s['media_attachments']) ? count($s['media_attachments']) : 0;
 | 
			
		||||
$mediaSuffix = $mediaCount < 2 ? '' : 's';
 | 
			
		||||
$ogDescription = $s['content_text'] ? $s['content_text'] : 'Attached: ' . $mediaCount . ' ' . $s['media_attachments'][0]['type'] . $mediaSuffix;
 | 
			
		||||
if($s['content_text']) {
 | 
			
		||||
    $captionLen = strlen($s['content_text']);
 | 
			
		||||
    $captionPreview = $captionLen > 40 ? substr($s['content_text'], 0, 40) . '…' : $s['content_text'];
 | 
			
		||||
}
 | 
			
		||||
$desc = false;
 | 
			
		||||
if($displayName && $captionPreview) {
 | 
			
		||||
    $desc = $displayName . ': "' . $captionPreview . '" - Pixelfed';
 | 
			
		||||
} else if($displayName) {
 | 
			
		||||
    $desc = $displayName . ': Shared a new post - Pixelfed';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@endphp
 | 
			
		||||
 | 
			
		||||
@section('content')
 | 
			
		||||
<noscript>
 | 
			
		||||
| 
						 | 
				
			
			@ -7,20 +33,28 @@
 | 
			
		|||
  </div>
 | 
			
		||||
</noscript>
 | 
			
		||||
<div class="mt-md-4"></div>
 | 
			
		||||
<post-component status-template="{{$status->viewType()}}" status-id="{{$status->id}}" status-username="{{$status->profile->username}}" status-url="{{$status->url()}}" status-profile-url="{{$status->profile->url()}}" status-avatar="{{$status->profile->avatarUrl()}}" status-profile-id="{{$status->profile_id}}" profile-layout="metro"></post-component>
 | 
			
		||||
<post-component
 | 
			
		||||
    status-template="{{$status->viewType()}}"
 | 
			
		||||
    status-id="{{$status->id}}"
 | 
			
		||||
    status-username="{{$s['account']['username']}}"
 | 
			
		||||
    status-url="{{$s['url']}}"
 | 
			
		||||
    status-profile-url="{{$s['account']['url']}}"
 | 
			
		||||
    status-avatar="{{$s['account']['avatar']}}"
 | 
			
		||||
    status-profile-id="{{$status->profile_id}}"
 | 
			
		||||
    profile-layout="metro" />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@endsection
 | 
			
		||||
 | 
			
		||||
@push('meta')
 | 
			
		||||
 | 
			
		||||
    <meta property="og:description" content="{{ $status->caption }}">
 | 
			
		||||
    <meta property="og:image" content="{{$status->thumb()}}">
 | 
			
		||||
    <link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
 | 
			
		||||
    <meta name="twitter:card" content="summary_large_image">
 | 
			
		||||
    @if($status->viewType() == "video" || $status->viewType() == "video:album")
 | 
			
		||||
        <meta property="og:video" content="{{$status->mediaUrl()}}">
 | 
			
		||||
    @endif
 | 
			
		||||
@push('meta')@if($mediaCount && $s['pf_type'] === "photo" || $s['pf_type'] === "photo:album")
 | 
			
		||||
<meta property="og:image" content="{{$s['media_attachments'][0]['url']}}">
 | 
			
		||||
    @elseif($mediaCount && $s['pf_type'] === "video" || $s['pf_type'] === "video:album")<meta property="og:video" content="{{$s['media_attachments'][0]['url']}}">
 | 
			
		||||
    @endif<meta property="og:description" content="{{ $ogDescription }}">
 | 
			
		||||
    <meta property="og:published_time" content="{{$s['created_at']}}">
 | 
			
		||||
    <meta property="profile:username" content="{{ $wf }}">
 | 
			
		||||
    <link href='{{$s['url']}}' rel='alternate' type='application/activity+json'>
 | 
			
		||||
    <meta name="twitter:card" content="summary">
 | 
			
		||||
    <meta name="description" content="{{ $ogDescription }}">
 | 
			
		||||
@endpush
 | 
			
		||||
 | 
			
		||||
@push('scripts')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue