349 lines
13 KiB
PHP
349 lines
13 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
define('IM_ADMIN_URL', getenv('IM_ADMIN_URL') ?: 'http://127.0.0.1:18080');
|
|
define('IM_ADMIN_SECRET', getenv('IM_ADMIN_SECRET') ?: '');
|
|
define('IM_DB_DSN', getenv('IM_DB_DSN') ?: 'mysql:host=127.0.0.1;port=3306;dbname=wfchat;charset=utf8mb4');
|
|
define('IM_DB_USER', getenv('IM_DB_USER') ?: '');
|
|
define('IM_DB_PASSWORD', getenv('IM_DB_PASSWORD') ?: '');
|
|
define('IM_APP_DB_DSN', getenv('IM_APP_DB_DSN') ?: 'mysql:host=127.0.0.1;port=3306;dbname=appdata;charset=utf8mb4');
|
|
define('IM_APP_DB_USER', getenv('IM_APP_DB_USER') ?: '');
|
|
define('IM_APP_DB_PASSWORD', getenv('IM_APP_DB_PASSWORD') ?: '');
|
|
define('IM_DB_TIMEZONE', getenv('IM_DB_TIMEZONE') ?: 'Asia/Bangkok');
|
|
|
|
function respond(int $code, string $message, array $result = []): void
|
|
{
|
|
echo json_encode([
|
|
'code' => $code,
|
|
'message' => $message,
|
|
'result' => $result,
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
function readJsonPayload(): array
|
|
{
|
|
$payload = json_decode(file_get_contents('php://input') ?: '[]', true);
|
|
return is_array($payload) ? $payload : [];
|
|
}
|
|
|
|
function imAdminPost(string $path, array $payload): array
|
|
{
|
|
$nonce = (string)random_int(1, 1000000);
|
|
$timestamp = (string)((int)floor(microtime(true) * 1000));
|
|
$sign = sha1($nonce . '|' . IM_ADMIN_SECRET . '|' . $timestamp);
|
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
$curl = curl_init(IM_ADMIN_URL . $path);
|
|
if ($curl === false) {
|
|
throw new RuntimeException('IM admin client init failed');
|
|
}
|
|
|
|
curl_setopt_array($curl, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $body,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0,
|
|
CURLOPT_CONNECTTIMEOUT_MS => 1000,
|
|
CURLOPT_TIMEOUT_MS => 4000,
|
|
CURLOPT_FRESH_CONNECT => true,
|
|
CURLOPT_FORBID_REUSE => true,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json; charset=utf-8',
|
|
'Connection: close',
|
|
'nonce: ' . $nonce,
|
|
'timestamp: ' . $timestamp,
|
|
'sign: ' . $sign,
|
|
],
|
|
]);
|
|
|
|
$responseBody = curl_exec($curl);
|
|
$curlError = curl_error($curl);
|
|
curl_close($curl);
|
|
|
|
if ($responseBody === false || $responseBody === '') {
|
|
throw new RuntimeException($curlError ?: 'IM admin request failed');
|
|
}
|
|
|
|
$data = json_decode($responseBody, true);
|
|
if (!is_array($data)) {
|
|
throw new RuntimeException('Invalid IM admin response');
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
function requireImSuccess(array $response, string $fallbackMessage): array
|
|
{
|
|
if (($response['code'] ?? -1) !== 0) {
|
|
throw new RuntimeException((string)($response['msg'] ?? $response['message'] ?? $fallbackMessage));
|
|
}
|
|
|
|
return is_array($response['result'] ?? null) ? $response['result'] : [];
|
|
}
|
|
|
|
function imDb(): PDO
|
|
{
|
|
static $pdo = null;
|
|
if ($pdo instanceof PDO) {
|
|
return $pdo;
|
|
}
|
|
|
|
$pdo = new PDO(IM_DB_DSN, IM_DB_USER, IM_DB_PASSWORD, [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]);
|
|
|
|
return $pdo;
|
|
}
|
|
|
|
function imAppDb(): PDO
|
|
{
|
|
static $pdo = null;
|
|
if ($pdo instanceof PDO) {
|
|
return $pdo;
|
|
}
|
|
|
|
$pdo = new PDO(IM_APP_DB_DSN, IM_APP_DB_USER, IM_APP_DB_PASSWORD, [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]);
|
|
|
|
return $pdo;
|
|
}
|
|
|
|
function ensureWebImAppTables(): void
|
|
{
|
|
static $ensured = false;
|
|
if ($ensured) {
|
|
return;
|
|
}
|
|
$ensured = true;
|
|
|
|
$db = imAppDb();
|
|
$db->exec(
|
|
"create table if not exists `t_web_groups` (
|
|
`id` bigint unsigned not null auto_increment,
|
|
`group_id` varchar(64) not null,
|
|
`category` varchar(20) not null default 'group',
|
|
`name` varchar(128) not null,
|
|
`portrait` varchar(1024) not null default '',
|
|
`owner_uid` varchar(64) not null,
|
|
`join_mode` varchar(20) not null default 'open',
|
|
`announcement` text null,
|
|
`native_group_id` varchar(64) not null default '',
|
|
`native_status` varchar(32) not null default 'pending',
|
|
`native_error` varchar(255) not null default '',
|
|
`created_by` varchar(64) not null,
|
|
`created_at_ms` bigint not null,
|
|
`updated_at_ms` bigint not null,
|
|
`deleted` tinyint not null default 0,
|
|
primary key (`id`),
|
|
unique key `uk_t_web_groups_group_id` (`group_id`),
|
|
key `idx_t_web_groups_owner` (`owner_uid`),
|
|
key `idx_t_web_groups_category` (`category`)
|
|
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci"
|
|
);
|
|
try {
|
|
$db->exec("alter table `t_web_groups` add column `welcome_text` text null after `announcement`");
|
|
} catch (Throwable) {
|
|
}
|
|
$db->exec(
|
|
"create table if not exists `t_web_group_members` (
|
|
`id` bigint unsigned not null auto_increment,
|
|
`group_id` varchar(64) not null,
|
|
`user_id` varchar(64) not null,
|
|
`role` varchar(20) not null default 'member',
|
|
`alias` varchar(128) not null default '',
|
|
`joined_at_ms` bigint not null,
|
|
`updated_at_ms` bigint not null,
|
|
`status` tinyint not null default 0,
|
|
primary key (`id`),
|
|
unique key `uk_t_web_group_members_gid_uid` (`group_id`, `user_id`),
|
|
key `idx_t_web_group_members_user` (`user_id`)
|
|
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci"
|
|
);
|
|
$db->exec(
|
|
"create table if not exists `t_web_group_messages` (
|
|
`id` bigint unsigned not null auto_increment,
|
|
`message_uid` varchar(64) not null,
|
|
`group_id` varchar(64) not null,
|
|
`sender_uid` varchar(64) not null,
|
|
`text` text not null,
|
|
`created_at_ms` bigint not null,
|
|
`edited_at_ms` bigint not null default 0,
|
|
`recalled` tinyint not null default 0,
|
|
primary key (`id`),
|
|
unique key `uk_t_web_group_messages_uid` (`message_uid`),
|
|
key `idx_t_web_group_messages_group_time` (`group_id`, `created_at_ms`),
|
|
key `idx_t_web_group_messages_sender` (`sender_uid`)
|
|
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci"
|
|
);
|
|
$db->exec(
|
|
"create table if not exists `t_web_message_attachments` (
|
|
`id` bigint unsigned not null auto_increment,
|
|
`message_uid` varchar(64) not null,
|
|
`conversation_type` varchar(16) not null default 'single',
|
|
`target_id` varchar(128) not null default '',
|
|
`sender_uid` varchar(64) not null default '',
|
|
`attachment_type` varchar(16) not null,
|
|
`url` varchar(1024) not null,
|
|
`name` varchar(255) not null default '',
|
|
`mime` varchar(128) not null default '',
|
|
`size_bytes` bigint not null default 0,
|
|
`created_at_ms` bigint not null,
|
|
primary key (`id`),
|
|
unique key `uk_t_web_message_attachments_uid` (`message_uid`),
|
|
key `idx_t_web_message_attachments_target` (`conversation_type`, `target_id`, `created_at_ms`)
|
|
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci"
|
|
);
|
|
$db->exec(
|
|
"create table if not exists `t_web_group_reads` (
|
|
`id` bigint unsigned not null auto_increment,
|
|
`group_id` varchar(64) not null,
|
|
`user_id` varchar(64) not null,
|
|
`read_at_ms` bigint not null default 0,
|
|
`updated_at_ms` bigint not null,
|
|
primary key (`id`),
|
|
unique key `uk_t_web_group_reads_gid_uid` (`group_id`, `user_id`)
|
|
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci"
|
|
);
|
|
$db->exec(
|
|
"create table if not exists `t_web_group_mute_rules` (
|
|
`id` bigint unsigned not null auto_increment,
|
|
`group_id` varchar(64) not null,
|
|
`mode` varchar(32) not null default 'off',
|
|
`schedule_type` varchar(16) not null default 'once',
|
|
`starts_at_ms` bigint not null default 0,
|
|
`ends_at_ms` bigint not null default 0,
|
|
`daily_start` varchar(8) not null default '',
|
|
`daily_end` varchar(8) not null default '',
|
|
`enabled` tinyint not null default 0,
|
|
`created_by` varchar(64) not null default '',
|
|
`updated_at_ms` bigint not null,
|
|
primary key (`id`),
|
|
unique key `uk_t_web_group_mute_rules_group` (`group_id`)
|
|
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci"
|
|
);
|
|
$db->exec(
|
|
"create table if not exists `t_web_broadcast_logs` (
|
|
`id` bigint unsigned not null auto_increment,
|
|
`sender_uid` varchar(64) not null,
|
|
`target_count` int not null default 0,
|
|
`success_count` int not null default 0,
|
|
`failed_count` int not null default 0,
|
|
`text` text not null,
|
|
`target_json` text null,
|
|
`result_json` text null,
|
|
`created_at_ms` bigint not null,
|
|
primary key (`id`),
|
|
key `idx_t_web_broadcast_logs_sender_time` (`sender_uid`, `created_at_ms`)
|
|
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci"
|
|
);
|
|
$db->exec(
|
|
"create table if not exists `t_web_user_remarks` (
|
|
`id` bigint unsigned not null auto_increment,
|
|
`owner_uid` varchar(64) not null,
|
|
`target_uid` varchar(64) not null,
|
|
`alias` varchar(128) not null default '',
|
|
`created_at_ms` bigint not null,
|
|
`updated_at_ms` bigint not null,
|
|
primary key (`id`),
|
|
unique key `uk_t_web_user_remarks_owner_target` (`owner_uid`, `target_uid`),
|
|
key `idx_t_web_user_remarks_target` (`target_uid`)
|
|
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci"
|
|
);
|
|
}
|
|
|
|
function webImPlaceholders(array $values): string
|
|
{
|
|
return implode(',', array_fill(0, count($values), '?'));
|
|
}
|
|
|
|
function fetchWebUserRemarkMap(PDO $db, string $ownerId, array $targetIds): array
|
|
{
|
|
$targetIds = array_values(array_unique(array_filter(array_map(static function ($targetId): string {
|
|
return trim((string)$targetId);
|
|
}, $targetIds))));
|
|
if ($ownerId === '' || !$targetIds) {
|
|
return [];
|
|
}
|
|
|
|
$statement = $db->prepare(
|
|
'select `target_uid`, `alias` from `t_web_user_remarks`'
|
|
. ' where `owner_uid` = ? and `target_uid` in (' . webImPlaceholders($targetIds) . ')'
|
|
);
|
|
$statement->execute(array_merge([$ownerId], $targetIds));
|
|
|
|
$remarks = [];
|
|
foreach ($statement->fetchAll() as $row) {
|
|
$remarks[(string)$row['target_uid']] = trim((string)($row['alias'] ?? ''));
|
|
}
|
|
|
|
return $remarks;
|
|
}
|
|
|
|
function saveWebUserRemark(PDO $db, string $ownerId, string $targetId, string $alias): void
|
|
{
|
|
if ($ownerId === '' || $targetId === '') {
|
|
return;
|
|
}
|
|
|
|
if ($alias === '') {
|
|
$delete = $db->prepare('delete from `t_web_user_remarks` where `owner_uid` = ? and `target_uid` = ?');
|
|
$delete->execute([$ownerId, $targetId]);
|
|
return;
|
|
}
|
|
|
|
$now = webImNowMs();
|
|
$statement = $db->prepare(
|
|
'insert into `t_web_user_remarks` (`owner_uid`, `target_uid`, `alias`, `created_at_ms`, `updated_at_ms`)'
|
|
. ' values (?, ?, ?, ?, ?)'
|
|
. ' on duplicate key update `alias` = values(`alias`), `updated_at_ms` = values(`updated_at_ms`)'
|
|
);
|
|
$statement->execute([$ownerId, $targetId, $alias, $now, $now]);
|
|
}
|
|
|
|
function webImNowMs(): int
|
|
{
|
|
return (int)floor(microtime(true) * 1000);
|
|
}
|
|
|
|
function webImMessageUid(): string
|
|
{
|
|
$timestampPart = (webImNowMs() - 1514736000000) << 21;
|
|
return (string)($timestampPart | random_int(1, 0x1FFFFF));
|
|
}
|
|
|
|
function javaStringHashCode(string $value): int
|
|
{
|
|
$hash = 0;
|
|
$length = strlen($value);
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$hash = (31 * $hash + ord($value[$i])) & 0xFFFFFFFF;
|
|
if ($hash >= 0x80000000) {
|
|
$hash -= 0x100000000;
|
|
}
|
|
}
|
|
|
|
return $hash;
|
|
}
|
|
|
|
function imUserMessagesTable(string $userId): string
|
|
{
|
|
return 't_user_messages_' . (abs(javaStringHashCode($userId)) % 128);
|
|
}
|
|
|
|
function imMessageTable(string $messageId): string
|
|
{
|
|
$timestamp = ((int)$messageId >> 21) + 1514736000000;
|
|
$date = (new DateTimeImmutable('@' . (int)floor($timestamp / 1000)))
|
|
->setTimezone(new DateTimeZone(IM_DB_TIMEZONE));
|
|
$year = (int)$date->format('Y');
|
|
$month = (int)$date->format('n') - 1;
|
|
|
|
return 't_messages_' . (($year % 3) * 12 + $month);
|
|
}
|