Skip to content

Instantly share code, notes, and snippets.

@blinds52
Forked from AMD-NICK/api.lua
Created May 29, 2020 19:36
Show Gist options
  • Save blinds52/13f8f034e567fe6ff9b4cc04aff2308f to your computer and use it in GitHub Desktop.
Save blinds52/13f8f034e567fe6ff9b4cc04aff2308f to your computer and use it in GitHub Desktop.
Monobank Alerts Telegram Bot
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)
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