Add Global Chat Options

Add Global Chat Options

1. emoji
2. Poll
3. Delete message
4. Edit message
This commit is contained in:
novgorodschi catalin
2026-09-09 11:49:59 +03:00
parent 009bba0ce6
commit 483f0ae30c
8 changed files with 1052 additions and 35 deletions
@@ -12,6 +12,10 @@
## Cerut de Catalin, 07.09.2026: chat general vizibil/scris de orice ##
## jucator logat, cu moderare pentru Admin (access 9) si MH (access 8). ##
## ##
## Faza 2 (09.09.2026): stergere mesaj (mod), editare mesaj propriu (user), ##
## sondaje (chat_global_polls / chat_global_poll_votes) si suport emoji ##
## (chat_global trecut pe utf8mb4 - vezi struct.sql). ##
## ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
## URLs: https://travianz.org ##
@@ -79,9 +83,10 @@ trait DatabaseGlobalChatQueries {
* singur JOIN (nu o interogare per mesaj) - reflecta mereu alianta
* curenta a jucatorului, nu una "inghetata" la momentul postarii.
*/
function getGlobalChatMessages($limit = 30, $sinceId = 0) {
function getGlobalChatMessages($limit = 30, $sinceId = 0, $viewerUid = 0) {
$limit = (int) $limit;
$sinceId = (int) $sinceId;
$viewerUid = (int) $viewerUid;
$where = $sinceId > 0 ? "WHERE c.id > $sinceId" : "";
$order = $sinceId > 0 ? "ORDER BY c.id ASC" : "ORDER BY c.id DESC";
@@ -90,7 +95,10 @@ trait DatabaseGlobalChatQueries {
// browser pe mobil), la revenire nu vrem un SELECT nemarginit
$limitSql = "LIMIT " . ($sinceId > 0 ? 200 : $limit);
$q = "SELECT c.id, c.id_user, c.date, c.msg, u.username, u.access, a.tag AS ally_tag, a.id AS ally_id
// Faza 2: c.type/c.poll_id/c.deleted/c.edited - vezi editGlobalChatMessage(),
// deleteGlobalChatMessage() si createGlobalChatPoll() mai jos
$q = "SELECT c.id, c.id_user, c.date, c.msg, c.type, c.poll_id, c.deleted, c.edited,
u.username, u.access, a.tag AS ally_tag, a.id AS ally_id
FROM " . TB_PREFIX . "chat_global c
LEFT JOIN " . TB_PREFIX . "users u ON u.id = c.id_user
LEFT JOIN " . TB_PREFIX . "alidata a ON a.id = u.alliance AND u.alliance > 0
@@ -106,9 +114,303 @@ trait DatabaseGlobalChatQueries {
$rows = array_reverse($rows);
}
foreach ($rows as &$row) {
if ((int) $row['deleted'] === 1) {
// golim textul server-side (nu doar in client) - un mesaj sters de
// moderator (de obicei vulgar) nu trebuie sa mai ajunga deloc, sub
// nicio forma, in raspunsul JSON catre ceilalti clienti
$row['msg'] = '';
} elseif ($row['type'] === 'poll' && $row['poll_id']) {
$row['poll'] = $this->getGlobalChatPollData($row['poll_id'], $viewerUid);
}
}
unset($row);
return $rows;
}
/**
* Editeaza un mesaj propriu (text simplu, nu sondaje) - vezi
* deleteGlobalChatMessage() pentru actiunea de moderare (mesajul altcuiva).
* Fara limita de timp la editare (nu a fost ceruta); usor de adaugat o
* fereastra (ex. doar primele 5 minute) daca se doreste ulterior.
*
* @return array ['ok'=>bool, 'reason'=>string|null]
*/
function editGlobalChatMessage($uid, $msgId, $newMsg) {
$uid = (int) $uid;
$msgId = (int) $msgId;
$newMsg = trim((string) $newMsg);
if ($uid <= 0 || $msgId <= 0) {
return ['ok' => false, 'reason' => 'invalid'];
}
if ($newMsg === '') {
return ['ok' => false, 'reason' => 'empty'];
}
$newMsg = function_exists('mb_substr') ? mb_substr($newMsg, 0, 250) : substr($newMsg, 0, 250);
$row = mysqli_fetch_assoc($this->query(
"SELECT id_user, type, deleted FROM " . TB_PREFIX . "chat_global WHERE id = $msgId"
));
if (!$row) {
return ['ok' => false, 'reason' => 'notfound'];
}
// doar propriul mesaj, doar daca nu a fost deja sters de un moderator,
// si doar mesaje text (ancora unui sondaj nu se editeaza ca text simplu)
if ((int) $row['id_user'] !== $uid || (int) $row['deleted'] === 1 || $row['type'] !== 'text') {
return ['ok' => false, 'reason' => 'forbidden'];
}
list($emsg) = $this->escape_input($newMsg);
$now = time();
$this->query(
"UPDATE " . TB_PREFIX . "chat_global SET msg = '$emsg', edited = 1, updated_at = $now WHERE id = $msgId"
);
return ['ok' => true];
}
/**
* Sterge (soft-delete) un mesaj din chat-ul general - actiune de moderare.
* Textul e golit aici (nu doar ascuns in client), acelasi motiv ca la
* golirea din getGlobalChatMessages() mai sus. Verificarea de rang (MH/
* Admin, access minim) se face in ajax.php, la fel ca la mute/block - DAR
* spre deosebire de mute/block, aici NU se compara rangul cu al autorului:
* stergerea vizeaza continutul (un mesaj vulgar), nu persoana, deci orice
* MH/Admin poate sterge orice mesaj, inclusiv al altui MH, ca sa poata
* curata rapid chat-ul.
*
* @return bool
*/
function deleteGlobalChatMessage($msgId) {
$msgId = (int) $msgId;
$now = time();
return $this->query(
"UPDATE " . TB_PREFIX . "chat_global SET msg = '', deleted = 1, updated_at = $now WHERE id = $msgId AND deleted = 0"
) ? true : false;
}
/**
* Mesaje editate/sterse de la ultimul cec al clientului ($sinceTs, unix) -
* necesar ca sa se poata actualiza "in loc" mesajele deja randate la un
* client care are panoul de chat deschis de mai mult timp: poll-ul normal
* de mesaje noi (getGlobalChatMessages, dupa id) nu ar mai prinde o
* editare/stergere pe un mesaj vechi, deja afisat, cu id mai mic decat
* ultimul id vazut de client.
*
* Include si datele proaspete de sondaj pentru randurile de tip 'poll'
* (ex. cand cineva voteaza, votul nu schimba mesajul in sine, dar
* voteGlobalChatPoll() atinge acest rand exact ca sa fie prins aici -
* altfel doar cel care voteaza ar vedea rezultatul actualizat).
*
* @return array ['rows'=>array, 'now'=>int] - 'now' devine noul watermark pe client
*/
function getGlobalChatUpdates($sinceTs, $viewerUid = 0, $limit = 100) {
$sinceTs = (int) $sinceTs;
$viewerUid = (int) $viewerUid;
$now = time();
$limit = (int) $limit;
$rows = $this->mysqli_fetch_all($this->query(
"SELECT id, msg, type, poll_id, deleted, edited FROM " . TB_PREFIX . "chat_global
WHERE updated_at > $sinceTs AND updated_at <= $now
ORDER BY updated_at ASC
LIMIT $limit"
));
foreach ($rows as &$row) {
if ((int) $row['deleted'] === 1) {
$row['msg'] = '';
} elseif ($row['type'] === 'poll' && $row['poll_id']) {
$row['poll'] = $this->getGlobalChatPollData($row['poll_id'], $viewerUid);
}
}
unset($row);
return ['rows' => $rows, 'now' => $now];
}
/**
* Creeaza un sondaj in chat-ul general: un rand-ancora in chat_global
* (type='poll', ca sa-si pastreze locul cronologic in flux de mesaje) +
* randul cu intrebarea/optiunile in chat_global_polls. Reutilizeaza
* acelasi mute-check ca postGlobalChatMessage (un user mutat nu poate
* nici posta mesaje, nici crea sondaje).
*
* @param array $options 2-6 optiuni (string-uri) - restul sunt ignorate
* @return array ['ok'=>bool, 'reason'=>string|null]
*/
function createGlobalChatPoll($uid, $question, $options) {
$uid = (int) $uid;
$question = trim((string) $question);
$question = function_exists('mb_substr') ? mb_substr($question, 0, 200) : substr($question, 0, 200);
if ($uid <= 0 || $question === '') {
return ['ok' => false, 'reason' => 'invalid'];
}
$clean = [];
foreach ((array) $options as $opt) {
$opt = trim((string) $opt);
if ($opt === '') {
continue;
}
$clean[] = function_exists('mb_substr') ? mb_substr($opt, 0, 60) : substr($opt, 0, 60);
if (count($clean) >= 6) {
break;
}
}
if (count($clean) < 2) {
return ['ok' => false, 'reason' => 'notenoughoptions'];
}
$mutedUntil = $this->getGlobalChatMuteStatus($uid);
if ($mutedUntil !== null) {
return ['ok' => false, 'reason' => 'muted', 'mutedUntil' => $mutedUntil];
}
list($euid) = $this->escape_input($uid);
list($eq) = $this->escape_input($question);
$now = time();
// 1) randul-ancora in chat_global (msg = intrebarea - fallback util daca
// ceva citeste chat_global fara sa stie de chat_global_polls)
$this->query(
"INSERT INTO " . TB_PREFIX . "chat_global (id_user, date, msg, type) VALUES ($euid, $now, '$eq', 'poll')"
);
$chatId = mysqli_insert_id($this->dblink);
// 2) intrebarea + optiunile (JSON)
list($eOptions) = $this->escape_input(json_encode(array_values($clean)));
$this->query(
"INSERT INTO " . TB_PREFIX . "chat_global_polls (chat_id, id_user, question, options, created)
VALUES ($chatId, $euid, '$eq', '$eOptions', $now)"
);
$pollId = mysqli_insert_id($this->dblink);
// 3) leaga ancora de randul de sondaj
$this->query(
"UPDATE " . TB_PREFIX . "chat_global SET poll_id = $pollId WHERE id = $chatId"
);
return ['ok' => true];
}
/**
* Inregistreaza votul unui user la un sondaj din chat-ul general. Userul
* isi poate schimba optiunea (ON DUPLICATE KEY UPDATE) - nu exista un
* "vot definitiv", nefiind cerut.
*
* @return array ['ok'=>bool, 'reason'=>string|null]
*/
function voteGlobalChatPoll($uid, $pollId, $optionIndex) {
$uid = (int) $uid;
$pollId = (int) $pollId;
$optionIndex = (int) $optionIndex;
if ($uid <= 0 || $pollId <= 0 || $optionIndex < 0) {
return ['ok' => false, 'reason' => 'invalid'];
}
$poll = mysqli_fetch_assoc($this->query(
"SELECT chat_id, options FROM " . TB_PREFIX . "chat_global_polls WHERE id = $pollId"
));
if (!$poll) {
return ['ok' => false, 'reason' => 'notfound'];
}
$options = json_decode($poll['options'], true);
if (!is_array($options) || !isset($options[$optionIndex])) {
return ['ok' => false, 'reason' => 'invalid'];
}
$now = time();
$this->query(
"INSERT INTO " . TB_PREFIX . "chat_global_poll_votes (poll_id, id_user, option_index, voted_at)
VALUES ($pollId, $uid, $optionIndex, $now)
ON DUPLICATE KEY UPDATE option_index = $optionIndex, voted_at = $now"
);
// atinge randul-ancora din chat_global ca votul sa fie prins de
// getGlobalChatUpdates() (watermark-ul de editari/stergeri) - altfel
// doar userul care a votat ar vedea rezultatul actualizat, ceilalti
// clienti cu sondajul deja afisat ar ramane cu numaratoarea veche
$chatId = (int) $poll['chat_id'];
$this->query(
"UPDATE " . TB_PREFIX . "chat_global SET updated_at = $now WHERE id = $chatId"
);
return ['ok' => true];
}
/**
* Rezultatele unui sondaj + optiunea aleasa de $viewerUid (daca a votat).
* Apelata per-rand din getGlobalChatMessages() pentru mesajele de tip
* 'poll' - sondajele sunt rare fata de mesajele normale, deci interogarile
* suplimentare (N+1) sunt neglijabile aici.
*/
function getGlobalChatPollData($pollId, $viewerUid) {
$pollId = (int) $pollId;
$viewerUid = (int) $viewerUid;
$poll = mysqli_fetch_assoc($this->query(
"SELECT id, question, options FROM " . TB_PREFIX . "chat_global_polls WHERE id = $pollId"
));
if (!$poll) {
return null;
}
$options = json_decode($poll['options'], true);
if (!is_array($options)) {
$options = [];
}
$counts = array_fill(0, count($options), 0);
$voteRows = $this->mysqli_fetch_all($this->query(
"SELECT option_index, COUNT(*) AS c FROM " . TB_PREFIX . "chat_global_poll_votes
WHERE poll_id = $pollId GROUP BY option_index"
));
$total = 0;
foreach ($voteRows as $vr) {
$idx = (int) $vr['option_index'];
if (isset($counts[$idx])) {
$counts[$idx] = (int) $vr['c'];
}
$total += (int) $vr['c'];
}
$myVote = null;
if ($viewerUid > 0) {
$mv = mysqli_fetch_assoc($this->query(
"SELECT option_index FROM " . TB_PREFIX . "chat_global_poll_votes
WHERE poll_id = $pollId AND id_user = $viewerUid"
));
if ($mv) {
$myVote = (int) $mv['option_index'];
}
}
return [
'id' => (int) $poll['id'],
'question' => $poll['question'],
'options' => $options,
'counts' => $counts,
'total' => $total,
'myVote' => $myVote,
];
}
/**
* Timestamp-ul (unix) pana la care userul e mutat din chat-ul general,
* sau null daca nu e mutat (inclusiv daca o mutare veche a expirat).