@umemurakisei

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

X(旧Twitter)ユーザーアカウントをBotから守るサービス

Discussion

mysqlとindex.phpでX(旧Twitter)ユーザーのアカウントをBotから守るサービスを作っているのですが、このようなエラーがで出ます。どうしたらいいのかわからずで教えていただけますでしょうか。

TOKEN ERROR : {"error":"unauthorized_client","error_description":"Missing valid authorization header"}

Mysql
/* =========================================================
Guardian Shield
MYSQL 完全修正版 V2
========================================================= */

CREATE DATABASE IF NOT EXISTS kaiba2365_00
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

USE kaiba2365_00;

/* =========================================================
USERS
========================================================= */

CREATE TABLE IF NOT EXISTS users (

id INT NOT NULL AUTO_INCREMENT,

x_id VARCHAR(255) DEFAULT NULL,

name VARCHAR(255) DEFAULT NULL,

username VARCHAR(255) DEFAULT NULL,

avatar TEXT DEFAULT NULL,

access_token LONGTEXT DEFAULT NULL,

refresh_token LONGTEXT DEFAULT NULL,

ip_address VARCHAR(255) DEFAULT NULL,

country VARCHAR(255) DEFAULT NULL,

region_name VARCHAR(255) DEFAULT NULL,

last_scan_at DATETIME DEFAULT NULL,

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY (id),

UNIQUE KEY uniq_x_id (x_id),

KEY idx_username (username)

) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;

/* =========================================================
DETECTED BOTS
========================================================= */

CREATE TABLE IF NOT EXISTS detected_bots (

id INT NOT NULL AUTO_INCREMENT,

owner_x_id VARCHAR(255) DEFAULT NULL,

target_x_id VARCHAR(255) DEFAULT NULL,

target_name VARCHAR(255) DEFAULT NULL,

target_username VARCHAR(255) DEFAULT NULL,

score INT NOT NULL DEFAULT 0,

risk_level VARCHAR(255) DEFAULT 'SAFE',

reasons LONGTEXT DEFAULT NULL,

followers_count INT NOT NULL DEFAULT 0,

following_count INT NOT NULL DEFAULT 0,

tweet_count INT NOT NULL DEFAULT 0,

listed_count INT NOT NULL DEFAULT 0,

account_created_at VARCHAR(255) DEFAULT NULL,

action_taken VARCHAR(255) DEFAULT 'NONE',

scan_type VARCHAR(255) DEFAULT 'AUTO_SCAN',

raw_json LONGTEXT DEFAULT NULL,

detected_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY (id),

UNIQUE KEY uniq_owner_target
(owner_x_id,target_x_id),

KEY idx_risk_level (risk_level),

KEY idx_score (score),

KEY idx_action_taken (action_taken)

) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;

/* =========================================================
SYSTEM LOGS
========================================================= */

CREATE TABLE IF NOT EXISTS system_logs (

id INT NOT NULL AUTO_INCREMENT,

log_type VARCHAR(255) DEFAULT NULL,

message LONGTEXT DEFAULT NULL,

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY (id),

KEY idx_log_type (log_type)

) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;

index.php

PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ] ); } catch (Throwable $e) { exit('DB ERROR : ' . $e->getMessage()); } /* ========================================================= HELPERS ========================================================= */ function h($value) { return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); } function addLog(PDO $pdo, string $type, string $message): void { $stmt = $pdo->prepare(" INSERT INTO system_logs ( log_type, message ) VALUES ( ?, ? ) "); $stmt->execute([$type, $message]); } function apiRequest(string $url, string $accessToken, string $method = 'GET', ?array $payload = null): array { $ch = curl_init(); $headers = [ 'Authorization: Bearer ' . $accessToken, 'Accept: application/json', ]; $options = [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_HTTPHEADER => $headers, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]; if ($method !== 'GET') { $options[CURLOPT_CUSTOMREQUEST] = $method; if ($payload !== null) { $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $headers[] = 'Content-Type: application/json'; $options[CURLOPT_HTTPHEADER] = $headers; } } curl_setopt_array($ch, $options); $raw = curl_exec($ch); $curlError = curl_error($ch); $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $json = null; if (is_string($raw) && $raw !== '') { $json = json_decode($raw, true); } return [ 'ok' => ($curlError === '' && $httpCode >= 200 && $httpCode < 300), 'http_code' => $httpCode, 'raw' => $raw, 'json' => is_array($json) ? $json : null, 'curl_error' => $curlError, ]; } function buildUrl(string $base, array $params = []): string { if (!$params) { return $base; } return $base . (str_contains($base, '?') ? '&' : '?') . http_build_query($params); } function createCodeChallenge(string $verifier): string { return rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '='); } function detectBotScore(array $user): array { $score = 0; $reasons = []; $followers = (int)($user['public_metrics']['followers_count'] ?? 0); $following = (int)($user['public_metrics']['following_count'] ?? 0); $tweets = (int)($user['public_metrics']['tweet_count'] ?? 0); $listed = (int)($user['public_metrics']['listed_count'] ?? 0); $description = trim((string)($user['description'] ?? '')); $verified = (bool)($user['verified'] ?? false); $protected = (bool)($user['protected'] ?? false); $username = strtolower((string)($user['username'] ?? '')); $createdAt = (string)($user['created_at'] ?? ''); $createdTs = $createdAt ? strtotime($createdAt) : false; $daysOld = $createdTs ? (int)floor((time() - $createdTs) / 86400) : 99999; $ratio = 0.0; if ($followers > 0) { $ratio = $following / max($followers, 1); } elseif ($following > 0) { $ratio = 999.0; } if ($followers === 0 && $following > 100) { $score += 20; $reasons[] = 'フォロワー0でフォロー過多'; } if ($following >= 2000) { $score += 25; $reasons[] = 'フォロー数が多すぎる'; } elseif ($following >= 1000) { $score += 15; $reasons[] = 'フォロー数が多い'; } if ($followers > 0 && $ratio >= 20) { $score += 20; $reasons[] = 'フォロー比率が不自然'; } if ($tweets < 3) { $score += 20; $reasons[] = '投稿が極端に少ない'; } elseif ($tweets < 10) { $score += 10; $reasons[] = '投稿数が少ない'; } if ($tweets > 50000) { $score += 10; $reasons[] = '投稿数が極端に多い'; } if ($description === '') { $score += 10; $reasons[] = '自己紹介が空'; } if (preg_match('/https?:\\/\\//i', $description)) { $score += 10; $reasons[] = '自己紹介にURL'; } if (preg_match('/[0-9]{5,}/', $username)) { $score += 10; $reasons[] = 'ユーザー名に長い数字列'; } if (preg_match('/^[a-z]{1,3}\\d{4,}$/i', $username)) { $score += 10; $reasons[] = 'ランダム文字列っぽい名前'; } if ($daysOld <= 7) { $score += 25; $reasons[] = '作成1週間以内'; } elseif ($daysOld <= 30) { $score += 15; $reasons[] = '新規アカウント'; } if ($listed === 0 && $followers < 20 && $daysOld <= 60) { $score += 10; $reasons[] = '新規かつリスト登録なし'; } if ($followers < 5 && $following > 50) { $score += 10; $reasons[] = 'フォロワーが極端に少ない'; } if ($protected && $followers < 5 && $following > 50) { $score += 5; $reasons[] = '保護状態だが挙動が不自然'; } if ($verified) { $score -= 20; } if ($followers > 1000) { $score -= 10; } if ($daysOld > 365) { $score -= 10; } if ($score < 0) { $score = 0; } if ($score > 100) { $score = 100; } $risk = 'SAFE'; if ($score >= 80) { $risk = 'CRITICAL'; } elseif ($score >= 60) { $risk = 'HIGH_RISK'; } elseif ($score >= 40) { $risk = 'MEDIUM_RISK'; } elseif ($score >= 20) { $risk = 'LOW_RISK'; } return [ 'score' => $score, 'risk' => $risk, 'reasons' => implode(' / ', $reasons), ]; } function saveOrUpdateUser(PDO $pdo, array $user, string $accessToken, string $refreshToken): void { $stmt = $pdo->prepare(" INSERT INTO users ( x_id, name, username, avatar, access_token, refresh_token, country, region_name, ip_address, last_scan_at ) VALUES ( :x_id, :name, :username, :avatar, :access_token, :refresh_token, NULL, NULL, NULL, NULL ) ON DUPLICATE KEY UPDATE name = VALUES(name), username = VALUES(username), avatar = VALUES(avatar), access_token = VALUES(access_token), refresh_token = VALUES(refresh_token) "); $stmt->execute([ ':x_id' => $user['id'] ?? null, ':name' => $user['name'] ?? null, ':username' => $user['username'] ?? null, ':avatar' => $user['profile_image_url'] ?? null, ':access_token' => $accessToken, ':refresh_token' => $refreshToken, ]); } function fetchUserById(array $list, string $id): ?array { foreach ($list as $user) { if (($user['id'] ?? '') === $id) { return $user; } } return null; } function blockUser(string $ownerId, string $targetId, string $accessToken): array { $url = 'https://api.x.com/2/users/' . rawurlencode($ownerId) . '/blocking'; return apiRequest($url, $accessToken, 'POST', [ 'target_user_id' => $targetId, ]); } function insertDetectedBot(PDO $pdo, array $owner, array $target, array $bot, string $action, string $scanType): void { $stmt = $pdo->prepare(" INSERT INTO detected_bots ( owner_x_id, target_x_id, target_name, target_username, score, risk_level, reasons, followers_count, following_count, tweet_count, listed_count, account_created_at, action_taken, scan_type ) VALUES ( :owner_x_id, :target_x_id, :target_name, :target_username, :score, :risk_level, :reasons, :followers_count, :following_count, :tweet_count, :listed_count, :account_created_at, :action_taken, :scan_type ) ON DUPLICATE KEY UPDATE target_name = VALUES(target_name), target_username = VALUES(target_username), score = VALUES(score), risk_level = VALUES(risk_level), reasons = VALUES(reasons), followers_count = VALUES(followers_count), following_count = VALUES(following_count), tweet_count = VALUES(tweet_count), listed_count = VALUES(listed_count), account_created_at = VALUES(account_created_at), action_taken = VALUES(action_taken), scan_type = VALUES(scan_type), detected_at = CURRENT_TIMESTAMP "); $stmt->execute([ ':owner_x_id' => $owner['id'], ':target_x_id' => $target['id'], ':target_name' => $target['name'] ?? '', ':target_username' => $target['username'] ?? '', ':score' => $bot['score'], ':risk_level' => $bot['risk'], ':reasons' => $bot['reasons'], ':followers_count' => (int)($target['public_metrics']['followers_count'] ?? 0), ':following_count' => (int)($target['public_metrics']['following_count'] ?? 0), ':tweet_count' => (int)($target['public_metrics']['tweet_count'] ?? 0), ':listed_count' => (int)($target['public_metrics']['listed_count'] ?? 0), ':account_created_at' => $target['created_at'] ?? '', ':action_taken' => $action, ':scan_type' => $scanType, ]); } function fetchAllUsersFromList(PDO $pdo, string $ownerId, string $accessToken, string $endpoint, string $scanType): array { $all = []; $seen = []; $pageToken = null; $pageCount = 0; $errors = []; do { $url = buildUrl($endpoint, [ 'max_results' => 100, 'user.fields' => 'profile_image_url,public_metrics,created_at,description,verified,protected', 'pagination_token' => $pageToken, ]); $res = apiRequest($url, $accessToken, 'GET'); if (!$res['ok'] || !isset($res['json'])) { $errors[] = [ 'scan_type' => $scanType, 'http_code' => $res['http_code'], 'curl_error' => $res['curl_error'], 'raw' => $res['raw'], ]; addLog( $pdo, 'API_ERROR', $scanType . ' 取得失敗 / HTTP ' . $res['http_code'] . ' / ' . ($res['curl_error'] ?: 'NO_CURL_ERROR') ); break; } $data = $res['json']['data'] ?? []; if (!is_array($data)) { $data = []; } foreach ($data as $user) { $id = (string)($user['id'] ?? ''); if ($id === '' || $id === $ownerId) { continue; } if (isset($seen[$id])) { continue; } $seen[$id] = true; $all[] = $user; } $pageToken = $res['json']['meta']['next_token'] ?? null; $pageCount++; if ($pageCount >= MAX_PAGES_PER_LIST) { break; } } while (!empty($pageToken)); return [ 'users' => $all, 'errors' => $errors, ]; } function scanForBots(PDO $pdo, array $me, string $accessToken): array { $owner = $me['data'] ?? []; $ownerId = (string)($owner['id'] ?? ''); if ($ownerId === '') { return [ 'scanned' => 0, 'detected' => 0, 'blocked' => 0, 'errors' => ['Authenticated user ID not found.'], ]; } $summary = [ 'scanned' => 0, 'detected' => 0, 'blocked' => 0, 'errors' => [], ]; $lists = [ [ 'type' => 'FOLLOWERS_SCAN', 'endpoint' => 'https://api.x.com/2/users/' . rawurlencode($ownerId) . '/followers', ], [ 'type' => 'FOLLOWING_SCAN', 'endpoint' => 'https://api.x.com/2/users/' . rawurlencode($ownerId) . '/following', ], ]; $globalSeen = []; foreach ($lists as $list) { $result = fetchAllUsersFromList($pdo, $ownerId, $accessToken, $list['endpoint'], $list['type']); if (!empty($result['errors'])) { foreach ($result['errors'] as $err) { $summary['errors'][] = $list['type'] . ' API error'; } } foreach ($result['users'] as $user) { $targetId = (string)($user['id'] ?? ''); if ($targetId === '' || isset($globalSeen[$targetId])) { continue; } $globalSeen[$targetId] = true; $summary['scanned']++; $bot = detectBotScore($user); if ($bot['score'] < MIN_SCORE_TO_SAVE) { continue; } $action = 'MONITOR'; if ($bot['score'] >= MIN_SCORE_TO_BLOCK) { if (ENABLE_AUTO_BLOCK) { $blockRes = blockUser($ownerId, $targetId, $accessToken); if ($blockRes['ok']) { $action = 'BLOCK'; $summary['blocked']++; } else { $action = 'BLOCK_FAILED'; $summary['errors'][] = 'BLOCK_FAILED:' . ($blockRes['http_code'] ?: '0'); addLog( $pdo, 'BLOCK_ERROR', '@' . ($user['username'] ?? '') . ' のブロック失敗 / HTTP ' . ($blockRes['http_code'] ?: '0') ); } } else { $action = 'BLOCK_DISABLED'; } } insertDetectedBot($pdo, $owner, $user, $bot, $action, $list['type']); $summary['detected']++; addLog( $pdo, 'BOT_SCAN', '@' . ($user['username'] ?? '') . ' を検知 / score=' . $bot['score'] . ' / ' . $bot['risk'] ); } } $update = $pdo->prepare(" UPDATE users SET last_scan_at = NOW() WHERE x_id = ? "); $update->execute([$ownerId]); return $summary; } /* ========================================================= LOGIN ========================================================= */ if (isset($_GET['login'])) { $state = bin2hex(random_bytes(16)); $_SESSION['oauth_state'] = $state; $codeVerifier = bin2hex(random_bytes(32)); $_SESSION['code_verifier'] = $codeVerifier; $codeChallenge = createCodeChallenge($codeVerifier); $scope = implode(' ', [ 'tweet.read', 'users.read', 'follows.read', 'block.read', 'block.write', 'offline.access', ]); $authUrl = buildUrl('https://x.com/i/oauth2/authorize', [ 'response_type' => 'code', 'client_id' => CLIENT_ID, 'redirect_uri' => REDIRECT_URI, 'scope' => $scope, 'state' => $state, 'code_challenge' => $codeChallenge, 'code_challenge_method' => 'S256', ]); header('Location: ' . $authUrl); exit; } /* ========================================================= CALLBACK ========================================================= */ if (isset($_GET['code'])) { if (!isset($_SESSION['oauth_state'], $_SESSION['code_verifier'])) { exit('STATE ERROR'); } if (!isset($_GET['state']) || !hash_equals($_SESSION['oauth_state'], (string)$_GET['state'])) { exit('INVALID STATE'); } $tokenUrl = 'https://api.x.com/2/oauth2/token'; $postFields = [ 'code' => (string)$_GET['code'], 'grant_type' => 'authorization_code', 'client_id' => CLIENT_ID, 'redirect_uri' => REDIRECT_URI, 'code_verifier' => $_SESSION['code_verifier'], ]; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $tokenUrl, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($postFields), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_USERPWD => CLIENT_ID . ':' . CLIENT_SECRET, CURLOPT_HTTPHEADER => [ 'Content-Type: application/x-www-form-urlencoded', 'Accept: application/json', ], CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); $response = curl_exec($ch); $curlError = curl_error($ch); curl_close($ch); if ($response === false || $curlError !== '') { exit('TOKEN ERROR : ' . ($curlError ?: 'unknown')); } $tokenData = json_decode($response, true); if (!is_array($tokenData) || !isset($tokenData['access_token'])) { exit('TOKEN ERROR : ' . h($response)); } $_SESSION['access_token'] = $tokenData['access_token']; $_SESSION['refresh_token'] = $tokenData['refresh_token'] ?? ''; $meRes = apiRequest( 'https://api.x.com/2/users/me?user.fields=profile_image_url,public_metrics,created_at,description,verified,protected', $_SESSION['access_token'] ); if (!isset($meRes['json']['data'])) { exit('ME ERROR : ' . h($meRes['raw'] ?? 'NO RESPONSE')); } $_SESSION['user'] = $meRes['json']['data']; saveOrUpdateUser($pdo, $_SESSION['user'], $_SESSION['access_token'], $_SESSION['refresh_token']); addLog($pdo, 'LOGIN', '@' . ($_SESSION['user']['username'] ?? '') . ' がログイン'); header('Location: index.php'); exit; } /* ========================================================= LOGOUT ========================================================= */ if (isset($_GET['logout'])) { session_destroy(); header('Location: index.php'); exit; } /* ========================================================= AUTO SCAN ========================================================= */ $scanSummary = [ 'scanned' => 0, 'detected' => 0, 'blocked' => 0, 'errors' => [], ]; if (isset($_SESSION['access_token'], $_SESSION['user'])) { $meRes = apiRequest( 'https://api.x.com/2/users/me?user.fields=profile_image_url,public_metrics,created_at,description,verified,protected', $_SESSION['access_token'] ); if (isset($meRes['json']['data'])) { $_SESSION['user'] = $meRes['json']['data']; saveOrUpdateUser($pdo, $_SESSION['user'], $_SESSION['access_token'], $_SESSION['refresh_token'] ?? ''); $scanSummary = scanForBots($pdo, $meRes['json'], $_SESSION['access_token']); } else { $scanSummary['errors'][] = 'ME API ERROR'; addLog($pdo, 'API_ERROR', 'users/me 取得失敗'); } } /* ========================================================= DATA ========================================================= */ $totalUsers = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(); $totalBots = (int)$pdo->query("SELECT COUNT(*) FROM detected_bots")->fetchColumn(); $totalBlocked = (int)$pdo->query(" SELECT COUNT(*) FROM detected_bots WHERE action_taken = 'BLOCK' ")->fetchColumn(); $dangerBots = $pdo->query(" SELECT * FROM detected_bots ORDER BY score DESC, id DESC LIMIT 30 ")->fetchAll(); $latestLogs = $pdo->query(" SELECT * FROM system_logs ORDER BY id DESC LIMIT 10 ")->fetchAll(); ?> Guardian Shield
Guardian Shield

Guardian Shield

独自BOT検知エンジンで Xアカウントを自動巡回監視し、 危険アカウントを検知・記録・必要に応じてブロックします。

● AUTO BOT SCAN ACTIVE
ブロック機能は X 側の権限・プラン条件により失敗することがあります。
総ユーザー
= number_format($totalUsers) ?>
検知Bot
= number_format($totalBots) ?>
Bot排除数
= number_format($totalBlocked) ?>
今回スキャン
= number_format($scanSummary['scanned']) ?>
検知 = number_format($scanSummary['detected']) ?> / ブロック = number_format($scanSummary['blocked']) ?>
今回のスキャン結果
WARN = h(implode(' / ', array_unique($scanSummary['errors']))) ?>
スキャン対象が取得できませんでした。ログイン後、権限とAPIレスポンスを確認してください。
取得ユーザー数 = number_format($scanSummary['scanned']) ?> / 検知 = number_format($scanSummary['detected']) ?> / ブロック = number_format($scanSummary['blocked']) ?>
危険Bot検知一覧
= h($bot['target_name'] ?? '') ?>
@= h($bot['target_username'] ?? '') ?>
= h($bot['risk_level'] ?? 'SAFE') ?> / = (int)($bot['score'] ?? 0) ?>
= h($bot['reasons'] ?? '') ?>
ACTION: = h($bot['action_taken'] ?? '') ?> / SCAN: = h($bot['scan_type'] ?? '') ?>
Botは検知されていません
最新ログ
= h($log['log_type'] ?? '') ?> = h($log['message'] ?? '') ?>
= h($log['created_at'] ?? '') ?>
ログはまだありません。
© 2026 Guardian Shield
0 likes

Xで取得するAPI Key、API Secret、Access Token、Access Token Secretのどれかが間違っていませんか?

0Like

Your answer might help someone💌