-
Notifications
You must be signed in to change notification settings - Fork 34
Feat/branding and bot enhancements #301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 4.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <div class="flex items-center gap-3 py-1"> | ||
| <img src="{{ asset('images/logo.svg') }}" alt="He4rt Developers" class="h-8 w-auto dark:invert" /> | ||
| <div class="flex flex-col leading-none"> | ||
| <span class="text-sm font-black tracking-tight text-zinc-950 dark:text-white uppercase">He4rt</span> | ||
| <span class="text-[10px] font-bold text-purple-600 uppercase tracking-widest">Developers</span> | ||
| </div> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ final class ResolveExternalIdentity | |
| { | ||
| public function handle(ResolveUserProviderDTO $dto): ExternalIdentity | ||
| { | ||
| return ExternalIdentity::query()->firstOrCreate( | ||
| $identity = ExternalIdentity::query()->firstOrCreate( | ||
| [ | ||
| 'provider' => $dto->provider, | ||
| 'tenant_id' => $dto->tenantId, | ||
|
|
@@ -24,13 +24,22 @@ public function handle(ResolveUserProviderDTO $dto): ExternalIdentity | |
| 'type' => $dto->provider->getType(), | ||
| 'credentials_type' => CredentialsType::OAuth2, | ||
| 'credentials' => ClientAccessManager::make(), | ||
| 'metadata' => array_filter([ | ||
| 'username' => $dto->username, | ||
| 'email' => $dto->email, | ||
| 'avatar' => $dto->avatar, | ||
| ]), | ||
| 'connected_at' => now(), | ||
| ] | ||
| ); | ||
|
|
||
| $metadata = array_filter([ | ||
| 'username' => $dto->username, | ||
| 'email' => $dto->email, | ||
| 'avatar' => $dto->avatar, | ||
| ]); | ||
|
|
||
| if ($metadata !== []) { | ||
| $identity->update([ | ||
| 'metadata' => array_merge($identity->metadata ?? [], $metadata), | ||
| ]); | ||
|
Comment on lines
+37
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Skip no-op metadata updates. Lines 37-40 execute an 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return $identity; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| <?php | ||
|
Check warning on line 1 in app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php
|
||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace He4rt\IntegrationDiscord\Sync\Console; | ||
|
|
||
| use DateTimeImmutable; | ||
| use He4rt\Activity\Message\Actions\NewMessage; | ||
| use He4rt\Activity\Message\DTOs\NewMessageDTO; | ||
| use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider; | ||
| use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity; | ||
| use He4rt\Identity\Tenant\Models\Tenant; | ||
| use He4rt\IntegrationDiscord\Transport\DiscordConnector; | ||
| use He4rt\IntegrationDiscord\Transport\Requests\Messages\ListChannelMessages; | ||
| use Illuminate\Console\Attributes\Description; | ||
| use Illuminate\Console\Attributes\Signature; | ||
| use Illuminate\Console\Command; | ||
| use Throwable; | ||
|
|
||
| #[Description('Sync historical messages from a Discord channel to ensure Meeting Showcase data is available.')] | ||
| #[Signature('discord:sync-history {channel_id} {--limit=100 : Number of messages to fetch}')] | ||
| final class SyncDiscordHistoryCommand extends Command | ||
| { | ||
| public function handle(NewMessage $newMessageAction): int | ||
| { | ||
| $channelId = (string) $this->argument('channel_id'); | ||
|
Check failure on line 26 in app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php
|
||
| $limit = (int) $this->option('limit'); | ||
|
|
||
| $botToken = config('discord.token') ?? env('HE4RT_DISCORD_BOT_KEY'); | ||
| $guildId = env('HE4RT_DISCORD_GUILD'); | ||
|
Comment on lines
+29
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php"
echo "== Target file =="
ls -l "$FILE" || true
sed -n '1,120p' "$FILE" | nl -ba | sed -n '15,70p'
echo
echo "== Search for discord config keys =="
rg -n "config\('discord\.(token|guild)" -S . || true
rg -n "discord\.token|HE4RT_DISCORD_(BOT_KEY|GUILD)" -S . || true
echo
echo "== Locate discord config file =="
# Try common config locations, but without assuming too much: find any config file referencing discord.*
fd -t f "discord*.php" config || true
fd -t f "*discord*" config || true
echo
echo "== Print any likely discord config =="
# Print files that mention HE4RT_DISCORD or "discord.token"
fd -t f "*discord*.php" config -x sh -c 'rg -n "HE4RT_DISCORD_|discord\.token|token" "$1" && echo "---" && sed -n "1,200p" "$1"' sh {} || true
echo
echo "== Check NewMessage::persist implementation (for other issues in scratchpad) =="
MSG="app-modules/activity/src/Message/Actions/NewMessage.php"
ls -l "$MSG" || true
sed -n '1,220p' "$MSG" | nl -ba | sed -n '1,220p'
echo
echo "== Look for avatar field handling in sync command =="
rg -n "\['avatar'\]|avatar" "$FILE" || trueRepository: he4rt/heartdevs.com Length of output: 270 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php"
echo "== Show relevant lines (29-40) =="
sed -n '20,45p' "$FILE" | cat -n
echo
echo "== Search for config/env usage for discord token/guild =="
rg -n "config\('discord\.(token|guild)" -S . || true
rg -n "HE4RT_DISCORD_(BOT_KEY|GUILD)|discord\.token|HE4RT_DISCORD_GUILD" -S . || true
rg -n "env\('HE4RT_DISCORD" -S . || true
echo
echo "== Locate discord-related config files =="
# These commands may fail if fd isn't present; handle gracefully.
if command -v fd >/dev/null 2>&1; then
fd -t f "*discord*" config || true
fd -t f "discord*.php" config || true
else
echo "fd not available; skipping file discovery"
fi
echo
echo "== Find any config files that mention HE4RT_DISCORD or discord token keys =="
rg -n "HE4RT_DISCORD_|discord\.token|discord\.guild" -S config || true
rg -n "discord\.(token|guild)" -S . || true
echo
echo "== Inspect NewMessage::persist for exception swallowing / return type =="
MSG="app-modules/activity/src/Message/Actions/NewMessage.php"
ls -l "$MSG" || true
sed -n '1,260p' "$MSG" | cat -n
echo
echo "== Search persist() signature/usages =="
rg -n "function persist|persist\(" "$MSG" || true
rg -n "NewMessage::persist" -S . || true
echo
echo "== Search avatar handling in the sync command payload =="
sed -n '1,140p' "$FILE" | cat -n
rg -n "avatar" "$FILE" || trueRepository: he4rt/heartdevs.com Length of output: 15012 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== config/discord.php =="
sed -n '1,120p' config/discord.php | cat -n
echo
echo "== config/he4rt.php (discord section) =="
rg -n "discord\s*=>" -n config/he4rt.php || true
sed -n '1,120p' config/he4rt.php | cat -n
sed -n '120,260p' config/he4rt.php | cat -n || true
echo
echo "== IntegrationDiscordServiceProvider (discord.token usage) =="
sed -n '1,120p' app-modules/integration-discord/src/IntegrationDiscordServiceProvider.php | cat -n
echo
echo "== SyncDiscordHistoryCommand full relevant section =="
sed -n '20,90p' app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php | cat -nRepository: he4rt/heartdevs.com Length of output: 8671 Replace runtime env() reads with config() in SyncDiscordHistoryCommand 🧰 Tools🪛 GitHub Check: Perform Phpstan Check / Run[failure] 30-30: [failure] 29-29: 🤖 Prompt for AI Agents |
||
|
|
||
| if (! $botToken) { | ||
| $this->error('Discord bot token not configured.'); | ||
| return self::FAILURE; | ||
| } | ||
|
|
||
| // Find the tenant associated with this guild to ensure multi-tenancy rules are respected | ||
| $tenantProvider = ExternalIdentity::query() | ||
| ->where('model_type', (new Tenant())->getMorphClass()) | ||
| ->where('external_account_id', (string) $guildId) | ||
| ->first(); | ||
|
|
||
| if (! $tenantProvider) { | ||
| $this->error("Tenant not found for guild {$guildId}. Check your environment configuration."); | ||
| return self::FAILURE; | ||
| } | ||
|
|
||
| $this->info("Fetching last {$limit} messages from channel {$channelId}..."); | ||
|
|
||
| try { | ||
| $connector = new DiscordConnector($botToken); | ||
| $response = $connector->send(new ListChannelMessages($channelId, limit: $limit)); | ||
|
|
||
| if (! $response->successful()) { | ||
| $this->error("Failed to fetch messages from Discord: " . $response->body()); | ||
| return self::FAILURE; | ||
| } | ||
|
|
||
| $messages = $response->json(); | ||
| $count = 0; | ||
|
|
||
| $this->withProgressBar($messages, function (array $m) use ($tenantProvider, $newMessageAction, &$count) { | ||
| if (isset($m['author']['bot']) && $m['author']['bot']) { | ||
| return; | ||
| } | ||
|
|
||
| $author = $m['author']; | ||
|
|
||
| try { | ||
| $newMessageAction->persist(new NewMessageDTO( | ||
| tenantId: $tenantProvider->tenant_id, | ||
| provider: IdentityProvider::Discord, | ||
| providerUsername: $author['username'], | ||
| externalAccountId: $author['id'], | ||
| providerMessageId: $m['id'], | ||
| channelId: $m['channel_id'], | ||
| content: $m['content'] ?? '', | ||
| sentAt: new DateTimeImmutable($m['timestamp']), | ||
| avatar: $author['avatar'] ?? null, | ||
| )); | ||
| $count++; | ||
| } catch (Throwable $e) { | ||
| // Silently skip individual message failures to keep progress | ||
| } | ||
|
Comment on lines
+69
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The success counter never sees persistence failures. Lines 69-84 assume 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| $this->newLine(); | ||
| $this->info("Successfully synced {$count} messages and updated participant profiles."); | ||
|
|
||
| return self::SUCCESS; | ||
| } catch (Throwable $e) { | ||
| $this->error("An error occurred during sync: " . $e->getMessage()); | ||
| return self::FAILURE; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,10 @@ public static function getEloquentQuery(): Builder | |
| ->withoutGlobalScopes([ | ||
| SoftDeletingScope::class, | ||
| ]) | ||
| ->when( | ||
| session('active_provider'), | ||
| fn (Builder $query, string $provider) => $query->where('provider', $provider) | ||
| ) | ||
|
Comment on lines
+69
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apply the provider scope to global search as well.
Also applies to: 79-84 🤖 Prompt for AI Agents |
||
| ->with(['model', 'connectedByUser']); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace He4rt\PanelAdmin\Http\Middleware; | ||
|
|
||
| use Closure; | ||
| use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider; | ||
| use Illuminate\Http\Request; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
|
|
||
| class SetProviderScope | ||
| { | ||
| public function handle(Request $request, Closure $next): Response | ||
| { | ||
| if ($request->has('provider')) { | ||
| $provider = IdentityProvider::tryFrom($request->query('provider')); | ||
|
|
||
| if ($provider) { | ||
| session(['active_provider' => $provider->value]); | ||
| } | ||
| } | ||
|
|
||
| if (!session()->has('active_provider')) { | ||
| session(['active_provider' => IdentityProvider::Discord->value]); | ||
| } | ||
|
|
||
| return $next($request); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| declare(strict_types=1); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -49,6 +49,17 @@ | |||||||||||||||||||||||||||||
| return __('panel-admin::marketing.navigation.meeting_showcase'); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| public function mount(): void | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| // Default to today's meeting time as a helpful preset | ||||||||||||||||||||||||||||||
| $tz = config('app.display_timezone', 'America/Sao_Paulo'); | ||||||||||||||||||||||||||||||
| $this->startDate = now($tz)->setTime(22, 0)->format('Y-m-d\TH:i'); | ||||||||||||||||||||||||||||||
| $this->endDate = now($tz)->setTime(23, 40)->format('Y-m-d\TH:i'); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Default meeting channel | ||||||||||||||||||||||||||||||
| $this->channelId = '853401652471398400'; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| public function loadParticipants(): void | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| if ($this->channelId === '' || $this->startDate === '' || $this->endDate === '') { | ||||||||||||||||||||||||||||||
|
|
@@ -68,17 +79,20 @@ | |||||||||||||||||||||||||||||
| ->orderByDesc('total_messages') | ||||||||||||||||||||||||||||||
| ->get(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| $identityIds = $messageStats->pluck('external_identity_id'); | ||||||||||||||||||||||||||||||
| $identityIds = $messageStats->pluck('external_identity_id')->unique()->filter()->values()->all(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| $identities = ExternalIdentity::query() | ||||||||||||||||||||||||||||||
| ->withoutGlobalScopes() | ||||||||||||||||||||||||||||||
| ->withTrashed() | ||||||||||||||||||||||||||||||
| ->whereIn('id', $identityIds) | ||||||||||||||||||||||||||||||
| ->get() | ||||||||||||||||||||||||||||||
| ->keyBy('id'); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| $this->participants = $messageStats->map(function (Message $stat) use ($identities): array { | ||||||||||||||||||||||||||||||
| $identity = $identities->get($stat->external_identity_id); | ||||||||||||||||||||||||||||||
| $identityId = (string) $stat->external_identity_id; | ||||||||||||||||||||||||||||||
| $identity = $identities->get($identityId); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return $this->extractDiscordData($identity, (int) $stat->total_messages); // @phpstan-ignore property.notFound | ||||||||||||||||||||||||||||||
| return $this->extractDiscordData($identity, (int) $stat->total_messages); | ||||||||||||||||||||||||||||||
| })->all(); | ||||||||||||||||||||||||||||||
|
Comment on lines
91
to
96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect type hint causing static analysis failure. The type hint 🔧 Proposed fix- $this->participants = $messageStats->map(function (Message $stat) use ($identities): array {
+ $this->participants = $messageStats->map(function (object $stat) use ($identities): array {
$identityId = (string) $stat->external_identity_id;
$identity = $identities->get($identityId);
return $this->extractDiscordData($identity, (int) $stat->total_messages);
})->all();📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: Perform Phpstan Check / Run[failure] 95-95: 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| $this->loaded = true; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 212
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 2582
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 105
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 1495
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 10461
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 3466
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 112
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 3473
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 5810
🌐 Web query:
DiscordPHP Discord\Parts\User\User avatar getter getAvatarAttribute💡 Result:
In DiscordPHP, the getAvatarAttribute method is a helper function used to generate the URL for a user's avatar [1][2]. Method Signature: public function getAvatarAttribute(?string $format = null, int $size = 1024): string Parameters: - $format (string|null): The desired image format (e.g., 'png', 'jpg', 'webp', or 'gif'). If not provided, it defaults to 'webp', or 'gif' if the avatar is animated [2]. - $size (int): The width/height of the image in pixels. It defaults to 1024 and is automatically clamped to allowed powers of two between 16 and 4096 [2]. Usage: While you can access the default avatar URL via the $user->avatar property, getAvatarAttribute is intended for cases where you need to specify a custom image format or size [1][2]. Example: $url = $user->getAvatarAttribute('png', 2048); echo $url; // Outputs: https://cdn.discordapp.com/avatars/:user_id/:avatar_hash.png?size=2048 If the user does not have a custom avatar set, the method returns the URL to their default Discord embed avatar based on their discriminator or ID [2]. Note that a similar method exists for the Member part to fetch guild-specific avatars [3].
Citations:
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 107
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 107
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 950
🏁 Script executed:
Repository: he4rt/heartdevs.com
Length of output: 2002
Use the explicit DiscordPHP avatar getter for static analysis
Replace
avatar: $message->author->avatarinapp-modules/bot-discord/src/Events/MessageReceivedEvent.phpwith the documented accessor ($message->author->getAvatarAttribute()). The repo’sphpstan.ignore.neonsuppressesDiscord\Parts\User\User::$avataronly for other files, notMessageReceivedEvent.php, so this magic-property access is likely still flagged by PHPStan.🧰 Tools
🪛 GitHub Check: Perform Phpstan Check / Run
[failure] 53-53:
Access to an undefined property Discord\Parts\User\User::$avatar.
🤖 Prompt for AI Agents