-- arz-notifier (v3 — релейный режим, zero-config для игрока) -- Ловит ключевые слова в чате Arizona RP и шлёт уведомление в Telegram через -- общий бот-релей (никаких токенов/ID у игрока — одна кнопка «Привязать Telegram»). -- Плюс удалённый выход с сервера: /q из Telegram или кнопка под уведомлением. -- Меню настроек: команда /notify (меняется в настройках). Окно закрывается на Esc. -- + авто-обновление: кнопка в меню качает свежую версию с сервера (как Arizona Helper). -- v3.7: слово ищется с начала слова («настройки» != «строй») и только в теле -- сообщения; плюс выбор чатов — рация РП (/r), рация NON-RP (/rb, по двойным -- скобкам «(( ... ))») и все остальные — каждый включается галочкой. -- v3.8: три списка «что ловить» — строи / упоминания по имени / упоминания по ID. -- Ник и ID берутся из игры сами и обновляются после релога. -- v3.9: у каждой категории СВОИ чаты (выпадашка «Чаты» в строке категории); -- из списка ID убрано голое число; после обновления просим Ctrl+R вручную. -- made by claude & cikriy67 script_name('arz-notifier') script_author('claude & cikriy67') script_version('4.3') require 'lib.moonloader' local imgui = require 'mimgui' local sampev = require 'samp.events' local effil = require 'effil' local dkjson = require 'dkjson' local ffi = require 'ffi' local encoding = require 'encoding' -- cjson отдаёт сырой UTF-8 (безопаснее для эмодзи), dkjson — запасной вариант local cjson_ok, cjson = pcall(require, 'cjson') local function json_encode(t) if cjson_ok then local ok, s = pcall(cjson.encode, t) if ok and s then return s end end return dkjson.encode(t) end encoding.default = 'CP1251' local u8 = encoding.UTF8 local new = imgui.new local VER = '4.3' local WATERMARK = 'made by claude & cikriy67' -- Адрес нашего релея на Railway и имя общего бота (см. bot_relay.py) local RELAY_BASE = 'https://arizonaparser-production.up.railway.app' local BOT_USERNAME = 'NotifyArizonabot' -- Авто-обновление: метаданные версии и сам файл скрипта на сервере local UPDATE_META_URL = RELAY_BASE .. '/api/script/arz_notifier.json' local UPDATE_LUA_URL = RELAY_BASE .. '/api/script/arz_notifier.lua' --==============================================================-- -- КОНФИГ (хранится в moonloader/config/arz_notifier.json) -- --==============================================================-- local CFG_DIR = getWorkingDirectory() .. '\\config' local CFG_PATH = CFG_DIR .. '\\arz_notifier.json' local DEF_BUILD = 'на явку' local cfg = { enabled = true, -- следить за чатом cmd = 'notify', -- команда открытия меню (меняется в настройках) keywords = 'на явку', -- (v3.7 и старее; в v3.8 переехало в kw_build) cooldown = 30, -- сек. защита от повторной отправки того же текста remote = true, -- принимать команду /q из Telegram (выход из игры) quit_cmd = '/q', -- команда выхода (если на сервере другая - поменяй тут) client_id = '', -- уникальный id привязки (генерится при первом запуске) -- v3.7: из каких чатов ловить (см. classify ниже) chat_rp = true, -- рация фракции, РП (/r) — то, ради чего всё chat_nonrp = false, -- рация фракции, NON-RP (/rb) — там флудят, по умолчанию ВЫКЛ chat_other = true, -- все остальные чаты (общий, VIP, семья, системные...) -- v3.8: три отдельных списка «что ловить» kw_build = DEF_BUILD, -- СТРОИ / свои слова kw_name = '', -- упоминания по ИМЕНИ (подставляется само из ника) kw_id = '', -- упоминания по ID (подставляется само, меняется после релога) on_build = true, on_name = false, on_id = false, -- v3.9: у КАЖДОЙ категории свои чаты (строи зовут по рации, а пингуют где угодно) ch_build = { rp = true, nonrp = false, other = true }, ch_name = { rp = true, nonrp = true, other = true }, ch_id = { rp = true, nonrp = true, other = true }, auto_name = true, -- список имени пересобирать самому (пока игрок не правил руками) auto_id = true, mig38 = false, -- перенос старых cfg.keywords -> kw_build выполнен -- v4.0: не будить на отчёты о наказаниях («уволен по причине: неявка в строй») skip_punish = true, } local function save_cfg() if not doesDirectoryExist(CFG_DIR) then createDirectory(CFG_DIR) end local f = io.open(CFG_PATH, 'w') if not f then return false end f:write(dkjson.encode(cfg, { indent = true })) f:close() return true end local function load_cfg() local f = io.open(CFG_PATH, 'r') if not f then return end local data = f:read('*a') f:close() local t = dkjson.decode(data) if type(t) == 'table' then for k, v in pairs(t) do cfg[k] = v end end end load_cfg() -- v3.8: у кого стоял v3.7 — переносим его слова из cfg.keywords в «Строи» if not cfg.mig38 then if type(cfg.keywords) == 'string' and cfg.keywords ~= '' then cfg.kw_build = cfg.keywords end cfg.mig38 = true end -- v3.9: общие галочки чатов разъехались по категориям + из списка ID убрано голое число local DEF_CH = { build = { rp = true, nonrp = false, other = true }, name = { rp = true, nonrp = true, other = true }, id = { rp = true, nonrp = true, other = true } } if not cfg.mig39 then cfg.ch_build = { rp = cfg.chat_rp ~= false, -- переносим старые общие nonrp = cfg.chat_nonrp == true, other = cfg.chat_other ~= false } if cfg.auto_id ~= false then cfg.kw_id = '' end -- пересоберётся без «123» cfg.mig39 = true end -- добиваем недостающие ключи, чтобы дальше можно было не проверять на nil for _, k in ipairs({ 'build', 'name', 'id' }) do local t = cfg['ch_' .. k] if type(t) ~= 'table' then t = {}; cfg['ch_' .. k] = t end for _, c in ipairs({ 'rp', 'nonrp', 'other' }) do if type(t[c]) ~= 'boolean' then t[c] = DEF_CH[k][c] end end end -- Уникальный client_id (один раз на установку). Telegram-payload: [A-Za-z0-9_-]. local function ensure_client_id() if type(cfg.client_id) ~= 'string' or cfg.client_id == '' then math.randomseed(os.time() + math.floor(os.clock() * 100000)) local t = {} for i = 1, 12 do t[i] = string.format('%x', math.random(0, 15)) end cfg.client_id = 'cl_' .. os.time() .. '_' .. table.concat(t) save_cfg() end end --==============================================================-- -- УТИЛИТЫ -- --==============================================================-- -- Сообщение в игровой чат (входной текст в UTF-8 -> конвертим в CP1251) local function addchat(text_utf8) sampAddChatMessage(u8:decode('{E91E63}[arz-notifier]{FFFFFF} ' .. text_utf8), -1) end -- Приведение CP1251-строки к нижнему регистру (включая кириллицу и Ё) local function cp1251_lower(s) -- v3.8: латиницу тоже опускаем — ники на Arizona латинские, а пишут их как попало return (s:gsub('[\168\192-\223A-Z]', function(c) local b = c:byte() if b == 168 then return string.char(184) end -- Ё -> ё return string.char(b + 32) -- А-Я -> а-я, A-Z -> a-z end)) end -- Безопасная конвертация CP1251 -> UTF-8 (чат с сервера может содержать любые байты) local function cp_to_u8(s) local ok, r = pcall(u8, s) if ok and type(r) == 'string' then return r end return s end -- Байт — часть слова? (латиница, цифры, подчёркивание, кириллица CP1251, Ё/ё) local function is_word_byte(b) if not b then return false end if b >= 48 and b <= 57 then return true end -- 0-9 if b >= 65 and b <= 90 then return true end -- A-Z if b >= 97 and b <= 122 then return true end -- a-z if b >= 192 and b <= 255 then return true end -- А-Я а-я (CP1251) return b == 95 or b == 168 or b == 184 -- _ Ё ё end -- v3.7: ключевое слово должно стоять В НАЧАЛЕ слова. -- «строй» ловит «строй», «стройся», «строй-сбор», но НЕ «наСТРОЙки/поСТРОЙка». -- Хвост слова не важен, поэтому ключ можно писать корнем («явк» = явка/явку/явки). -- whole=true — слово целиком (обе границы). Нужно для ID: иначе «123» поймается -- внутри «1234», «в 12345» и любой цены в чате. local function find_word(hay, kw, whole) local init = 1 while true do local s, e = hay:find(kw, init, true) if not s then return false end local left = not is_word_byte(hay:byte(s - 1)) local right = (not whole) or (not is_word_byte(hay:byte(e + 1))) if left and right then return true end init = s + 1 end end -- Три списка ключей (CP1251, нижний регистр) — «строи», имя, id local kwBuild, kwName, kwId = {}, {}, {} local function split_kw(s) local t = {} for part in tostring(s or ''):gmatch('[^,]+') do local x = part:gsub('^%s+', ''):gsub('%s+$', '') if #x > 0 then t[#t + 1] = cp1251_lower(u8:decode(x)) end end return t end local function rebuild_keywords() kwBuild = split_kw(cfg.kw_build) kwName = split_kw(cfg.kw_name) kwId = split_kw(cfg.kw_id) end rebuild_keywords() -- v4.0: ОТЧЁТЫ О НАКАЗАНИЯХ — не зов на строй. -- «[R] Вице-Губернатор Ivan[416]: Сотрудник Холи Граил был уволен по причине: -- неявка в строй» содержит слово «строй», но бежать к компу не нужно. -- Ловим по маркерам самого отчёта (прошедшее действие + причина), а не по «строю». -- ⚠️ Действует ТОЛЬКО на категорию «строи»: если в таком сообщении назван ТЫ, -- упоминание по имени/ID сработает как обычно — про своё увольнение знать полезно. -- ⛔ v4.3, ПОЙМАНО ЖИВЬЁМ 06.08.2026: юзер чуть не пропустил строй. -- Было: список из одиннадцати слов, и ЛЮБОЕ одно глушило всю категорию строёв -- ещё до проверки ключевых слов. А объявление обычно звучит «Строй через 5 минут, -- за НЕЯВКУ — ВЫГОВОР» — то есть фича «не будить на уволенных» убивала ровно то, -- ради чего скрипт и написан. -- Стало: глушим только ЗАПИСЬ О НАКАЗАНИИ — там всегда И глагол («уволен», -- «снят с должности»), И причина («по причине:»). Одного слова мало. -- Асимметрия намеренная: лишнее уведомление о чужом увольнении переживём, -- пропущенный строй — нет. local PUNISH_VERB = { 'уволен', 'уволил', 'снят с должност', 'снял с должност', 'понижен', 'выговор' } local PUNISH_CAUSE = { 'по причине', 'причина:', 'причине:' } local punishVerb, punishCause = {}, {} for _, w in ipairs(PUNISH_VERB) do punishVerb[#punishVerb + 1] = cp1251_lower(u8:decode(w)) end for _, w in ipairs(PUNISH_CAUSE) do punishCause[#punishCause + 1] = cp1251_lower(u8:decode(w)) end local function has_any(low, list) for _, kw in ipairs(list) do if find_word(low, kw, false) then return true end end return false end -- low — уже приведённое к нижнему регистру ТЕЛО сообщения (CP1251) local function is_punish(low) return has_any(low, punishVerb) and has_any(low, punishCause) end --==============================================================-- -- КТО Я: живые ник и ID (v3.8) -- --==============================================================-- -- ID выдаётся сервером и МЕНЯЕТСЯ после каждого релога, поэтому просто -- перечитываем его фоном; списки упоминаний пересобираются сами. local my_id, my_nick = -1, '' local function default_name_list(nick) if not nick or nick == '' then return '' end return '@' .. nick .. ', ' .. nick:gsub('_', ' ') -- @Ivan_Petrov, Ivan Petrov end -- v3.9: голое число («123») из списка УБРАНО по просьбе юзера — в чате слишком -- много чисел. Остаются только явные формы обращения: @123 и 1-2-3. local function default_id_list(id) if not id or id < 0 then return '' end local s = tostring(id) local dashed = s:gsub('%d', '%0-'):gsub('%-$', '') -- 123 -> 1-2-3 return '@' .. s .. (dashed ~= s and (', ' .. dashed) or '') end local ui_sync = false -- попросить окно перечитать поля (буферы imgui) -- вернуть true, если что-то изменилось local function refresh_me() local ok, id = pcall(function() local r, i = sampGetPlayerIdByCharHandle(PLAYER_PED) if r then return i end return nil end) if not ok or type(id) ~= 'number' or id < 0 then return false end local nick = '' pcall(function() nick = tostring(sampGetPlayerNickname(id) or '') end) local changed = false if id ~= my_id then my_id = id if cfg.auto_id ~= false then cfg.kw_id = default_id_list(my_id); changed = true end end if nick ~= '' and nick ~= my_nick then my_nick = nick if cfg.auto_name ~= false then cfg.kw_name = default_name_list(cp_to_u8(my_nick)); changed = true end end if changed then rebuild_keywords(); save_cfg(); ui_sync = true end return changed end --==============================================================-- -- ОТКУДА СООБЩЕНИЕ: /r, /rb или прочее (v3.7) -- --==============================================================-- -- Строка чата Arizona: «[ТЕГ] Ранг Имя_Фамилия[id]: текст» -- (у игрока может быть свой тег вторыми скобками: «[R] [злая зая] Юрист Ivan[5]: ...»). -- -- ⚠️ ГЛАВНОЕ: фракционная рация РП (/r) и NON-RP (/rb) идут под ОДНИМ тегом [R] -- и одним цветом — тегом/цветом их НЕ различить. Отличие одно: NON-RP пишется -- в ДВОЙНЫХ СКОБКАХ вокруг ВСЕГО сообщения: «(( текст ))». Так же и игра делит -- вкладки «Фракция» / «Фракция NON-RP». Скобки внутри фразы («я ((тут))») — не в счёт. local FRACTION_COLOR = 766526463 -- 0x2DB043FF, цвет фракционных чатов Arizona local RADIO_TAG = 'R' -- тег рации фракции -- сообщение целиком обёрнуто в (( ... )) ? local function is_nonrp(body) local t = body:gsub('^%s+', ''):gsub('%s+$', '') return #t >= 4 and t:sub(1, 2) == '((' and t:sub(-2) == '))' end -- -> 'rp' | 'nonrp' | 'other', тело сообщения (без тега/ранга/ника), id автора local function classify(text, color) local clean = text:gsub('{%x%x%x%x%x%x}', '') local body = clean:match('%[%d+%]:%s*(.+)$') or clean -- всё после «[id]:» local tag = clean:match('^%s*%[(.-)%]') or '' local sid = tonumber(clean:match('%[(%d+)%]:')) -- id написавшего if tag == RADIO_TAG or color == FRACTION_COLOR then if is_nonrp(body) then return 'nonrp', body, sid end return 'rp', body, sid end return 'other', body, sid end -- v3.9: чаты выбираются ОТДЕЛЬНО под каждую категорию (t = cfg.ch_build/ch_name/ch_id) local function chat_allowed(t, kind) return t[kind] == true end -- Открыть ссылку в браузере/Telegram (ShellExecute через ffi) pcall(ffi.cdef, [[ int __stdcall ShellExecuteA(void* hwnd, const char* op, const char* file, const char* params, const char* dir, int show); ]]) local ok_shell, shell32 = pcall(ffi.load, 'shell32') local function open_url(url) if not ok_shell then return false end return pcall(function() shell32.ShellExecuteA(nil, 'open', url, nil, nil, 1) end) end local function tg_deeplink() return 'https://t.me/' .. BOT_USERNAME .. '?start=' .. tostring(cfg.client_id) end --==============================================================-- -- СОСТОЯНИЕ UI -- --==============================================================-- local window = new.bool(false) local buf_build = new.char[256](tostring(cfg.kw_build or '')) local buf_name = new.char[192](tostring(cfg.kw_name or '')) local buf_id = new.char[96](tostring(cfg.kw_id or '')) local cb_on_build = new.bool(cfg.on_build ~= false) local cb_on_name = new.bool(cfg.on_name == true) local cb_on_id = new.bool(cfg.on_id == true) local cb_punish = new.bool(cfg.skip_punish ~= false) -- v4.0 local buf_cmd = new.char[32](tostring(cfg.cmd or 'notify')) -- v3.9: выбор чатов у каждой категории — прячем в выпадашку, чтобы меню -- не превращалось в простыню из девяти галочек. local cb_ch = new.bool(false) local CH_ROWS = { { 'rp', 'Рация фракции РП (/r)' }, { 'nonrp', 'Рация NON-RP (/rb)' }, { 'other', 'Остальные чаты (общий, VIP, семья...)' } } local function ch_label(t) local a = {} if t.rp then a[#a + 1] = 'РП' end if t.nonrp then a[#a + 1] = 'НРП' end if t.other then a[#a + 1] = 'пр.' end if #a == 3 then return 'Чаты: все' end if #a == 0 then return 'Чаты: нет' end return 'Чаты: ' .. table.concat(a, '+') end -- переписать содержимое поля ввода (когда ник/ID подставились сами) local function set_buf(buf, s, size) s = tostring(s or '') if #s > size - 1 then s = s:sub(1, size - 1) end ffi.fill(buf, size, 0) ffi.copy(buf, s) end local buf_code = new.char[16]('') local cb_enabled = new.bool(cfg.enabled and true or false) local cb_remote = new.bool(cfg.remote and true or false) local relay_linked = false -- привязан ли Telegram (обновляется опросом релея) local conn_status = { text = 'нажми «Привязать Telegram»', col = { 0.70, 0.70, 0.70, 1 } } local checking = false -- состояние авто-обновления local update_avail = false local update_ver = '' local update_notes = '' local update_busy = false local update_done = false -- скачано, ждём Ctrl+R от игрока -- v4.2: НАВЯЗЧИВОЕ обновление. Раньше про новую версию можно было узнать, только -- открыв меню, — то есть не узнать вообще. Если выкатывается обнова, которую -- игроки проигнорят, вся затея мертва. Поэтому отдельное окно, которое всплывает -- само и возвращается, пока не обновишься. -- ⚠️ LockPlayer у него ВЫКЛЮЧЕН: ловить человека посреди замеса нельзя. local nag = new.bool(false) local nag_next = 0 -- os.clock(), когда показать снова local nag_count = 0 -- сколько раз отложил — от этого зависит тон local NAG_WAIT = 600 -- «Позже» = 10 минут тишины --==============================================================-- -- АСИНХРОННЫЙ HTTPS (через effil, чтобы не фризить игру) -- --==============================================================-- local host_path, host_cpath = package.path, package.cpath -- Универсальный async-запрос: method = 'GET'/'POST', body = nil для GET. -- v3.6 ФИКС КРАША: раньше на КАЖДЫЙ запрос создавалась своя корутина с wait-циклом. -- На серверном фризе (пейдей) планировщик корутин MoonLoader ре-энтрился и падал -- «cannot resume non-suspended coroutine» → скрипт умирал (нужен был Ctrl+R). -- Теперь запрос просто кладётся в ОЧЕРЕДЬ, а обслуживает её ОДИН постоянный -- поток-диспетчер (start_http_pump), созданный один раз. Никаких корутин на запрос. local http_jobs = {} local function http_async(method, url, body, on_done) on_done = on_done or function() end local ok, th = pcall(function() return effil.thread(function(path, cpath, method, url, body) package.path, package.cpath = path, cpath local http = require 'socket.http' local https = require 'ssl.https' local ltn12 = require 'ltn12' http.TIMEOUT = 10 local resp = {} local req = { url = url, method = method, headers = { ['Connection'] = 'close' }, sink = ltn12.sink.table(resp), } if body then req.headers['Content-Type'] = 'application/json' req.headers['Content-Length'] = tostring(#body) req.source = ltn12.source.string(body) end local ok2, code = https.request(req) if ok2 then return true, tonumber(code) or -1, table.concat(resp) else return false, tostring(code) end end)(host_path, host_cpath, method, url, body) end) if not ok or not th then pcall(on_done, false, nil, 'spawn_error'); return end http_jobs[#http_jobs + 1] = { th = th, cb = on_done, t0 = os.clock() } end -- ЕДИНСТВЕННЫЙ поток-диспетчер: раз в 250мс проверяет статусы всех effil-задач и -- дёргает их колбэки. Создаётся ОДИН раз в main(). Всё в pcall: сбой любой задачи -- НЕ роняет поток и не убивает скрипт. local http_pump_started = false local function start_http_pump() if http_pump_started then return end http_pump_started = true lua_thread.create(function() while true do wait(250) local i = 1 while i <= #http_jobs do local job = http_jobs[i] local remove = false local ok_s, st = pcall(function() return job.th:status() end) if not ok_s then remove = true; pcall(job.cb, false, nil, 'status_error') elseif st == 'completed' then remove = true local ok_g, r1, r2, r3 = pcall(function() return job.th:get() end) if ok_g and r1 == true then pcall(job.cb, true, r2, r3) else pcall(job.cb, false, nil, 'get_error') end elseif st == 'failed' or st == 'canceled' then remove = true; pcall(job.cb, false, nil, 'thread_' .. tostring(st)) elseif (os.clock() - job.t0) > 30 then remove = true pcall(function() job.th:cancel(0) end) pcall(job.cb, false, nil, 'timeout') end if remove then table.remove(http_jobs, i) else i = i + 1 end end end end) end --==============================================================-- -- РЕЛЕЙ: уведомления + опрос привязки/команд -- --==============================================================-- -- Шлём явку через релей (он доставит игроку общим ботом). with_quit = добавить -- кнопку «Выйти из игры» под уведомлением. local function relay_notify(text_utf8, with_quit, on_done) if cfg.client_id == '' then if on_done then on_done(false, nil, 'no_client') end return end local body = json_encode({ client = cfg.client_id, text = text_utf8, with_quit = with_quit and true or false }) if not body then if on_done then on_done(false, nil, 'encode') end return end http_async('POST', RELAY_BASE .. '/api/tg/notify', body, on_done) end local function relay_poll(on_done) if cfg.client_id == '' then return end http_async('GET', RELAY_BASE .. '/api/tg/poll?client=' .. cfg.client_id, nil, on_done) end -- Привязка по коду (способ 2): шлём код, сервер связывает client_id с chat бота local function relay_claim(code, on_done) if cfg.client_id == '' then return end local body = json_encode({ client = cfg.client_id, code = code }) if not body then return end http_async('POST', RELAY_BASE .. '/api/tg/claim', body, on_done) end --==============================================================-- -- УДАЛЁННЫЙ ВЫХОД С СЕРВЕРА -- --==============================================================-- local quit_guard = 0 -- Сам выход с сервера (то же, что вручную написать /q). -- ВАЖНО: /q на Arizona — клиентская команда, поэтому нужен sampProcessChatInput -- (имитирует ручной ввод), а не sampSendChat (шлёт команду напрямую на сервер). local function execute_quit(reason) local now = os.time() if now - quit_guard < 5 then return end -- не спамим /q quit_guard = now addchat('{E74C3C}выхожу с сервера (' .. tostring(reason) .. ')...') relay_notify('🚪 Команда принята — выхожу с сервера. До скорого!', false) local cmd = cfg.quit_cmd or '/q' if sampProcessChatInput then sampProcessChatInput(cmd) else sampSendChat(cmd) end end -- Один цикл опроса релея: обновить статус привязки + выполнить команды (/q) local poll_inflight = false local function do_poll() if poll_inflight then return end -- не плодим параллельные опросы (меньше корутин) poll_inflight = true relay_poll(function(success, code, body) poll_inflight = false if not (success and code == 200) then return end local ok, j = pcall(dkjson.decode, body) if not ok or type(j) ~= 'table' then return end relay_linked = j.linked and true or false if cfg.remote and type(j.pending) == 'table' then for _, cmd in ipairs(j.pending) do if cmd == 'quit' then execute_quit('команда из Telegram') end end end end) end --==============================================================-- -- ПРОВЕРКА СВЯЗИ (тест-уведомление через релей) -- --==============================================================-- local function do_check() if cfg.client_id == '' then return end checking = true conn_status = { text = 'отправляю тест...', col = { 0.95, 0.80, 0.20, 1 } } relay_notify('✅ arz-notifier на связи! Это тестовое уведомление.', false, function(success, code, body) checking = false local j = (success and body) and dkjson.decode(body) or nil if success and code == 200 and type(j) == 'table' and j.ok then relay_linked = true conn_status = { text = 'связь работает! проверь Telegram', col = { 0.18, 0.80, 0.44, 1 } } addchat('{2ECC71}тест отправлен — проверь Telegram.') elseif type(j) == 'table' and j.error == 'not_linked' then relay_linked = false conn_status = { text = 'не привязан — нажми «Привязать Telegram»', col = { 0.95, 0.30, 0.20, 1 } } addchat('{F1C40F}Telegram не привязан. Нажми «Привязать Telegram» и Start в боте.') else conn_status = { text = 'сервер не ответил, попробуй ещё', col = { 0.90, 0.25, 0.20, 1 } } addchat('{E74C3C}релей не ответил: ' .. tostring(code or body)) end end) end -- Подтвердить код привязки из поля ввода (способ 2) local function do_claim() local code = (ffi.string(buf_code):gsub('%s+', '')) if code == '' then addchat('{F1C40F}сначала введи код, который прислал бот.'); return end conn_status = { text = 'проверяю код...', col = { 0.95, 0.80, 0.20, 1 } } relay_claim(code, function(success, hcode, body) local j = (success and body) and dkjson.decode(body) or nil if success and hcode == 200 and type(j) == 'table' and j.ok then relay_linked = true conn_status = { text = 'привязано по коду!', col = { 0.18, 0.80, 0.44, 1 } } addchat('{2ECC71}Telegram привязан по коду!') elseif type(j) == 'table' and j.error == 'bad_code' then conn_status = { text = 'код неверный или просрочен', col = { 0.95, 0.30, 0.20, 1 } } addchat('{E74C3C}код неверный или просрочен — напиши боту Start и получи новый.') else conn_status = { text = 'ошибка сервера', col = { 0.90, 0.25, 0.20, 1 } } addchat('{E74C3C}сервер не ответил: ' .. tostring(hcode or body)) end end) end -- Открытие/закрытие меню по команде local function toggle_menu() window[0] = not window[0] end -- Имя команды без слэша и пробелов local function clean_cmd(s) return (tostring(s or ''):gsub('%s+', ''):gsub('^/+', '')) end -- Живая регистрация команды меню (меняется на лету, без перезагрузки скрипта) local current_menu_cmd = nil local pending_cmd = nil -- команда, ожидающая регистрации (ставится из imgui, применяется в потоке) local function register_menu_cmd(name) name = clean_cmd(name) if name == '' then name = 'notify' end if name == current_menu_cmd then return name end pcall(sampRegisterChatCommand, name, toggle_menu) -- старую НЕ снимаем (до Ctrl+R работают обе) current_menu_cmd = name return name end local function apply_and_save() cfg.kw_build = ffi.string(buf_build) cfg.kw_name = ffi.string(buf_name) cfg.kw_id = ffi.string(buf_id) cfg.enabled = cb_enabled[0] local newcmd = clean_cmd(ffi.string(buf_cmd)) if newcmd == '' then newcmd = 'notify' end pending_cmd = newcmd -- регистрацию делает фоновый поток (НЕ из imgui-кадра!) rebuild_keywords() return save_cfg() end --==============================================================-- -- АВТО-ОБНОВЛЕНИЕ (как у Arizona Helper) -- --==============================================================-- -- true, если remote-версия СТРОГО новее локальной (сравнение по числам через точки) local function ver_newer(remote, local_v) local function parts(s) local t = {} for n in tostring(s):gmatch('%d+') do t[#t + 1] = tonumber(n) end return t end local a, b = parts(remote), parts(local_v) for i = 1, math.max(#a, #b) do local x, y = a[i] or 0, b[i] or 0 if x > y then return true elseif x < y then return false end end return false end -- Спрашиваем у сервера версию. Если новее нашей — показываем кнопку «Обновить». -- По кнопке качаем свежий .lua прямо поверх себя и перезагружаемся. local function check_update(silent) http_async('GET', UPDATE_META_URL, nil, function(success, code, body) if not (success and code == 200) then if not silent then addchat('{E74C3C}не удалось проверить обновления (сервер недоступен).') end return end local j = dkjson.decode(body) if type(j) ~= 'table' or not j.version then if not silent then addchat('{E74C3C}сервер обновлений ответил некорректно.') end return end if ver_newer(j.version, VER) then update_avail = true update_ver = tostring(j.version) update_notes = tostring(j.notes or '') addchat('{F1C40F}доступно обновление {FFFFFF}v' .. update_ver .. '{F1C40F} — открой /' .. tostring(current_menu_cmd or cfg.cmd) .. ' и нажми «Обновить».') nag[0] = true -- показать окно сразу, не дожидаясь, пока откроют меню nag_next = 0 else update_avail = false if not silent then addchat('{2ECC71}у тебя последняя версия (v' .. VER .. ').') end end end) end local function do_update() if update_busy then return end update_busy = true addchat('{F1C40F}скачиваю обновление v' .. update_ver .. '...') -- страховка: если за 30с не перезагрузились — снять блокировку кнопки lua_thread.create(function() wait(30000); update_busy = false end) local newpath = thisScript().path .. '.new' downloadUrlToFile(UPDATE_LUA_URL, newpath, function(id, status) if status == 6 then -- 6 = загрузка завершена (как в Arizona Helper) local f = io.open(newpath, 'rb') local content = f and f:read('*a') if f then f:close() end -- простая проверка целостности: непустой и похож на наш скрипт if content and #content > 1000 and content:find('script_name', 1, true) then local w = io.open(thisScript().path, 'wb') if w then w:write(content); w:close() end pcall(os.remove, newpath) -- ⚠️ НЕ вызываем thisScript():reload() — после него у mimgui плывёт -- отрисовка окна (v3.8 наступила на это). Просим ручной Ctrl+R. update_done = true update_avail = false nag[0] = false -- отстали: человек обновился window[0] = false addchat('{2ECC71}обновление v' .. update_ver .. ' установлено!') addchat('{F1C40F}ТЕПЕРЬ НАЖМИ {FFFFFF}Ctrl+R{F1C40F} — без этого работает старая версия,') addchat('{F1C40F}а меню может отрисоваться криво.') else pcall(os.remove, newpath) update_busy = false addchat('{E74C3C}файл обновления повреждён — отменено.') end end end) end --==============================================================-- -- УВЕДОМЛЕНИЕ О ЯВКЕ -- --==============================================================-- local last_text, last_time = '', 0 -- безопасная конвертация CP1251 -> UTF-8 (чат с сервера может содержать любые байты) local safe_u8 = cp_to_u8 local HEAD = { build = '🚨 ПОРА БЕЖАТЬ К КОМПУ! 🚨', name = '💬 ТЕБЯ УПОМЯНУЛИ ПО ИМЕНИ', id = '💬 ТЕБЯ УПОМЯНУЛИ ПО ID', } local function build_notification(raw_cp1251, why) local clean = raw_cp1251:gsub('{%x%x%x%x%x%x}', '') -- убираем цветовые теги SAMP clean = clean:gsub('^%s+', ''):gsub('%s+$', '') return (HEAD[why] or HEAD.build) .. '\n\n' .. safe_u8(clean) -- CP1251 -> UTF-8 .. '\n\n🕒 ' .. os.date('%H:%M:%S') end local function notify(raw_cp1251, why) local now = os.time() if raw_cp1251 == last_text and (now - last_time) < (tonumber(cfg.cooldown) or 30) then return -- тот же текст недавно уже отправляли end last_text, last_time = raw_cp1251, now local text = build_notification(raw_cp1251, why) relay_notify(text, cfg.remote, function(success, code, body) local j = (success and body) and dkjson.decode(body) or nil if success and code == 200 and type(j) == 'table' and j.ok then addchat('{2ECC71}явка! уведомление отправлено в Telegram.') elseif type(j) == 'table' and j.error == 'not_linked' then addchat('{F1C40F}явка поймана, но Telegram не привязан — открой /' .. tostring(current_menu_cmd or cfg.cmd) .. ' -> «Привязать Telegram».') else addchat('{E74C3C}явка поймана, но релей не ответил (' .. tostring(code or body) .. ')') end end) end --==============================================================-- -- ХУК ЧАТА -- --==============================================================-- function sampev.onServerMessage(color, text) -- весь разбор в pcall, чтобы ошибка никогда не сломала обработку чата pcall(function() if not cfg.enabled then return end -- v3.7: сначала понимаем, ИЗ КАКОГО чата строка (/r, /rb или прочее) local kind, body, sid = classify(text, color) if sid and my_id >= 0 and sid == my_id then return end -- своё же сообщение -- ищем только в ТЕЛЕ сообщения (не в теге чата, ранге и нике) -- и только со СТАРТА слова: «настройки» больше не считается за «строй». -- v3.9: у каждой категории свой набор чатов. local low = cp1251_lower(body) local why = nil if cfg.on_build ~= false and chat_allowed(cfg.ch_build, kind) then local hit = false for _, kw in ipairs(kwBuild) do if find_word(low, kw, false) then hit = true; break end end if hit then if cfg.skip_punish ~= false and is_punish(low) then -- ⚠️ Молчаливый пропуск — худшее, что может сделать этот скрипт: -- юзер узнаёт о нём, только когда УЖЕ пропустил строй. Раз слово -- поймано, но заглушено — пишем об этом в чат, пусть решает сам. addchat('{F1C40F}строй в сообщении есть, но похоже на запись о наказании — не бужу. ' .. 'Если это было объявление, выключи «не будить на выговоры» в /' .. tostring(current_menu_cmd or cfg.cmd or 'notify')) else why = 'build' end end end if not why and cfg.on_name == true and chat_allowed(cfg.ch_name, kind) then for _, kw in ipairs(kwName) do if find_word(low, kw, false) then why = 'name'; break end end end if not why and cfg.on_id == true and chat_allowed(cfg.ch_id, kind) then -- ID — только СЛОВОМ ЦЕЛИКОМ, иначе «1-2-3» поймается внутри длинных чисел for _, kw in ipairs(kwId) do if find_word(low, kw, true) then why = 'id'; break end end end if why then notify(text, why) end end) -- ничего не возвращаем -> сообщение проходит в чат как обычно end -- (фоновый сбор данных лавок временно убран по просьбе; код сохранён в -- D:\sampackupsrz_notifier_v3.2_lavka_2026-06-30.lua) --==============================================================-- -- ТЕМА (немного пафоса) -- --==============================================================-- imgui.OnInitialize(function() local style = imgui.GetStyle() style.WindowRounding = 8 style.ChildRounding = 8 style.FrameRounding = 6 style.GrabRounding = 6 style.PopupRounding = 6 style.ScrollbarRounding = 8 style.WindowPadding = imgui.ImVec2(16, 16) style.FramePadding = imgui.ImVec2(9, 6) style.ItemSpacing = imgui.ImVec2(8, 8) local C = imgui.Col local col = style.Colors col[C.WindowBg] = imgui.ImVec4(0.07, 0.07, 0.09, 0.97) col[C.Border] = imgui.ImVec4(0.91, 0.12, 0.39, 0.35) col[C.FrameBg] = imgui.ImVec4(0.16, 0.16, 0.20, 1.00) col[C.FrameBgHovered] = imgui.ImVec4(0.24, 0.20, 0.26, 1.00) col[C.FrameBgActive] = imgui.ImVec4(0.30, 0.22, 0.30, 1.00) col[C.TitleBg] = imgui.ImVec4(0.10, 0.10, 0.13, 1.00) col[C.TitleBgActive] = imgui.ImVec4(0.91, 0.12, 0.39, 0.85) col[C.Button] = imgui.ImVec4(0.91, 0.12, 0.39, 0.80) col[C.ButtonHovered] = imgui.ImVec4(0.95, 0.20, 0.47, 0.95) col[C.ButtonActive] = imgui.ImVec4(0.74, 0.09, 0.31, 1.00) col[C.CheckMark] = imgui.ImVec4(0.95, 0.24, 0.52, 1.00) col[C.Separator] = imgui.ImVec4(0.91, 0.12, 0.39, 0.50) end) --==============================================================-- -- ОКНО НАСТРОЕК -- --==============================================================-- -- ВАЖНО: файл в UTF-8, поэтому в imgui строки передаём как есть (без u8()). -- Навязчивое окно обновления. Отдельно от меню: его можно не открывать неделями, -- а обновиться нужно. Закрывается только двумя кнопками, крестика нет. local nagFrame = imgui.OnFrame( function() return nag[0] and update_avail and not update_done end, function() local res_x, res_y = getScreenResolution() imgui.SetNextWindowPos(imgui.ImVec2(res_x / 2, res_y / 2), imgui.Cond.Always, imgui.ImVec2(0.5, 0.5)) imgui.SetNextWindowSize(imgui.ImVec2(430, 0), imgui.Cond.Always) imgui.Begin('arz-notifier: нужно обновиться', nil, imgui.WindowFlags.NoCollapse + imgui.WindowFlags.NoResize + imgui.WindowFlags.NoMove + imgui.WindowFlags.AlwaysAutoResize) imgui.PushStyleColor(imgui.Col.Text, imgui.ImVec4(0.98, 0.80, 0.20, 1)) imgui.TextWrapped('%s', 'Вышла новая версия arz-notifier: v' .. update_ver .. ' (у тебя v' .. VER .. ')') imgui.PopStyleColor() imgui.Spacing() -- чем дольше откладывает, тем прямее формулировка local said if nag_count == 0 then said = 'Обновление занимает пару секунд и не сбрасывает настройки.' elseif nag_count < 3 then said = 'Ты уже откладывал ' .. nag_count .. ' раз. На старой версии часть' .. ' уведомлений может не приходить.' else said = 'Отложено ' .. nag_count .. ' раз. Пока не обновишься, это окно' .. ' будет возвращаться каждые 10 минут.' end imgui.TextWrapped('%s', said) if update_notes ~= '' then imgui.Spacing() imgui.TextDisabled('Что нового:') imgui.TextWrapped('%s', update_notes) end imgui.Spacing() imgui.Separator() imgui.Spacing() imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0.18, 0.55, 0.30, 1)) if imgui.Button(update_busy and 'Обновляюсь...' or 'Обновить сейчас', imgui.ImVec2(250, 32)) then if not update_busy then do_update() end end imgui.PopStyleColor() imgui.SameLine() if imgui.Button('Позже', imgui.ImVec2(-1, 32)) then nag[0] = false nag_count = nag_count + 1 nag_next = os.clock() + NAG_WAIT end imgui.End() end ) nagFrame.LockPlayer = false -- ⚠️ персонажа не запираем: можно поймать в замесе local mainFrame = imgui.OnFrame( function() return window[0] end, function() local res_x, res_y = getScreenResolution() imgui.SetNextWindowPos(imgui.ImVec2(res_x / 2, res_y / 2), imgui.Cond.FirstUseEver, imgui.ImVec2(0.5, 0.5)) imgui.SetNextWindowSize(imgui.ImVec2(490, 820), imgui.Cond.Always) imgui.Begin('arz-notifier - уведомления о явках', window, imgui.WindowFlags.NoCollapse + imgui.WindowFlags.NoResize) imgui.TextColored(imgui.ImVec4(0.95, 0.18, 0.45, 1.0), '* ARZ-NOTIFIER *') imgui.SameLine() imgui.TextDisabled('v' .. VER) imgui.SameLine() if imgui.SmallButton('проверить обновления') then check_update(false) end imgui.Separator() imgui.Spacing() -- обновление скачано и ждёт Ctrl+R (перезагружать себя сами не рискуем — плывёт mimgui) if update_done then imgui.PushStyleColor(imgui.Col.Text, imgui.ImVec4(0.35, 0.90, 0.50, 1)) imgui.TextWrapped('%s', 'Обновление v' .. update_ver .. ' скачано.') imgui.PopStyleColor() imgui.PushStyleColor(imgui.Col.Text, imgui.ImVec4(0.98, 0.80, 0.20, 1)) imgui.TextWrapped('%s', 'НАЖМИ Ctrl+R — до этого работает старая версия, а меню может рисоваться криво.') imgui.PopStyleColor() imgui.Separator() imgui.Spacing() end -- баннер обновления (если сервер сообщил о новой версии) if update_avail then imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0.18, 0.55, 0.30, 1)) if imgui.Button(update_busy and 'Обновляюсь...' or ('Обновить до v' .. update_ver), imgui.ImVec2(-1, 30)) then if not update_busy then do_update() end end imgui.PopStyleColor() if update_notes ~= '' then imgui.TextDisabled('Что нового:') imgui.TextWrapped('%s', update_notes) end imgui.Separator() imgui.Spacing() end if imgui.Checkbox('Следить за чатом и слать в Telegram', cb_enabled) then cfg.enabled = cb_enabled[0] save_cfg() end imgui.Spacing() imgui.Separator() imgui.Spacing() -- Привязка Telegram imgui.Text('Telegram:') imgui.SameLine() if relay_linked then imgui.TextColored(imgui.ImVec4(0.18, 0.80, 0.44, 1), 'привязан') else imgui.TextColored(imgui.ImVec4(0.95, 0.65, 0.20, 1), 'не привязан') end if relay_linked then -- уже привязан — только кнопка теста if imgui.Button(checking and 'Отправляю...' or 'Тест-уведомление', imgui.ImVec2(-1, 26)) then if not checking then do_check() end end else -- СПОСОБ 1: по ссылке (если Telegram открыт на этом же ПК) if imgui.Button('Способ 1: привязать по ссылке', imgui.ImVec2(-1, 30)) then local link = tg_deeplink() open_url(link) addchat('{2ECC71}открываю бота — нажми там {FFFFFF}Start{2ECC71}. Если не открылось, ссылка ниже:') addchat('{74B9FF}' .. link) end imgui.TextDisabled('Откроется @' .. BOT_USERNAME .. ' -> жми Start. (если ТГ на этом ПК)') imgui.Spacing() -- СПОСОБ 2: по коду (если Telegram на телефоне) imgui.TextDisabled('--- или по коду (если Telegram на телефоне) ---') if imgui.Button('Открыть бота', imgui.ImVec2(150, 24)) then open_url('https://t.me/' .. BOT_USERNAME) end imgui.SameLine() imgui.TextDisabled('-> напиши Start, бот пришлёт код') imgui.Text('Код от бота:') imgui.SameLine() imgui.PushItemWidth(110) imgui.InputText('##code', buf_code, ffi.sizeof(buf_code)) imgui.PopItemWidth() imgui.SameLine() if imgui.Button('Подтвердить##code') then do_claim() end end imgui.Text('Статус:') imgui.SameLine() local c = conn_status.col imgui.TextColored(imgui.ImVec4(c[1], c[2], c[3], c[4]), '%s', conn_status.text) imgui.Spacing() imgui.Separator() imgui.Spacing() -- ==== ЧТО ЛОВИТЬ: три отдельных списка (v3.8) ==== if ui_sync then -- ник/ID подставились сами — обновляем поля set_buf(buf_name, cfg.kw_name, ffi.sizeof(buf_name)) set_buf(buf_id, cfg.kw_id, ffi.sizeof(buf_id)) ui_sync = false end imgui.Text('Что ловить (через запятую):') -- одна строка категории: [галочка] / [список] [Чаты v] [Сброс] local function category(key, title, cb, buf, chats, hint, on_edit, on_reset) if imgui.Checkbox(title, cb) then cfg['on_' .. key] = cb[0]; save_cfg() end if hint ~= '' then imgui.SameLine(); imgui.TextDisabled('%s', hint) end imgui.PushItemWidth(-180) if imgui.InputText('##kw' .. key, buf, ffi.sizeof(buf)) then on_edit() end imgui.PopItemWidth(); imgui.SameLine() if imgui.Button(ch_label(chats) .. '##ch' .. key, imgui.ImVec2(104, 0)) then imgui.OpenPopup('pop' .. key) end if imgui.BeginPopup('pop' .. key) then imgui.TextDisabled('Ловить в чатах:') imgui.Separator() for _, r in ipairs(CH_ROWS) do cb_ch[0] = chats[r[1]] == true if imgui.Checkbox(r[2] .. '##' .. key .. r[1], cb_ch) then chats[r[1]] = cb_ch[0]; save_cfg() end end imgui.EndPopup() end imgui.SameLine() if imgui.Button('Сброс##' .. key, imgui.ImVec2(60, 0)) then on_reset() end end category('build', 'Строи и свои слова', cb_on_build, buf_build, cfg.ch_build, '', function() cfg.kw_build = ffi.string(buf_build); rebuild_keywords() end, function() cfg.kw_build = DEF_BUILD set_buf(buf_build, DEF_BUILD, ffi.sizeof(buf_build)) rebuild_keywords(); save_cfg() end) category('name', 'Упоминания по имени', cb_on_name, buf_name, cfg.ch_name, my_nick ~= '' and ('(' .. cp_to_u8(my_nick) .. ')') or '(жду ник)', function() cfg.kw_name = ffi.string(buf_name); cfg.auto_name = false; rebuild_keywords() end, function() cfg.auto_name = true cfg.kw_name = default_name_list(cp_to_u8(my_nick)) set_buf(buf_name, cfg.kw_name, ffi.sizeof(buf_name)) rebuild_keywords(); save_cfg() end) category('id', 'Упоминания по ID', cb_on_id, buf_id, cfg.ch_id, my_id >= 0 and ('(' .. my_id .. ')') or '(жду ID)', function() cfg.kw_id = ffi.string(buf_id); cfg.auto_id = false; rebuild_keywords() end, function() cfg.auto_id = true cfg.kw_id = default_id_list(my_id) set_buf(buf_id, cfg.kw_id, ffi.sizeof(buf_id)) rebuild_keywords(); save_cfg() end) -- v4.0: замы пишут «уволен по причине: неявка в строй» — в слове «строй» -- скрипт видел зов и будил зря. Галочка выключается, если такое нужно ловить. if imgui.Checkbox('Не будить на выговоры и увольнения', cb_punish) then cfg.skip_punish = cb_punish[0]; save_cfg() end imgui.SameLine() imgui.TextDisabled('(«...уволен по причине: неявка в строй»)') imgui.TextDisabled('Ник и ID подставляются сами; ID обновляется после релога.') imgui.TextDisabled('«Чаты» — где искать именно эту строку. NON-RP это (( текст )).') imgui.Spacing() imgui.Text('Команда открытия меню (без слэша):') imgui.PushItemWidth(140) imgui.InputText('##cmd', buf_cmd, ffi.sizeof(buf_cmd)) imgui.PopItemWidth() imgui.SameLine() if imgui.Button('Применить##cmd') then apply_and_save() end imgui.TextDisabled('Сейчас: /' .. tostring(current_menu_cmd or cfg.cmd) .. ' (по умолчанию /notify)') imgui.Spacing() imgui.Separator() imgui.Spacing() if imgui.Checkbox('Удалённый выход из игры через Telegram', cb_remote) then cfg.remote = cb_remote[0] save_cfg() end imgui.TextDisabled('Бот примет /q или кнопку под уведомлением и выйдет с сервера.') imgui.Spacing() if imgui.Button('Сохранить', imgui.ImVec2(135, 30)) then apply_and_save() addchat('{2ECC71}настройки сохранены.') end -- водяной знак в уголке imgui.Spacing() local tw = imgui.CalcTextSize(WATERMARK) imgui.SetCursorPosX(imgui.GetWindowWidth() - tw.x - 12) imgui.TextDisabled(WATERMARK) imgui.End() end ) mainFrame.LockPlayer = true -- пока меню открыто, персонаж не бегает --==============================================================-- -- ESC ЗАКРЫВАЕТ ОКНО НАСТРОЕК -- --==============================================================-- -- Пока окно открыто, перехватываем Esc и гасим окно. consumeWindowMessage -- не даёт игре открыть своё меню паузы. Крестик в углу тоже продолжает работать. local VK_ESCAPE = 0x1B function onWindowMessage(msg, wparam) if window[0] and wparam == VK_ESCAPE and (msg == 0x100 or msg == 0x104) then consumeWindowMessage(true, true) window[0] = false end end --==============================================================-- -- СТАРТ -- --==============================================================-- function main() while not isSampAvailable() do wait(0) end ensure_client_id() start_http_pump() -- v3.6: один диспетчер HTTP (фикс краша «cannot resume coroutine») register_menu_cmd(cfg.cmd) -- основная команда меню (по умолчанию /notify) -- запасная команда на случай, если сменишь основную и забудешь if current_menu_cmd ~= 'arznotify' then pcall(sampRegisterChatCommand, 'arznotify', toggle_menu) end -- применяем смену команды из меню в правильном контексте (не из imgui-кадра) lua_thread.create(function() while true do wait(150) if pending_cmd then local c = pending_cmd; pending_cmd = nil register_menu_cmd(c) if cfg.cmd ~= current_menu_cmd then cfg.cmd = current_menu_cmd; save_cfg() end addchat('{2ECC71}команда меню: {FFFFFF}/' .. tostring(current_menu_cmd) .. '{2ECC71} — закрой меню (Esc) и пиши её.') end end end) -- фоновый опрос релея: статус привязки + команды /q lua_thread.create(function() wait(1500) while true do if cfg.client_id ~= '' then do_poll() end wait(4000) end end) -- v4.2: возвращаем окно обновления, пока человек не обновится. Плюс строка в -- чат — окно можно закрыть и не заметить, а чат остаётся в истории. lua_thread.create(function() while true do wait(15000) if update_avail and not update_done and not nag[0] and nag_next > 0 and os.clock() >= nag_next then nag[0] = true nag_next = os.clock() + NAG_WAIT addchat('{E74C3C}напоминание: {FFFFFF}обнови arz-notifier до v' .. update_ver .. '{E74C3C} — на старой версии часть уведомлений может не приходить.') end end end) -- v3.8: следим за своим ником и ID (ID меняется после каждого релога) lua_thread.create(function() while true do wait(3000) pcall(refresh_me) end end) addchat('загружен! меню: {E91E63}/' .. tostring(current_menu_cmd or cfg.cmd)) addchat('{F1C40F}не привязан Telegram? Открой /' .. tostring(current_menu_cmd or cfg.cmd) .. ' -> «Привязать Telegram».') -- тихая проверка обновлений на старте (сообщит только если есть новая версия) lua_thread.create(function() wait(4000); check_update(true) end) wait(-1) end