split api

This commit is contained in:
Er2 2021-08-16 11:56:28 +03:00
parent 2b27291cfc
commit f2db621150
9 changed files with 7 additions and 697 deletions

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "etc/api"]
path = etc/api
url = https://gitdab.com/er2/tg-api-lua

1
etc/api Submodule

@ -0,0 +1 @@
Subproject commit 30f7203c5fe69e875dbd29a4da9fe0c5ab1ddc9b

View File

@ -1,137 +0,0 @@
--[[ API Library
-- (c) Er2 2021 <er2@dismail.de>
-- Zlib License
--]]
local tools = require 'etc.api.tools'
local json = require 'etc.json'
local events = require 'etc.events'
local api = {
request = function(s, ...) return tools.request(s.token, ...) end
}
api.__index = api -- Make class
events(api) -- inheritance
-- parse arguments
local function argp(cid, rmp, pmod, dwp)
return
type(cid) == 'table' and cid.chat.id or cid,
type(rmp) == 'table' and json.encode(rmp) or rmp,
(type(pmod) == 'boolean' and pmod == true) and 'MarkdownV2' or pmod,
dwp == nil and true or dwp
end
-- Getters without params
function api:getMe() return self:request 'getMe' end
function api:getMyCommands() return self:request 'getMyCommands' end
-- Getters with params
function api:getChat(cid) return self:request('getChat', {chat_id = cid}) end
-- Setters
function api:send(msg, txt, pmod, dwp, dnot, rtmid, rmp)
msg, rmp, pmod, dwp = argp(msg, rmp, pmod, dwp)
if txt and #txt >= 4096 then
txt = txt:sub(0, 4092) .. '...'
end
return self:request('sendMessage', {
chat_id = msg,
text = txt,
parse_mode = pmod,
disable_web_page_preview = dwp,
disable_notification = dnot,
reply_to_message_id = rtmid,
reply_markup = rmp,
})
end
function api:reply(msg, txt, pmod, dwp, rmp, dnot)
_, rmp, pmod, dwp = argp(msg, rmp, pmod, dwp)
return self:request('sendMessage', {
chat_id = msg.chat.id,
text = txt,
parse_mode = pmod,
disable_web_page_preview = dwp,
disable_notification = dnot,
reply_to_message_id = msg.message_id,
reply_markup = rmp,
})
end
function api:forward(cid, frcid, mid, dnot)
return self:request('forwardMessage', {
chat_id = cid,
from_chat_id = frcid,
disable_notification = dnot,
message_id = mid,
})
end
function api:sendPhoto(cid, f, cap, pmod, dnot, rtmid, rmp)
cid, rmp, pmod = argp(cid, rmp, pmod)
return self:request('sendPhoto', {
chat_id = cid,
caption = cap,
parse_mode = pmod,
disable_notification = dnot,
reply_to_message_id = rtmid,
reply_markup = rmp,
}, { photo = f })
end
function api:sendDocument(cid, f, cap, pmod, dnot, rtmid, rmp)
cid, rmp, pmod = argp(cid, rmp, pmod)
return self:request('sendDocument', {
chat_id = cid,
caption = cap,
parse_mode = pmod,
disable_notification = dnot,
reply_to_message_id = rtmid,
reply_markup = rmp,
}, { document = f })
end
function api:sendPoll(cid, q, opt, anon, ptype, mansw, coptid, expl, pmode, oper, cdate, closed, dnot, rtmid, rmp)
cid, rmp, pmode = argp(cid, rmp, pmode)
opt = type(opt) == 'string' and opt or json.encode(opt)
anon = type(anon) == 'boolean' and anon or false
mansw = type(mansw) == 'boolean' and mansw or false
return self:request('sendPoll', {
chat_id = cid,
question = q,
options = opt,
is_anonymous = anon,
type = ptype,
allows_multiple_answers = mansw,
correct_option_id = coptid,
explanation = expl,
explanation_parse_mode = pmode,
open_period = oper,
close_date = cdate,
is_closed = closed,
disable_notification = dnot,
reply_to_message_id = rtmid,
reply_markup = rmp,
})
end
function api:answerCallback(id, txt, alrt, url, ctime)
return self:request('answerCallbackQuery', {
callback_query_id = id,
text = txt,
show_alert = alrt,
url = url,
cache_time = ctime,
})
end
function api:setMyCommands(cmds)
return self:request('setMyCommands', { commands = json.encode(cmds) })
end
return api

View File

@ -1,133 +0,0 @@
--[[ Core file
-- (c) Er2 2021 <er2@dismail.de>
-- Zlib License
--]]
local tools = require 'etc.api.tools'
local api = require 'etc.api.api'
api.__index = api
function api:_ev(t, i, name, ...)
local v = t[i]
if v.name == name then
v.fn(self, ...)
if v.type == 'once' then table.remove(t, i) end
end
end
-- UPDATES --
function api:getUpdates(lim, offs, tout, allowed)
allowed = type(allowed) == 'table' and tools.json.encode(allowed) or allowed
return self:request('getUpdates', {
timeout = tout,
offset = offs,
limit = lim,
allowed_updates = allowed
})
end
local function receiveUpdate(self, update)
if update.message then
local msg = update.message
local cmd, to = tools.fetchCmd(msg.text or '')
if cmd and (not to or to == self.info.username) then
-- need /cmd@bot in groups
if (msg.chat.type == 'group' or msg.chat.type == 'supergroup')
and not to then return end
local args = {}
msg.text = msg.text:sub(#cmd + #(to or '') + 3)
for s in msg.text:gmatch '%S+' do table.insert(args, s) end
msg.cmd = cmd
msg.args = args
return self:emit('command', msg)
elseif cmd then return end
self:emit('message', msg)
elseif update.edited_message then
self:emit('messageEdit', update.edited_message)
elseif update.channel_post then self:emit('channelPost', update.channel_post)
elseif update.edited_channel_post then self:emit('channelPostEdit', update.edited_channel_post)
elseif update.poll then self:emit('poll', update.poll)
elseif update.poll_answer then self:emit('pollAnswer', update.poll_answer)
elseif update.callback_query then self:emit('callbackQuery', update.callback_query)
elseif update.inline_query then self:emit('inlineQuery', update.inline_query)
elseif update.shipping_query then self:emit('shippingQuery', update.shipping_query)
elseif update.pre_checkout_query then self:emit('preCheckoutQuery', update.pre_checkout_query)
elseif update.chosen_inline_result then self:emit('inlineResult', update.chosen_inline_result)
end
end
function api:_getUpd(lim, offs, ...)
local u, ok = self:getUpdates(lim, offs, ...)
if not ok or not u or (u and type(u) ~= 'table') or not u.result then return end
for _, v in pairs(u.result) do
offs = v.update_id + 1
if type(v) == 'table' then
self:emit('update', v)
receiveUpdate(self, v)
end
end
return offs
end
function api:_loop(lim, offs, ...)
while api.runs do
local o = self:_getUpd(lim, offs, ...)
offs = o and o or offs
end
self:getUpdates(lim, offs, ...)
end
-- RUN --
function api:run(lim, offs, tout, al)
lim = tonumber(lim) or 1
offs = tonumber(offs) or 0
tout = tonumber(tout) or 0
self.runs = true
self:emit 'ready'
self.co = coroutine.create(api._loop)
coroutine.resume(self.co, self, lim, tout, offs, al)
end
function api:destroy() self.runs = false end
function api:login(token, thn)
self.token = assert(token or self.token, 'Provide token!')
repeat
local r, o = self:getMe()
if o and r then self.info = r end
until (self.info or {}).result
self.info = self.info.result
self.info.name = self.info.first_name
if type(thn) == 'function' then thn(self) end
if not self.nr then self:run() end
end
return function(opts)
if not token or type(token) ~= 'string' then token = nil end
local self = setmetatable({}, api)
if type(opts) == 'table' then
if opts.token then self.token = opts.token end
if opts.norun then self.nr = true end
if not opts.noinl then
self.inline = require('etc.api.inline')(self)
end
end
return self
end

View File

@ -1 +0,0 @@
return require 'etc.api.core'

View File

@ -1,156 +0,0 @@
--[[ Inline query library
-- (c) Er2 2021 <er2@dismail.de>
-- Zlib License
--]]
local inline = {}
inline.__index = inline -- Make class
function inline.query(id, from, q, off, ct, loc)
return {
id = tostring(id),
from = from,
query = q,
offset = off,
chat_type = ct,
location = loc,
}
end
function inline.result(type, id, ...)
type = tostring(type)
local t = setmetatable({
type = type,
id = tostring(tonumber(id) or 1),
}, inline)
local a = {...}
if t.type == 'article' then t.title, t.url, t.hide_url, t.description = table.unpack(a)
elseif t.type == 'photo' then
t.photo_url, t.photo_width, t.photo_height, t.title, t.description,
t.caption, t.parse_mode, t.caption_entities
= table.unpack(a)
elseif t.type == 'gif' or t.type == 'mpeg4_gif' then
local url, width, height, duration
url, width, height, duration, t.title, t.caption, t.parse_mode, t.caption_entities
= table.unpack(a)
if t.type == 'gif' then
t.gif_url, t.gif_width, t.gif_height, t.gif_duration
= url, width, height, duration
else
t.mpeg4_url, t.mpeg4_width, t.mpeg4_height, t.mpeg4_duration
= url, width, height, duration
end
elseif t.type == 'video' then
t.video_url, t.mime_type, t.title, t.caption, t.parse_mode,
t.caption_entities, t.video_width, t.video_height, t.video_duration, t.description
= table.unpack(a)
elseif t.type == 'audio' or t.type == 'voice' then
t.title, t.caption, t.parse_mode, t.caption_entities = table.unpack(a, 2)
if t.type == 'audio' then
t.audio_url, t.performer, t.audio_duration = a[1], a[6], a[7]
else
t.voice_url, t.voice_duration = a[1], a[6]
end
elseif t.type == 'document' then
t.title, t.caption, t.parse_mode, t.caption_entities, t.document_url,
t.mime_type, t.description = table.unpack(a)
elseif t.type == 'location' or t.type == 'venue' then
t.latitude, t.longitude, t.title = table.unpack(a, 1, 3)
if t.type ~= 'venue' then
t.horizontal_accurancy, t.live_period, t.heading, t.proximity_alert_radius
= table.unpack(a, 4, 7)
else
t.address, t.foursquare_id, t.foursquare_type, t.google_place_id, t.google_place_type
= table.unpack(a, 4, 8)
end
elseif t.type == 'contact' then
t.phone_number, t.first_name, t.last_name, t.vcard,
t.reply_markup, t.input_message_content
= table.unpack(a)
elseif t.type == 'game' then t.game_short_name = a[1]
end
return t
end
function inline:thumb(url, width, height, mime)
if self.type == 'audio'
or self.type == 'voice'
or self.type == 'game'
then return self end
self.thumb_url = tostring(url)
if width and height and (
self.type == 'article'
or self.type == 'document'
or self.type == 'contact'
or self.type == 'location'
or self.type == 'venue'
) then
self.thumb_width = tonumber(width)
self.thumb_height = tonumber(height)
end
if mime and (
self.type == 'gif'
or self.type == 'mpeg4_gif'
) then self.thumb_mime_type = mime end
return self
end
function inline:keyboard(...)
if not self.type then return self end
local k = {}
for _, v in pairs {...} do
if type(v) == 'table' then
table.insert(k, v)
end
end
self.reply_markup = k
return self
end
-- Author itself not understands why this funciton needed
-- so not recommends to use it
function inline:messCont(a)
if self.type == 'game' or self.type == 'article' then
self.input_message_content = a
end
return self
end
function inline:answer(id, res, ctime, per, noff, pmt, pmp)
print(dump(res))
if res.id then res = {res} end
return self:request('answerInlineQuery', {
inline_query_id = id,
results = res,
cache_time = ctime,
is_personal = per,
next_offset = noff,
switch_pm_text = pmt,
switch_pm_parameter = pmp,
})
end
return function(api)
local self = setmetatable({
request = function(_, ...) api:request(...) end
}, inline)
return self
end

View File

@ -1,103 +0,0 @@
--[[ Additional tools
-- (c) Er2 2021 <er2@dismail.de>
-- Zlib license
--]]
local tools = {
json = require 'etc.json',
}
local json = tools.json
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
local mp = require 'etc.multipart'
function tools.fetchCmd(text)
return
text:match '/([%w_]+)', -- cmd
text:match '/[%w_]+@([%w_]+)' -- to
end
function tools._req(url, meth, data, ctype)
assert(url, 'Provide URL!')
assert(meth, 'Provide method!')
local resp = {}
local head = {
url = url,
method = meth,
headers = {
['Content-Type'] = ctype,
['Content-Length'] = #(data or ''),
},
source = ltn12.source.string(data),
sink = ltn12.sink.table(resp),
}
local succ, res = https.request(head)
if not succ then
print('Connection error [' .. res .. ']')
return nil, false
end
return resp[1], true
end
function tools.preq(url, par)
par = par or {}
local body, bound = mp.encode(par)
return tools._req(
url,
'POST',
body,
'multipart/form-data; boundary=' .. bound
)
end
function tools.greq(url, par, f)
par = json.encode(par)
return tools._req(url, 'GET', par, 'application/json')
end
function tools.req(url, par, f, dbg)
local res, succ
par = par or {}
-- files
if f and next(f) ~= nil then
par = par or {}
for k, v in pairs(par) do par[k] = tostring(v) end
local ft, fn = next(f)
local fr = io.open(fn, 'r')
if fr then
par[ft] = {
filename = fn,
data = fr:read '*a'
}
fr:close()
else par[ft] = fn end
res, succ = tools.preq(url, par)
else -- text
res, succ = tools.greq(url, par)
end
if dbg then print(url, succ, res, par)
-- dump(par))
end
res = json.decode(res or '{}')
if not succ or not res then return {}, false end
return res, true
end
function tools.request(token, endpoint, param, f)
assert(token, 'Provide token!')
assert(endpoint, 'Provide endpoint!')
local url = 'https://api.telegram.org/bot' ..token.. '/' ..endpoint
dbg = true
local resp = tools.req(url, param, f, dbg)
return resp, resp.ok or false
end
return tools

View File

@ -1,167 +0,0 @@
-- based on https://github.com/catwell/lua-multipart-post
-- MIT License
local ltn12 = require 'ltn12'
local mp = {
CHARSET = 'UTF-8',
LANGUAGE = ''
}
-- https://gist.github.com/liukun/f9ce7d6d14fa45fe9b924a3eed5c3d99
local function urlencode(url)
if url == nil then return end
url = url:gsub("\n", "\r\n")
url = url:gsub("([^%w_%- . ~])", function(c) return string.format("%%%02X", string.byte(c)) end)
url = url:gsub(" ", "+")
return url
end
mp.urlencode = urlencode
local function fmt(p, ...)
if select('#', ...) == 0 then
return p
end
return string.format(p, ...)
end
local function tprintf(t, p, ...)
t[#t+1] = fmt(p, ...)
end
local function section_header(r, k, extra)
tprintf(r, 'content-disposition: form-data; name="%s"', k)
if extra.filename then
tprintf(r, '; filename="%s"', extra.filename)
tprintf(
r, "; filename*=%s'%s'%s",
mp.CHARSET, mp.LANGUAGE, urlencode(extra.filename)
)
end
if extra.content_type then
tprintf(r, '\r\ncontent-type: %s', extra.content_type)
end
if extra.content_transfer_encoding then
tprintf(
r, '\r\ncontent-transfer-encoding: %s',
extra.content_transfer_encoding
)
end
tprintf(r, '\r\n\r\n')
end
function mp.boundary()
local t = {"BOUNDARY-"}
for i=2,17 do t[i] = string.char(math.random(65, 90)) end
t[18] = "-BOUNDARY"
return table.concat(t)
end
local function encode_header_to_table(r, k, v, boundary)
local _t = type(v)
tprintf(r, "--%s\r\n", boundary)
if _t == "string" then
section_header(r, k, {})
elseif _t == "table" then
assert(v.data, "invalid input")
local extra = {
filename = v.filename or v.name,
content_type = v.content_type or v.mimetype
or "application/octet-stream",
content_transfer_encoding = v.content_transfer_encoding
or "binary",
}
section_header(r, k, extra)
else
error(string.format("unexpected type %s", _t))
end
end
local function encode_header_as_source(k, v, boundary, ctx)
local r = {}
encode_header_to_table(r, k, v, boundary, ctx)
local s = table.concat(r)
if ctx then
ctx.headers_length = ctx.headers_length + #s
end
return ltn12.source.string(s)
end
local function data_len(d)
local _t = type(d)
if _t == "string" then
return string.len(d)
elseif _t == "table" then
if type(d.data) == "string" then
return string.len(d.data)
end
if d.len then return d.len end
error("must provide data length for non-string datatypes")
end
end
local function content_length(t, boundary, ctx)
local r = ctx and ctx.headers_length or 0
for k, v in pairs(t) do
if not ctx then
local tmp = {}
encode_header_to_table(tmp, k, v, boundary)
r = r + #table.concat(tmp)
end
r = r + data_len(v) + 2 -- `\r\n`
end
return r + #boundary + 6 -- `--BOUNDARY--\r\n`
end
local function get_data_src(v)
local _t = type(v)
if v.source then
return v.source
elseif _t == "string" then
return ltn12.source.string(v)
elseif _t == "table" then
_t = type(v.data)
if _t == "string" then
return ltn12.source.string(v.data)
elseif _t == "table" then
return ltn12.source.table(v.data)
elseif _t == "userdata" then
return ltn12.source.file(v.data)
elseif _t == "function" then
return v.data
end
end
error("invalid input")
end
local function set_ltn12_blksz(sz)
assert(type(sz) == "number", "set_ltn12_blksz expects a number")
ltn12.BLOCKSIZE = sz
end
mp.set_ltn12_blksz = set_ltn12_blksz
local function source(t, boundary, ctx)
local sources, n = {}, 1
for k, v in pairs(t) do
sources[n] = encode_header_as_source(k, v, boundary, ctx)
sources[n+1] = get_data_src(v)
sources[n+2] = ltn12.source.string("\r\n")
n = n + 3
end
sources[n] = ltn12.source.string(string.format("--%s--\r\n", boundary))
return ltn12.source.cat(table.unpack(sources))
end
mp.source = source
function mp.encode(t, boundary)
boundary = boundary or mp.boundary()
local r = {}
assert(ltn12.pump.all(
(source(t, boundary)),
(ltn12.sink.table(r))
))
return table.concat(r), boundary
end
return mp

View File

@ -1 +1,3 @@
package.path = 'etc/?.lua;etc/?/init.lua;' .. package.path
require 'src.parts.core'