First beta

This commit is contained in:
Ertu 2021-04-14 09:03:25 +03:00
commit 134c1a519d
14 changed files with 888 additions and 0 deletions

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
zlib License
Copyright (c) 2020 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.

50
cmds/eval.lua Normal file
View File

@ -0,0 +1,50 @@
local function prind(...)
local t = {...}
local s = ''
for i = 1, #t do
if i > 1 then s = s..'\t' end
s = s .. tostring(t[i] or 'nil')
end
return s .. '\n'
end
local env = {
assert = assert,
error = error,
ipairs = ipairs,
pairs = pairs,
next = next,
tonumber = tonumber,
tostring = tostring,
type = type,
pcall = pcall,
xpcall = xpcall,
math = math,
string = string,
table = table,
dump = dump,
}
return {
args = '<code>',
desc = 'evaluates code',
run = function(C, msg, owner)
local s = ''
local t = {
msg = msg,
print = function(...) s = s .. prind(...) end,
C = owner and C or nil,
api = owner and C.api or nil,
}
for k,v in pairs(env) do t[k] = v end
local e, err = load(table.concat(msg.args, ' '), 'eval', 't', t)
xpcall(function()
if err then error(err) end
e = tostring(e() or '...')
end, function(err) e = err end)
C.api:send(msg, s .. '\n' .. e)
end
}

6
cmds/ping.lua Normal file
View File

@ -0,0 +1,6 @@
return {
desc = 'ping pong',
run = function(C, msg)
C.api:send(msg, 'Pong! ' .. (os.time() - msg.date) .. 's')
end
}

64
cmds/rub.lua Normal file
View File

@ -0,0 +1,64 @@
local rub = {
url = 'https://api.factmaven.com/xml-to-json/?xml='
.. 'https://www.cbr.ru/scripts/XML_daily.asp',
fmt = function(v, fmt)
fmt = type(fmt) == 'string' and fmt or '%d %s = %f'
return fmt:format(v.Nominal, v.Name, v.Value:gsub(',', '.'))
end
}
function rub:course(wants, fmt)
local resp, succ = (require'tg.tools').requ(self.url)
if not succ then
return {}, '[ошибка]', {}
end
resp = resp.ValCurs
wants = type(wants) == 'table' and wants or {}
local r, founds = {}, {}
for i = 1, #resp.Valute do
local v = resp.Valute[i]
if table.find(wants, v.CharCode) then
table.insert(founds, v.CharCode)
table.insert(r, self.fmt(v, fmt))
end
end
local i = table.find(wants, 'RUB')
if i then
table.insert(founds, 'RUB')
table.insert(r, i, self.fmt({
Nominal = 1,
Name = 'Российский рубль',
Value = '1'
}, fmt) .. ' :D')
end
return r, resp.Date, founds
end
function rub.msg(C, msg)
local wants = {'USD', 'EUR', table.unpack(msg.args)}
for i = 1, #wants do wants[i] = wants[i]:upper() end
local v, d, f = rub:course(wants, '%d %s - %f ₽')
local nf = {}
for i = 1, #wants do
if not table.find(f, wants[i]) then
table.insert(nf, wants[i])
end
end
local s = 'Курс на ' .. d .. ':\n' .. table.concat(v, '\n')
if #nf > 0 then s = s .. '\n\n' .. 'Не нашлось: ' .. table.concat(nf, ', ') end
C.api:reply(msg, s .. '\nДанные от Центробанка России')
end
return {
args = '[valute]...',
desc = 'ruble course',
run = rub.msg
}

14
config.lua Normal file
View File

@ -0,0 +1,14 @@
return {
token = 'atokenyanedam',
owner = 'Er2Official', -- hehe
cmds = {
'eval',
'rub',
'ping',
},
events = {
'command',
'message',
'ready',
},
}

22
events/command.lua Normal file
View File

@ -0,0 +1,22 @@
return function(C, api, msg)
local cmd = C.cmds[msg.cmd]
local owner = msg.from.username == C.config.owner
if cmd == nil then
api:send(msg, 'Invaid command provided.')
elseif type(cmd.run) ~= 'function' then
api:send(msg, 'Command cannot be executed.')
elseif cmd.private and not owner then
api:send(msg, 'You can\'t execute private commands!')
else
local succ, err = pcall(cmd.run, C, msg, owner)
if not succ then
api:reply(msg, 'Произошла ошибочка, которая была отправлена создателю')
local cid = api:getChat('@' .. C.config.owner).id
api:forward(cid, msg.chat.id, msg.message_id, false)
api:send(cid, err)
end
end
end

3
events/message.lua Normal file
View File

@ -0,0 +1,3 @@
return function(C, api, msg)
-- api:reply(msg, 'хай!')
end

40
events/ready.lua Normal file
View File

@ -0,0 +1,40 @@
function table.indexOf(t, w)
local i = {}
for k,v in pairs(t) do i[v] = k end
return i[w]
end
function table.find(t, w)
local i
for k,v in pairs(t) do
if v == w then
i = k
break
end
end
return i
end
function dump(t, d)
if not tonumber(d) or d < 0 then d = 0 end
local c = ''
for k,v in pairs(t) do
if type(v) == 'table' then v = '\n' .. dump(v, d + 1) end
c = c .. string.format('%s%s = %s\n', (' '):rep(d), k, v)
end
return c
end
return function(C, api)
C:load 'cmds'
local a = {}
for k, v in pairs(C.cmds) do
if not v.private then
table.insert(a, {
command = k,
description = (v.args and v.args .. ' - ' or '') .. v.desc or 'no description'
})
end
end
api:setMyCommands(a)
end

45
init.lua Normal file
View File

@ -0,0 +1,45 @@
local config = require 'config'
local Core = {
tg = require 'tg',
tools = tools,
config = config,
cmds = {},
}
local tg = Core.tg
function Core:load(what)
local c = config[what]
local s = #c
for i = 1, s do
local v = c[i]
print(('Loading %s (%d / %d) %s...'):format(what:sub(0, -2), i, s, v))
if not pcall(require, what .. '.' .. v) then print 'fail'; goto f end
local a = require(what .. '.' .. v)
if what == 'events' then
self.api['on' .. v:sub(1, 1):upper() .. v:sub(2)] = function(...)
local succ = pcall(a, self, ...)
if not succ then print('event ' .. v .. ' was failed') end
end
elseif what == 'cmds' then self.cmds[v] = a
end
::f::
end
print(('Loaded %d %s'):format(s, what))
end
function Core:init()
self.api = tg(config.token)
self.config.token = nil
print('Logged on as @' .. self.api.info.username)
print 'Client initialization...'
self:load 'events'
print 'Done!'
self.api:run()
end
Core:init()

93
tg/api.lua Normal file
View File

@ -0,0 +1,93 @@
-- API Library
--- (c) Er2 <er2@dismail.de>
--- Zlib License
local tools = require 'tg.tools'
local json = require 'tg.json'
local api = {
request = function(self, ...) return tools.request(self.token, ...) end,
}
api.__index = api -- Make class
-- 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)
rmp = type(rmp) == 'table' and json.encode(rmp) or rmp
msg = (type(msg) == 'table' and msg.chat and msg.chat.id) and msg.chat.id or msg
pmod = (type(pmod) == 'boolean' and pmod == true) and 'markdown' or pmod
if dwp == nil then dwp = true 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)
if type(msg) ~= 'table' or not msg.chat or not msg.chat.id or not msg.message_id then return false end
rmp = type(rmp) == 'table' and json.encode(rmp) or rmp
pmod = (type(pmod) == 'boolean' and pmod == true) and 'markdown' or pmod
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:sendPoll(cid, q, opt, anon, ptype, mansw, coptid, expl, pmode, oper, cdate, closed, dnot, rtmid, rmp)
opt = type(opt) == 'string' and opt or json.encode(opt)
rmp = type(rmp) == 'table' and json.encode(rmp) or rmp
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:setMyCommands(cmds)
return self:request('setMyCommands', { commands = json.encode(cmds) })
end
return api

110
tg/core.lua Normal file
View File

@ -0,0 +1,110 @@
-- Core file
--- (c) Er2 <er2@dismail.de>
--- Zlib License
local tools = require 'tg.tools'
local api = require 'tg.api'
api.__index = api -- Make class
-- EVENT PROTOTYPES --
function api.onCommand(_) end
function api.onChannelPost(_) end
function api.onChannelPostEdit(_) end
function api.onMessage(_) end
function api.onMessageEdit(_) end
function api.onInlineResult(_) end
function api.onPoll(_) end
function api.onPollAnswer(_) end
function api.onReady(_) end
function api.onQuery(_) end
function api.onUpdate(_) end
-- UPDATES --
function api:getUpdates(tout, offs, lim, 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 then self:onUpdate(update) end
if update.message then
local txt = update.message.text
local cmd, to = tools.fetchCmd(txt)
if cmd and (not to or to == self.info.username) then
local args = {}
txt = txt:sub(#cmd + #(to or {}) + 3)
for s in txt:gmatch '%S+' do table.insert(args, s) end
update.message.cmd = cmd
update.message.args = args
return self:onCommand(update.message, update.message.chat.type)
elseif cmd then return end
self:onMessage(update.message, update.message.chat.type)
elseif update.edited_message then
self:onMessageEdit(update.edited_message, update.edited_message.chat.type)
elseif update.channel_post then self:onChannelPost(update.channel_post)
elseif update.edited_channel_post then self:onChannelPostEdit(update.edited_channel_post)
elseif update.poll then self:onPoll(update.poll)
elseif update.poll_answer then self:onPollAnswer(update.poll_answer)
elseif update.callback_query then self:onQuery('callback', update.callback_query)
elseif update.inline_query then self:onQuery('inline', update.inline_query)
elseif update.shipping_query then self:onQuery('shipping', update.shipping_query)
elseif update.pre_checkout_query then self:onQuery('preCheckout', update.pre_checkout_query)
elseif update.chosen_inline_result then self:onInlineResult(update.chosen_inline_result)
end
end
function api:_loop(lim, tout, offs, al)
while true do
local u, ok = self:getUpdates(tout, offs, lim, al)
if not ok or not u or (u and type(u) ~= 'table') or not u.result then goto f end
for _, v in pairs(u.result) do
offs = v.update_id + 1
receiveUpdate(self, v)
end
::f::
end
self:getUpdates(tout, offs, lim, al)
end
-- RUN --
function api:run(lim, tout, offs, al)
lim = tonumber(lim) or 1
tout = tonumber(tout) or 0
offs = tonumber(offs) or 0
self.runs = true
self:onReady()
self.co = coroutine.create(api._loop)
coroutine.resume(self.co, self, lim, tout, offs, al)
end
function api:destroy() self.runs = false end
return function(token)
if not token or type(token) ~= 'string' then token = nil end
local self = setmetatable({}, api)
self.token = assert(token, 'Provide token!')
repeat
local b,a = self:getMe()
if a then self.info = b end
until (self.info or {}).result
self.info = self.info.result
self.info.name = self.info.first_name
return self
end

1
tg/init.lua Normal file
View File

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

357
tg/json.lua Normal file
View File

@ -0,0 +1,357 @@
-- JSON Library
--- (c) 2020 rxi
--- MIT License
local json = { _version = "0.1.2" }
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json

64
tg/tools.lua Normal file
View File

@ -0,0 +1,64 @@
local tools = {
json = require 'tg.json',
}
local json = tools.json
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
function tools.fetchCmd(text)
local cmd = text:match '/[%w_]+'
local to = text:match '/[%w_]+(@[%w_]+)'
if to then to = to:sub(2) end
if cmd then cmd = cmd:sub(2) end
return cmd, to
end
-- https://gist.github.com/liukun/f9ce7d6d14fa45fe9b924a3eed5c3d99
function tools.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
function tools.req(url)
local resp = {}
local succ, res = https.request {
url = url,
method = 'GET',
sink = ltn12.sink.table(resp),
}
if not succ then
print('Connection error [' .. res .. ']')
return nil, false
end
return resp[1], true
end
function tools.requ(url)
local res, succ = tools.req(url)
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)
assert(token, 'Provide token!')
assert(endpoint, 'Provide endpoint!')
local params = ''
for k, v in pairs(param or {}) do
params = params .. '&' .. k .. '=' .. tools.urlencode(tostring(v))
end
local url = 'https://api.telegram.org/bot' .. token .. '/' .. endpoint
if #params > 1 then url = url .. '?' .. params:sub(2) end
local resp = tools.requ(url)
return resp, resp.ok or false
end
return tools