@ -221,25 +221,107 @@ class UserStatus extends Command
return;
}
$rows = [
['profile id', $this->format($profile->id)],
['user_id', $this->format($profile->user_id)],
['username', $this->format($profile->username)],
['name', $this->format($profile->name)],
['domain', $this->format($profile->domain).($profile->domain ? '' : ' (local)')],
['is_private', $this->format($profile->is_private)],
['status', $this->format($profile->status ?? null)],
['deleted_at', $this->format($profile->deleted_at)],
['status_count', $this->format($profile->status_count)],
['created_at', $this->format($profile->created_at)],
];
$this->table(['Field', 'Value'], $rows);
// Dump every column on the profiles row, redacting crypto keys and
// trimming long text so the table stays readable.
$keysToRedact = ['private_key', 'public_key'];
$longText = ['bio', 'note', 'private_key', 'public_key'];
$rows = [];
foreach ($profile->getAttributes() as $key => $value) {
if (in_array($key, $keysToRedact, true)) {
$rows[] = [$key, empty($value) ? '(empty!)' : 'present ('.strlen((string) $value).' chars)'];
continue;
}
$display = $this->format($value);
if (in_array($key, $longText, true) & & is_string($value) & & strlen($value) > 60) {
$display = mb_strimwidth($value, 0, 60, '…');
}
$rows[] = [$key, $display];
}
$this->table(['Profile Column', 'Value'], $rows);
// Derived / computed metadata not stored directly on the row.
$this->newLine();
$this->comment('Derived metadata:');
$meta = [];
$isLocal = empty($profile->domain);
$meta[] = ['type', $isLocal ? 'LOCAL' : 'REMOTE ('.$profile->domain.')'];
$meta[] = ['profile url', $this->safeCall(fn () => $profile->url())];
$meta[] = ['permalink', $this->safeCall(fn () => $profile->permalink())];
if (! $isLocal) {
$meta[] = ['remote_url', $this->format($profile->remote_url ?? null)];
$meta[] = ['inbox_url', $this->format($this->attrOf($profile, 'inbox_url'))];
$meta[] = ['outbox_url', $this->format($this->attrOf($profile, 'outbox_url'))];
$meta[] = ['shared_inbox', $this->format($this->attrOf($profile, 'sharedInbox'))];
$meta[] = ['last_fetched_at', $this->format($profile->last_fetched_at)];
}
$meta[] = ['followers (live count)', (string) $this->safeCount(fn () => $profile->followers()->count())];
$meta[] = ['following (live count)', (string) $this->safeCount(fn () => $profile->following()->count())];
$meta[] = ['followers_count (cached)', $this->format($this->attrOf($profile, 'followers_count'))];
$meta[] = ['following_count (cached)', $this->format($this->attrOf($profile, 'following_count'))];
$meta[] = ['statuses (live count)', (string) $this->safeCount(fn () => $profile->statuses()->count())];
$meta[] = ['status_count (cached)', $this->format($profile->status_count)];
$meta[] = ['last_status_at', $this->format($profile->last_status_at)];
$meta[] = ['avatar row', $this->profileAvatar($profile)];
$this->table(['Metadata', 'Value'], $meta);
// Consistency / health checks.
$problems = [];
if ($profile->deleted_at) {
$this->error(' ✗ Profile is soft-deleted.');
$problems[] = 'Profile is soft-deleted (deleted_at='.$profile->deleted_at.').' ;
}
if ($profile->user_id & & $profile->user_id != $user->id) {
$this->error(' ✗ MISMATCH: profile.user_id ('.$profile->user_id.') != user.id ('.$user->id.').');
$problems[] = 'MISMATCH: profile.user_id ('.$profile->user_id.') != user.id ('.$user->id.').';
}
if ($user->profile_id & & (string) $user->profile_id !== (string) $profile->id) {
$problems[] = 'MISMATCH: user.profile_id ('.$user->profile_id.') != profile.id ('.$profile->id.').';
}
if ($isLocal & & empty($this->attrOf($profile, 'private_key'))) {
$problems[] = 'Local profile is MISSING its private_key — ActivityPub signing/federation will fail.';
}
if ($isLocal & & empty($this->attrOf($profile, 'public_key'))) {
$problems[] = 'Local profile is MISSING its public_key — remote servers cannot verify this actor.';
}
$cachedFollowers = (int) $this->attrOf($profile, 'followers_count');
$liveFollowers = (int) $this->safeCount(fn () => $profile->followers()->count());
if ($cachedFollowers !== $liveFollowers) {
$problems[] = 'followers_count ('.$cachedFollowers.') is out of sync with live count ('.$liveFollowers.').';
}
if ($problems) {
$this->newLine();
$this->error('PROFILE ISSUES:');
foreach ($problems as $p) {
$this->line(' ✗ '.$p);
}
}
}
protected function profileAvatar(Profile $profile): string
{
try {
$avatar = $profile->avatar;
if (! $avatar) {
return 'MISSING';
}
return 'present (media_path='.($avatar->media_path ?? 'null').')';
} catch (\Throwable $e) {
return 'error';
}
}
protected function attrOf($model, string $key)
{
return $model->getAttributes()[$key] ?? null;
}
protected function safeCall(callable $fn): string
{
try {
return (string) ($fn() ?? 'null');
} catch (\Throwable $e) {
return 'error';
}
}