Added Chatbox

Added Chatbox
This commit is contained in:
novgorodschi catalin
2026-09-08 12:56:02 +03:00
parent 551c58fc3c
commit 009bba0ce6
10 changed files with 831 additions and 30 deletions
+17 -26
View File
@@ -1,20 +1,23 @@
<?php
#################################################################################
## ##
## -= YOU MUST NOT REMOVE OR CHANGE THIS NOTICE =- ##
## ##
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : admin.php ##
## Type : Admin Main Page (engine) ##
## --------------------------------------------------------------------------- ##
## Developed by : Dzoki ##
## Refactored by : Shadow ##
## Redesign by : Shadow ##
## --------------------------------------------------------------------------- ##
## Contact : cata7007@gmail.com ##
## Project : TravianZ ##
## URLs: : https://travianz.org ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
## ##
## Project: TravianZ ##
## Version: 05.03.2026 ##
## Filename: Admin/admin.php ##
## Developed by: Dzoki ##
## Refactored by: Shadow ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
## URLs: https://travianz.org ##
## https://github.com/Shadowss/TravianZ ##
## ##
#################################################################################
// ─── SESSION ─────────────────────────────────────────────────────────────────
@@ -176,11 +179,6 @@ if ($page !== '') {
break;
case 'message':
// NOTE: original code had this case duplicated (second occurrence
// overrode with 'Search IGMs/Reports'). The first definition
// ('Players Message') is intentional for the ?p=message route.
// The 'Search IGMs/Reports' label belongs to ?p=search sub-section
// which is already covered by the search template include logic.
$subpage = ADMIN_PLAYERS_MESSAGE;
break;
@@ -444,8 +442,6 @@ if ($page !== '') {
break;
case 'userlogin':
// SECURITY FIX: was raw mysqli_query with direct $_GET interpolation.
// Now uses admin_get_user_by_id() which internally uses a prepared statement.
$uid = admin_input_id($_GET, 'uid');
if ($uid !== null) {
$player = admin_get_user_by_id($uid);
@@ -458,7 +454,6 @@ if ($page !== '') {
break;
case 'userillegallog':
// SECURITY FIX: same as userlogin above.
$uid = admin_input_id($_GET, 'uid');
if ($uid !== null) {
$player = admin_get_user_by_id($uid);
@@ -500,7 +495,6 @@ if ($page !== '') {
}
break;
// ── Village-context pages (require a valid ?did=) ────────────────────
case 'village':
$did = admin_input_id($_GET, 'did');
if ($did !== null) {
@@ -525,8 +519,6 @@ if ($page !== '') {
$user = $database->getUserArray($village['owner'], 1);
$subpage = ADMIN_EDIT_RESOURCES . ' (' . e($village['name']) . ' » ' . e($user['username']) . ')';
} else {
// BUGFIX: original used $did which was only set in 'village' case,
// causing an undefined variable notice here. Now always defined above.
$subpage = ADMIN_EDIT_RESOURCES . $did . ' not found)';
$village = null;
}
@@ -568,7 +560,6 @@ if ($page !== '') {
}
break;
// ── Alliance-context pages (require a valid ?aid=) ───────────────────
case 'alliance':
$aid = admin_input_id($_GET, 'aid');
if ($aid !== null) {
@@ -1227,7 +1227,13 @@ trait AutomationBattleResolution {
*/
private function applyOwnDefenceCasualties($data, $targettribe, $battlepart) {
global $database;
$targettribe = (int)$targettribe;
// FIX: daca satul a fost sters / owner 0 / oaza fara trib, nu avem ce sterge
if($targettribe < 1 || $targettribe > 9) {
return [];
}
$owndead = [];
$unitlist = $database->getUnit($data['to'], false);
$start = ($targettribe - 1) * 10 + 1;
@@ -1319,7 +1325,7 @@ trait AutomationBattleResolution {
} else {
$tribe = 4;
}
if($tribe < 1 || $tribe > 9) $tribe = 4; // fallback la natura
$start = ($tribe - 1) * 10 + 1;
$end = ($tribe * 10);
unset($dead);
+3
View File
@@ -60,6 +60,8 @@ include_once __DIR__ . '/Database/DatabaseHeroQueries.php';
include_once __DIR__ . '/Database/DatabaseStatisticsQueries.php';
include_once __DIR__ . '/Database/DatabaseArtefactQueries.php';
include_once __DIR__ . '/Database/DatabaseSystemQueries.php';
// Chat general (server-wide), cerut de Catalin 07.09.2026 - separat de alliance chat (chat.php)
include_once __DIR__ . '/Database/DatabaseGlobalChatQueries.php';
use App\Database\IDbConnection;
use App\Utils\Math;
@@ -80,6 +82,7 @@ class MYSQLi_DB implements IDbConnection {
use DatabaseStatisticsQueries;
use DatabaseArtefactQueries;
use DatabaseSystemQueries;
use DatabaseGlobalChatQueries;
@@ -0,0 +1,176 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Project: TravianZ ##
## Filename: DatabaseGlobalChatQueries.php ##
## Purpose: Server-wide ("World Chat") messages + moderation ##
## (mute/block), separate from the existing alliance chat ##
## (chat table, see GameEngine/Chat.php). ##
## ##
## Cerut de Catalin, 07.09.2026: chat general vizibil/scris de orice ##
## jucator logat, cu moderare pentru Admin (access 9) si MH (access 8). ##
## ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
## URLs: https://travianz.org ##
## https://github.com/Shadowss/TravianZ ##
#################################################################################
trait DatabaseGlobalChatQueries {
/**
* Posteaza un mesaj in chat-ul general, daca userul nu e mutat si a
* trecut rate-limit-ul minim intre 2 mesaje (anti-spam simplu).
*
* @return array ['ok'=>bool, 'reason'=>string|null, 'mutedUntil'=>int|null]
*/
function postGlobalChatMessage($uid, $msg) {
$uid = (int) $uid;
$msg = trim((string) $msg);
if ($uid <= 0) {
return ['ok' => false, 'reason' => 'notloggedin'];
}
if ($msg === '') {
return ['ok' => false, 'reason' => 'empty'];
}
// acelasi plafon ca la chat-ul de alianta existent (chat.msg e varchar(255);
// 250 lasa loc de siguranta pentru escaping/multi-byte)
$msg = function_exists('mb_substr') ? mb_substr($msg, 0, 250) : substr($msg, 0, 250);
$mutedUntil = $this->getGlobalChatMuteStatus($uid);
if ($mutedUntil !== null) {
return ['ok' => false, 'reason' => 'muted', 'mutedUntil' => $mutedUntil];
}
list($euid) = $this->escape_input($uid);
$lastRow = mysqli_fetch_assoc($this->query(
"SELECT date FROM " . TB_PREFIX . "chat_global WHERE id_user = $euid ORDER BY id DESC LIMIT 1"
));
$now = time();
// rate-limit: minim 3 secunde intre 2 mesaje ale aceluiasi user
if ($lastRow && ($now - (int) $lastRow['date']) < 3) {
return ['ok' => false, 'reason' => 'ratelimit'];
}
list($emsg) = $this->escape_input($msg);
$this->query(
"INSERT INTO " . TB_PREFIX . "chat_global (id_user, date, msg) VALUES ($euid, $now, '$emsg')"
);
return ['ok' => true];
}
/**
* Mesajele din chat-ul general, gata de afisat (ordine cronologica).
*
* Mod 1 - incarcare initiala: ultimele $limit mesaje ($sinceId = 0).
* Mod 2 - poll incremental: doar mesajele noi, mai mari decat $sinceId
* (evita re-trimiterea ferestrei intregi la fiecare interogare din JS).
*
* Tag-ul de alianta si nivelul de access sunt rezolvate live, printr-un
* 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) {
$limit = (int) $limit;
$sinceId = (int) $sinceId;
$where = $sinceId > 0 ? "WHERE c.id > $sinceId" : "";
$order = $sinceId > 0 ? "ORDER BY c.id ASC" : "ORDER BY c.id DESC";
// plafon si pe polling-ul incremental (nu doar la incarcarea initiala) -
// daca un tab a stat minimizat/in background ore intregi (throttling de
// 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
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
$where
$order
$limitSql";
$rows = $this->mysqli_fetch_all($this->query($q));
// la incarcarea initiala am luat descrescator (ca sa prindem exact
// ultimele $limit), le intoarcem in ordine cronologica normala
if ($sinceId <= 0) {
$rows = array_reverse($rows);
}
return $rows;
}
/**
* 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).
*/
function getGlobalChatMuteStatus($uid) {
$uid = (int) $uid;
$row = mysqli_fetch_assoc($this->query(
"SELECT muted_until FROM " . TB_PREFIX . "chat_mutes WHERE id_user = $uid"
));
if (!$row) {
return null;
}
$until = (int) $row['muted_until'];
return $until > time() ? $until : null;
}
/**
* Info despre viewer-ul curent, folosita de client ca sa stie daca
* afiseaza controalele de moderare si daca dezactiveaza inputul (mutat).
*/
function getGlobalChatViewerInfo($uid) {
$uid = (int) $uid;
$access = (int) $this->getUserField($uid, 'access', 0);
return [
'uid' => $uid,
'access' => $access,
'isMod' => $access >= MULTIHUNTER,
'mutedUntil' => $this->getGlobalChatMuteStatus($uid),
];
}
/**
* Muteaza (temporar sau permanent, dupa $untilTimestamp) un user din
* chat-ul general. Ierarhia de rang (cine pe cine poate muta) se
* verifica in ajax.php inainte de a apela asta - metoda doar scrie
* starea, ca sa ramana usor de testat separat.
*/
function muteGlobalChatUser($targetUid, $mutedBy, $untilTimestamp, $reason = '') {
$targetUid = (int) $targetUid;
$mutedBy = (int) $mutedBy;
$untilTimestamp = (int) $untilTimestamp;
list($ereason) = $this->escape_input((string) $reason);
$now = time();
$q = "INSERT INTO " . TB_PREFIX . "chat_mutes (id_user, muted_until, muted_by, reason, created)
VALUES ($targetUid, $untilTimestamp, $mutedBy, '$ereason', $now)
ON DUPLICATE KEY UPDATE muted_until = $untilTimestamp, muted_by = $mutedBy, reason = '$ereason', created = $now";
return $this->query($q) ? true : false;
}
function unmuteGlobalChatUser($targetUid) {
$targetUid = (int) $targetUid;
return $this->query(
"DELETE FROM " . TB_PREFIX . "chat_mutes WHERE id_user = $targetUid"
) ? true : false;
}
}
+19
View File
@@ -4404,3 +4404,22 @@ tz_def('TRAIN_BONUS_ARTIFACT', 'Artifact bonus');
tz_def('TRAIN_BONUS_HERO', 'Hero bonus');
tz_def('TRAIN_BONUS_ALLIANCE', 'Alliance bonus');
tz_def('TRAIN_BONUS_FINAL', 'Training time');
//////////////////////////////////////////////////////////////////////////////////////////////////////
// GENERAL (SERVER-WIDE) CHAT - floating widget, footer.tpl + Templates/GlobalChat/widget.tpl
//////////////////////////////////////////////////////////////////////////////////////////////////////
tz_def('GCHAT_TITLE', 'World Chat');
tz_def('GCHAT_PLACEHOLDER', 'Message...');
tz_def('GCHAT_SEND', 'Send');
tz_def('GCHAT_EMPTY', 'No messages yet.');
tz_def('GCHAT_MUTED', 'You are muted from chat.');
tz_def('GCHAT_RATELIMIT', 'You are sending messages too fast.');
tz_def('GCHAT_CONFIRM_BLOCK', 'Block this player from chat permanently?');
tz_def('GCHAT_MUTE_5M', 'Mute 5m');
tz_def('GCHAT_MUTE_30M', 'Mute 30m');
tz_def('GCHAT_MUTE_1H', 'Mute 1h');
tz_def('GCHAT_MUTE_24H', 'Mute 24h');
tz_def('GCHAT_BLOCK', 'Block');
tz_def('GCHAT_UNMUTE', 'Unmute');
tz_def('GCHAT_ADMIN_BADGE', 'Admin');
tz_def('GCHAT_MH_BADGE', 'MH');
+19 -1
View File
@@ -4141,4 +4141,22 @@ tz_def('SITTER_P_NOT_SITTING', 'Nu esti sitter pe niciun cont.');
tz_def('TRAIN_BONUS_ARTIFACT', 'Bonus artefact');
tz_def('TRAIN_BONUS_HERO', 'Bonus erou');
tz_def('TRAIN_BONUS_ALLIANCE', 'Bonus alianta');
tz_def('TRAIN_BONUS_FINAL', 'Timp instruire');
tz_def('TRAIN_BONUS_FINAL', 'Timp instruire');
//////////////////////////////////////////////////////////////////////////////////////////////////////
// CHAT GENERAL (server-wide) - widget plutitor, footer.tpl + Templates/GlobalChat/widget.tpl
//////////////////////////////////////////////////////////////////////////////////////////////////////
tz_def('GCHAT_TITLE', 'Chat general');
tz_def('GCHAT_PLACEHOLDER', 'Mesaj...');
tz_def('GCHAT_SEND', 'Trimite');
tz_def('GCHAT_EMPTY', 'Niciun mesaj inca.');
tz_def('GCHAT_MUTED', 'Esti mutat din chat.');
tz_def('GCHAT_RATELIMIT', 'Trimiti mesaje prea repede.');
tz_def('GCHAT_CONFIRM_BLOCK', 'Blochezi permanent acest jucator din chat?');
tz_def('GCHAT_MUTE_5M', 'Mute 5m');
tz_def('GCHAT_MUTE_30M', 'Mute 30m');
tz_def('GCHAT_MUTE_1H', 'Mute 1h');
tz_def('GCHAT_MUTE_24H', 'Mute 24h');
tz_def('GCHAT_BLOCK', 'Blocheaza');
tz_def('GCHAT_UNMUTE', 'Anuleaza mute');
tz_def('GCHAT_ADMIN_BADGE', 'Admin');
tz_def('GCHAT_MH_BADGE', 'MH');
+459
View File
@@ -0,0 +1,459 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Project: TravianZ ##
## Filename: Templates/GlobalChat/widget.tpl ##
## Purpose: Floating server-wide chat widget (bubble + panel), included ##
## from Templates/footer.tpl for any logged-in user. ##
## ##
## Cerut de Catalin, 07.09.2026: ##
## - chat general, vazut/scris de orice jucator logat ##
## - MH (access 8) si Admin (access 9) pot muta/bloca jucatori care ##
## vorbesc urat ##
## - MH si Admin evidentiati fata de restul jucatorilor ##
## - format afisare: [TAG] Nume, sau doar Nume daca nu are alianta ##
## ##
## Nu depinde de MooTools/jQuery (evita coliziuni cu $ / $$ din unx.js / ##
## mt-full.js) - JS vanilla, prefixat "gchat_" peste tot. ##
## ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
#################################################################################
?>
<div id="gchat_root" data-uid="<?php echo (int) $session->uid; ?>">
<button id="gchat_bubble" type="button" title="<?php echo GCHAT_TITLE; ?>">
💬<span id="gchat_badge" style="display:none">0</span>
</button>
<div id="gchat_panel" style="display:none">
<div id="gchat_head">
<span><?php echo GCHAT_TITLE; ?></span>
<button id="gchat_close" type="button" title="&times;">&times;</button>
</div>
<div id="gchat_messages"></div>
<div id="gchat_notice" style="display:none"></div>
<form id="gchat_form" autocomplete="off">
<input id="gchat_input" type="text" maxlength="250" placeholder="<?php echo GCHAT_PLACEHOLDER; ?>"/>
<button id="gchat_send_btn" type="submit" title="<?php echo GCHAT_SEND; ?>">&#9658;</button>
</form>
</div>
</div>
<style>
#gchat_root, #gchat_root * { box-sizing: border-box; }
#gchat_bubble {
position: fixed;
right: 18px;
bottom: 18px;
width: 48px;
height: 48px;
border-radius: 50%;
background: #4a7c2f;
border: 2px solid #d9e8c8;
box-shadow: 0 2px 8px rgba(0,0,0,.35);
font-size: 20px;
line-height: 1;
cursor: pointer;
z-index: 9998;
}
#gchat_badge {
position: absolute;
top: -4px;
right: -4px;
background: #c0392b;
color: #fff;
font-size: 11px;
font-weight: bold;
border-radius: 9px;
min-width: 18px;
height: 18px;
line-height: 18px;
padding: 0 4px;
}
#gchat_panel {
position: fixed;
right: 18px;
bottom: 76px;
width: 300px;
max-width: calc(100vw - 36px);
height: 380px;
max-height: calc(100vh - 110px);
background: #fdfcf7;
border: 1px solid #ab9770;
border-radius: 6px;
box-shadow: 0 4px 18px rgba(0,0,0,.4);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 9999;
font-size: 12px;
font-family: Verdana, Arial, sans-serif;
}
#gchat_head {
background: #6b8f47;
color: #fff;
padding: 6px 8px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
#gchat_head button {
background: none;
border: none;
color: #fff;
font-size: 16px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
}
#gchat_messages {
flex: 1;
overflow-y: auto;
padding: 6px 8px;
}
.gchat_msg { margin-bottom: 6px; word-wrap: break-word; }
.gchat_time { color: #999; font-size: 10px; margin-right: 3px; }
.gchat_name { font-weight: bold; text-decoration: none; }
.gchat_name:hover { text-decoration: underline; }
.gchat_tag { font-weight: bold; text-decoration: none; }
.gchat_tag:hover { text-decoration: underline; }
.gchat_text { color: #222; }
/* evidentiere MH / Admin fata de restul jucatorilor */
.gchat_name_admin, .gchat_tag_admin { color: #b8290a; }
.gchat_name_mh, .gchat_tag_mh { color: #1a5aa8; }
.gchat_badge_role {
font-size: 9px;
font-weight: bold;
border-radius: 3px;
padding: 0 3px;
margin-right: 3px;
color: #fff;
vertical-align: middle;
}
.gchat_badge_role.admin { background: #b8290a; }
.gchat_badge_role.mh { background: #1a5aa8; }
.gchat_mod_actions { margin-left: 4px; }
.gchat_mod_actions a {
font-size: 10px;
color: #888;
text-decoration: none;
border-bottom: 1px dotted #aaa;
margin-right: 4px;
cursor: pointer;
}
.gchat_mod_actions a:hover { color: #b8290a; }
#gchat_notice {
padding: 4px 8px;
background: #fff3cd;
color: #7a5c00;
border-top: 1px solid #e0d29a;
flex-shrink: 0;
}
#gchat_form {
display: flex;
border-top: 1px solid #ddd;
flex-shrink: 0;
}
#gchat_input {
flex: 1;
border: none;
padding: 6px 8px;
font-size: 12px;
outline: none;
}
#gchat_send_btn {
border: none;
background: #6b8f47;
color: #fff;
width: 36px;
cursor: pointer;
}
#gchat_input:disabled { background: #eee; color: #999; }
@media (max-width: 480px) {
#gchat_panel { right: 8px; bottom: 66px; }
#gchat_bubble { right: 8px; bottom: 8px; }
}
</style>
<script>
(function () {
"use strict";
var GCHAT_TXT = {
muted: <?php echo json_encode(GCHAT_MUTED); ?>,
ratelimit: <?php echo json_encode(GCHAT_RATELIMIT); ?>,
empty: <?php echo json_encode(GCHAT_EMPTY); ?>,
confirmBlock: <?php echo json_encode(GCHAT_CONFIRM_BLOCK); ?>,
mute5: <?php echo json_encode(GCHAT_MUTE_5M); ?>,
mute30: <?php echo json_encode(GCHAT_MUTE_30M); ?>,
mute60: <?php echo json_encode(GCHAT_MUTE_1H); ?>,
mute1440: <?php echo json_encode(GCHAT_MUTE_24H); ?>,
block: <?php echo json_encode(GCHAT_BLOCK); ?>,
unmute: <?php echo json_encode(GCHAT_UNMUTE); ?>,
adminBadge: <?php echo json_encode(GCHAT_ADMIN_BADGE); ?>,
mhBadge: <?php echo json_encode(GCHAT_MH_BADGE); ?>
};
var ACCESS_ADMIN = 9, ACCESS_MH = 8;
var AJAX_URL = 'ajax.php';
var root = document.getElementById('gchat_root');
if (!root) { return; }
var bubble = document.getElementById('gchat_bubble');
var badge = document.getElementById('gchat_badge');
var panel = document.getElementById('gchat_panel');
var closeBtn = document.getElementById('gchat_close');
var messagesBox = document.getElementById('gchat_messages');
var notice = document.getElementById('gchat_notice');
var form = document.getElementById('gchat_form');
var input = document.getElementById('gchat_input');
var myUid = parseInt(root.getAttribute('data-uid'), 10) || 0;
var lastId = 0;
var unread = 0;
var isOpen = false;
var viewerIsMod = false;
var pollTimer = null;
function fmtTime(unixTs) {
var d = new Date(unixTs * 1000);
function pad(n) { return (n < 10 ? '0' : '') + n; }
return pad(d.getHours()) + ':' + pad(d.getMinutes());
}
function showNotice(text) {
notice.textContent = text;
notice.style.display = text ? 'block' : 'none';
}
function escapeForLog(s) { return s; } // (folosim textContent peste tot mai jos - nu innerHTML pe input de utilizator)
function renderModActions(row) {
if (!viewerIsMod || row.id_user === myUid || (row.access || 0) >= ACCESS_MH) {
return null;
}
var span = document.createElement('span');
span.className = 'gchat_mod_actions';
function actionLink(label, handler) {
var a = document.createElement('a');
a.textContent = label;
a.addEventListener('click', handler);
span.appendChild(a);
}
actionLink(GCHAT_TXT.mute5, function () { doModerate('gchat_mute', row.id_user, 5); });
actionLink(GCHAT_TXT.mute30, function () { doModerate('gchat_mute', row.id_user, 30); });
actionLink(GCHAT_TXT.mute60, function () { doModerate('gchat_mute', row.id_user, 60); });
actionLink(GCHAT_TXT.mute1440, function () { doModerate('gchat_mute', row.id_user, 1440); });
actionLink(GCHAT_TXT.block, function () {
if (window.confirm(GCHAT_TXT.confirmBlock)) {
doModerate('gchat_block', row.id_user, 0);
}
});
actionLink(GCHAT_TXT.unmute, function () { doModerate('gchat_unmute', row.id_user, 0); });
return span;
}
function renderMessage(row) {
var line = document.createElement('div');
line.className = 'gchat_msg';
var time = document.createElement('span');
time.className = 'gchat_time';
time.textContent = fmtTime(row.date);
line.appendChild(time);
var access = row.access || 0;
var roleClass = access >= ACCESS_ADMIN ? 'admin' : (access >= ACCESS_MH ? 'mh' : '');
if (roleClass) {
var roleBadge = document.createElement('span');
roleBadge.className = 'gchat_badge_role ' + roleClass;
roleBadge.textContent = roleClass === 'admin' ? GCHAT_TXT.adminBadge : GCHAT_TXT.mhBadge;
line.appendChild(roleBadge);
}
if (row.ally_tag) {
var tagLink = document.createElement('a');
tagLink.className = 'gchat_tag' + (roleClass ? ' gchat_tag_' + roleClass : '');
tagLink.href = 'allianz.php?aid=' + encodeURIComponent(row.ally_id);
tagLink.textContent = '[' + row.ally_tag + ']';
line.appendChild(tagLink);
line.appendChild(document.createTextNode(' '));
}
var nameLink = document.createElement('a');
nameLink.className = 'gchat_name' + (roleClass ? ' gchat_name_' + roleClass : '');
nameLink.href = 'spieler.php?uid=' + encodeURIComponent(row.id_user);
nameLink.textContent = row.username || ('#' + row.id_user);
line.appendChild(nameLink);
line.appendChild(document.createTextNode(': '));
var text = document.createElement('span');
text.className = 'gchat_text';
text.textContent = row.msg;
line.appendChild(text);
var mod = renderModActions(row);
if (mod) { line.appendChild(mod); }
return line;
}
function appendMessages(rows) {
var wasAtBottom = messagesBox.scrollTop + messagesBox.clientHeight >= messagesBox.scrollHeight - 4;
rows.forEach(function (row) {
messagesBox.appendChild(renderMessage(row));
lastId = Math.max(lastId, row.id);
});
if (rows.length && (wasAtBottom || !isOpen)) {
messagesBox.scrollTop = messagesBox.scrollHeight;
}
if (rows.length && !isOpen) {
unread += rows.length;
badge.textContent = unread > 99 ? '99+' : String(unread);
badge.style.display = 'inline-block';
}
}
function applyViewerState(viewer) {
viewerIsMod = !!viewer.isMod;
if (viewer.mutedUntil) {
input.disabled = true;
var mins = Math.max(1, Math.ceil((viewer.mutedUntil - Date.now() / 1000) / 60));
showNotice(GCHAT_TXT.muted + ' (' + mins + 'm)');
} else {
input.disabled = false;
if (notice.textContent && notice.textContent.indexOf(GCHAT_TXT.muted) === 0) {
showNotice('');
}
}
}
function poll() {
fetch(AJAX_URL + '?f=gchat_poll&sinceId=' + lastId, { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data || !data.ok) { return; }
appendMessages(data.messages || []);
applyViewerState(data.viewer || {});
})
.catch(function () { /* hiccup de retea - reincercam la urmatorul tick */ });
}
function loadInitial() {
fetch(AJAX_URL + '?f=gchat_poll&sinceId=0', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data || !data.ok) { return; }
messagesBox.innerHTML = '';
if (!data.messages || !data.messages.length) {
var empty = document.createElement('div');
empty.style.color = '#999';
empty.textContent = GCHAT_TXT.empty;
messagesBox.appendChild(empty);
}
appendMessages(data.messages || []);
applyViewerState(data.viewer || {});
});
}
function doModerate(action, targetUid, minutes) {
var body = 'target=' + encodeURIComponent(targetUid);
if (minutes) { body += '&minutes=' + encodeURIComponent(minutes); }
fetch(AJAX_URL + '?f=' + action, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body
}).then(function (r) { return r.json(); })
.then(function (data) {
if (data && data.ok) {
poll();
}
});
}
function openPanel() {
isOpen = true;
panel.style.display = 'flex';
unread = 0;
badge.style.display = 'none';
messagesBox.scrollTop = messagesBox.scrollHeight;
input.focus();
}
function closePanel() {
isOpen = false;
panel.style.display = 'none';
}
bubble.addEventListener('click', function () {
if (isOpen) { closePanel(); } else { openPanel(); }
});
closeBtn.addEventListener('click', closePanel);
form.addEventListener('submit', function (e) {
e.preventDefault();
var msg = input.value.trim();
if (!msg || input.disabled) { return; }
fetch(AJAX_URL + '?f=gchat_send', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'msg=' + encodeURIComponent(msg)
}).then(function (r) { return r.json(); })
.then(function (data) {
if (data && data.ok) {
input.value = '';
showNotice('');
poll();
} else if (data && data.reason === 'muted') {
applyViewerState({ isMod: viewerIsMod, mutedUntil: data.mutedUntil });
} else if (data && data.reason === 'ratelimit') {
showNotice(GCHAT_TXT.ratelimit);
}
});
});
loadInitial();
pollTimer = window.setInterval(poll, 2500);
window.addEventListener('beforeunload', function () {
if (pollTimer) { window.clearInterval(pollTimer); }
});
})();
</script>
+14 -1
View File
@@ -91,4 +91,17 @@ $serverVersion = 'v.11 Full Refactor';
<!-- Footer extra content -->
<div id="cfoot"></div>
</div>
</div>
<?php
/**
* Chat general (server-wide) - cerut de Catalin, 07.09.2026.
* footer.tpl e inclus si de pagini fara user logat (login/anmelden/logout/
* banned/maintenance) - widget-ul apare DOAR daca exista o sesiune valida
* si contul nu e blocat (access == BANNED inseamna cont blocat, nu "vizitator
* neautentificat" - ambele cazuri trebuie excluse aici).
*/
if (isset($session) && !empty($session->uid) && (!isset($session->access) || $session->access != BANNED)) {
include __DIR__ . '/GlobalChat/widget.tpl';
}
?>
+81
View File
@@ -107,5 +107,86 @@ switch(isset($_GET['f']) ? $_GET['f'] : '') {
$ok = $database->setMovementMarker($_POST['moveid'] ?? 0, $_POST['marker'] ?? 0, $uid);
echo json_encode(['ok' => $ok ? 1 : 0]);
break;
// Chat general (server-wide), cerut de Catalin 07.09.2026. Separat de
// alliance chat (Chat.php, SAJAX) - vazut/scris de orice user logat,
// moderat de MH (access 8) si Admin (access 9).
case 'gchat_poll':
header('Content-Type: application/json');
if (!isset($_SESSION)) {
session_start();
}
include_once($autoprefix.'GameEngine/Database.php');
$uid = (int) ($_SESSION['id_user'] ?? 0);
if (!$uid) {
http_response_code(403);
echo json_encode(['ok' => 0, 'reason' => 'notloggedin']);
break;
}
$sinceId = (int) ($_GET['sinceId'] ?? 0);
echo json_encode([
'ok' => 1,
'messages' => $database->getGlobalChatMessages(30, $sinceId),
'viewer' => $database->getGlobalChatViewerInfo($uid),
]);
break;
case 'gchat_send':
header('Content-Type: application/json');
if (!isset($_SESSION)) {
session_start();
}
include_once($autoprefix.'GameEngine/Database.php');
$uid = (int) ($_SESSION['id_user'] ?? 0);
if (!$uid) {
http_response_code(403);
echo json_encode(['ok' => 0, 'reason' => 'notloggedin']);
break;
}
echo json_encode($database->postGlobalChatMessage($uid, $_POST['msg'] ?? ''));
break;
case 'gchat_mute':
case 'gchat_block':
case 'gchat_unmute':
header('Content-Type: application/json');
if (!isset($_SESSION)) {
session_start();
}
include_once($autoprefix.'GameEngine/Database.php');
$modUid = (int) ($_SESSION['id_user'] ?? 0);
$targetUid = (int) ($_POST['target'] ?? 0);
if (!$modUid || !$targetUid) {
http_response_code(403);
echo json_encode(['ok' => 0, 'reason' => 'notloggedin']);
break;
}
$modAccess = (int) $database->getUserField($modUid, 'access', 0);
$targetAccess = (int) $database->getUserField($targetUid, 'access', 0);
// doar MH (8) si Admin (9) modereaza chat-ul general, si nimeni nu
// poate modera pe cineva de rang egal sau mai mare - deci MH nu
// poate muta alt MH/Admin, iar Admin nu poate muta alt Admin
if ($modAccess < MULTIHUNTER || $targetAccess >= $modAccess) {
http_response_code(403);
echo json_encode(['ok' => 0, 'reason' => 'forbidden']);
break;
}
if ($_GET['f'] == 'gchat_unmute') {
$ok = $database->unmuteGlobalChatUser($targetUid);
} else {
$reason = substr((string) ($_POST['reason'] ?? ''), 0, 255);
// 'gchat_block' = permanent (~100 de ani); 'gchat_mute' = temporar, in minute (POST['minutes'])
$until = ($_GET['f'] == 'gchat_block')
? (time() + 3600 * 24 * 365 * 100)
: (time() + max(1, (int) ($_POST['minutes'] ?? 0)) * 60);
$ok = $database->muteGlobalChatUser($targetUid, $modUid, $until, $reason);
}
echo json_encode(['ok' => $ok ? 1 : 0]);
break;
}
?>
+35
View File
@@ -575,6 +575,41 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%chat` (
-- Dumping data for table `%prefix%chat`
--
-- --------------------------------------------------------
--
-- Chat general (server-wide), cerut de Catalin 07.09.2026 - separat de
-- chat-ul de alianta de mai sus (acela e filtrat pe `alli`, asta e vazut
-- de toti jucatorii de pe server, indiferent de alianta)
--
CREATE TABLE IF NOT EXISTS `%PREFIX%chat_global` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`id_user` int(11) NOT NULL,
`date` int(11) NOT NULL,
`msg` varchar(250) NOT NULL,
PRIMARY KEY (`id`),
KEY `id_user_date` (`id_user`,`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Stare activa de mute/block pe chat-ul general - un singur rand per user
-- (id_user e PRIMARY KEY, deci un nou mute suprascrie direct pe cel vechi
-- via ON DUPLICATE KEY UPDATE, fara sa acumuleze istoric).
-- muted_until in viitorul indepartat (~100 ani) = block permanent.
--
CREATE TABLE IF NOT EXISTS `%PREFIX%chat_mutes` (
`id_user` int(11) NOT NULL,
`muted_until` int(11) NOT NULL,
`muted_by` int(11) NOT NULL,
`reason` varchar(255) NULL,
`created` int(11) NOT NULL,
PRIMARY KEY (`id_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------