Browse Source

Merge branch 'dev' of github.com:pixelfed/pixelfed into jippi-fork

Christian Winther 1 year ago
parent
commit
890827d60e

+ 1 - 0
CHANGELOG.md

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

+ 118 - 0
app/Console/Commands/ImportEmojis.php

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

+ 21 - 0
app/Http/Controllers/Api/ApiV1Controller.php

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

+ 3 - 0
app/Http/Controllers/ComposeController.php

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

+ 23 - 0
app/Http/Controllers/UserRolesController.php

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

+ 23 - 0
app/Models/UserRoles.php

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

+ 1 - 1
app/Providers/AuthServiceProvider.php

@@ -14,7 +14,7 @@ class AuthServiceProvider extends ServiceProvider
      * @var array
      */
     protected $policies = [
-        'App\Model' => 'App\Policies\ModelPolicy',
+        // 'App\Model' => 'App\Policies\ModelPolicy',
     ];
 
     /**

+ 35 - 0
app/Services/AccountService.php

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

+ 119 - 0
app/Services/UserRoleService.php

@@ -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'
+            ],
+        ];
+    }
+}

+ 54 - 37
app/Util/Webfinger/Webfinger.php

@@ -4,43 +4,60 @@ namespace App\Util\Webfinger;
 
 class Webfinger
 {
-	protected $user;
-	protected $subject;
-	protected $aliases;
-	protected $links;
+    protected $user;
+    protected $subject;
+    protected $aliases;
+    protected $links;
 
-	public function __construct($user)
-	{
-		$this->subject = 'acct:'.$user->username.'@'.parse_url(config('app.url'), PHP_URL_HOST);
-		$this->aliases = [
-			$user->url(),
-			$user->permalink(),
-		];
-		$this->links = [
-			[
-				'rel'  => 'http://webfinger.net/rel/profile-page',
-				'type' => 'text/html',
-				'href' => $user->url(),
-			],
-			[
-				'rel'  => 'http://schemas.google.com/g/2010#updates-from',
-				'type' => 'application/atom+xml',
-				'href' => $user->permalink('.atom'),
-			],
-			[
-				'rel'  => 'self',
-				'type' => 'application/activity+json',
-				'href' => $user->permalink(),
-			],
-		];
-	}
+    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';
 
-	public function generate()
-	{
-		return [
-			'subject' => $this->subject,
-			'aliases' => $this->aliases,
-			'links'   => $this->links,
-		];
-	}
+        $this->subject = 'acct:'.$user->username.'@'.parse_url(config('app.url'), PHP_URL_HOST);
+        $this->aliases = [
+            $user->url(),
+            $user->permalink(),
+        ];
+        $this->links = [
+            [
+                'rel'  => 'http://webfinger.net/rel/profile-page',
+                'type' => 'text/html',
+                'href' => $user->url(),
+            ],
+            [
+                'rel'  => 'http://schemas.google.com/g/2010#updates-from',
+                'type' => 'application/atom+xml',
+                'href' => $user->permalink('.atom'),
+            ],
+            [
+                'rel'  => 'self',
+                'type' => 'application/activity+json',
+                'href' => $user->permalink(),
+            ],
+            [
+                'rel' => 'http://webfinger.net/rel/avatar',
+                'type' => $avatarType,
+                'href' => $avatar,
+            ],
+        ];
+    }
+
+    public function generate()
+    {
+        return [
+            'subject' => $this->subject,
+            'aliases' => $this->aliases,
+            'links'   => $this->links,
+        ];
+    }
 }

+ 31 - 0
database/migrations/2023_12_27_081801_create_user_roles_table.php

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

+ 32 - 0
database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php

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

+ 20 - 2
resources/assets/js/components/NotificationCard.vue

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

+ 6 - 6
resources/views/layouts/app.blade.php

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

+ 24 - 11
resources/views/profile/show.blade.php

@@ -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()}}">
-		<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
+@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">
+    <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>

+ 45 - 11
resources/views/status/show.blade.php

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