Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/Enums/PostPlatform/ContentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ enum ContentType: string
// Discord
case DiscordMessage = 'discord_message';

// VK
case VkPost = 'vk_post';

/**
* AI generation format for an Instagram carousel. Not a content type —
* carousel posts are persisted as InstagramFeed.
Expand Down Expand Up @@ -84,6 +87,7 @@ public function label(): string
self::MastodonPost => 'Post',
self::TelegramPost => 'Post',
self::DiscordMessage => 'Message',
self::VkPost => 'Post',
};
}

Expand All @@ -108,6 +112,7 @@ public function platform(): SocialPlatform
self::MastodonPost => SocialPlatform::Mastodon,
self::TelegramPost => SocialPlatform::Telegram,
self::DiscordMessage => SocialPlatform::Discord,
self::VkPost => SocialPlatform::Vk,
};
}

Expand Down Expand Up @@ -177,6 +182,7 @@ public function maxMediaCount(): int
self::MastodonPost => 4,
self::TelegramPost => 10,
self::DiscordMessage => 10,
self::VkPost => 10,
};
}

Expand Down Expand Up @@ -458,6 +464,7 @@ public function supportsVideo(): bool
self::MastodonPost => true,
self::TelegramPost => true,
self::DiscordMessage => true,
self::VkPost => true,
};
}

Expand Down Expand Up @@ -609,6 +616,7 @@ public static function defaultFor(SocialPlatform $platform): self
SocialPlatform::Mastodon => self::MastodonPost,
SocialPlatform::Telegram => self::TelegramPost,
SocialPlatform::Discord => self::DiscordMessage,
SocialPlatform::Vk => self::VkPost,
};
}
}
13 changes: 12 additions & 1 deletion app/Enums/SocialAccount/Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum Platform: string
case Mastodon = 'mastodon';
case Telegram = 'telegram';
case Discord = 'discord';
case Vk = 'vk';

/**
* The social network this platform belongs to. Variants that represent the
Expand Down Expand Up @@ -69,6 +70,7 @@ public function label(): string
self::Mastodon => 'Mastodon',
self::Telegram => 'Telegram',
self::Discord => 'Discord',
self::Vk => 'VK',
};
}

Expand All @@ -88,6 +90,7 @@ public function color(): string
self::Mastodon => '#6364FF',
self::Telegram => '#26A5E4',
self::Discord => '#5865F2',
self::Vk => '#0077FF',
};
}

Expand All @@ -106,6 +109,7 @@ public function allowedMediaTypes(): array
self::Mastodon => [MediaType::Image, MediaType::Video],
self::Telegram => [MediaType::Image, MediaType::Video],
self::Discord => [MediaType::Image, MediaType::Video],
self::Vk => [MediaType::Image, MediaType::Video],
};
}

Expand All @@ -124,6 +128,7 @@ public function maxImages(): int
self::Mastodon => 4,
self::Telegram => 10,
self::Discord => 10,
self::Vk => 10,
};
}

Expand All @@ -147,7 +152,7 @@ public function altTextMaxLength(): ?int
self::Threads => 1000,
self::Pinterest => 500,
self::Discord => 1024,
self::TikTok, self::YouTube, self::Telegram => null,
self::TikTok, self::YouTube, self::Telegram, self::Vk => null,
};
}

Expand Down Expand Up @@ -181,6 +186,7 @@ public function supportsAltText(): bool
* - Mastodon: 500 default; instances may be higher (we stay conservative)
* - Telegram: 4096 for a text message (media captions are capped at 1024,
* handled in the publisher by sending long text as its own message)
* - VK: 15895 characters for a wall post
*/
public function maxContentLength(): int
{
Expand All @@ -197,6 +203,7 @@ public function maxContentLength(): int
self::Mastodon => 500,
self::Telegram => 4096,
self::Discord => 2000,
self::Vk => 15895,
};
}

Expand Down Expand Up @@ -242,6 +249,8 @@ public function recommendedAiContentLength(): int
self::Telegram => 400,
// Discord — conversational community posts read best when concise
self::Discord => 280,
// VK — feed favors short posts; long reads live in Articles
self::Vk => 400,
};
}

Expand All @@ -265,6 +274,7 @@ public function requiredPublishScopes(): array
self::Mastodon => ['write:statuses'],
self::Telegram => [],
self::Discord => [],
self::Vk => [],
};
}

Expand All @@ -283,6 +293,7 @@ public function supportsTextOnly(): bool
self::Mastodon => true,
self::Telegram => true,
self::Discord => true,
self::Vk => true,
};
}

Expand Down
60 changes: 60 additions & 0 deletions app/Exceptions/Social/VkPublishException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace App\Exceptions\Social;

use App\Exceptions\TokenExpiredException;
use Illuminate\Http\Client\Response;

class VkPublishException extends SocialPublishException
{
public static function fromApiResponse(mixed $response): static
{
/** @var Response $response */
$rawResponse = $response->body();
$error = $response->json('error');

// VK reports failures as HTTP 200 with an `error` object; transport
// failures (5xx) have no such object.
$code = (int) data_get($error, 'error_code', 0);
$message = (string) data_get($error, 'error_msg', 'An unknown VK error occurred.');

if (self::isConfirmedDeadToken($response)) {
throw new TokenExpiredException(
message: $message,
platformErrorCode: (string) $code,
);
}

return new static(
userMessage: $message,
category: match (true) {
in_array($code, [6, 9, 29], true) => ErrorCategory::RateLimit,
in_array($code, [7, 15, 200, 214, 219], true) => ErrorCategory::Permission,
in_array($code, [100, 118, 129], true) => ErrorCategory::MediaFormat,
$code === 0 && $response->serverError() => ErrorCategory::ServerError,
default => ErrorCategory::Unknown,
},
platformErrorCode: $code > 0 ? (string) $code : (string) $response->status(),
rawResponse: $rawResponse,
);
}

public function platform(): string
{
return 'vk';
}

/**
* Whether this response confirms the account's own access_token is dead.
* VK error 5 is "User authorization failed" — the token was revoked or
* invalidated (password change, security logout). Shared with
* ConnectionVerifier so publish and verify agree on what a dead token
* looks like.
*/
public static function isConfirmedDeadToken(Response $response): bool
{
return (int) $response->json('error.error_code') === 5;
}
}
3 changes: 3 additions & 0 deletions app/Http/Controllers/App/AnalyticsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use App\Services\Social\LinkedInPageAnalytics;
use App\Services\Social\PinterestAnalytics;
use App\Services\Social\Telegram\TelegramAnalytics;
use App\Services\Social\Vk\VkAnalytics;
use App\Services\Social\ThreadsAnalytics;
use App\Services\Social\TikTokAnalytics;
use App\Services\Social\XAnalytics;
Expand All @@ -38,6 +39,7 @@ class AnalyticsController extends Controller
Platform::Pinterest,
Platform::YouTube,
Platform::Telegram,
Platform::Vk,
];

public function index(Request $request): Response
Expand Down Expand Up @@ -99,6 +101,7 @@ private function metricsFor(SocialAccount $account, ?Carbon $since, ?Carbon $unt
Platform::Pinterest => app(PinterestAnalytics::class)->getMetrics($account, $since, $until),
Platform::YouTube => app(YouTubeAnalytics::class)->getMetrics($account, $since, $until),
Platform::Telegram => app(TelegramAnalytics::class)->getMetrics($account),
Platform::Vk => app(VkAnalytics::class)->getMetrics($account),
default => [],
};
} catch (PlatformUnavailableException|ConnectionException $e) {
Expand Down
Loading