diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..510857e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "etc/api"] + path = etc/api + url = https://gitdab.com/er2/tg-api-lua + diff --git a/etc/api b/etc/api new file mode 160000 index 0000000..30f7203 --- /dev/null +++ b/etc/api @@ -0,0 +1 @@ +Subproject commit 30f7203c5fe69e875dbd29a4da9fe0c5ab1ddc9b diff --git a/etc/api/api.lua b/etc/api/api.lua deleted file mode 100644 index 187164c..0000000 --- a/etc/api/api.lua +++ /dev/null @@ -1,137 +0,0 @@ ---[[ API Library - -- (c) Er2 2021 - -- 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 diff --git a/etc/api/core.lua b/etc/api/core.lua deleted file mode 100644 index 1a306cb..0000000 --- a/etc/api/core.lua +++ /dev/null @@ -1,133 +0,0 @@ ---[[ Core file - -- (c) Er2 2021 - -- 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 diff --git a/etc/api/init.lua b/etc/api/init.lua deleted file mode 100644 index 51454ae..0000000 --- a/etc/api/init.lua +++ /dev/null @@ -1 +0,0 @@ -return require 'etc.api.core' diff --git a/etc/api/inline.lua b/etc/api/inline.lua deleted file mode 100644 index 46ca17d..0000000 --- a/etc/api/inline.lua +++ /dev/null @@ -1,156 +0,0 @@ ---[[ Inline query library - -- (c) Er2 2021 - -- 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 diff --git a/etc/api/tools.lua b/etc/api/tools.lua deleted file mode 100644 index 5219707..0000000 --- a/etc/api/tools.lua +++ /dev/null @@ -1,103 +0,0 @@ ---[[ Additional tools - -- (c) Er2 2021 - -- 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 diff --git a/etc/multipart.lua b/etc/multipart.lua deleted file mode 100644 index 361470b..0000000 --- a/etc/multipart.lua +++ /dev/null @@ -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 diff --git a/init.lua b/init.lua index 49637ba..be69f72 100644 --- a/init.lua +++ b/init.lua @@ -1 +1,3 @@ +package.path = 'etc/?.lua;etc/?/init.lua;' .. package.path + require 'src.parts.core'