op-packages/deepbot/files/usr/bin/deepbot.lua
github-actions[bot] b2ad010c6f 🎨 Sync 2026-06-11 21:20:11
2026-06-11 21:20:11 +08:00

970 lines
32 KiB
Lua
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env lua
-- AI Telegram Bot for OpenWrt (Lua 5.1.5)
-- Config: /etc/config/deepbot
-- Depends: curl lua luasocket lua-cjson lsqlite3 uci
local socket = require("socket")
local json = require("cjson")
local sqlite3 = require("lsqlite3")
-- ===== UCI =====
local function uci_get(key)
local f = io.popen("uci -q get deepbot." .. key .. " 2>/dev/null")
if not f then return nil end
local v = f:read("*l")
f:close()
return (v and v ~= "") and v or nil
end
local function uci_get_bool(key, default)
local v = uci_get(key)
if v == nil then return default end
return v == "1" or v == "true" or v == "yes"
end
local function uci_get_int(key, default)
local v = uci_get(key)
return v and tonumber(v) or default
end
-- ===== Config =====
local TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
or uci_get("main.token")
or "YOUR_BOT_TOKEN"
local DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
or uci_get("main.deepseek_key")
or "YOUR_DEEPSEEK_KEY"
local DEEPSEEK_API_URL = os.getenv("DEEPSEEK_API_URL")
or uci_get("main.deepseek_url")
or "https://api.deepseek.com/v1/chat/completions"
local AI_MODEL = os.getenv("AI_MODEL")
or uci_get("main.model")
or "deepseek-chat"
local PROXY_URL = os.getenv("PROXY_URL")
or uci_get("main.proxy")
or ""
local DATABASE_NAME = os.getenv("DATABASE_NAME")
or uci_get("main.database")
or "/usr/share/deepbot/chat_history.db"
local TOKEN_LIMIT_PER_DAY = tonumber(os.getenv("TOKEN_LIMIT_PER_DAY"))
or uci_get_int("main.token_limit", 10000)
local RATE_LIMIT = tonumber(os.getenv("RATE_LIMIT"))
or uci_get_int("main.rate_limit", 5)
local DEBUG = os.getenv("DEBUG") == "1"
or uci_get_bool("main.debug", false)
local GROUP_MENTION_ONLY = os.getenv("GROUP_MENTION_ONLY") == "1"
or uci_get_bool("main.group_mention_only", true)
local ADMIN_ID = os.getenv("ADMIN_ID")
or uci_get("main.admin_id")
or ""
local WHITELIST_MODE = os.getenv("WHITELIST_MODE")
or uci_get("main.whitelist_mode")
or "none" -- none, all, private, group
local TG_API_BASE = "https://api.telegram.org/bot" .. TELEGRAM_BOT_TOKEN
local BOT_USERNAME = ""
-- ===== LOG =====
local function log(level, msg)
io.stderr:write(string.format("[%s] [%s] %s\n",
os.date("%Y-%m-%d %H:%M:%S"), level, tostring(msg)))
io.stderr:flush()
end
local function info(msg) log("INFO", msg) end
local function err(msg) log("ERROR", msg) end
local function dbg(msg) if DEBUG then log("DEBUG", msg) end end
-- ===== CURL =====
local function curl_post(url, headers, body)
local header_args = ""
for k, v in pairs(headers) do
header_args = header_args .. string.format(" -H '%s: %s'", k, v)
end
local proxy_arg = (PROXY_URL ~= "") and (" -x '" .. PROXY_URL .. "'") or ""
local pid_f = io.popen("echo $$")
local pid = pid_f and pid_f:read("*l") or "0"
if pid_f then pid_f:close() end
local tmpfile = string.format("/tmp/deepbot_%s_%s.json", pid, tostring(os.time()))
local f = io.open(tmpfile, "w")
if not f then
err("curl_post: cannot write tmpfile " .. tmpfile)
return nil
end
f:write(body)
f:flush()
f:close()
local cmd = string.format(
"curl -s -m 30 -X POST%s%s --data-binary @'%s' '%s'",
header_args, proxy_arg, tmpfile, url
)
dbg("curl_post CMD: " .. cmd)
local pipe = io.popen(cmd)
if not pipe then
os.remove(tmpfile)
err("curl_post: popen failed")
return nil
end
local resp = pipe:read("*a")
pipe:close()
os.remove(tmpfile)
dbg("curl_post RESP (" .. #resp .. " bytes): " .. resp:sub(1, 300))
return resp
end
-- ===== TELEGRAM API =====
local function tg_call(method, params, silent)
silent = silent or false
local body = params and json.encode(params) or "{}"
if DEBUG then dbg("tg_call " .. method .. " body: " .. body:sub(1, 200)) end
local resp = curl_post(
TG_API_BASE .. "/" .. method,
{ ["Content-Type"] = "application/json" },
body
)
if not resp or resp == "" then
if not silent then err("tg_call " .. method .. ": empty response") end
return nil
end
local ok, data = pcall(json.decode, resp)
if not ok then
if not silent then
err("tg_call " .. method .. " JSON error: " .. tostring(data))
if DEBUG then err("raw: " .. resp:sub(1, 300)) end
end
return nil
end
if not data.ok then
if not silent then
err("tg_call " .. method .. " API error: " ..
tostring(data.description or json.encode(data)))
end
end
return data
end
-- ===== DATABASE =====
local db
local function init_database()
os.execute("mkdir -p /usr/share/deepbot")
db = sqlite3.open(DATABASE_NAME)
local rc = db:exec([[
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
tokens INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_user_id ON conversations (user_id);
CREATE INDEX IF NOT EXISTS idx_timestamp ON conversations (timestamp);
CREATE TABLE IF NOT EXISTS token_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
date DATE NOT NULL,
tokens_used INTEGER DEFAULT 0,
UNIQUE(user_id, date)
);
CREATE INDEX IF NOT EXISTS idx_token_usage ON token_usage (user_id, date);
CREATE TABLE IF NOT EXISTS allowed_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
added_by INTEGER
);
CREATE INDEX IF NOT EXISTS idx_allowed_user_id ON allowed_users (user_id);
]])
if rc ~= sqlite3.OK then
err("DB init error: " .. tostring(db:errmsg()))
else
info("Database: " .. DATABASE_NAME)
end
end
local function validate_utf8(text)
if not text then return "" end
local clean = text:gsub("[^\128-\191]", function(c)
if c:byte() < 128 then return c
elseif c:byte() >= 194 and c:byte() <= 244 then return c
else return "" end
end)
clean = clean:gsub("'", "''")
return clean
end
local function get_user_messages(user_id, limit)
limit = limit or 10
local messages = {}
local stmt = db:prepare(
"SELECT role, content FROM conversations WHERE user_id=? ORDER BY timestamp ASC LIMIT ?"
)
stmt:bind_values(user_id, limit)
for row in stmt:nrows() do
table.insert(messages, { role = row.role, content = row.content })
end
stmt:finalize()
return messages
end
local function get_history_messages(user_id, limit)
limit = limit or 20
local messages = {}
local stmt = db:prepare(
"SELECT role, content FROM conversations WHERE user_id=? ORDER BY timestamp DESC LIMIT ?"
)
stmt:bind_values(user_id, limit)
for row in stmt:nrows() do
table.insert(messages, { role = row.role, content = row.content })
end
stmt:finalize()
return messages
end
local function save_message(user_id, role, content, tokens)
tokens = tokens or 0
content = validate_utf8(content)
local stmt = db:prepare(
"INSERT INTO conversations (user_id, role, content, tokens) VALUES (?,?,?,?)"
)
stmt:bind_values(user_id, role, content, tokens)
stmt:step()
stmt:finalize()
local today = os.date("%Y-%m-%d")
db:exec(string.format(
"INSERT OR IGNORE INTO token_usage (user_id, date, tokens_used) VALUES (%d,'%s',0)",
user_id, today
))
db:exec(string.format(
"UPDATE token_usage SET tokens_used=tokens_used+%d WHERE user_id=%d AND date='%s'",
tokens, user_id, today
))
end
local function clear_user_conversation(user_id)
db:exec(string.format("DELETE FROM conversations WHERE user_id=%d", user_id))
end
local function get_daily_token_usage(user_id)
local today = os.date("%Y-%m-%d")
local stmt = db:prepare(
"SELECT tokens_used FROM token_usage WHERE user_id=? AND date=?"
)
stmt:bind_values(user_id, today)
local result = 0
for row in stmt:nrows() do result = row.tokens_used end
stmt:finalize()
return result
end
-- ===== WHITELIST DATABASE FUNCTIONS =====
local function add_allowed_user(user_id, added_by)
local stmt = db:prepare(
"INSERT OR IGNORE INTO allowed_users (user_id, added_by) VALUES (?, ?)"
)
stmt:bind_values(user_id, added_by)
stmt:step()
stmt:finalize()
end
local function remove_allowed_user(user_id)
db:exec(string.format("DELETE FROM allowed_users WHERE user_id=%d", user_id))
end
local function get_allowed_users_full()
local users = {}
for row in db:nrows("SELECT user_id, added_at, added_by FROM allowed_users ORDER BY added_at DESC") do
table.insert(users, {
user_id = row.user_id,
added_at = row.added_at,
added_by = row.added_by
})
end
return users
end
local function get_allowed_users_list()
local users = {}
for row in db:nrows("SELECT user_id FROM allowed_users ORDER BY user_id") do
table.insert(users, tostring(row.user_id))
end
return users
end
-- ===== ACCESS CONTROL =====
local function is_admin(user_id)
if ADMIN_ID == "" then
return false
end
return tostring(user_id) == ADMIN_ID
end
local function is_user_allowed(user_id, chat_type)
-- Режим none: все могут писать
if WHITELIST_MODE == "none" then
return true
end
-- Режим all: везде проверяем whitelist
if WHITELIST_MODE == "all" then
if is_admin(user_id) then
return true
end
local stmt = db:prepare("SELECT 1 FROM allowed_users WHERE user_id=?")
stmt:bind_values(user_id)
local allowed = false
for row in stmt:nrows() do allowed = true; break end
stmt:finalize()
return allowed
end
-- Режим private: личка - whitelist, группы - всем
if WHITELIST_MODE == "private" then
if chat_type == "private" then
if is_admin(user_id) then
return true
end
local stmt = db:prepare("SELECT 1 FROM allowed_users WHERE user_id=?")
stmt:bind_values(user_id)
local allowed = false
for row in stmt:nrows() do allowed = true; break end
stmt:finalize()
return allowed
else
-- В группах всем можно
return true
end
end
-- Режим group: группы - whitelist, личка - только админ
if WHITELIST_MODE == "group" then
if chat_type == "private" then
-- В личке только админ
return is_admin(user_id)
else
-- В группах проверяем whitelist
if is_admin(user_id) then
return true
end
local stmt = db:prepare("SELECT 1 FROM allowed_users WHERE user_id=?")
stmt:bind_values(user_id)
local allowed = false
for row in stmt:nrows() do allowed = true; break end
stmt:finalize()
return allowed
end
end
-- По умолчанию - запрещено
return false
end
-- ===== utils =====
local function estimate_tokens(text)
return math.max(1, math.floor(#text / 4))
end
local function sanitize_text(text)
if #text > 4000 then text = text:sub(1, 4000) .. "" end
return text
end
-- ===== RATE LIMITER =====
local rate_table = {}
local function check_rate(user_id)
local now = socket.gettime()
local ts = rate_table[user_id] or {}
local fresh = {}
for _, t in ipairs(ts) do
if now - t < 60 then fresh[#fresh+1] = t end
end
if #fresh >= RATE_LIMIT then
rate_table[user_id] = fresh
return false
end
fresh[#fresh+1] = now
rate_table[user_id] = fresh
return true
end
-- ===== LRU CACHE =====
local response_cache = {}
local cache_order = {}
local CACHE_MAX = 200
local function cache_get(key) return response_cache[key] end
local function cache_set(key, val)
if not response_cache[key] then
table.insert(cache_order, key)
if #cache_order > CACHE_MAX then
response_cache[table.remove(cache_order, 1)] = nil
end
end
response_cache[key] = val
end
-- ===== CHAT: reply filter =====
local function group_filter(msg)
local chat_type = msg.chat.type
if chat_type == "private" then
return true, msg.text
end
if not GROUP_MENTION_ONLY then
return true, msg.text
end
local text = msg.text or ""
if msg.reply_to_message and msg.reply_to_message.from then
if msg.reply_to_message.from.username == BOT_USERNAME then
dbg("group_filter: reply to bot")
return true, text
end
end
if BOT_USERNAME ~= "" then
local mention = "@" .. BOT_USERNAME
local lower_text = text:lower()
local lower_mention = mention:lower()
if lower_text:find(lower_mention, 1, true) then
local clean = text:gsub("(?i)" .. mention, "")
clean = lower_text:gsub(lower_mention, "")
clean = clean:match("^%s*(.-)%s*$")
if clean == "" then clean = text end
dbg("group_filter: mention found, clean text: " .. clean)
return true, clean
end
end
dbg("group_filter: not addressed to bot, skipping")
return false, nil
end
-- ===== API =====
local function call_deepseek(user_id, prompt)
local cache_key = tostring(user_id) .. "\0" .. prompt
if cache_get(cache_key) then
info("Cache hit user=" .. user_id)
return cache_get(cache_key)
end
local daily_usage = get_daily_token_usage(user_id)
if daily_usage >= TOKEN_LIMIT_PER_DAY then
return nil, "Daily token limit reached"
end
local conversation = get_user_messages(user_id, 10)
if #conversation == 0 then
local sys = "You are a helpful AI assistant for Telegram users."
table.insert(conversation, { role = "system", content = sys })
save_message(user_id, "system", sys)
end
local total_est = estimate_tokens(prompt) + 100
for _, m in ipairs(conversation) do
total_est = total_est + estimate_tokens(m.content)
end
if daily_usage + total_est > TOKEN_LIMIT_PER_DAY then
return nil, "This request would exceed daily token limit"
end
local messages = {}
for _, m in ipairs(conversation) do messages[#messages+1] = m end
messages[#messages+1] = { role = "user", content = prompt }
local payload = json.encode({
model = AI_MODEL,
messages = messages,
temperature = 0.5,
})
local headers = {
["Authorization"] = "Bearer " .. DEEPSEEK_API_KEY,
["Content-Type"] = "application/json"
}
local resp_body = curl_post(DEEPSEEK_API_URL, headers, payload)
if not resp_body or resp_body == "" then
return nil, "Empty response from API"
end
local ok, data = pcall(json.decode, resp_body)
if not ok then
err("JSON error: " .. tostring(data))
err("Raw: " .. resp_body:sub(1, 300))
return nil, "JSON parse error"
end
if data.error then
local emsg = data.error.message or json.encode(data.error)
err("API error: " .. tostring(emsg))
return nil, tostring(emsg)
end
if data.choices then
local ai_reply = data.choices[1].message.content
local usage = data.usage or {}
local reply_tokens = usage.completion_tokens or estimate_tokens(ai_reply)
save_message(user_id, "assistant", ai_reply, reply_tokens)
if #prompt < 100 then cache_set(cache_key, data) end
info(string.format("AI OK user=%d tokens=%d model=%s",
user_id, reply_tokens, data.model or AI_MODEL))
return data
else
err("Unknown response: " .. resp_body:sub(1, 300))
return nil, "Unknown API response"
end
end
-- ===== Send =====
local function send_message(chat_id, text, reply_to)
local payload = {
chat_id = chat_id,
text = sanitize_text(text),
parse_mode = "Markdown",
disable_web_page_preview = true,
}
if reply_to then payload.reply_to_message_id = reply_to end
local res = tg_call("sendMessage", payload)
if not res or not res.ok then
payload.parse_mode = nil
res = tg_call("sendMessage", payload)
end
return res
end
-- ===== HANDLERS =====
local function handle_start(msg)
local user_id = msg.from.id
info("handle_start user=" .. user_id)
clear_user_conversation(user_id)
save_message(user_id, "system", "You are a helpful AI assistant for Telegram users.")
local mode_desc = {
none = "⚪ Disabled (all users)",
all = "🔴 Strict (whitelist only)",
private = "🟡 Private only (whitelist for PM)",
group = "🟠 Group only (whitelist for groups)"
}
send_message(msg.chat.id,
"🤖 *AI Bot*\n\n"
.. "Conversation history is saved locally.\n\n"
.. "*Commands:*\n"
.. "/clear — start a new conversation\n"
.. "/history — show recent messages\n"
.. "/tokens — daily token usage\n"
.. "/admin — show admin info\n"
.. "/allowed — show whitelist (admin only)\n"
.. "/whitelist — manage whitelist (admin only)\n"
.. "/whoami — show your Telegram ID\n\n"
.. "*Whitelist mode:* " .. (mode_desc[WHITELIST_MODE] or WHITELIST_MODE),
msg.message_id
)
end
local function handle_clear(msg)
info("handle_clear user=" .. msg.from.id)
clear_user_conversation(msg.from.id)
send_message(msg.chat.id, "🔄 *Conversation reset*", msg.message_id)
end
local function handle_history(msg)
local messages = get_history_messages(msg.from.id, 20)
if #messages == 0 then
send_message(msg.chat.id, "📜 *Conversation history is empty*", msg.message_id)
return
end
local lines = { "📜 *Recent Conversation:*\n" }
for i, m in ipairs(messages) do
local role = (m.role == "user") and "👤 You" or "🤖 AI"
local text = m.content:sub(1, 200)
if #m.content > 200 then text = text .. "" end
lines[#lines+1] = string.format("%s: %s\n", role, text)
if i >= 10 then
lines[#lines+1] = "\n*... and more*"
break
end
end
send_message(msg.chat.id, table.concat(lines, "\n"), msg.message_id)
end
local function handle_tokens(msg)
local user_id = msg.from.id
local daily_usage = get_daily_token_usage(user_id)
local pct = string.format("%.1f", daily_usage / TOKEN_LIMIT_PER_DAY * 100)
send_message(msg.chat.id,
string.format(
"🧮 *Daily Token Usage:*\n\n"
.. "• Used today: %d\n"
.. "• Daily limit: %d\n"
.. "• Percentage: %s%%\n\n"
.. "Stats reset at midnight UTC.",
daily_usage, TOKEN_LIMIT_PER_DAY, pct
),
msg.message_id
)
end
local function handle_admin_info(msg)
local user_id = msg.from.id
local is_user_admin = is_admin(user_id)
local message = "👑 *Admin System*\n\n"
if ADMIN_ID ~= "" then
message = message .. "• Admin ID: `" .. ADMIN_ID .. "`\n"
message = message .. "• Your ID: `" .. user_id .. "`\n\n"
if is_user_admin then
message = message .. "✅ *You are the admin*\n"
message = message .. "You can use whitelist commands."
else
message = message .. "❌ *You are not the admin*\n"
message = message .. "Contact the bot administrator."
end
else
message = message .. "⚠️ *No admin configured*\n"
message = message .. "Set `admin_id` in UCI config."
end
send_message(msg.chat.id, message, msg.message_id)
end
local function handle_whoami(msg)
local user_id = msg.from.id
local username = msg.from.username or "no username"
local first_name = msg.from.first_name or ""
local message = string.format(
"🆔 *Your Telegram ID*\n\n"
.. "• ID: `%d`\n"
.. "• Username: @%s\n"
.. "• Name: %s\n\n"
.. "Share this ID with admin to be added to whitelist.",
user_id, username, first_name
)
send_message(msg.chat.id, message, msg.message_id)
end
local function handle_allowed(msg)
local user_id = msg.from.id
local is_user_admin = is_admin(user_id)
if not is_user_admin then
send_message(msg.chat.id, "⛔ *Access denied!* Only admin can view whitelist.", msg.message_id)
return
end
if WHITELIST_MODE == "none" then
send_message(msg.chat.id,
"⚪ *Whitelist is disabled*\n\n"
.. "Enable with: `/whitelist mode all/private/group`",
msg.message_id
)
return
end
local users = get_allowed_users_full()
if #users == 0 then
send_message(msg.chat.id,
"📭 *Whitelist is empty*\n\n"
.. "Add users with: `/whitelist add USER_ID`",
msg.message_id
)
return
end
local mode_desc = {
all = "🔴 Strict mode (all chats)",
private = "🟡 Private mode (whitelist for PM only)",
group = "🟠 Group mode (whitelist for groups only)"
}
local message = "👥 *Allowed Users*\n"
message = message .. string.format("└ *Mode:* %s\n", mode_desc[WHITELIST_MODE] or WHITELIST_MODE)
message = message .. string.format("└ *Total:* %d user(s)\n\n", #users)
for i, u in ipairs(users) do
local date_str = u.added_at or "unknown"
date_str = date_str:match("(%d+-%d+-%d+)") or date_str
local added_by_str = u.added_by and string.format(" (by `%d`)", u.added_by) or ""
message = message .. string.format("%d. `%d`%s\n", i, u.user_id, added_by_str)
message = message .. string.format(" └ added: %s\n", date_str)
if #message > 3500 then
message = message .. "\n*... and more*"
break
end
end
send_message(msg.chat.id, message, msg.message_id)
end
local function handle_whitelist_status(msg)
local user_id = msg.from.id
if not is_admin(user_id) then
send_message(msg.chat.id, "⛔ *Admin only command*", msg.message_id)
return
end
local mode_desc = {
none = "⚪ *Disabled* - all users can use the bot",
all = "🔴 *Strict* - only whitelisted users (all chats)",
private = "🟡 *Private only* - whitelist for PM, open in groups",
group = "🟠 *Group only* - whitelist for groups, PM restricted to admin"
}
local users = get_allowed_users_list()
local message = string.format(
"🛡️ *Whitelist System*\n\n"
.. "*Mode:* %s\n"
.. "*Users:* %d\n\n",
mode_desc[WHITELIST_MODE] or "Unknown",
#users
)
if #users > 0 and WHITELIST_MODE ~= "none" then
message = message .. "*Allowed users:*\n"
for i, uid in ipairs(users) do
message = message .. string.format("%d. `%s`\n", i, uid)
if #message > 3500 then break end
end
message = message .. "\n"
end
message = message .. "*Commands:*\n"
message = message .. "• `/whitelist mode none` - disable\n"
message = message .. "• `/whitelist mode all` - strict mode\n"
message = message .. "• `/whitelist mode private` - PM only\n"
message = message .. "• `/whitelist mode group` - group only\n"
message = message .. "• `/whitelist add USER_ID` - add user\n"
message = message .. "• `/whitelist remove USER_ID` - remove user"
send_message(msg.chat.id, message, msg.message_id)
end
local function handle_whitelist_mode(msg, text)
local user_id = msg.from.id
if not is_admin(user_id) then
send_message(msg.chat.id, "⛔ *Admin only command*", msg.message_id)
return
end
local mode = text:match("/whitelist mode (%w+)")
if not mode or not (mode == "none" or mode == "all" or mode == "private" or mode == "group") then
send_message(msg.chat.id, "❌ *Usage:* `/whitelist mode {none|all|private|group}`", msg.message_id)
return
end
os.execute("uci set deepbot.main.whitelist_mode='" .. mode .. "'")
os.execute("uci commit deepbot")
-- Обновляем переменную в текущем процессе
WHITELIST_MODE = mode
local messages = {
none = "⚪ *Whitelist disabled* - all users can use the bot",
all = "🔴 *Whitelist mode: ALL* - only whitelisted users in all chats",
private = "🟡 *Whitelist mode: PRIVATE* - whitelist for PM, open in groups",
group = "🟠 *Whitelist mode: GROUP* - whitelist for groups, PM restricted to admin"
}
send_message(msg.chat.id, messages[mode], msg.message_id)
info("Whitelist mode changed to: " .. mode)
end
local function handle_whitelist_add(msg, text)
local user_id = msg.from.id
if not is_admin(user_id) then
send_message(msg.chat.id, "⛔ *Admin only command*", msg.message_id)
return
end
local new_user = text:match("/whitelist add (%d+)")
if not new_user then
send_message(msg.chat.id, "❌ *Usage:* `/whitelist add USER_ID`", msg.message_id)
return
end
add_allowed_user(tonumber(new_user), user_id)
send_message(msg.chat.id, "✅ User `" .. new_user .. "` added to whitelist", msg.message_id)
info("Added user " .. new_user .. " to whitelist")
end
local function handle_whitelist_remove(msg, text)
local user_id = msg.from.id
if not is_admin(user_id) then
send_message(msg.chat.id, "⛔ *Admin only command*", msg.message_id)
return
end
local remove_user = text:match("/whitelist remove (%d+)")
if not remove_user then
send_message(msg.chat.id, "❌ *Usage:* `/whitelist remove USER_ID`", msg.message_id)
return
end
remove_allowed_user(tonumber(remove_user))
send_message(msg.chat.id, "🗑️ User `" .. remove_user .. "` removed from whitelist", msg.message_id)
info("Removed user " .. remove_user .. " from whitelist")
end
local function handle_text(msg, clean_text)
local user_id = msg.from.id
local chat_id = msg.chat.id
local chat_type = msg.chat.type
if not is_user_allowed(user_id, chat_type) then
info("Access denied for user=" .. user_id .. " in " .. chat_type)
local message = ""
if WHITELIST_MODE == "all" then
message = "⛔ *Access denied!* You are not authorized to use this bot."
elseif WHITELIST_MODE == "private" and chat_type == "private" then
message = "⛔ *Access denied!* You are not in the whitelist.\nUse `/whoami` to get your ID and contact admin."
elseif WHITELIST_MODE == "group" and chat_type == "private" then
message = "⛔ *Access denied!* Private messages are restricted to admin only."
elseif WHITELIST_MODE == "group" and chat_type ~= "private" then
message = "⛔ *Access denied!* You are not authorized to use this bot in groups."
else
message = "⛔ *Access denied!*"
end
send_message(chat_id, message, msg.message_id)
return
end
local text = clean_text or msg.text
info(string.format("handle_text user=%d chat=%d text=%q",
user_id, chat_id, text:sub(1, 80)))
if not check_rate(user_id) then
send_message(chat_id, "⏳ *Too many requests!* Please wait 1 minute.", msg.message_id)
return
end
tg_call("sendChatAction", { chat_id = chat_id, action = "typing" }, true)
local data, api_err = call_deepseek(user_id, text)
if api_err then
err("Error user=" .. user_id .. ": " .. api_err)
send_message(chat_id, "❌ *Error:* " .. api_err, msg.message_id)
return
end
local answer = data.choices[1].message.content
send_message(chat_id, answer, msg.message_id)
end
-- ===== DISPATCH =====
local function dispatch(msg)
if not msg then dbg("dispatch: nil message") return end
if not msg.text then dbg("dispatch: no text") return end
dbg(string.format("dispatch: chat_type=%s text=%q",
tostring(msg.chat and msg.chat.type), msg.text:sub(1, 80)))
local text = msg.text
if text == "/start" or text:find("^/start@") then handle_start(msg)
elseif text == "/clear" or text:find("^/clear@") then handle_clear(msg)
elseif text == "/history" or text:find("^/history@") then handle_history(msg)
elseif text == "/tokens" or text:find("^/tokens@") then handle_tokens(msg)
elseif text == "/admin" or text:find("^/admin@") then handle_admin_info(msg)
elseif text == "/whoami" or text:find("^/whoami@") then handle_whoami(msg)
elseif text == "/allowed" or text:find("^/allowed@") then handle_allowed(msg)
elseif text == "/whitelist" or text:find("^/whitelist@") then handle_whitelist_status(msg)
elseif text:find("^/whitelist mode ") then handle_whitelist_mode(msg, text)
elseif text:find("^/whitelist add ") then handle_whitelist_add(msg, text)
elseif text:find("^/whitelist remove ") then handle_whitelist_remove(msg, text)
elseif text:sub(1,1) ~= "/" then
local should_reply, clean_text = group_filter(msg)
if should_reply then
handle_text(msg, clean_text)
end
end
end
-- ===== LONG POLLING =====
local function run()
info("Lua version: " .. _VERSION)
info("Token configured: " .. (TELEGRAM_BOT_TOKEN ~= "YOUR_BOT_TOKEN" and "YES" or "NO"))
info("AI key configured: " .. (DEEPSEEK_API_KEY ~= "YOUR_DEEPSEEK_KEY" and "YES" or "NO"))
info("AI Model: " .. AI_MODEL)
info("Debug mode: " .. (DEBUG and "ON" or "OFF"))
info("Proxy: " .. (PROXY_URL ~= "" and PROXY_URL or "none"))
info("Group mention only: " .. (GROUP_MENTION_ONLY and "ON" or "OFF"))
info("Admin ID: " .. (ADMIN_ID ~= "" and ADMIN_ID or "NOT SET"))
info("Whitelist mode: " .. WHITELIST_MODE)
if WHITELIST_MODE ~= "none" and ADMIN_ID == "" then
err("WARNING: Whitelist mode is '" .. WHITELIST_MODE .. "' but no admin_id configured!")
err("Set admin_id via: uci set deepbot.main.admin_id='YOUR_ID'")
end
local test = io.popen("curl --version 2>&1 | head -1")
if test then info("curl: " .. (test:read("*l") or "?")); test:close() end
info("Checking token via getMe...")
local me = tg_call("getMe", {}, true)
if me and me.ok then
BOT_USERNAME = me.result.username or ""
info("Bot: @" .. BOT_USERNAME)
else
err("getMe failed — check TELEGRAM_BOT_TOKEN!")
os.exit(1)
end
init_database()
info("Bot started. Long polling...")
local offset = 0
while true do
dbg("getUpdates offset=" .. offset)
local res = tg_call("getUpdates", {
offset = offset,
timeout = 30,
allowed_updates = { "message" },
}, true)
if res and res.ok and res.result then
local count = #res.result
if count > 0 then info("getUpdates: " .. count .. " update(s)") end
for _, update in ipairs(res.result) do
offset = update.update_id + 1
dbg("update_id=" .. update.update_id)
local ok, e = pcall(dispatch, update.message)
if not ok then err("dispatch error: " .. tostring(e)) end
end
else
if not res then
socket.sleep(5)
end
end
end
end
run()