move from bot

This commit is contained in:
Er2 2021-08-16 11:47:49 +03:00
commit 30f7203c5f
9 changed files with 749 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
# Emacs
*~
\#*#

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Zlib License
Copyright (c) 2021 Er2 <er2@dismail.de>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgement in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

137
api.lua Normal file
View file

@ -0,0 +1,137 @@
--[[ API Library
-- (c) Er2 2021 <er2@dismail.de>
-- Zlib License
--]]
local tools = require 'api.tools'
local json = require 'json'
local events = require '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

133
core.lua Normal file
View file

@ -0,0 +1,133 @@
--[[ Core file
-- (c) Er2 2021 <er2@dismail.de>
-- Zlib License
--]]
local tools = require 'api.tools'
local api = require '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

1
init.lua Normal file
View file

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

156
inline.lua Normal file
View file

@ -0,0 +1,156 @@
--[[ 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

167
multipart.lua Normal file
View file

@ -0,0 +1,167 @@
-- 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

29
readme.org Normal file
View file

@ -0,0 +1,29 @@
* Telegram API
This is bindings of Telegram API on Lua, part of [[https://gitdab.com/er2/comp-tg][my bot]].
* Installation
We recommended to use *.gitmodules* file for that.
To include, add to gitmodules this:
#+begin_src
[submodule "tg-api"]
path = # enter your path
url = https://gitdab.com/er2/tg-api-lua
#+end_src
Also you need to:
+ Install *LuaSec* for https requests.
+ Execute this code *before* include this API:
#+begin_src lua
package.path = 'yourpath/?.lua;yourpath/?/init.lua;' .. package.path
#+end_src
+ Copy to yourpath +/api+ files *event.lua* and *json.lua*
They can be taken from [[https://gitdab.com/er2/comp-tg/src/branch/main/etc][here]].

103
tools.lua Normal file
View file

@ -0,0 +1,103 @@
--[[ Additional tools
-- (c) Er2 2021 <er2@dismail.de>
-- Zlib license
--]]
local tools = {
json = require 'json',
}
local json = tools.json
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
local mp = require 'api.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