Laravel and PHP
A complete setup for a Laravel admin panel: keep a local copy of every SentinelShare ban, refresh it every five minutes, and check players against it when they join. At the end there is a plain-PHP lookup if you don't use Laravel.
Works on Laravel 10 and 11 with MySQL, MariaDB or PostgreSQL. You need a community API key (see Admin panel API).
1. Configuration
Add the key to .env (never commit it):
SENTINEL_API_URL=https://sentinelshare.com/api/v1/integration
SENTINEL_API_KEY=ssk_your_key_hereAnd to config/services.php:
'sentinelshare' => [
'url' => env('SENTINEL_API_URL', 'https://sentinelshare.com/api/v1/integration'),
'key' => env('SENTINEL_API_KEY'),
],2. A table for network bans
php artisan make:migration create_sentinel_bans_table, then:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('sentinel_bans', function (Blueprint $table) {
$table->uuid('id')->primary(); // SentinelShare ban id
$table->uuid('player_id')->index();
$table->string('player_alias');
$table->json('arma_uids');
$table->json('discord_ids');
$table->string('status', 16)->index(); // active | lifted
$table->string('type', 32); // CHEATING, EXPLOITING, ...
$table->string('verification_state', 32); // UNVERIFIED, PEER_VERIFIED, ...
$table->text('reason')->nullable();
$table->string('filed_by')->nullable();
$table->string('filed_by_community')->nullable();
$table->timestamp('filed_at')->nullable();
$table->timestamp('changed_at')->nullable(); // SentinelShare's updated_at
$table->json('record'); // the full record as received
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('sentinel_bans');
}
};3. The model
app/Models/SentinelBan.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class SentinelBan extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
protected $casts = [
'arma_uids' => 'array',
'discord_ids' => 'array',
'record' => 'array',
'filed_at' => 'datetime',
'changed_at' => 'datetime',
];
/** Columns for one ban record from the SentinelShare API. */
public static function attributesFrom(array $r): array
{
return [
'player_id' => $r['player']['id'],
'player_alias' => $r['player']['alias'],
'arma_uids' => $r['player']['arma_uids'],
'discord_ids' => $r['player']['discord_ids'],
'status' => $r['status'],
'type' => $r['type'],
'verification_state' => $r['verification']['state'],
'reason' => $r['reason'],
'filed_by' => $r['filed_by']['name'],
'filed_by_community' => $r['filed_by']['community'],
'filed_at' => $r['filed_at'],
'changed_at' => $r['updated_at'],
'record' => $r,
];
}
public function scopeActive(Builder $query): Builder
{
return $query->where('status', 'active');
}
public function scopeVerified(Builder $query): Builder
{
return $query->whereIn('verification_state', ['PEER_VERIFIED', 'STAFF_VERIFIED']);
}
/** SentinelShare stores Arma UIDs in lower case. */
public function scopeForArmaUid(Builder $query, string $armaUid): Builder
{
return $query->whereJsonContains('arma_uids', strtolower(trim($armaUid)));
}
public function scopeForDiscordId(Builder $query, string $discordId): Builder
{
return $query->whereJsonContains('discord_ids', trim($discordId));
}
}4. The API client
app/Services/SentinelShare.php:
<?php
namespace App\Services;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
class SentinelShare
{
private function http(): PendingRequest
{
return Http::baseUrl(config('services.sentinelshare.url'))
->withToken(config('services.sentinelshare.key'))
->acceptJson()
->timeout(15)
->retry(3, 5000); // covers brief outages and 429s
}
/** Bans created or changed since the cursor. */
public function bans(?string $since = null, int $limit = 200): array
{
return $this->http()
->get('/bans', array_filter(['since' => $since, 'limit' => $limit]))
->throw()
->json();
}
/** One player and all their bans. Pass any of arma_uid, discord_id, ip, name. */
public function lookup(array $identifiers): array
{
return $this->http()->get('/lookup', array_filter($identifiers))->throw()->json();
}
}5. The sync command
php artisan make:command SyncSentinelBans, then:
<?php
namespace App\Console\Commands;
use App\Models\SentinelBan;
use App\Services\SentinelShare;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class SyncSentinelBans extends Command
{
protected $signature = 'sentinel:sync {--full : Ignore the saved cursor and sync everything}';
protected $description = 'Pull new and changed bans from SentinelShare';
public function handle(SentinelShare $sentinel): int
{
$cursor = $this->option('full') ? null : Cache::get('sentinelshare.cursor');
$saved = 0;
do {
$page = $sentinel->bans($cursor);
foreach ($page['items'] as $ban) {
SentinelBan::updateOrCreate(['id' => $ban['id']], SentinelBan::attributesFrom($ban));
$saved++;
}
$cursor = $page['next_cursor'];
if ($cursor) {
Cache::forever('sentinelshare.cursor', $cursor);
}
} while ($page['has_more']);
$this->info("Saved {$saved} bans.");
return self::SUCCESS;
}
}Run it once by hand: php artisan sentinel:sync. The first run pages through every ban on the network.
If your cache gets cleared the next run starts over from the beginning. That's safe (bans are replaced by id), just slower. To keep the cursor through cache clears, store it in a database table instead.
6. Run it every five minutes
Laravel 11, in routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('sentinel:sync')->everyFiveMinutes()->withoutOverlapping();Laravel 10, in app/Console/Kernel.php:
protected function schedule(Schedule $schedule): void
{
$schedule->command('sentinel:sync')->everyFiveMinutes()->withoutOverlapping();
}Both need Laravel's scheduler in the server's crontab:
* * * * * cd /var/www/your-panel && php artisan schedule:run >> /dev/null 2>&17. Check players when they join
Check the local table first: it's instant and costs no API calls. Put this wherever your panel learns a player connected (a game-server webhook, a log watcher, a queue job):
use App\Models\SentinelBan;
$bans = SentinelBan::active()->forArmaUid($armaUid)->get();
if ($bans->isEmpty()) {
return; // nothing on the network
}
if (SentinelBan::active()->verified()->forArmaUid($armaUid)->exists()) {
// Verified network ban: kick, block, or alert staff - your community's rule
} else {
// Active but unverified: flag the player for staff to look at
}For identifiers you don't sync, such as a Discord ID from your whitelist form, ask the API directly:
$result = app(\App\Services\SentinelShare::class)->lookup(['discord_id' => $discordId]);
if ($result['found']) {
$active = collect($result['bans'])->where('status', 'active');
// $result['url'] links staff to the player's SentinelShare profile
}8. Show it on a player page
A Blade partial for your player view:
@php($networkBans = \App\Models\SentinelBan::forArmaUid($player->arma_uid)->orderByDesc('filed_at')->get())
@if ($networkBans->isNotEmpty())
<div class="alert alert-warning">
<strong>SentinelShare:</strong>
{{ $networkBans->where('status', 'active')->count() }} active network ban(s)
<ul>
@foreach ($networkBans as $ban)
<li>
{{ ucfirst(strtolower(str_replace('_', ' ', $ban->type))) }},
{{ $ban->filed_by_community }}, {{ $ban->filed_at?->format('d M Y') }},
{{ $ban->status }}, {{ strtolower(str_replace('_', ' ', $ban->verification_state)) }}
<a href="{{ $ban->record['url'] }}" target="_blank" rel="noopener">profile</a>
</li>
@endforeach
</ul>
</div>
@endifPlain PHP (no framework)
A lookup with cURL:
<?php
function sentinel_lookup(array $identifiers): array
{
$url = 'https://sentinelshare.com/api/v1/integration/lookup?' . http_build_query(array_filter($identifiers));
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SENTINEL_API_KEY'),
'Accept: application/json',
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("SentinelShare lookup failed: HTTP {$status}");
}
return json_decode($body, true);
}
$result = sentinel_lookup(['arma_uid' => 'f91f26e6-aea4-4822-b26b-c9135f3c3b66']);
echo $result['found'] ? count($result['bans']) . " bans\n" : "clean\n";