Spaces:
Paused
Paused
Update bot.js
Browse files
bot.js
CHANGED
|
@@ -1,62 +1,11 @@
|
|
| 1 |
-
const mineflayer = require("mineflayer");
|
| 2 |
-
const mineflayerViewer = require('prismarine-viewer').mineflayer;
|
| 3 |
-
const Movements = require("mineflayer-pathfinder").Movements;
|
| 4 |
-
const pathfinder = require("mineflayer-pathfinder").pathfinder;
|
| 5 |
-
const { GoalBlock, GoalXZ } = require("mineflayer-pathfinder").goals;
|
| 6 |
-
const util = require("util");
|
| 7 |
-
const { SocksClient } = require('socks');
|
| 8 |
-
const { Client } = require('ssh2');
|
| 9 |
-
|
| 10 |
-
const config = require("./settings.json");
|
| 11 |
-
const loggers = require("./logging.js");
|
| 12 |
-
const logger = loggers.logger;
|
| 13 |
-
|
| 14 |
-
const https = require('https');
|
| 15 |
-
|
| 16 |
-
// Küresel tanımlamalar (Orijinal yerlerinde kaldı)
|
| 17 |
-
const HOME_ARRIVAL_DELAY_MS = 2000;
|
| 18 |
-
const POST_ACTION_DELAY_MS = 4000;
|
| 19 |
-
const WINDOW_OPEN_TIMEOUT_MS = 6000;
|
| 20 |
-
const PERIODIC_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
| 21 |
-
const FULL_FEED_COOLDOWN_MS = 50 * 60 * 1000;
|
| 22 |
-
const GO_HOME_COMMAND = "/home";
|
| 23 |
-
const RETURN_ISLAND_COMMAND = "/is home";
|
| 24 |
-
const BESLEYICI_NAMES = (config.utils && config.utils["besleyici-names"]) || ["Besleyici", "ʙᴇsʟᴇʏiᴄi"];
|
| 25 |
-
let viewerInstance = null;
|
| 26 |
-
let viewerTimeout = null;
|
| 27 |
-
let bot = null;
|
| 28 |
-
let botIsReady = false;
|
| 29 |
-
let baglantiTuru = "Yok";
|
| 30 |
-
let activeMessages = [];
|
| 31 |
-
let messagesTimeout = null;
|
| 32 |
-
const ansiToHtmlConvert = new (require('ansi-to-html'))({ newline: true });
|
| 33 |
-
|
| 34 |
-
let isFinished = false;
|
| 35 |
-
|
| 36 |
-
// Dış IP Öğrenme İsteği
|
| 37 |
-
https.get('https://api.ipify.org?format=json', (res) => {
|
| 38 |
-
let data = '';
|
| 39 |
-
res.on('data', chunk => { data += chunk; });
|
| 40 |
-
res.on('end', () => {
|
| 41 |
-
try {
|
| 42 |
-
const ipInfo = JSON.parse(data);
|
| 43 |
-
console.log(`Dış IP adresin: ${ipInfo.ip}`);
|
| 44 |
-
} catch (err) {
|
| 45 |
-
console.error('JSON parse hatası:', err);
|
| 46 |
-
}
|
| 47 |
-
});
|
| 48 |
-
}).on('error', (err) => {
|
| 49 |
-
console.error('İstek hatası:', err);
|
| 50 |
-
});
|
| 51 |
-
|
| 52 |
async function createBot() {
|
| 53 |
const net = require('net');
|
| 54 |
const sshClient = new Client();
|
| 55 |
isFinished = false; // Her yeni bot oluştuğunda durumu sıfırla
|
| 56 |
|
| 57 |
-
//
|
| 58 |
const githubRawUrl = 'https://raw.githubusercontent.com/suvarileader/che/refs/heads/main/doc/ower/doc.json';
|
| 59 |
-
logger.info("[GitHub Config] Güncel
|
| 60 |
|
| 61 |
let remoteData = null;
|
| 62 |
try {
|
|
@@ -73,39 +22,46 @@ async function createBot() {
|
|
| 73 |
});
|
| 74 |
}).on('error', (err) => reject(err));
|
| 75 |
});
|
| 76 |
-
logger.info("[GitHub Config]
|
| 77 |
} catch (err) {
|
| 78 |
-
logger.error(`[GitHub Config Hatası]
|
| 79 |
}
|
| 80 |
|
| 81 |
-
//
|
| 82 |
-
let
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
|
|
|
| 86 |
if (remoteData && Array.isArray(remoteData)) {
|
| 87 |
-
// Listede local
|
| 88 |
const myConfig = remoteData.find(item => item.account === currentBotUsername);
|
| 89 |
if (myConfig) {
|
| 90 |
-
logger.info(`[GitHub Config] '${currentBotUsername}'
|
|
|
|
|
|
|
| 91 |
if (myConfig.serverName && myConfig.serverName !== "empty") {
|
| 92 |
-
|
| 93 |
}
|
|
|
|
| 94 |
if (myConfig.username && myConfig.username !== "empty") {
|
| 95 |
-
|
| 96 |
}
|
|
|
|
| 97 |
} else {
|
| 98 |
-
logger.warn(`[GitHub Config] Listede '${currentBotUsername}'
|
| 99 |
}
|
| 100 |
}
|
| 101 |
|
| 102 |
-
//
|
| 103 |
-
const
|
| 104 |
-
|
| 105 |
-
port: 22,
|
| 106 |
-
username: 'suvaa-vpnjantit.com',
|
| 107 |
-
password: 'suvaa'
|
| 108 |
-
};
|
| 109 |
|
| 110 |
// Mineflayer Başlatılıyor
|
| 111 |
bot = mineflayer.createBot({
|
|
@@ -119,7 +75,7 @@ async function createBot() {
|
|
| 119 |
let fallbackTriggered = false;
|
| 120 |
let isFinalFailureHandled = false;
|
| 121 |
|
| 122 |
-
//
|
| 123 |
const handleFinalFailure = (reason) => {
|
| 124 |
if (isFinalFailureHandled) return;
|
| 125 |
isFinalFailureHandled = true;
|
|
@@ -143,7 +99,7 @@ async function createBot() {
|
|
| 143 |
}
|
| 144 |
};
|
| 145 |
|
| 146 |
-
//
|
| 147 |
const connectDirectly = (reason) => {
|
| 148 |
if (fallbackTriggered) return;
|
| 149 |
fallbackTriggered = true;
|
|
@@ -157,7 +113,6 @@ async function createBot() {
|
|
| 157 |
port: parseInt(targetServerPort) || 25565
|
| 158 |
});
|
| 159 |
|
| 160 |
-
// Normal IP bağlantısı için 10 saniye limit
|
| 161 |
const directTimeout = setTimeout(() => {
|
| 162 |
socket.destroy();
|
| 163 |
handleFinalFailure("Normal IP bağlantısı zaman aşımına uğradı (Sunucu yanıt vermiyor)");
|
|
@@ -183,9 +138,9 @@ async function createBot() {
|
|
| 183 |
});
|
| 184 |
};
|
| 185 |
|
| 186 |
-
|
|
|
|
| 187 |
|
| 188 |
-
// SSH İlk Bağlantı Zaman Aşımı (10 Saniye)
|
| 189 |
const connectionTimeout = setTimeout(() => {
|
| 190 |
connectDirectly("SSH Bağlantı Zaman Aşımı (10 saniye yanıt alınamadı)");
|
| 191 |
}, 10000);
|
|
@@ -194,6 +149,7 @@ async function createBot() {
|
|
| 194 |
clearTimeout(connectionTimeout);
|
| 195 |
logger.info('[SSH Tünel] SSH bağlantısı başarılı. Minecraft sunucusuna tünel açılıyor...');
|
| 196 |
|
|
|
|
| 197 |
sshClient.forwardOut(
|
| 198 |
'127.0.0.1',
|
| 199 |
12345,
|
|
@@ -238,490 +194,4 @@ async function createBot() {
|
|
| 238 |
|
| 239 |
bot.loadPlugin(pathfinder);
|
| 240 |
|
| 241 |
-
// ---
|
| 242 |
-
let lastFullFeedAt = 0;
|
| 243 |
-
let windowOpenResolvers = [];
|
| 244 |
-
let periodicHandle = null;
|
| 245 |
-
let activeTimeouts = [];
|
| 246 |
-
|
| 247 |
-
function setSafeTimeout(fn, delay) {
|
| 248 |
-
const t = setTimeout(fn, delay);
|
| 249 |
-
activeTimeouts.push(t);
|
| 250 |
-
return t;
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
function stopPeriodicHomeChecks() {
|
| 254 |
-
if (periodicHandle) clearInterval(periodicHandle);
|
| 255 |
-
periodicHandle = null;
|
| 256 |
-
activeTimeouts.forEach(t => clearTimeout(t));
|
| 257 |
-
activeTimeouts = [];
|
| 258 |
-
}
|
| 259 |
-
// ------------------------------------------------
|
| 260 |
-
|
| 261 |
-
// --- SENİN ORİJİNAL FONKSİYONLARIN ---
|
| 262 |
-
function stripColorCodes(s) {
|
| 263 |
-
if (!s || typeof s !== "string") return s;
|
| 264 |
-
return s.replace(/§[0-9a-fk-or]/gi, "");
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
function safeDisplayNameToString(e) {
|
| 268 |
-
try {
|
| 269 |
-
if (e && e.displayName && typeof e.displayName.toString === "function") {
|
| 270 |
-
return e.displayName.toString();
|
| 271 |
-
}
|
| 272 |
-
} catch (err) {}
|
| 273 |
-
return null;
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
function getEntityReadableName(e) {
|
| 277 |
-
if (!e) return "";
|
| 278 |
-
const disp = safeDisplayNameToString(e);
|
| 279 |
-
if (disp) return stripColorCodes(disp);
|
| 280 |
-
if (e.name && typeof e.name === "string") return stripColorCodes(e.name);
|
| 281 |
-
if (e.username && typeof e.username === "string") return stripColorCodes(e.username);
|
| 282 |
-
return "";
|
| 283 |
-
}
|
| 284 |
-
|
| 285 |
-
function normalizeForMatch(s) {
|
| 286 |
-
if (!s || typeof s !== "string") return "";
|
| 287 |
-
let t = stripColorCodes(s);
|
| 288 |
-
if (t.normalize) t = t.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
|
| 289 |
-
return t.toLowerCase();
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
-
const TARGET_NAME_NORMS = BESLEYICI_NAMES.map(n => normalizeForMatch(n));
|
| 293 |
-
|
| 294 |
-
function isBesleyiciName(name) {
|
| 295 |
-
if (!name) return false;
|
| 296 |
-
const norm = normalizeForMatch(name);
|
| 297 |
-
return TARGET_NAME_NORMS.some(t => norm.includes(t));
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
// safeGet, deepSearchForString, parseWindowTitleToString, extractMinyonCanFromSlot fonksiyonları aynen korundu
|
| 301 |
-
function safeGet(obj, pathArray) {
|
| 302 |
-
try {
|
| 303 |
-
let cur = obj;
|
| 304 |
-
for (const p of pathArray) {
|
| 305 |
-
if (cur == null) return undefined;
|
| 306 |
-
cur = cur[p];
|
| 307 |
-
}
|
| 308 |
-
return cur;
|
| 309 |
-
} catch (e) { return undefined; }
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
function deepSearchForString(root, targetLower) {
|
| 313 |
-
const results = [];
|
| 314 |
-
const visited = new WeakSet();
|
| 315 |
-
function recurse(obj, path) {
|
| 316 |
-
if (obj == null || typeof obj === "function") return;
|
| 317 |
-
if (typeof obj === "string") {
|
| 318 |
-
const norm = normalizeForMatch(obj);
|
| 319 |
-
if (norm.includes(targetLower)) results.push({ path, value: obj });
|
| 320 |
-
return;
|
| 321 |
-
}
|
| 322 |
-
if (typeof obj !== "object") return;
|
| 323 |
-
if (visited.has(obj)) return;
|
| 324 |
-
visited.add(obj);
|
| 325 |
-
for (const key of Object.keys(obj)) {
|
| 326 |
-
recurse(obj[key], `${path}.${key}`);
|
| 327 |
-
}
|
| 328 |
-
}
|
| 329 |
-
try { recurse(root, "root"); } catch (e) {}
|
| 330 |
-
return results;
|
| 331 |
-
}
|
| 332 |
-
|
| 333 |
-
function parseWindowTitleToString(titleObj) {
|
| 334 |
-
try {
|
| 335 |
-
if (!titleObj) return "";
|
| 336 |
-
if (typeof titleObj === "string") return titleObj;
|
| 337 |
-
const s = titleObj.toString();
|
| 338 |
-
if (s && s !== "[object Object]") return s;
|
| 339 |
-
return JSON.stringify(titleObj);
|
| 340 |
-
} catch (err) { return ""; }
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
function extractMinyonCanFromSlot(slot) {
|
| 344 |
-
try {
|
| 345 |
-
if (!slot) return null;
|
| 346 |
-
function tryParseLoreText(text) {
|
| 347 |
-
try {
|
| 348 |
-
if (!text || typeof text !== "string") return null;
|
| 349 |
-
let plain = text;
|
| 350 |
-
try {
|
| 351 |
-
const maybe = JSON.parse(text);
|
| 352 |
-
if (maybe && typeof maybe === "object") {
|
| 353 |
-
if (maybe.extra && Array.isArray(maybe.extra) && maybe.extra.length > 0) {
|
| 354 |
-
if (typeof maybe.extra[0].text === "string") plain = maybe.extra[0].text;
|
| 355 |
-
} else if (typeof maybe.text === "string") {
|
| 356 |
-
plain = maybe.text;
|
| 357 |
-
}
|
| 358 |
-
}
|
| 359 |
-
} catch (e) {}
|
| 360 |
-
const xyMatch = plain.match(/(\d{1,4})\s*\/\s*(\d{1,4})/);
|
| 361 |
-
if (xyMatch) return Number(xyMatch[1]);
|
| 362 |
-
const numMatch = plain.match(/(\d{1,4})/);
|
| 363 |
-
if (numMatch) return Number(numMatch[1]);
|
| 364 |
-
return null;
|
| 365 |
-
} catch (e) { return null; }
|
| 366 |
-
}
|
| 367 |
-
if (slot.nbt) {
|
| 368 |
-
const lore = safeGet(slot.nbt, ["value", "display", "value", "Lore", "value"]);
|
| 369 |
-
if (Array.isArray(lore)) {
|
| 370 |
-
for (const line of lore) {
|
| 371 |
-
const res = tryParseLoreText(line.value || line);
|
| 372 |
-
if (res != null) return res;
|
| 373 |
-
}
|
| 374 |
-
}
|
| 375 |
-
}
|
| 376 |
-
const full = util.inspect(slot, { depth: null });
|
| 377 |
-
return tryParseLoreText(full);
|
| 378 |
-
} catch (err) { return null; }
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
async function goHome() {
|
| 382 |
-
logger.info(`[${currentBotUsername}] ${GO_HOME_COMMAND} komutu gönderiliyor.`);
|
| 383 |
-
bot.chat(GO_HOME_COMMAND);
|
| 384 |
-
}
|
| 385 |
-
|
| 386 |
-
async function returnIsland() {
|
| 387 |
-
logger.info(`[${currentBotUsername}] ${RETURN_ISLAND_COMMAND} komutu gönderiliyor.`);
|
| 388 |
-
bot.chat(RETURN_ISLAND_COMMAND);
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
async function scanAndFeed() {
|
| 392 |
-
if (Date.now() < (lastFullFeedAt + FULL_FEED_COOLDOWN_MS)) return;
|
| 393 |
-
const arr = Object.values(bot.entities);
|
| 394 |
-
let best = null;
|
| 395 |
-
|
| 396 |
-
for (const e of arr) {
|
| 397 |
-
const name = getEntityReadableName(e);
|
| 398 |
-
if (isBesleyiciName(name)) {
|
| 399 |
-
best = { entity: e, path: "displayName/name" };
|
| 400 |
-
break;
|
| 401 |
-
}
|
| 402 |
-
for (const target of TARGET_NAME_NORMS) {
|
| 403 |
-
const deep = deepSearchForString(e, target);
|
| 404 |
-
if (deep.length > 0) {
|
| 405 |
-
best = { entity: e, path: deep[0].path };
|
| 406 |
-
break;
|
| 407 |
-
}
|
| 408 |
-
}
|
| 409 |
-
if (best) break;
|
| 410 |
-
}
|
| 411 |
-
|
| 412 |
-
if (!best) {
|
| 413 |
-
logger.info(`[${currentBotUsername}] /home sonrası tarama: besleyici minyon bulunamadı.`);
|
| 414 |
-
bot.chat("/skyblock");
|
| 415 |
-
return;
|
| 416 |
-
}
|
| 417 |
-
|
| 418 |
-
const dist = bot.entity.position.distanceTo(best.entity.position);
|
| 419 |
-
logger.info(`[${currentBotUsername}] '${best.entity.name || 'Armor Stand'}' bulundu id:${best.entity.id} path:${best.path} dist:${dist.toFixed(2)} — aktive ediliyor.`);
|
| 420 |
-
|
| 421 |
-
if (dist > 7) {
|
| 422 |
-
logger.warn(`safeActivateEntityPreferAt: entity id:${best.entity.id} uzak (${dist.toFixed(2)}). Max 7 blok.`);
|
| 423 |
-
logger.warn(`[${currentBotUsername}] 'Besleyici' aktive edilemedi (id:${best.entity.id}).`);
|
| 424 |
-
return;
|
| 425 |
-
}
|
| 426 |
-
|
| 427 |
-
try {
|
| 428 |
-
if (bot.lookAt) await bot.lookAt(best.entity.position.offset(0, 1.0, 0), true);
|
| 429 |
-
await new Promise(r => setTimeout(r, 200));
|
| 430 |
-
|
| 431 |
-
logger.info(`activateEntityAt çağrıldı id:${best.entity.id}`);
|
| 432 |
-
if (typeof bot.activateEntityAt === "function") bot.activateEntityAt(best.entity, best.entity.position);
|
| 433 |
-
else if (typeof bot.activateEntity === "function") bot.activateEntity(best.entity);
|
| 434 |
-
|
| 435 |
-
await new Promise((resolve) => {
|
| 436 |
-
const timer = setSafeTimeout(() => resolve(), WINDOW_OPEN_TIMEOUT_MS);
|
| 437 |
-
windowOpenResolvers.push(() => { clearTimeout(timer); resolve(); });
|
| 438 |
-
});
|
| 439 |
-
} catch (err) {}
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
bot.once("spawn", async () => {
|
| 443 |
-
bot.pathfinder.setMovements(new Movements(bot));
|
| 444 |
-
logger.info("Bot sunucuya giriş yaptı.");
|
| 445 |
-
botIsReady = true;
|
| 446 |
-
isFinished = true;
|
| 447 |
-
|
| 448 |
-
if (config.utils && config.utils["auto-auth"] && config.utils["auto-auth"].enabled) {
|
| 449 |
-
const password = config.utils["auto-auth"].password;
|
| 450 |
-
|
| 451 |
-
setSafeTimeout(() => {
|
| 452 |
-
bot.chat(`/login ${password}`);
|
| 453 |
-
logger.info(`[${currentBotUsername}] Login komutu kullanıldı.`);
|
| 454 |
-
}, 2500);
|
| 455 |
-
|
| 456 |
-
setSafeTimeout(() => {
|
| 457 |
-
bot.chat(`/skyblock`);
|
| 458 |
-
logger.info(`[${currentBotUsername}] Skyblock'a ışınlanma komutu kullanıldı.`);
|
| 459 |
-
}, 7500);
|
| 460 |
-
|
| 461 |
-
setSafeTimeout(async () => {
|
| 462 |
-
await goHome();
|
| 463 |
-
await new Promise(r => setTimeout(r, HOME_ARRIVAL_DELAY_MS));
|
| 464 |
-
await scanAndFeed();
|
| 465 |
-
await new Promise(r => setTimeout(r, POST_ACTION_DELAY_MS));
|
| 466 |
-
await returnIsland();
|
| 467 |
-
|
| 468 |
-
periodicHandle = setInterval(async () => {
|
| 469 |
-
if (Date.now() < (lastFullFeedAt + FULL_FEED_COOLDOWN_MS)) return;
|
| 470 |
-
await goHome();
|
| 471 |
-
await new Promise(r => setTimeout(r, HOME_ARRIVAL_DELAY_MS));
|
| 472 |
-
await scanAndFeed();
|
| 473 |
-
await new Promise(r => setTimeout(r, POST_ACTION_DELAY_MS));
|
| 474 |
-
await returnIsland();
|
| 475 |
-
}, PERIODIC_CHECK_INTERVAL_MS);
|
| 476 |
-
}, 12500);
|
| 477 |
-
}
|
| 478 |
-
});
|
| 479 |
-
|
| 480 |
-
bot.on("windowOpen", (window) => {
|
| 481 |
-
const title = parseWindowTitleToString(window.title).toLowerCase();
|
| 482 |
-
if (!title.includes("besleyici") && !parseWindowTitleToString(window.title).includes("ʙᴇsʟᴇʏiᴄi")) {
|
| 483 |
-
logger.info(`[${currentBotUsername}] Bi chest açıldı ama window title ı uyuşmadığı için kapatıldı. ${parseWindowTitleToString(window.title)}`);
|
| 484 |
-
bot.closeWindow(window);
|
| 485 |
-
return;
|
| 486 |
-
}
|
| 487 |
-
const beefSlot = window.slots.find(s => s?.name === "cooked_beef");
|
| 488 |
-
const can = extractMinyonCanFromSlot(beefSlot);
|
| 489 |
-
if (can != null) {
|
| 490 |
-
if (can <= 2) {
|
| 491 |
-
bot.clickWindow(36, 0, 0);
|
| 492 |
-
lastFullFeedAt = Date.now();
|
| 493 |
-
logger.info(`[${currentBotUsername}] Minyon canı ${can} olduğu için beslendi. 50dk bekleme aktif.`);
|
| 494 |
-
} else if (can >= 10) {
|
| 495 |
-
lastFullFeedAt = Date.now();
|
| 496 |
-
logger.info(`[${currentBotUsername}] Minyon canı ${can} (tam). 50dk bekleme aktif.`);
|
| 497 |
-
} else {
|
| 498 |
-
logger.info(`[${currentBotUsername}] Minyon canı ${can} olduğu için yenilenmedi.`);
|
| 499 |
-
}
|
| 500 |
-
}
|
| 501 |
-
setSafeTimeout(() => { try { bot.closeWindow(window); } catch(e){} }, 500);
|
| 502 |
-
while (windowOpenResolvers.length > 0) windowOpenResolvers.shift()();
|
| 503 |
-
});
|
| 504 |
-
|
| 505 |
-
bot.on("end", () => {
|
| 506 |
-
if (isFinished) return;
|
| 507 |
-
isFinished = true;
|
| 508 |
-
|
| 509 |
-
logger.warn("Bağlantı koptu. Tüm zamanlayıcılar sıfırlanıyor...");
|
| 510 |
-
stopPeriodicHomeChecks();
|
| 511 |
-
botIsReady = false;
|
| 512 |
-
baglantiTuru = "Yok";
|
| 513 |
-
bot = null;
|
| 514 |
-
|
| 515 |
-
if (config.utils && config.utils["auto-reconnect"]) {
|
| 516 |
-
setTimeout(() => createBot(), config.utils["auto-reconnect-delay"]);
|
| 517 |
-
}
|
| 518 |
-
});
|
| 519 |
-
|
| 520 |
-
bot.on("kicked", (reason) => logger.warn(`Bot sunucudan atıldı: ${util.inspect(reason)}`));
|
| 521 |
-
bot.on("error", (err) => logger.error(`${err}`));
|
| 522 |
-
}
|
| 523 |
-
|
| 524 |
-
createBot();
|
| 525 |
-
|
| 526 |
-
// --- EXPRESS SUNUCUSU VE PANEL ENTEGRASYONU ---
|
| 527 |
-
const express = require("express");
|
| 528 |
-
const app = express();
|
| 529 |
-
const http = require('http');
|
| 530 |
-
|
| 531 |
-
function minecraftMessageListener(jsonMsg) {
|
| 532 |
-
try {
|
| 533 |
-
const chatMessage = bot.chat.ChatMessage ? new bot.chat.ChatMessage(jsonMsg) : jsonMsg;
|
| 534 |
-
const coloredAnsi = typeof chatMessage.toAnsi === 'function' ? chatMessage.toAnsi() : chatMessage.toString();
|
| 535 |
-
const htmlMessage = ansiToHtmlConvert.toHtml(coloredAnsi);
|
| 536 |
-
const timeStr = new Date().toLocaleTimeString('tr-TR');
|
| 537 |
-
|
| 538 |
-
activeMessages.push({ time: timeStr, text: htmlMessage });
|
| 539 |
-
|
| 540 |
-
if (activeMessages.length > 50) {
|
| 541 |
-
activeMessages.shift();
|
| 542 |
-
}
|
| 543 |
-
} catch (err) {
|
| 544 |
-
logger.error("Mesaj işlenirken hata oluştu: " + err.message);
|
| 545 |
-
}
|
| 546 |
-
}
|
| 547 |
-
|
| 548 |
-
app.get("/status", (req, res) => {
|
| 549 |
-
res.send({
|
| 550 |
-
ready: botIsReady,
|
| 551 |
-
username: bot ? bot.username : null,
|
| 552 |
-
connection_type: baglantiTuru
|
| 553 |
-
});
|
| 554 |
-
});
|
| 555 |
-
|
| 556 |
-
app.get("/", (req, res) => res.send("Bot Aktif"));
|
| 557 |
-
|
| 558 |
-
app.get("/messages", (req, res) => {
|
| 559 |
-
if (messagesTimeout) {
|
| 560 |
-
clearTimeout(messagesTimeout);
|
| 561 |
-
messagesTimeout = null;
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
-
if (bot && botIsReady) {
|
| 565 |
-
if (!bot.listeners('message').includes(minecraftMessageListener)) {
|
| 566 |
-
bot.on('message', minecraftMessageListener);
|
| 567 |
-
logger.info("[Sohbet Tasarrufu] Sohbet izlenmeye başlandı. 'message' eventi aktif.");
|
| 568 |
-
}
|
| 569 |
-
} else {
|
| 570 |
-
return res.status(503).send("Bot hazır olmadığı için mesajlar dinlenemiyor.");
|
| 571 |
-
}
|
| 572 |
-
|
| 573 |
-
res.send({
|
| 574 |
-
server_time: new Date().toLocaleTimeString('tr-TR'),
|
| 575 |
-
logs: activeMessages
|
| 576 |
-
});
|
| 577 |
-
|
| 578 |
-
messagesTimeout = setTimeout(() => {
|
| 579 |
-
if (bot) {
|
| 580 |
-
bot.off('message', minecraftMessageListener);
|
| 581 |
-
activeMessages = [];
|
| 582 |
-
messagesTimeout = null;
|
| 583 |
-
logger.info("[Sohbet Tasarrufu] 1 dakikadır izlenmiyor. 'message' eventi kapatıldı ve RAM temizlendi.");
|
| 584 |
-
}
|
| 585 |
-
}, 60000);
|
| 586 |
-
});
|
| 587 |
-
|
| 588 |
-
const checkAndStartViewer = (req, res, next) => {
|
| 589 |
-
if (viewerTimeout) {
|
| 590 |
-
clearTimeout(viewerTimeout);
|
| 591 |
-
viewerTimeout = null;
|
| 592 |
-
}
|
| 593 |
-
|
| 594 |
-
const isBotReady = bot && botIsReady;
|
| 595 |
-
|
| 596 |
-
if (!viewerInstance && isBotReady) {
|
| 597 |
-
try {
|
| 598 |
-
mineflayerViewer(bot, { port: 3000, firstPerson: true });
|
| 599 |
-
viewerInstance = bot.viewer;
|
| 600 |
-
logger.info("[Viewer] 3D Harita motoru arka planda (3000) başlatıldı.");
|
| 601 |
-
setTimeout(() => next(), 600);
|
| 602 |
-
return;
|
| 603 |
-
} catch (err) {
|
| 604 |
-
logger.error("Görüntüleyici başlatılamadı: " + err);
|
| 605 |
-
}
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
if (!isBotReady && !viewerInstance) {
|
| 609 |
-
return res.status(503).send("Bot henüz sunucuya giriş yapmadığı için ekran görüntülenemiyor.");
|
| 610 |
-
}
|
| 611 |
-
|
| 612 |
-
next();
|
| 613 |
-
};
|
| 614 |
-
|
| 615 |
-
app.use('/3dview', checkAndStartViewer, (req, res) => {
|
| 616 |
-
if (req.originalUrl === '/3dview' && !req.originalUrl.endsWith('/')) {
|
| 617 |
-
return res.redirect(301, '/3dview/');
|
| 618 |
-
}
|
| 619 |
-
|
| 620 |
-
const targetPath = req.url;
|
| 621 |
-
const proxyReq = http.request({
|
| 622 |
-
host: '127.0.0.1',
|
| 623 |
-
port: 3000,
|
| 624 |
-
path: targetPath,
|
| 625 |
-
method: req.method,
|
| 626 |
-
headers: req.headers
|
| 627 |
-
}, (proxyRes) => {
|
| 628 |
-
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
| 629 |
-
proxyRes.pipe(res, { end: true });
|
| 630 |
-
});
|
| 631 |
-
|
| 632 |
-
proxyReq.on('error', (err) => {
|
| 633 |
-
logger.error("[Asset Hatası] Veri 3000 portundan çekilemedi: " + err.message);
|
| 634 |
-
if (!res.headersSent) res.status(502).send("Harita yükleniyor, lütfen sayfayı yenileyin.");
|
| 635 |
-
});
|
| 636 |
-
|
| 637 |
-
req.pipe(proxyReq, { end: true });
|
| 638 |
-
});
|
| 639 |
-
|
| 640 |
-
app.get("/restart", (req, res) => {
|
| 641 |
-
if (bot && botIsReady) {
|
| 642 |
-
logger.info("[Sistem] Web paneli üzerinden tam restart isteği geldi. Proje kapatılıyor...");
|
| 643 |
-
try { bot.chat("Bot web paneli uzerinden tum kodlariyla yeniden baslatiliyor..."); } catch(e){}
|
| 644 |
-
res.send({ success: true, message: "Tüm proje kapatıldı. Hugging Face sistemi birkaç saniye içinde kodu sıfırdan başlatacak." });
|
| 645 |
-
setTimeout(() => {
|
| 646 |
-
if (bot) bot.quit();
|
| 647 |
-
logger.info("[Sistem] process.exit(0) çağrıldı. Konteyner yeniden başlatılacak.");
|
| 648 |
-
process.exit(0);
|
| 649 |
-
}, 1000);
|
| 650 |
-
} else {
|
| 651 |
-
logger.info("[Sistem] Bot bağlı değilken web panelinden tam restart istendi.");
|
| 652 |
-
res.send({ success: true, message: "Bot bağlı değildi ancak tüm proje kökten kapatıldı, yeniden başlatılıyor." });
|
| 653 |
-
setTimeout(() => {
|
| 654 |
-
process.exit(0);
|
| 655 |
-
}, 1000);
|
| 656 |
-
}
|
| 657 |
-
});
|
| 658 |
-
|
| 659 |
-
const server = app.listen(process.env.PORT || 7860, () => {
|
| 660 |
-
logger.info("Ana Express sunucusu 7860 portunda aktif.");
|
| 661 |
-
});
|
| 662 |
-
|
| 663 |
-
server.on('upgrade', (req, socket, head) => {
|
| 664 |
-
if (req.url.includes('socket.io') || req.url.startsWith('/3dview')) {
|
| 665 |
-
if (viewerTimeout) {
|
| 666 |
-
clearTimeout(viewerTimeout);
|
| 667 |
-
viewerTimeout = null;
|
| 668 |
-
}
|
| 669 |
-
|
| 670 |
-
let targetWsPath = req.url;
|
| 671 |
-
if (targetWsPath.startsWith('/3dview')) {
|
| 672 |
-
targetWsPath = targetWsPath.replace('/3dview', '');
|
| 673 |
-
}
|
| 674 |
-
if (!targetWsPath.startsWith('/')) {
|
| 675 |
-
targetWsPath = '/' + targetWsPath;
|
| 676 |
-
}
|
| 677 |
-
|
| 678 |
-
const proxyReq = http.request({
|
| 679 |
-
host: '127.0.0.1',
|
| 680 |
-
port: 3000,
|
| 681 |
-
path: targetWsPath,
|
| 682 |
-
method: req.method,
|
| 683 |
-
headers: req.headers
|
| 684 |
-
});
|
| 685 |
-
|
| 686 |
-
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
|
| 687 |
-
socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
|
| 688 |
-
Object.keys(proxyRes.headers).map(key => `${key}: ${proxyRes.headers[key]}`).join('\r\n') +
|
| 689 |
-
'\r\n\r\n');
|
| 690 |
-
|
| 691 |
-
proxySocket.pipe(socket);
|
| 692 |
-
socket.pipe(proxySocket);
|
| 693 |
-
|
| 694 |
-
socket.on('close', () => {
|
| 695 |
-
if (viewerTimeout) clearTimeout(viewerTimeout);
|
| 696 |
-
viewerTimeout = setTimeout(() => {
|
| 697 |
-
if (viewerInstance && typeof viewerInstance.close === 'function') {
|
| 698 |
-
try {
|
| 699 |
-
viewerInstance.close();
|
| 700 |
-
bot.viewer = null;
|
| 701 |
-
viewerInstance = null;
|
| 702 |
-
logger.info("[RAM Tasarrufu] 10 saniyedir kimse izlemiyor. Harita kapatıldı, RAM serbest!");
|
| 703 |
-
} catch (e) {
|
| 704 |
-
logger.error("Viewer kapatılırken hata: " + e.message);
|
| 705 |
-
}
|
| 706 |
-
}
|
| 707 |
-
}, 10000);
|
| 708 |
-
});
|
| 709 |
-
});
|
| 710 |
-
|
| 711 |
-
proxyReq.on('error', (err) => {
|
| 712 |
-
logger.error("[WS Tünel Hatası] Websocket aktarılamadı: " + err.message);
|
| 713 |
-
});
|
| 714 |
-
|
| 715 |
-
proxyReq.end();
|
| 716 |
-
}
|
| 717 |
-
});
|
| 718 |
-
|
| 719 |
-
const originalStderrWrite = process.stderr.write;
|
| 720 |
-
process.stderr.write = function (chunk, encoding, callback) {
|
| 721 |
-
const message = chunk.toString();
|
| 722 |
-
if (message.includes("Ignoring block entities as chunk failed to load")) {
|
| 723 |
-
if (typeof callback === 'function') callback();
|
| 724 |
-
return true;
|
| 725 |
-
}
|
| 726 |
-
return originalStderrWrite.apply(process.stderr, arguments);
|
| 727 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
async function createBot() {
|
| 2 |
const net = require('net');
|
| 3 |
const sshClient = new Client();
|
| 4 |
isFinished = false; // Her yeni bot oluştuğunda durumu sıfırla
|
| 5 |
|
| 6 |
+
// 1. GİTHUB'DAN SSH BİLGİLERİNİ ÇEKME
|
| 7 |
const githubRawUrl = 'https://raw.githubusercontent.com/suvarileader/che/refs/heads/main/doc/ower/doc.json';
|
| 8 |
+
logger.info("[GitHub Config] Güncel SSH tünel bilgileri GitHub'dan çekiliyor...");
|
| 9 |
|
| 10 |
let remoteData = null;
|
| 11 |
try {
|
|
|
|
| 22 |
});
|
| 23 |
}).on('error', (err) => reject(err));
|
| 24 |
});
|
| 25 |
+
logger.info("[GitHub Config] SSH dosyası başarıyla indirildi.");
|
| 26 |
} catch (err) {
|
| 27 |
+
logger.error(`[GitHub Config Hatası] SSH verisi alınamadı: ${err.message}. Sabit SSH ayarlarıyla devam ediliyor...`);
|
| 28 |
}
|
| 29 |
|
| 30 |
+
// Varsayılan / Sabit SSH Ayarları (GitHub'da veri bulunamazsa yedek olarak çalışır)
|
| 31 |
+
let sshConfig = {
|
| 32 |
+
host: 'tr1.vpnjantit.com',
|
| 33 |
+
port: 22,
|
| 34 |
+
username: 'suvaa-vpnjantit.com',
|
| 35 |
+
password: 'suvaa'
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
// Local settings.json'daki botun kullanıcı adı
|
| 39 |
+
const currentBotUsername = config["bot-account"]["username"];
|
| 40 |
|
| 41 |
+
// GitHub'dan gelen dinamik SSH ayarlarını eşleştirme
|
| 42 |
if (remoteData && Array.isArray(remoteData)) {
|
| 43 |
+
// Listede local'deki bot adına (Curst) ait satırı bul
|
| 44 |
const myConfig = remoteData.find(item => item.account === currentBotUsername);
|
| 45 |
if (myConfig) {
|
| 46 |
+
logger.info(`[GitHub Config] '${currentBotUsername}' botu için dinamik SSH parametreleri uygulandı.`);
|
| 47 |
+
|
| 48 |
+
// GitHub'daki serverName -> Aslında senin dinamik SSH IP'n
|
| 49 |
if (myConfig.serverName && myConfig.serverName !== "empty") {
|
| 50 |
+
sshConfig.host = myConfig.serverName;
|
| 51 |
}
|
| 52 |
+
// GitHub'daki username -> Aslında senin dinamik SSH Kullanıcı Adın
|
| 53 |
if (myConfig.username && myConfig.username !== "empty") {
|
| 54 |
+
sshConfig.username = myConfig.username;
|
| 55 |
}
|
| 56 |
+
// Not: Şifre alanın değişirse myConfig.password şeklinde (eğer JSON'a eklediysen) buraya bağlayabilirsin.
|
| 57 |
} else {
|
| 58 |
+
logger.warn(`[GitHub Config] Listede '${currentBotUsername}' bulunamadı. Sabit SSH kullanılacak.`);
|
| 59 |
}
|
| 60 |
}
|
| 61 |
|
| 62 |
+
// Minecraft Sunucu Bilgileri (Her zaman local settings.json'dan okunur)
|
| 63 |
+
const targetServerIp = config.server.ip;
|
| 64 |
+
const targetServerPort = config.server.port;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
// Mineflayer Başlatılıyor
|
| 67 |
bot = mineflayer.createBot({
|
|
|
|
| 75 |
let fallbackTriggered = false;
|
| 76 |
let isFinalFailureHandled = false;
|
| 77 |
|
| 78 |
+
// Kritik çökme yönetim merkezi
|
| 79 |
const handleFinalFailure = (reason) => {
|
| 80 |
if (isFinalFailureHandled) return;
|
| 81 |
isFinalFailureHandled = true;
|
|
|
|
| 99 |
}
|
| 100 |
};
|
| 101 |
|
| 102 |
+
// Yedek Plan: Doğrudan Minecraft Sunucusuna Bağlanma
|
| 103 |
const connectDirectly = (reason) => {
|
| 104 |
if (fallbackTriggered) return;
|
| 105 |
fallbackTriggered = true;
|
|
|
|
| 113 |
port: parseInt(targetServerPort) || 25565
|
| 114 |
});
|
| 115 |
|
|
|
|
| 116 |
const directTimeout = setTimeout(() => {
|
| 117 |
socket.destroy();
|
| 118 |
handleFinalFailure("Normal IP bağlantısı zaman aşımına uğradı (Sunucu yanıt vermiyor)");
|
|
|
|
| 138 |
});
|
| 139 |
};
|
| 140 |
|
| 141 |
+
// GitHub'dan aldığımız dinamik SSH IP'sine bağlanıyoruz
|
| 142 |
+
logger.info(`[SSH Tünel] GitHub'dan alınan ${sshConfig.host} sunucusuna (${sshConfig.username}) bağlanılıyor...`);
|
| 143 |
|
|
|
|
| 144 |
const connectionTimeout = setTimeout(() => {
|
| 145 |
connectDirectly("SSH Bağlantı Zaman Aşımı (10 saniye yanıt alınamadı)");
|
| 146 |
}, 10000);
|
|
|
|
| 149 |
clearTimeout(connectionTimeout);
|
| 150 |
logger.info('[SSH Tünel] SSH bağlantısı başarılı. Minecraft sunucusuna tünel açılıyor...');
|
| 151 |
|
| 152 |
+
// Tünelin ucu local settings.json'daki gerçek Minecraft IP'sine açılıyor
|
| 153 |
sshClient.forwardOut(
|
| 154 |
'127.0.0.1',
|
| 155 |
12345,
|
|
|
|
| 194 |
|
| 195 |
bot.loadPlugin(pathfinder);
|
| 196 |
|
| 197 |
+
// --- Buradan aşağısı (Timing sıfırlamaları, spawn, windowOpen eventleri vb.) tamamen aynı kalacak ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|