-
-
Save blinds52/13f8f034e567fe6ff9b4cc04aff2308f to your computer and use it in GitHub Desktop.
Monobank Alerts Telegram Bot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local APIURL = "https://api.monobank.ua" | |
/* | |
Ошибки: | |
400 = Period must be no more than 31 days | |
400 = Missing required field 'from' | |
403 = Unknown 'X-Token' (левый токен) | |
429 = Too many requests | |
*/ | |
local MONO_MT = {} | |
MONO_MT.__index = MONO_MT | |
local function logError(token, err) | |
local line = DateTime() .. ": " .. token .. " > " .. err | |
MsgN(line) | |
file.Append("mono_errors.txt", line .. "\n") | |
end | |
-- В cb 1 - okdata, 2 - errdescr, 3 - errcode (mb nil) | |
function MONO_MT:_Get(cb, method) | |
local REQ = http.request(APIURL .. method, "GET") | |
REQ:SetHeaders({["X-Token"] = self.token}) | |
REQ:Exec(function(code, sDat) | |
local dat = assert(util.JSONToTable(sDat),"MONO No Json: " .. tostring(sDat)) | |
if code ~= 200 then | |
local err = dat.errorDescription | |
logError(self.token, err) | |
cb(nil, err, code) | |
return | |
end | |
cb(dat) | |
end,function(reason) | |
logError(self.token, "REQUEST ERROR: " .. reason) | |
cb(nil, reason, nil) | |
end) | |
end | |
-- 1 раз в 5 мин | |
-- https://api.monobank.ua/docs/#operation--bank-currency-get | |
function MONO_MT:GetCurrencyRates(cb) | |
self:_Get(cb, "/bank/currency") | |
end | |
-- 1 раз в 60 сек | |
-- https://api.monobank.ua/docs/#operation--personal-client-info-get | |
function MONO_MT:GetPersonalInfo(cb) | |
self:_Get(function(dat, err, code) | |
if dat then | |
self.cached = os.time() | |
self.name = dat.name | |
self.accs = dat.accounts -- balance, id, currencyCode, creditLimit, cashbackType | |
end | |
cb(dat, err, code) | |
end, "/personal/client-info") | |
end | |
-- 1 раз в 60 сек | |
-- Максимальний час за який можливо отримувати виписку 31 доба (2678400 секунд) | |
-- https://api.monobank.ua/docs/#operation--personal-statement--account---from---to--get | |
function MONO_MT:GetStatement(cb, iFrom, iTo_, sAccount_) | |
self:_Get(cb, "/personal/statement/" .. (sAccount_ or "0") .. "/" .. iFrom .. "/" .. (iTo_ or "")) | |
end | |
function Monobank(token) | |
return setmetatable({ | |
token = token | |
},MONO_MT) | |
end | |
-- local MONO = Monobank("") | |
-- MONO:GetCurrencyRates(PRINT) | |
-- MONO:GetPersonalInfo(PRINT) | |
-- MONO:GetStatement(PRINT, os.time() - DAYS(7), nil, 0) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if !KOSSON then | |
print("MONOALERTS БОТ НЕ ЗАПУЩЕН") | |
return | |
end | |
local BOT = TLG.SocketBot("monoalerts", "commands", | |
"bot_token", "TLG_MONO" | |
) | |
BOT:StartPolling() | |
-- ISO 4217 | |
BOT.CURRENCIES = setmetatable({ | |
[840] = "$", -- USD | |
[980] = "₴", -- UAH | |
[643] = "₽", -- RUB | |
[203] = "Kč", -- CZK | |
[978] = "€", -- EUR | |
},{ | |
__index = function(_, k) | |
return "currency #" .. tostring(k) | |
end | |
}) | |
BOT.ACCOUNTS = {OBJECTS = {}} | |
local ACCS = BOT.ACCOUNTS | |
function ACCS:New(chat_id, token, bCacheOnly) | |
self[chat_id] = self[chat_id] or {} | |
if self[chat_id][token] then return false end | |
self[chat_id][token] = table.insert(self[chat_id], token) | |
-- Create MONO object | |
self.OBJECTS[token] = self.OBJECTS[token] or Monobank(token) | |
self.OBJECTS[token].chats = self.OBJECTS[token].chats or {} | |
self.OBJECTS[token].chats[chat_id] = true | |
if !bCacheOnly then | |
bib.sadd("mono", token .. tostring(chat_id)) | |
end | |
end | |
function ACCS:Del(chat_id, token, bCacheOnly) | |
local id = self[chat_id] and self[chat_id][token] | |
if !id then return false end | |
self.OBJECTS[token].chats[chat_id] = nil | |
-- объект больше не используется ни в каких чатах | |
if table.Count(self.OBJECTS[token].chats) == 0 then | |
self.OBJECTS[token] = nil | |
end | |
local max_id = #self[chat_id] | |
if max_id == 1 then -- была одна запись в чате | |
self[chat_id] = nil | |
else | |
for i = id,max_id do | |
local tok = self[chat_id][id] | |
self[chat_id][id] = self[chat_id][i + 1] -- теперь это место занимает тот, кто был дальше | |
self[chat_id][tok] = i - 1 -- обновляем зависимость id за токеном, смещая id назад | |
end | |
end | |
if !bCacheOnly then | |
bib.srem("mono", token .. tostring(chat_id)) | |
end | |
return id | |
end | |
function ACCS:Load() | |
local members = bib.smembers("mono") | |
for _,mem in ipairs(members) do | |
local token, chat_id = mem:sub(1,44), tonumber(mem:sub(45,#mem)) | |
self:New(chat_id, token, true) | |
end | |
end | |
ACCS:Load() | |
function BOT:TxToText(tx, currencyCode, creditLimit) | |
currencyCode = currencyCode or 980 | |
creditLimit = creditLimit or 0 | |
local icon = tx.amount < 0 and "🔴" or "💚" | |
local decimal = tx.amount / 100 | |
local acc_currency = self.CURRENCIES[currencyCode] | |
local nice_ago = string.NiceTime(os.time() - tx.time) | |
local txt = "" | |
txt = txt .. string.format("%s %g %s %s назад\n", icon, decimal, acc_currency, nice_ago) | |
if tx.currencyCode ~= currencyCode then -- не в валюте счета | |
local tx_currency = self.CURRENCIES[tx.currencyCode] | |
txt = txt .. string.format("В валюте: %g %s\n", tx.operationAmount / 100, tx_currency) | |
end | |
if tx.commissionRate ~= 0 then | |
txt = txt .. string.format("Комиссия: %g %s\n", tx.commissionRate / 100, acc_currency) | |
end | |
txt = txt .. string.format("Остаток: %g %s\n", (tx.balance - creditLimit) / 100, acc_currency) | |
txt = txt .. string.format("``` %s ```", tx.description) | |
return txt | |
end | |
function BOT:SendHistory(chat_ids, token, from, to, acc_id, acc_curr, creditLimit) | |
local MONO = ACCS.OBJECTS[token] or Monobank(token) | |
MONO:GetStatement(function(dat) | |
local msg = "" | |
if dat then | |
for _,tx in ipairs(dat) do | |
msg = msg .. self:TxToText(tx, acc_curr, creditLimit) .. "\n\n" | |
end | |
else | |
msg = "Кажется, токен устарел" | |
end | |
-- for _,v in ipairs(string.Split(msg,"\n\n")) do MsgN(v .. "\n\n") end | |
for _,chat_id in pairs(chat_ids) do | |
BOT:Message(chat_id, msg):SetParseMode("markdown"):Send() | |
end | |
end, from, to, acc_id) | |
end | |
include("monobank_api.lua") | |
BOT:AddCommand("start", function() | |
local msg = | |
"Я присылаю оповещения, когда по карте проходит новая транзакция и рассчитан на использование семьей.\n\n" .. | |
"Создайте семейный чат, или добавьте меня в существующий, затем попросите каждого ввести /addaccount.\n\n" .. | |
"Конечно, вам никто не запрещает делать все это в личном чате" | |
return msg | |
end) | |
BOT:AddCommand("addaccount", function(MSG, args) | |
local chat_id = MSG:Chat():ID() | |
local token = args[1] | |
if !token or #token ~= 44 then | |
local msg = "Для работы нужен API токен\n\n" .. | |
"Перейдите по адресу https://api.monobank.ua, получите токен и повторите ввод команды, указав его после нее\n\n" .. | |
"Должно получиться как на скриншоте" | |
BOT:Photo(chat_id, "AgADBAAD3KkxGxm1nVBCuITEXA6MfrT_sBoABNuKBB9IV-K-wZ0CAAEC"):SetCaption(msg):Send() | |
return | |
end | |
local MONO = Monobank(token) | |
MONO:GetPersonalInfo(function(dat, _, code) | |
local msg = "" | |
if code == 403 then | |
msg = msg .. "Некорректный токен. Возможно, он уже устарел" | |
elseif dat then | |
msg = msg .. "Вы привязали аккаунт `" .. dat.name .. "` 🎉\nВведите /accounts для детальной информации" | |
ACCS:New(chat_id, token) | |
end | |
BOT:Message(chat_id, msg):SetParseMode("markdown"):Send() | |
end) | |
end) | |
local function getAccountText(token) | |
local msg = "" | |
assert(token, "разок проебался и думал что за хуйня") | |
local MONO = ACCS.OBJECTS[token] | |
if !MONO then return "Кажется, это сообщение уже не актуально" end -- мб попытка нажать кнопку удаления со старого сообщения | |
if MONO.cached then | |
msg = msg .. "`" .. MONO.name .. "`\n\n" | |
msg = msg .. "Счета:\n" | |
for _,acc in ipairs(MONO.accs) do | |
msg = msg .. ("- " .. ((acc.balance - acc.creditLimit) / 100) .. " " .. BOT.CURRENCIES[acc.currencyCode]) .. "\n" | |
end | |
-- #todo добавить подсчет общего баланса в USD | |
else | |
msg = "Аккаунт с токеном `" .. token .. "` еще не успел обновиться. Повторите через минуту" | |
end | |
return msg | |
end | |
BOT:AddCommand("accounts", function(MSG) | |
local chat_id = MSG:Chat():ID() | |
local accs = ACCS[chat_id] | |
if !accs then return "У вас нет аккаунтов, но вы можете добавить их через /addaccount" end | |
for _,token in ipairs(accs) do | |
local msg = getAccountText(token) | |
local IKB = TLG.InlineKeyboard() | |
IKB:Line( | |
IKB:Button("Выписка за неделю"):SetData({statement = token}), | |
IKB:Button("❌ Удалить"):SetData({delete = token}) | |
) | |
BOT:Message(chat_id, msg):SetReplyMarkup(IKB):SetParseMode("markdown"):Send() | |
end | |
end) | |
function BOT:OnCBQ(CBQ) | |
local MSG = CBQ:Message() | |
local chat_id = MSG:Chat():ID() | |
local data = CBQ:Data() | |
if data.statement then | |
self:SendHistory({chat_id}, data.statement, os.time() - DAYS(7)) | |
elseif data.delete then | |
local acc_text = getAccountText(data.delete) -- нужно ДО удаления | |
local deleted = ACCS:Del(chat_id, data.delete) | |
local appendix = deleted and "Аккаунт больше не отслеживается" or "Аккаунт не найден. Возможно, уже удален" | |
BOT:EditMessage(MSG, acc_text .. "\n\n" .. appendix):SetParseMode("markdown"):Send() | |
end | |
end | |
local function checkNewTransactions() | |
for token,MONO in pairs(ACCS.OBJECTS) do | |
local cache_time = MONO.cached -- mb nil | |
local cached_accs = MONO.accs | |
MONO:GetPersonalInfo(function(dat) | |
if !cache_time then return end -- просто закешировали на будущее (кеширование внутри) | |
for i,acc in ipairs(dat.accounts) do | |
-- PRINT(cached_accs[i].balance, acc.balance, os.time() - cache_time) | |
if cached_accs[i].balance ~= acc.balance then | |
BOT:SendHistory(table.GetKeys(MONO.chats), token, cache_time, nil, acc.id, acc.currencyCode, acc.creditLimit) | |
end | |
end | |
end) | |
end | |
end | |
-- Лимит раз в 60 сек. 65 взят на всякий случай | |
timer.Create("MONO.Alerts", 65, 0, checkNewTransactions) | |
checkNewTransactions() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment