mirror of
https://github.com/HDInnovations/UNIT3D-Community-Edition.git
synced 2026-01-28 23:09:17 -06:00
update: working optimized announce
This commit is contained in:
@@ -49,7 +49,7 @@ class AutoHighspeedTag extends Command
|
||||
->leftJoinSub(
|
||||
Peer::distinct()
|
||||
->select('torrent_id')
|
||||
->whereRaw("INET6_NTOA(ip) IN ('".$seedboxIps->implode("','")."')"),
|
||||
->whereRaw("ip IN ('".$seedboxIps->implode("','")."')"),
|
||||
'highspeed_torrents',
|
||||
fn ($join) => $join->on('torrents.id', '=', 'highspeed_torrents.torrent_id')
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ use App\Exceptions\TrackerException;
|
||||
use App\Helpers\Bencode;
|
||||
use App\Jobs\ProcessAnnounce;
|
||||
use App\Models\BlacklistClient;
|
||||
use App\Models\Group;
|
||||
use App\Models\Peer;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -99,7 +99,7 @@ class AnnounceController extends Controller
|
||||
$queries = $this->checkAnnounceFields($request);
|
||||
|
||||
// Check user via supplied passkey.
|
||||
[$user, $group] = $this->checkUser($passkey, $queries);
|
||||
$user = $this->checkUser($passkey, $queries);
|
||||
|
||||
// Get Torrent Info Array from queries and judge if user can reach it.
|
||||
$torrent = $this->checkTorrent($queries['info_hash']);
|
||||
@@ -117,14 +117,14 @@ class AnnounceController extends Controller
|
||||
|
||||
// Check Download Slots.
|
||||
if (\config('announce.slots_system.enabled')) {
|
||||
$this->checkDownloadSlots($queries, $user, $group);
|
||||
$this->checkDownloadSlots($queries, $user);
|
||||
}
|
||||
|
||||
// Generate A Response For The Torrent Client.
|
||||
$repDict = $this->generateSuccessAnnounceResponse($queries, $torrent, $user);
|
||||
|
||||
// Process Annnounce Job.
|
||||
$this->processAnnounceJob($queries, $user, $torrent, $group);
|
||||
$this->processAnnounceJob($queries, $user, $torrent);
|
||||
} catch (TrackerException $exception) {
|
||||
$repDict = $this->generateFailedAnnounceResponse($exception);
|
||||
} finally {
|
||||
@@ -169,9 +169,8 @@ class AnnounceController extends Controller
|
||||
(string) $userAgent
|
||||
), new TrackerException(121));
|
||||
|
||||
$clientBlacklist = \cache()->rememberForever('client_blacklist', fn () => BlacklistClient::all()->pluck('name')->toArray());
|
||||
|
||||
// Block Blacklisted Clients
|
||||
$clientBlacklist = \cache()->rememberForever('client_blacklist', fn () => BlacklistClient::all()->pluck('name')->toArray());
|
||||
\throw_if(
|
||||
\in_array($userAgent, $clientBlacklist),
|
||||
new TrackerException(128, [':ua' => $request->header('User-Agent')])
|
||||
@@ -184,7 +183,7 @@ class AnnounceController extends Controller
|
||||
* @throws \App\Exceptions\TrackerException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function checkPasskey(string $passkey): void
|
||||
protected function checkPasskey($passkey): void
|
||||
{
|
||||
// If Passkey Is Not Provided Return Error to Client
|
||||
\throw_if($passkey === null, new TrackerException(130, [':attribute' => 'passkey']));
|
||||
@@ -279,7 +278,7 @@ class AnnounceController extends Controller
|
||||
), new TrackerException(135, [':port' => $queries['port']]));
|
||||
|
||||
// Part.4 Get User Ip Address
|
||||
$queries['ip-address'] = \inet_pton($request->getClientIp());
|
||||
$queries['ip-address'] = $request->getClientIp();
|
||||
|
||||
// Part.5 Get Users Agent
|
||||
$queries['user-agent'] = $request->headers->get('user-agent');
|
||||
@@ -287,6 +286,9 @@ class AnnounceController extends Controller
|
||||
// Part.6 bin2hex info_hash
|
||||
$queries['info_hash'] = \bin2hex($queries['info_hash']);
|
||||
|
||||
// Part.7 bin2hex peer_id
|
||||
$queries['peer_id'] = \bin2hex($queries['peer_id']);
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
@@ -296,57 +298,48 @@ class AnnounceController extends Controller
|
||||
* @throws \App\Exceptions\TrackerException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function checkUser(string $passkey, array $queries): array
|
||||
protected function checkUser($passkey, $queries): object
|
||||
{
|
||||
// Cached System Required Groups
|
||||
$deniedGroups = \cache()->rememberForever(
|
||||
'denied_groups',
|
||||
fn () => Group::query()
|
||||
$deniedGroups = \cache()->remember('denied_groups', 300, function () {
|
||||
return DB::table('groups')
|
||||
->selectRaw("min(case when slug = 'banned' then id end) as banned_id")
|
||||
->selectRaw("min(case when slug = 'validating' then id end) as validating_id")
|
||||
->selectRaw("min(case when slug = 'disabled' then id end) as disabled_id")
|
||||
->first()
|
||||
);
|
||||
->first();
|
||||
});
|
||||
|
||||
// Check Passkey Against Users Table
|
||||
$user = \cache()->rememberForever('user:'.$passkey, fn () => User::query()
|
||||
->select(['id', 'group_id', 'can_download'])
|
||||
$user = User::with('group')
|
||||
->select(['id', 'group_id', 'can_download', 'uploaded', 'downloaded'])
|
||||
->where('passkey', '=', $passkey)
|
||||
->first());
|
||||
|
||||
$group = \cache()->rememberForever('group:'.$user->group_id, fn () => Group::query()
|
||||
->select(['id', 'download_slots', 'is_immune', 'is_freeleech', 'is_double_upload'])
|
||||
->where('id', '=', $user->group_id)
|
||||
->first());
|
||||
->first();
|
||||
|
||||
// If User Doesn't Exist Return Error to Client
|
||||
\throw_if($user === null, new TrackerException(140));
|
||||
\throw_if($user === null,
|
||||
new TrackerException(140)
|
||||
);
|
||||
|
||||
// If User Account Is Unactivated/Validating Return Error to Client
|
||||
\throw_if(
|
||||
$user->group_id === $deniedGroups->validating_id,
|
||||
\throw_if($user->group_id === $deniedGroups->validating_id,
|
||||
new TrackerException(141, [':status' => 'Unactivated/Validating'])
|
||||
);
|
||||
|
||||
// If User Download Rights Are Disabled Return Error to Client
|
||||
\throw_if(
|
||||
$user->can_download === 0 && $queries['left'] !== '0',
|
||||
\throw_if($user->can_download === 0 && $queries['left'] !== '0',
|
||||
new TrackerException(142)
|
||||
);
|
||||
|
||||
// If User Is Banned Return Error to Client
|
||||
\throw_if(
|
||||
$user->group_id === $deniedGroups->banned_id,
|
||||
\throw_if($user->group_id === $deniedGroups->banned_id,
|
||||
new TrackerException(141, [':status' => 'Banned'])
|
||||
);
|
||||
|
||||
// If User Is Disabled Return Error to Client
|
||||
throw_if(
|
||||
$user->group_id === $deniedGroups->disabled_id,
|
||||
throw_if($user->group_id === $deniedGroups->disabled_id,
|
||||
new TrackerException(141, [':status' => 'Disabled'])
|
||||
);
|
||||
|
||||
return [$user, $group];
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,16 +348,12 @@ class AnnounceController extends Controller
|
||||
* @throws \App\Exceptions\TrackerException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function checkTorrent(string $infoHash): Torrent
|
||||
protected function checkTorrent($infoHash): object
|
||||
{
|
||||
// Check Info Hash Against Torrents Table
|
||||
$torrent = Torrent::withAnyStatus()
|
||||
->with([
|
||||
'peers' => fn ($query) => $query
|
||||
->select(['id', 'torrent_id', 'peer_id', 'user_id', 'left', 'seeder', 'port'])
|
||||
->selectRaw('INET6_NTOA(ip) as ip')
|
||||
])
|
||||
$torrent = Torrent::with('peers')
|
||||
->select(['id', 'free', 'doubleup', 'seeders', 'leechers', 'times_completed', 'status'])
|
||||
->withAnyStatus()
|
||||
->where('info_hash', '=', $infoHash)
|
||||
->first();
|
||||
|
||||
@@ -372,20 +361,17 @@ class AnnounceController extends Controller
|
||||
\throw_if($torrent === null, new TrackerException(150));
|
||||
|
||||
// If Torrent Is Pending Moderation Return Error to Client
|
||||
\throw_if(
|
||||
$torrent->status === self::PENDING,
|
||||
\throw_if($torrent->status === self::PENDING,
|
||||
new TrackerException(151, [':status' => 'PENDING In Moderation'])
|
||||
);
|
||||
|
||||
// If Torrent Is Rejected Return Error to Client
|
||||
\throw_if(
|
||||
$torrent->status === self::REJECTED,
|
||||
\throw_if($torrent->status === self::REJECTED,
|
||||
new TrackerException(151, [':status' => 'REJECTED In Moderation'])
|
||||
);
|
||||
|
||||
// If Torrent Is Postponed Return Error to Client
|
||||
\throw_if(
|
||||
$torrent->status === self::POSTPONED,
|
||||
\throw_if($torrent->status === self::POSTPONED,
|
||||
new TrackerException(151, [':status' => 'POSTPONED In Moderation'])
|
||||
);
|
||||
|
||||
@@ -398,10 +384,9 @@ class AnnounceController extends Controller
|
||||
* @throws \App\Exceptions\TrackerException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function checkPeer(Torrent $torrent, array $queries, User $user): void
|
||||
private function checkPeer($torrent, $queries, $user): void
|
||||
{
|
||||
\throw_if(
|
||||
\strtolower($queries['event']) === 'completed'
|
||||
\throw_if(\strtolower($queries['event']) === 'completed'
|
||||
&& $torrent->peers
|
||||
->where('peer_id', $queries['peer_id'])
|
||||
->where('user_id', '=', $user->id)
|
||||
@@ -417,7 +402,7 @@ class AnnounceController extends Controller
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function checkMinInterval(Torrent $torrent, array $queries, User $user): void
|
||||
private function checkMinInterval($torrent, $queries, $user): void
|
||||
{
|
||||
$prevAnnounce = $torrent->peers
|
||||
->where('peer_id', '=', $queries['peer_id'])
|
||||
@@ -425,8 +410,7 @@ class AnnounceController extends Controller
|
||||
->first();
|
||||
$setMin = \config('announce.min_interval.interval') ?? self::MIN;
|
||||
$randomMinInterval = \random_int($setMin, $setMin * 2);
|
||||
\throw_if(
|
||||
$prevAnnounce && $prevAnnounce->updated_at->greaterThan(\now()->subSeconds($randomMinInterval))
|
||||
\throw_if($prevAnnounce && $prevAnnounce->updated_at->greaterThan(\now()->subSeconds($randomMinInterval))
|
||||
&& \strtolower($queries['event']) !== 'completed' && \strtolower($queries['event']) !== 'stopped',
|
||||
new TrackerException(162, [':min' => $randomMinInterval])
|
||||
);
|
||||
@@ -438,7 +422,7 @@ class AnnounceController extends Controller
|
||||
* @throws \App\Exceptions\TrackerException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function checkMaxConnections(Torrent $torrent, User $user): void
|
||||
private function checkMaxConnections($torrent, $user): void
|
||||
{
|
||||
// Pull Count On Users Peers Per Torrent For Rate Limiting
|
||||
$connections = $torrent->peers
|
||||
@@ -446,8 +430,7 @@ class AnnounceController extends Controller
|
||||
->count();
|
||||
|
||||
// If Users Peer Count On A Single Torrent Is Greater Than X Return Error to Client
|
||||
\throw_if(
|
||||
$connections > \config('announce.rate_limit'),
|
||||
\throw_if($connections > \config('announce.rate_limit'),
|
||||
new TrackerException(138, [':limit' => \config('announce.rate_limit')])
|
||||
);
|
||||
}
|
||||
@@ -458,19 +441,18 @@ class AnnounceController extends Controller
|
||||
* @throws \App\Exceptions\TrackerException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function checkDownloadSlots(array $queries, User $user, Group $group): void
|
||||
private function checkDownloadSlots($queries, $user): void
|
||||
{
|
||||
$max = $group->download_slots;
|
||||
$max = $user->group->download_slots;
|
||||
|
||||
if ($max !== null && $max >= 0 && $queries['left'] != 0) {
|
||||
$count = DB::table('peers')
|
||||
$count = Peer::query()
|
||||
->where('user_id', '=', $user->id)
|
||||
->where('peer_id', '!=', $queries['peer_id'])
|
||||
->where('seeder', '=', 0)
|
||||
->count();
|
||||
|
||||
\throw_if(
|
||||
$count >= $max,
|
||||
\throw_if($count >= $max,
|
||||
new TrackerException(164, [':max' => $max])
|
||||
);
|
||||
}
|
||||
@@ -481,11 +463,11 @@ class AnnounceController extends Controller
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function generateSuccessAnnounceResponse(array $queries, Torrent $torrent, User $user): array
|
||||
private function generateSuccessAnnounceResponse($queries, $torrent, $user): array
|
||||
{
|
||||
// Build Response For Bittorrent Client
|
||||
$repDict = [
|
||||
'interval' => random_int(self::MIN, self::MAX),
|
||||
'interval' => \random_int(self::MIN, self::MAX),
|
||||
'min interval' => self::MIN,
|
||||
'complete' => (int) $torrent->seeders,
|
||||
'incomplete' => (int) $torrent->leechers,
|
||||
@@ -498,7 +480,7 @@ class AnnounceController extends Controller
|
||||
* We query peers from database and send peerlist, otherwise just quick return.
|
||||
*/
|
||||
if (\strtolower($queries['event']) !== 'stopped') {
|
||||
$limit = (min($queries['numwant'], 25));
|
||||
$limit = (\min($queries['numwant'], 25));
|
||||
|
||||
// Get Torrents Peers (Only include leechers in a seeder's peerlist)
|
||||
$peers = $torrent->peers
|
||||
@@ -523,9 +505,9 @@ class AnnounceController extends Controller
|
||||
/**
|
||||
* Process Announce Database Queries.
|
||||
*/
|
||||
private function processAnnounceJob(array $queries, User $user, Torrent $torrent, Group $group): void
|
||||
private function processAnnounceJob($queries, $user, $torrent): void
|
||||
{
|
||||
ProcessAnnounce::dispatch($queries, $user, $torrent, $group);
|
||||
ProcessAnnounce::dispatch($queries, $user, $torrent);
|
||||
}
|
||||
|
||||
protected function generateFailedAnnounceResponse(TrackerException $trackerException): array
|
||||
@@ -539,8 +521,8 @@ class AnnounceController extends Controller
|
||||
/**
|
||||
* Send Final Announce Response.
|
||||
*/
|
||||
protected function sendFinalAnnounceResponse(array|null $repDict): Response
|
||||
protected function sendFinalAnnounceResponse($repDict): Response
|
||||
{
|
||||
return response(Bencode::bencode($repDict), headers: self::HEADERS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,6 @@ class TorrentPeerController extends Controller
|
||||
$torrent = Torrent::withAnyStatus()->findOrFail($id);
|
||||
$peers = Peer::query()
|
||||
->with(['user'])
|
||||
->select(['torrent_id', 'user_id', 'uploaded', 'downloaded', 'left', 'port', 'agent', 'created_at', 'updated_at', 'seeder'])
|
||||
->selectRaw('INET6_NTOA(ip) as ip')
|
||||
->where('torrent_id', '=', $id)
|
||||
->latest('seeder')
|
||||
->get()
|
||||
|
||||
@@ -75,7 +75,7 @@ class UserController extends Controller
|
||||
|
||||
$clients = $user->peers()
|
||||
->select('agent', 'port')
|
||||
->selectRaw('INET6_NTOA(ip) as ip, MIN(created_at), MAX(updated_at), COUNT(*) as num_peers')
|
||||
->selectRaw('ip as ip, MIN(created_at), MAX(updated_at), COUNT(*) as num_peers')
|
||||
->groupBy(['ip', 'port', 'agent'])
|
||||
->get();
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ class UserActive extends Component
|
||||
->join('torrents', 'peers.torrent_id', '=', 'torrents.id')
|
||||
->select(
|
||||
'peers.id',
|
||||
'peers.ip',
|
||||
'peers.port',
|
||||
'peers.agent',
|
||||
'peers.uploaded',
|
||||
@@ -96,7 +97,6 @@ class UserActive extends Component
|
||||
'torrents.leechers',
|
||||
'torrents.times_completed',
|
||||
)
|
||||
->selectRaw('INET6_NTOA(ip) as ip')
|
||||
->selectRaw('(1 - (peers.left / NULLIF(torrents.size, 0))) AS progress')
|
||||
->where('peers.user_id', '=', $this->user->id)
|
||||
->when(
|
||||
|
||||
@@ -33,7 +33,7 @@ class ProcessAnnounce implements ShouldQueue
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(protected $queries, protected $user, protected $torrent, protected $group)
|
||||
public function __construct(protected $queries, protected $user, protected $torrent)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class ProcessAnnounce implements ShouldQueue
|
||||
);
|
||||
|
||||
if ($personalFreeleech ||
|
||||
$this->group->is_freeleech == 1 ||
|
||||
$this->user->group->is_freeleech == 1 ||
|
||||
$freeleechToken ||
|
||||
\config('other.freeleech') == 1) {
|
||||
$modDownloaded = 0;
|
||||
@@ -125,7 +125,7 @@ class ProcessAnnounce implements ShouldQueue
|
||||
}
|
||||
|
||||
if ($this->torrent->doubleup == 1 ||
|
||||
$this->group->is_double_upload == 1 ||
|
||||
$this->user->group->is_double_upload == 1 ||
|
||||
\config('other.doubleup') == 1) {
|
||||
$modUploaded = $uploaded * 2;
|
||||
} else {
|
||||
@@ -159,7 +159,7 @@ class ProcessAnnounce implements ShouldQueue
|
||||
|
||||
$history->active = 1;
|
||||
// Allow downgrading from `immune`, but never upgrade to it
|
||||
$history->immune = (int) ($history->immune === null ? $this->group->is_immune : (bool) $history->immune && (bool) $this->group->is_immune);
|
||||
$history->immune = (int) ($history->immune === null ? $this->user->group->is_immune : (bool) $history->immune && (bool) $this->user->group->is_immune);
|
||||
$history->save();
|
||||
break;
|
||||
|
||||
@@ -182,10 +182,9 @@ class ProcessAnnounce implements ShouldQueue
|
||||
|
||||
// User Update
|
||||
if ($modUploaded > 0 || $modDownloaded > 0) {
|
||||
$this->user->update([
|
||||
'uploaded' => DB::raw('uploaded + '. (int) $modUploaded),
|
||||
'downloaded' => DB::raw('downloaded + '. (int) $modDownloaded),
|
||||
]);
|
||||
$this->user->uploaded += $modUploaded;
|
||||
$this->user->downloaded += $modDownloaded;
|
||||
$this->user->save();
|
||||
}
|
||||
// End User Update
|
||||
|
||||
@@ -213,10 +212,9 @@ class ProcessAnnounce implements ShouldQueue
|
||||
|
||||
// User Update
|
||||
if ($modUploaded > 0 || $modDownloaded > 0) {
|
||||
$this->user->update([
|
||||
'uploaded' => DB::raw('uploaded + '. (int) $modUploaded),
|
||||
'downloaded' => DB::raw('downloaded + '. (int) $modDownloaded),
|
||||
]);
|
||||
$this->user->uploaded += $modUploaded;
|
||||
$this->user->downloaded += $modDownloaded;
|
||||
$this->user->save();
|
||||
}
|
||||
// End User Update
|
||||
break;
|
||||
@@ -240,10 +238,9 @@ class ProcessAnnounce implements ShouldQueue
|
||||
|
||||
// User Update
|
||||
if ($modUploaded > 0 || $modDownloaded > 0) {
|
||||
$this->user->update([
|
||||
'uploaded' => DB::raw('uploaded + '. (int) $modUploaded),
|
||||
'downloaded' => DB::raw('downloaded + '. (int) $modDownloaded),
|
||||
]);
|
||||
$this->user->uploaded += $modUploaded;
|
||||
$this->user->downloaded += $modDownloaded;
|
||||
$this->user->save();
|
||||
}
|
||||
// End User Update
|
||||
}
|
||||
@@ -252,13 +249,13 @@ class ProcessAnnounce implements ShouldQueue
|
||||
->torrent
|
||||
->peers
|
||||
->where('left', '=', 0)
|
||||
->where('peer_id', '<>', $this->queries['peer_id'])
|
||||
->where('peer_id', '!=', $this->queries['peer_id'])
|
||||
->count();
|
||||
$otherLeechers = $this
|
||||
->torrent
|
||||
->peers
|
||||
->where('left', '>', 0)
|
||||
->where('peer_id', '<>', $this->queries['peer_id'])
|
||||
->where('peer_id', '!=', $this->queries['peer_id'])
|
||||
->count();
|
||||
|
||||
$this->torrent->seeders = $otherSeeders + (int) ($this->queries['left'] == 0);
|
||||
|
||||
@@ -59,7 +59,7 @@ class Peer extends Model
|
||||
public function updateConnectableStateIfNeeded(): void
|
||||
{
|
||||
if (\config('announce.connectable_check')) {
|
||||
$tmp_ip = inet_ntop(pack('A'.\strlen($this->ip), $this->ip));
|
||||
$tmp_ip = $this->ip;
|
||||
// IPv6 Check
|
||||
if (filter_var($tmp_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$tmp_ip = '['.$tmp_ip.']';
|
||||
|
||||
@@ -16,8 +16,8 @@ class PeerFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'peer_id' => $this->faker->asciify('-qB4450-************'),
|
||||
'ip' => \inet_pton($this->faker->ipv4()),
|
||||
'peer_id' => $this->faker->randomNumber(),
|
||||
'ip' => $this->faker->ipv4(),
|
||||
'port' => $this->faker->numberBetween(0, 65535),
|
||||
'agent' => $this->faker->word(),
|
||||
'uploaded' => $this->faker->randomNumber(),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Models\Peer;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class () extends Migration {
|
||||
@@ -13,8 +14,6 @@ return new class () extends Migration {
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Peer::truncate();
|
||||
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
Schema::table('peers', function (Blueprint $table) {
|
||||
@@ -30,8 +29,5 @@ return new class () extends Migration {
|
||||
});
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
|
||||
DB::statement('ALTER TABLE `peers` MODIFY `peer_id` BINARY(20) NOT NULL');
|
||||
DB::statement('ALTER TABLE `peers` MODIFY `ip` VARBINARY(16) NOT NULL');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user