tab -> 4 spaces

This commit is contained in:
topkecleon 2016-08-13 22:46:18 -04:00
parent 6fbd718af0
commit 148d4b0dc5
67 changed files with 4168 additions and 4167 deletions

View File

@ -6,4 +6,5 @@ insert_final_newline = true
[*.lua] [*.lua]
charset = utf-8 charset = utf-8
indent_style = tab indent_style = space
indent_size = 4

View File

@ -292,26 +292,26 @@ Additionally, any method can be called as a key in the `bindings` table (for exa
``` ```
bindings.request( bindings.request(
self, self,
'sendMessage', 'sendMessage',
{ {
chat_id = 987654321, chat_id = 987654321,
text = 'Quick brown fox.', text = 'Quick brown fox.',
reply_to_message_id = 54321, reply_to_message_id = 54321,
disable_web_page_preview = false, disable_web_page_preview = false,
parse_method = 'Markdown' parse_method = 'Markdown'
} }
) )
bindings.sendMessage( bindings.sendMessage(
self, self,
{ {
chat_id = 987654321, chat_id = 987654321,
text = 'Quick brown fox.', text = 'Quick brown fox.',
reply_to_message_id = 54321, reply_to_message_id = 54321,
disable_web_page_preview = false, disable_web_page_preview = false,
parse_method = 'Markdown' parse_method = 'Markdown'
} }
) )
``` ```
@ -342,20 +342,20 @@ Alone, the database will have this structure:
``` ```
{ {
users = { users = {
["55994550"] = { ["55994550"] = {
id = 55994550, id = 55994550,
first_name = "Drew", first_name = "Drew",
username = "topkecleon" username = "topkecleon"
} }
}, },
userdata = { userdata = {
["55994550"] = { ["55994550"] = {
nickname = "Worst coder ever", nickname = "Worst coder ever",
lastfm = "topkecleon" lastfm = "topkecleon"
} }
}, },
version = "3.11" version = "3.11"
} }
``` ```

View File

@ -1,159 +1,159 @@
-- For details on configuration values, see README.md#configuration. -- For details on configuration values, see README.md#configuration.
return { return {
-- Your authorization token from the botfather. -- Your authorization token from the botfather.
bot_api_key = nil, bot_api_key = nil,
-- Your Telegram ID. -- Your Telegram ID.
admin = nil, admin = nil,
-- Two-letter language code. -- Two-letter language code.
lang = 'en', lang = 'en',
-- The channel, group, or user to send error reports to. -- The channel, group, or user to send error reports to.
-- If this is not set, errors will be printed to the console. -- If this is not set, errors will be printed to the console.
log_chat = nil, log_chat = nil,
-- The port used to communicate with tg for administration.lua. -- The port used to communicate with tg for administration.lua.
-- If you change this, make sure you also modify launch-tg.sh. -- If you change this, make sure you also modify launch-tg.sh.
cli_port = 4567, cli_port = 4567,
-- The symbol that starts a command. Usually noted as '/' in documentation. -- The symbol that starts a command. Usually noted as '/' in documentation.
cmd_pat = '/', cmd_pat = '/',
-- If drua is used, should a user be blocked when he's blacklisted? -- If drua is used, should a user be blocked when he's blacklisted?
drua_block_on_blacklist = false, drua_block_on_blacklist = false,
-- The filename of the database. If left nil, defaults to $username.db. -- The filename of the database. If left nil, defaults to $username.db.
database_name = nil, database_name = nil,
-- The block of text returned by /start and /about.. -- The block of text returned by /start and /about..
about_text = [[ about_text = [[
I am otouto, the plugin-wielding, multipurpose Telegram bot. I am otouto, the plugin-wielding, multipurpose Telegram bot.
Send /help to get started. Send /help to get started.
]], ]],
errors = { -- Generic error messages. errors = { -- Generic error messages.
generic = 'An unexpected error occurred.', generic = 'An unexpected error occurred.',
connection = 'Connection error.', connection = 'Connection error.',
results = 'No results found.', results = 'No results found.',
argument = 'Invalid argument.', argument = 'Invalid argument.',
syntax = 'Invalid syntax.' syntax = 'Invalid syntax.'
}, },
-- https://datamarket.azure.com/dataset/bing/search -- https://datamarket.azure.com/dataset/bing/search
bing_api_key = nil, bing_api_key = nil,
-- http://console.developers.google.com -- http://console.developers.google.com
google_api_key = nil, google_api_key = nil,
-- https://cse.google.com/cse -- https://cse.google.com/cse
google_cse_key = nil, google_cse_key = nil,
-- http://openweathermap.org/appid -- http://openweathermap.org/appid
owm_api_key = nil, owm_api_key = nil,
-- http://last.fm/api -- http://last.fm/api
lastfm_api_key = nil, lastfm_api_key = nil,
-- http://api.biblia.com -- http://api.biblia.com
biblia_api_key = nil, biblia_api_key = nil,
-- http://thecatapi.com/docs.html -- http://thecatapi.com/docs.html
thecatapi_key = nil, thecatapi_key = nil,
-- http://api.nasa.gov -- http://api.nasa.gov
nasa_api_key = nil, nasa_api_key = nil,
-- http://tech.yandex.com/keys/get -- http://tech.yandex.com/keys/get
yandex_key = nil, yandex_key = nil,
-- Interval (in minutes) for hackernews.lua to update. -- Interval (in minutes) for hackernews.lua to update.
hackernews_interval = 60, hackernews_interval = 60,
-- Whether hackernews.lua should update at load/reload. -- Whether hackernews.lua should update at load/reload.
hackernews_onstart = false, hackernews_onstart = false,
-- Whether luarun should use serpent instead of dkjson for serialization. -- Whether luarun should use serpent instead of dkjson for serialization.
luarun_serpent = false, luarun_serpent = false,
remind = { remind = {
persist = true, persist = true,
max_length = 1000, max_length = 1000,
max_duration = 526000, max_duration = 526000,
max_reminders_group = 10, max_reminders_group = 10,
max_reminders_private = 50 max_reminders_private = 50
}, },
chatter = { chatter = {
cleverbot_api = 'https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text=', cleverbot_api = 'https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text=',
connection = 'I don\'t feel like talking right now.', connection = 'I don\'t feel like talking right now.',
response = 'I don\'t know what to say to that.' response = 'I don\'t know what to say to that.'
}, },
greetings = { greetings = {
["Hello, #NAME."] = { ["Hello, #NAME."] = {
"hello", "hello",
"hey", "hey",
"hi", "hi",
"good morning", "good morning",
"good day", "good day",
"good afternoon", "good afternoon",
"good evening" "good evening"
}, },
["Goodbye, #NAME."] = { ["Goodbye, #NAME."] = {
"good%-?bye", "good%-?bye",
"bye", "bye",
"later", "later",
"see ya", "see ya",
"good night" "good night"
}, },
["Welcome back, #NAME."] = { ["Welcome back, #NAME."] = {
"i'm home", "i'm home",
"i'm back" "i'm back"
}, },
["You're welcome, #NAME."] = { ["You're welcome, #NAME."] = {
"thanks", "thanks",
"thank you" "thank you"
} }
}, },
reactions = { reactions = {
['shrug'] = '¯\\_(ツ)_/¯', ['shrug'] = '¯\\_(ツ)_/¯',
['lenny'] = '( ͡° ͜ʖ ͡°)', ['lenny'] = '( ͡° ͜ʖ ͡°)',
['flip'] = '(╯°□°)╯︵ ┻━┻', ['flip'] = '(╯°□°)╯︵ ┻━┻',
['look'] = 'ಠ_ಠ', ['look'] = 'ಠ_ಠ',
['shots'] = 'SHOTS FIRED', ['shots'] = 'SHOTS FIRED',
['facepalm'] = '(-‸ლ)' ['facepalm'] = '(-‸ლ)'
}, },
administration = { administration = {
-- Whether moderators can set a group's message of the day. -- Whether moderators can set a group's message of the day.
moderator_setmotd = false, moderator_setmotd = false,
-- Default antiflood values. -- Default antiflood values.
antiflood = { antiflood = {
text = 5, text = 5,
voice = 5, voice = 5,
audio = 5, audio = 5,
contact = 5, contact = 5,
photo = 10, photo = 10,
video = 10, video = 10,
location = 10, location = 10,
document = 10, document = 10,
sticker = 20 sticker = 20
} }
}, },
plugins = { -- To enable a plugin, add its name to the list. plugins = { -- To enable a plugin, add its name to the list.
'about', 'about',
'blacklist', 'blacklist',
'calc', 'calc',
'cats', 'cats',
'commit', 'commit',
'control', 'control',
'currency', 'currency',
'dice', 'dice',
'echo', 'echo',
'eightball', 'eightball',
'gMaps', 'gMaps',
'hackernews', 'hackernews',
'imdb', 'imdb',
'nick', 'nick',
'ping', 'ping',
'pun', 'pun',
'reddit', 'reddit',
'shout', 'shout',
'slap', 'slap',
'time', 'time',
'urbandictionary', 'urbandictionary',
'whoami', 'whoami',
'wikipedia', 'wikipedia',
'xkcd', 'xkcd',
-- Put new plugins above this line. -- Put new plugins above this line.
'help', 'help',
'greetings' 'greetings'
} }
} }

View File

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
while true; do while true; do
lua main.lua lua main.lua
echo 'otouto has stopped. ^C to exit.' echo 'otouto has stopped. ^C to exit.'
sleep 5s sleep 5s
done done

View File

@ -1,10 +1,10 @@
--[[ --[[
bindings.lua (rev. 2016/05/28) bindings.lua (rev. 2016/05/28)
otouto's bindings for the Telegram bot API. otouto's bindings for the Telegram bot API.
https://core.telegram.org/bots/api https://core.telegram.org/bots/api
Copyright 2016 topkecleon. Published under the AGPLv3. Copyright 2016 topkecleon. Published under the AGPLv3.
See the "Bindings" section of README.md for usage information. See the "Bindings" section of README.md for usage information.
]]-- ]]--
local bindings = {} local bindings = {}
@ -22,56 +22,56 @@ local MP_ENCODE = require('multipart-post').encode
-- response with failure. Returns false and false with a connection error. -- response with failure. Returns false and false with a connection error.
-- To mimic old/normal behavior, it errs if used with an invalid method. -- To mimic old/normal behavior, it errs if used with an invalid method.
function bindings:request(method, parameters, file) function bindings:request(method, parameters, file)
parameters = parameters or {} parameters = parameters or {}
for k,v in pairs(parameters) do for k,v in pairs(parameters) do
parameters[k] = tostring(v) parameters[k] = tostring(v)
end end
if file and next(file) ~= nil then if file and next(file) ~= nil then
local file_type, file_name = next(file) local file_type, file_name = next(file)
local file_file = io.open(file_name, 'r') local file_file = io.open(file_name, 'r')
local file_data = { local file_data = {
filename = file_name, filename = file_name,
data = file_file:read('*a') data = file_file:read('*a')
} }
file_file:close() file_file:close()
parameters[file_type] = file_data parameters[file_type] = file_data
end end
if next(parameters) == nil then if next(parameters) == nil then
parameters = {''} parameters = {''}
end end
local response = {} local response = {}
local body, boundary = MP_ENCODE(parameters) local body, boundary = MP_ENCODE(parameters)
local success, code = HTTPS.request{ local success, code = HTTPS.request{
url = self.BASE_URL .. method, url = self.BASE_URL .. method,
method = 'POST', method = 'POST',
headers = { headers = {
["Content-Type"] = "multipart/form-data; boundary=" .. boundary, ["Content-Type"] = "multipart/form-data; boundary=" .. boundary,
["Content-Length"] = #body, ["Content-Length"] = #body,
}, },
source = ltn12.source.string(body), source = ltn12.source.string(body),
sink = ltn12.sink.table(response) sink = ltn12.sink.table(response)
} }
local data = table.concat(response) local data = table.concat(response)
if not success or success == 1 then if not success or success == 1 then
print(method .. ': Connection error. [' .. code .. ']') print(method .. ': Connection error. [' .. code .. ']')
return false, false return false, false
else else
local result = JSON.decode(data) local result = JSON.decode(data)
if not result then if not result then
return false, false return false, false
elseif result.ok then elseif result.ok then
return result return result
else else
assert(result.description ~= 'Method not found', method .. ': Method not found.') assert(result.description ~= 'Method not found', method .. ': Method not found.')
return false, result return false, result
end end
end end
end end
function bindings.gen(_, key) function bindings.gen(_, key)
return function(self, params, file) return function(self, params, file)
return bindings.request(self, key, params, file) return bindings.request(self, key, params, file)
end end
end end
setmetatable(bindings, { __index = bindings.gen }) setmetatable(bindings, { __index = bindings.gen })

View File

@ -7,190 +7,190 @@ bot.version = '3.13'
-- Function to be run on start and reload. -- Function to be run on start and reload.
function bot:init(config) function bot:init(config)
bindings = require('otouto.bindings') bindings = require('otouto.bindings')
utilities = require('otouto.utilities') utilities = require('otouto.utilities')
assert( assert(
config.bot_api_key, config.bot_api_key,
'You did not set your bot token in the config!' 'You did not set your bot token in the config!'
) )
self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/' self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/'
-- Fetch bot information. Try until it succeeds. -- Fetch bot information. Try until it succeeds.
repeat repeat
print('Fetching bot information...') print('Fetching bot information...')
self.info = bindings.getMe(self) self.info = bindings.getMe(self)
until self.info until self.info
self.info = self.info.result self.info = self.info.result
-- Load the "database"! ;) -- Load the "database"! ;)
self.database_name = config.database_name or self.info.username .. '.db' self.database_name = config.database_name or self.info.username .. '.db'
if not self.database then if not self.database then
self.database = utilities.load_data(self.database_name) self.database = utilities.load_data(self.database_name)
end end
-- Migration code 1.12 -> 1.13 -- Migration code 1.12 -> 1.13
-- Back to administration global ban list; copy over current blacklist. -- Back to administration global ban list; copy over current blacklist.
if self.database.version ~= '3.13' then if self.database.version ~= '3.13' then
if self.database.administration then if self.database.administration then
self.database.administration.globalbans = self.database.administration.globalbans or self.database.blacklist or {} self.database.administration.globalbans = self.database.administration.globalbans or self.database.blacklist or {}
utilities.save_data(self.database_name, self.database) utilities.save_data(self.database_name, self.database)
self.database = utilities.load_data(self.database_name) self.database = utilities.load_data(self.database_name)
end end
end end
-- End migration code. -- End migration code.
-- Table to cache user info (usernames, IDs, etc). -- Table to cache user info (usernames, IDs, etc).
self.database.users = self.database.users or {} self.database.users = self.database.users or {}
-- Table to store userdata (nicknames, lastfm usernames, etc). -- Table to store userdata (nicknames, lastfm usernames, etc).
self.database.userdata = self.database.userdata or {} self.database.userdata = self.database.userdata or {}
-- Table to store the IDs of blacklisted users. -- Table to store the IDs of blacklisted users.
self.database.blacklist = self.database.blacklist or {} self.database.blacklist = self.database.blacklist or {}
-- Save the bot's version in the database to make migration simpler. -- Save the bot's version in the database to make migration simpler.
self.database.version = bot.version self.database.version = bot.version
-- Add updated bot info to the user info cache. -- Add updated bot info to the user info cache.
self.database.users[tostring(self.info.id)] = self.info self.database.users[tostring(self.info.id)] = self.info
-- All plugins go into self.plugins. Plugins which accept forwarded messages -- All plugins go into self.plugins. Plugins which accept forwarded messages
-- and messages from blacklisted users also go into self.panoptic_plugins. -- and messages from blacklisted users also go into self.panoptic_plugins.
self.plugins = {} self.plugins = {}
self.panoptic_plugins = {} self.panoptic_plugins = {}
for _, pname in ipairs(config.plugins) do for _, pname in ipairs(config.plugins) do
local plugin = require('otouto.plugins.'..pname) local plugin = require('otouto.plugins.'..pname)
table.insert(self.plugins, plugin) table.insert(self.plugins, plugin)
if plugin.init then plugin.init(self, config) end if plugin.init then plugin.init(self, config) end
if plugin.panoptic then table.insert(self.panoptic_plugins, plugin) end if plugin.panoptic then table.insert(self.panoptic_plugins, plugin) end
if plugin.doc then plugin.doc = '```\n'..plugin.doc..'\n```' end if plugin.doc then plugin.doc = '```\n'..plugin.doc..'\n```' end
if not plugin.triggers then plugin.triggers = {} end if not plugin.triggers then plugin.triggers = {} end
end end
print('@' .. self.info.username .. ', AKA ' .. self.info.first_name ..' ('..self.info.id..')') print('@' .. self.info.username .. ', AKA ' .. self.info.first_name ..' ('..self.info.id..')')
-- Set loop variables. -- Set loop variables.
self.last_update = self.last_update or 0 -- Update offset. self.last_update = self.last_update or 0 -- Update offset.
self.last_cron = self.last_cron or os.date('%M') -- Last cron job. self.last_cron = self.last_cron or os.date('%M') -- Last cron job.
self.last_database_save = self.last_database_save or os.date('%H') -- Last db save. self.last_database_save = self.last_database_save or os.date('%H') -- Last db save.
self.is_started = true self.is_started = true
end end
-- Function to be run on each new message. -- Function to be run on each new message.
function bot:on_msg_receive(msg, config) function bot:on_msg_receive(msg, config)
-- Do not process old messages. -- Do not process old messages.
if msg.date < os.time() - 5 then return end if msg.date < os.time() - 5 then return end
-- plugint is the array of plugins we'll check the message against. -- plugint is the array of plugins we'll check the message against.
-- If the message is forwarded or from a blacklisted user, the bot will only -- If the message is forwarded or from a blacklisted user, the bot will only
-- check against panoptic plugins. -- check against panoptic plugins.
local plugint = self.plugins local plugint = self.plugins
local from_id_str = tostring(msg.from.id) local from_id_str = tostring(msg.from.id)
-- Cache user info for those involved. -- Cache user info for those involved.
self.database.users[from_id_str] = msg.from self.database.users[from_id_str] = msg.from
if msg.reply_to_message then if msg.reply_to_message then
self.database.users[tostring(msg.reply_to_message.from.id)] = msg.reply_to_message.from self.database.users[tostring(msg.reply_to_message.from.id)] = msg.reply_to_message.from
elseif msg.forward_from then elseif msg.forward_from then
-- Forwards only go to panoptic plugins. -- Forwards only go to panoptic plugins.
plugint = self.panoptic_plugins plugint = self.panoptic_plugins
self.database.users[tostring(msg.forward_from.id)] = msg.forward_from self.database.users[tostring(msg.forward_from.id)] = msg.forward_from
elseif msg.new_chat_member then elseif msg.new_chat_member then
self.database.users[tostring(msg.new_chat_member.id)] = msg.new_chat_member self.database.users[tostring(msg.new_chat_member.id)] = msg.new_chat_member
elseif msg.left_chat_member then elseif msg.left_chat_member then
self.database.users[tostring(msg.left_chat_member.id)] = msg.left_chat_member self.database.users[tostring(msg.left_chat_member.id)] = msg.left_chat_member
end end
-- Messages from blacklisted users only go to panoptic plugins. -- Messages from blacklisted users only go to panoptic plugins.
if self.database.blacklist[from_id_str] then if self.database.blacklist[from_id_str] then
plugint = self.panoptic_plugins plugint = self.panoptic_plugins
end end
-- If no text, use captions. -- If no text, use captions.
msg.text = msg.text or msg.caption or '' msg.text = msg.text or msg.caption or ''
msg.text_lower = msg.text:lower() msg.text_lower = msg.text:lower()
if msg.reply_to_message then if msg.reply_to_message then
msg.reply_to_message.text = msg.reply_to_message.text or msg.reply_to_message.caption or '' msg.reply_to_message.text = msg.reply_to_message.text or msg.reply_to_message.caption or ''
end end
-- Support deep linking. -- Support deep linking.
if msg.text:match('^'..config.cmd_pat..'start .+') then if msg.text:match('^'..config.cmd_pat..'start .+') then
msg.text = config.cmd_pat .. utilities.input(msg.text) msg.text = config.cmd_pat .. utilities.input(msg.text)
msg.text_lower = msg.text:lower() msg.text_lower = msg.text:lower()
end end
-- If the message is forwarded or comes from a blacklisted yser, -- If the message is forwarded or comes from a blacklisted yser,
-- Do the thing. -- Do the thing.
for _, plugin in ipairs(plugint) do for _, plugin in ipairs(plugint) do
for _, trigger in ipairs(plugin.triggers) do for _, trigger in ipairs(plugin.triggers) do
if string.match(msg.text_lower, trigger) then if string.match(msg.text_lower, trigger) then
local success, result = pcall(function() local success, result = pcall(function()
return plugin.action(self, msg, config) return plugin.action(self, msg, config)
end) end)
if not success then if not success then
-- If the plugin has an error message, send it. If it does -- If the plugin has an error message, send it. If it does
-- not, use the generic one specified in config. If it's set -- not, use the generic one specified in config. If it's set
-- to false, do nothing. -- to false, do nothing.
if plugin.error then if plugin.error then
utilities.send_reply(self, msg, plugin.error) utilities.send_reply(self, msg, plugin.error)
elseif plugin.error == nil then elseif plugin.error == nil then
utilities.send_reply(self, msg, config.errors.generic) utilities.send_reply(self, msg, config.errors.generic)
end end
utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config)
msg = nil msg = nil
return return
-- Continue if the return value is true. -- Continue if the return value is true.
elseif result ~= true then elseif result ~= true then
msg = nil msg = nil
return return
end end
end end
end end
end end
msg = nil msg = nil
end end
-- main -- main
function bot:run(config) function bot:run(config)
bot.init(self, config) bot.init(self, config)
while self.is_started do while self.is_started do
-- Update loop. -- Update loop.
local res = bindings.getUpdates(self, { timeout = 20, offset = self.last_update + 1 } ) local res = bindings.getUpdates(self, { timeout = 20, offset = self.last_update + 1 } )
if res then if res then
-- Iterate over every new message. -- Iterate over every new message.
for _,v in ipairs(res.result) do for _,v in ipairs(res.result) do
self.last_update = v.update_id self.last_update = v.update_id
if v.message then if v.message then
bot.on_msg_receive(self, v.message, config) bot.on_msg_receive(self, v.message, config)
end end
end end
else else
print('Connection error while fetching updates.') print('Connection error while fetching updates.')
end end
-- Run cron jobs every minute. -- Run cron jobs every minute.
if self.last_cron ~= os.date('%M') then if self.last_cron ~= os.date('%M') then
self.last_cron = os.date('%M') self.last_cron = os.date('%M')
for i,v in ipairs(self.plugins) do for i,v in ipairs(self.plugins) do
if v.cron then -- Call each plugin's cron function, if it has one. if v.cron then -- Call each plugin's cron function, if it has one.
local result, err = pcall(function() v.cron(self, config) end) local result, err = pcall(function() v.cron(self, config) end)
if not result then if not result then
utilities.handle_exception(self, err, 'CRON: ' .. i, config) utilities.handle_exception(self, err, 'CRON: ' .. i, config)
end end
end end
end end
end end
-- Save the "database" every hour. -- Save the "database" every hour.
if self.last_database_save ~= os.date('%H') then if self.last_database_save ~= os.date('%H') then
self.last_database_save = os.date('%H') self.last_database_save = os.date('%H')
utilities.save_data(self.database_name, self.database) utilities.save_data(self.database_name, self.database)
end end
end end
-- Save the database before exiting. -- Save the database before exiting.
utilities.save_data(self.database_name, self.database) utilities.save_data(self.database_name, self.database)
print('Halted.') print('Halted.')
end end
return bot return bot

View File

@ -1,13 +1,13 @@
--[[ --[[
drua-tg drua-tg
Based on JuanPotato's lua-tg (https://github.com/juanpotato/lua-tg), Based on JuanPotato's lua-tg (https://github.com/juanpotato/lua-tg),
modified to work more naturally from an API bot. modified to work more naturally from an API bot.
Usage: Usage:
drua = require('drua-tg') drua = require('drua-tg')
drua.IP = 'localhost' -- 'localhost' is default drua.IP = 'localhost' -- 'localhost' is default
drua.PORT = 4567 -- 4567 is default drua.PORT = 4567 -- 4567 is default
drua.message(chat_id, text) drua.message(chat_id, text)
The MIT License (MIT) The MIT License (MIT)
@ -35,150 +35,150 @@ SOFTWARE.
local SOCKET = require('socket') local SOCKET = require('socket')
local comtab = { local comtab = {
add = { 'chat_add_user %s %s', 'channel_invite %s %s' }, add = { 'chat_add_user %s %s', 'channel_invite %s %s' },
kick = { 'chat_del_user %s %s', 'channel_kick %s %s' }, kick = { 'chat_del_user %s %s', 'channel_kick %s %s' },
rename = { 'rename_chat %s "%s"', 'rename_channel %s "%s"' }, rename = { 'rename_chat %s "%s"', 'rename_channel %s "%s"' },
link = { 'export_chat_link %s', 'export_channel_link %s' }, link = { 'export_chat_link %s', 'export_channel_link %s' },
photo_set = { 'chat_set_photo %s %s', 'channel_set_photo %s %s' }, photo_set = { 'chat_set_photo %s %s', 'channel_set_photo %s %s' },
photo_get = { [0] = 'load_user_photo %s', 'load_chat_photo %s', 'load_channel_photo %s' }, photo_get = { [0] = 'load_user_photo %s', 'load_chat_photo %s', 'load_channel_photo %s' },
info = { [0] = 'user_info %s', 'chat_info %s', 'channel_info %s' } info = { [0] = 'user_info %s', 'chat_info %s', 'channel_info %s' }
} }
local format_target = function(target) local format_target = function(target)
target = tonumber(target) target = tonumber(target)
if target < -1000000000000 then if target < -1000000000000 then
target = 'channel#' .. math.abs(target) - 1000000000000 target = 'channel#' .. math.abs(target) - 1000000000000
return target, 2 return target, 2
elseif target < 0 then elseif target < 0 then
target = 'chat#' .. math.abs(target) target = 'chat#' .. math.abs(target)
return target, 1 return target, 1
else else
target = 'user#' .. target target = 'user#' .. target
return target, 0 return target, 0
end end
end end
local escape = function(text) local escape = function(text)
text = text:gsub('\\', '\\\\') text = text:gsub('\\', '\\\\')
text = text:gsub('\n', '\\n') text = text:gsub('\n', '\\n')
text = text:gsub('\t', '\\t') text = text:gsub('\t', '\\t')
text = text:gsub('"', '\\"') text = text:gsub('"', '\\"')
return text return text
end end
local drua = { local drua = {
IP = 'localhost', IP = 'localhost',
PORT = 4567 PORT = 4567
} }
drua.send = function(command, do_receive) drua.send = function(command, do_receive)
local s = SOCKET.connect(drua.IP, drua.PORT) local s = SOCKET.connect(drua.IP, drua.PORT)
assert(s, '\nUnable to connect to tg session.') assert(s, '\nUnable to connect to tg session.')
s:send(command..'\n') s:send(command..'\n')
local output local output
if do_receive then if do_receive then
output = string.match(s:receive('*l'), 'ANSWER (%d+)') output = string.match(s:receive('*l'), 'ANSWER (%d+)')
output = s:receive(tonumber(output)):gsub('\n$', '') output = s:receive(tonumber(output)):gsub('\n$', '')
end end
s:close() s:close()
return output return output
end end
drua.message = function(target, text) drua.message = function(target, text)
target = format_target(target) target = format_target(target)
text = escape(text) text = escape(text)
local command = 'msg %s "%s"' local command = 'msg %s "%s"'
command = command:format(target, text) command = command:format(target, text)
return drua.send(command) return drua.send(command)
end end
drua.send_photo = function(target, photo) drua.send_photo = function(target, photo)
target = format_target(target) target = format_target(target)
local command = 'send_photo %s %s' local command = 'send_photo %s %s'
command = command:format(target, photo) command = command:format(target, photo)
return drua.send(command) return drua.send(command)
end end
drua.add_user = function(chat, target) drua.add_user = function(chat, target)
local a local a
chat, a = format_target(chat) chat, a = format_target(chat)
target = format_target(target) target = format_target(target)
local command = comtab.add[a]:format(chat, target) local command = comtab.add[a]:format(chat, target)
return drua.send(command) return drua.send(command)
end end
drua.kick_user = function(chat, target) drua.kick_user = function(chat, target)
-- Get the group info so tg will recognize the target. -- Get the group info so tg will recognize the target.
drua.get_info(chat) drua.get_info(chat)
local a local a
chat, a = format_target(chat) chat, a = format_target(chat)
target = format_target(target) target = format_target(target)
local command = comtab.kick[a]:format(chat, target) local command = comtab.kick[a]:format(chat, target)
return drua.send(command) return drua.send(command)
end end
drua.rename_chat = function(chat, name) drua.rename_chat = function(chat, name)
local a local a
chat, a = format_target(chat) chat, a = format_target(chat)
local command = comtab.rename[a]:format(chat, name) local command = comtab.rename[a]:format(chat, name)
return drua.send(command) return drua.send(command)
end end
drua.export_link = function(chat) drua.export_link = function(chat)
local a local a
chat, a = format_target(chat) chat, a = format_target(chat)
local command = comtab.link[a]:format(chat) local command = comtab.link[a]:format(chat)
return drua.send(command, true) return drua.send(command, true)
end end
drua.get_photo = function(chat) drua.get_photo = function(chat)
local a local a
chat, a = format_target(chat) chat, a = format_target(chat)
local command = comtab.photo_get[a]:format(chat) local command = comtab.photo_get[a]:format(chat)
local output = drua.send(command, true) local output = drua.send(command, true)
if output:match('FAIL') then if output:match('FAIL') then
return false return false
else else
return output:match('Saved to (.+)') return output:match('Saved to (.+)')
end end
end end
drua.set_photo = function(chat, photo) drua.set_photo = function(chat, photo)
local a local a
chat, a = format_target(chat) chat, a = format_target(chat)
local command = comtab.photo_set[a]:format(chat, photo) local command = comtab.photo_set[a]:format(chat, photo)
return drua.send(command) return drua.send(command)
end end
drua.get_info = function(target) drua.get_info = function(target)
local a local a
target, a = format_target(target) target, a = format_target(target)
local command = comtab.info[a]:format(target) local command = comtab.info[a]:format(target)
return drua.send(command, true) return drua.send(command, true)
end end
drua.channel_set_admin = function(chat, user, rank) drua.channel_set_admin = function(chat, user, rank)
chat = format_target(chat) chat = format_target(chat)
user = format_target(user) user = format_target(user)
local command = 'channel_set_admin %s %s %s' local command = 'channel_set_admin %s %s %s'
command = command:format(chat, user, rank) command = command:format(chat, user, rank)
return drua.send(command) return drua.send(command)
end end
drua.channel_set_about = function(chat, text) drua.channel_set_about = function(chat, text)
chat = format_target(chat) chat = format_target(chat)
text = escape(text) text = escape(text)
local command = 'channel_set_about %s "%s"' local command = 'channel_set_about %s "%s"'
command = command:format(chat, text) command = command:format(chat, text)
return drua.send(command) return drua.send(command)
end end
drua.block = function(user) drua.block = function(user)
return drua.send('block_user user#' .. user) return drua.send('block_user user#' .. user)
end end
drua.unblock = function(user) drua.unblock = function(user)
return drua.send('unblock_user user#' .. user) return drua.send('unblock_user user#' .. user)
end end
return drua return drua

View File

@ -7,13 +7,13 @@ about.command = 'about'
about.doc = 'Returns information about the bot.' about.doc = 'Returns information about the bot.'
function about:init(config) function about:init(config)
about.text = config.about_text .. '\nBased on [otouto](http://github.com/topkecleon/otouto) v'..bot.version..' by topkecleon.' about.text = config.about_text .. '\nBased on [otouto](http://github.com/topkecleon/otouto) v'..bot.version..' by topkecleon.'
about.triggers = utilities.triggers(self.info.username, config.cmd_pat) about.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('about'):t('start').table :t('about'):t('start').table
end end
function about:action(msg, config) function about:action(msg, config)
utilities.send_message(self, msg.chat.id, about.text, true, nil, true) utilities.send_message(self, msg.chat.id, about.text, true, nil, true)
end end
return about return about

File diff suppressed because it is too large Load Diff

View File

@ -10,47 +10,47 @@ local utilities = require('otouto.utilities')
apod.command = 'apod [date]' apod.command = 'apod [date]'
function apod:init(config) function apod:init(config)
apod.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('apod', true).table apod.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('apod', true).table
apod.doc = [[ apod.doc = [[
/apod [YYYY-MM-DD] /apod [YYYY-MM-DD]
Returns the Astronomy Picture of the Day. Returns the Astronomy Picture of the Day.
Source: nasa.gov Source: nasa.gov
]] ]]
apod.doc = apod.doc:gsub('/', config.cmd_pat) apod.doc = apod.doc:gsub('/', config.cmd_pat)
apod.base_url = 'https://api.nasa.gov/planetary/apod?api_key=' .. (config.nasa_api_key or 'DEMO_KEY') apod.base_url = 'https://api.nasa.gov/planetary/apod?api_key=' .. (config.nasa_api_key or 'DEMO_KEY')
end end
function apod:action(msg, config) function apod:action(msg, config)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
local url = apod.base_url local url = apod.base_url
local date = os.date('%F') local date = os.date('%F')
if input then if input then
if input:match('^(%d+)%-(%d+)%-(%d+)$') then if input:match('^(%d+)%-(%d+)%-(%d+)$') then
url = url .. '&date=' .. URL.escape(input) url = url .. '&date=' .. URL.escape(input)
date = input date = input
end end
end end
local jstr, code = HTTPS.request(url) local jstr, code = HTTPS.request(url)
if code ~= 200 then if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
if data.error then if data.error then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local output = string.format( local output = string.format(
'<b>%s (</b><a href="%s">%s</a><b>)</b>\n%s', '<b>%s (</b><a href="%s">%s</a><b>)</b>\n%s',
utilities.html_escape(data.title), utilities.html_escape(data.title),
utilities.html_escape(data.hdurl or data.url), utilities.html_escape(data.hdurl or data.url),
date, date,
utilities.html_escape(data.explanation) utilities.html_escape(data.explanation)
) )
utilities.send_message(self, msg.chat.id, output, false, nil, 'html') utilities.send_message(self, msg.chat.id, output, false, nil, 'html')
end end
return apod return apod

View File

@ -5,8 +5,8 @@ local utilities = require('otouto.utilities')
bandersnatch.command = 'bandersnatch' bandersnatch.command = 'bandersnatch'
function bandersnatch:init(config) function bandersnatch:init(config)
bandersnatch.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bandersnatch'):t('bc').table bandersnatch.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bandersnatch'):t('bc').table
bandersnatch.doc = 'Shun the frumious Bandersnatch. \nAlias: ' .. config.cmd_pat .. 'bc' bandersnatch.doc = 'Shun the frumious Bandersnatch. \nAlias: ' .. config.cmd_pat .. 'bc'
end end
local fullnames = { "Wimbledon Tennismatch", "Rinkydink Curdlesnoot", "Butawhiteboy Cantbekhan", "Benadryl Claritin", "Bombadil Rivendell", "Wanda's Crotchfruit", "Biblical Concubine", "Syphilis Cankersore", "Buckminster Fullerene", "Bourgeoisie Capitalist" } local fullnames = { "Wimbledon Tennismatch", "Rinkydink Curdlesnoot", "Butawhiteboy Cantbekhan", "Benadryl Claritin", "Bombadil Rivendell", "Wanda's Crotchfruit", "Biblical Concubine", "Syphilis Cankersore", "Buckminster Fullerene", "Bourgeoisie Capitalist" }
@ -17,15 +17,15 @@ local lastnames = { "Coddleswort", "Crumplesack", "Curdlesnoot", "Calldispatch",
function bandersnatch:action(msg) function bandersnatch:action(msg)
local output local output
if math.random(10) == 10 then if math.random(10) == 10 then
output = fullnames[math.random(#fullnames)] output = fullnames[math.random(#fullnames)]
else else
output = firstnames[math.random(#firstnames)] .. ' ' .. lastnames[math.random(#lastnames)] output = firstnames[math.random(#firstnames)] .. ' ' .. lastnames[math.random(#lastnames)]
end end
utilities.send_message(self, msg.chat.id, '_'..output..'_', true, nil, true) utilities.send_message(self, msg.chat.id, '_'..output..'_', true, nil, true)
end end

View File

@ -5,12 +5,12 @@ local URL = require('socket.url')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function bible:init(config) function bible:init(config)
assert(config.biblia_api_key, assert(config.biblia_api_key,
'bible.lua requires a Biblia API key from http://api.biblia.com.' 'bible.lua requires a Biblia API key from http://api.biblia.com.'
) )
bible.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bible', true):t('b', true).table bible.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bible', true):t('b', true).table
bible.doc = config.cmd_pat .. [[bible <reference> bible.doc = config.cmd_pat .. [[bible <reference>
Returns a verse from the American Standard Version of the Bible, or an apocryphal verse from the King James Version. Results from biblia.com. Returns a verse from the American Standard Version of the Bible, or an apocryphal verse from the King James Version. Results from biblia.com.
Alias: ]] .. config.cmd_pat .. 'b' Alias: ]] .. config.cmd_pat .. 'b'
end end
@ -19,30 +19,30 @@ bible.command = 'bible <reference>'
function bible:action(msg, config) function bible:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, bible.doc, true) utilities.send_reply(self, msg, bible.doc, true)
return return
end end
local url = 'http://api.biblia.com/v1/bible/content/ASV.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input) local url = 'http://api.biblia.com/v1/bible/content/ASV.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input)
local output, res = HTTP.request(url) local output, res = HTTP.request(url)
if not output or res ~= 200 or output:len() == 0 then if not output or res ~= 200 or output:len() == 0 then
url = 'http://api.biblia.com/v1/bible/content/KJVAPOC.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input) url = 'http://api.biblia.com/v1/bible/content/KJVAPOC.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input)
output, res = HTTP.request(url) output, res = HTTP.request(url)
end end
if not output or res ~= 200 or output:len() == 0 then if not output or res ~= 200 or output:len() == 0 then
output = config.errors.results output = config.errors.results
end end
if output:len() > 4000 then if output:len() > 4000 then
output = 'The text is too long to post here. Try being more specific.' output = 'The text is too long to post here. Try being more specific.'
end end
utilities.send_reply(self, msg, output) utilities.send_reply(self, msg, output)
end end

View File

@ -14,65 +14,65 @@ bing.command = 'bing <query>'
bing.search_url = 'https://api.datamarket.azure.com/Data.ashx/Bing/Search/Web?Query=\'%s\'&$format=json' bing.search_url = 'https://api.datamarket.azure.com/Data.ashx/Bing/Search/Web?Query=\'%s\'&$format=json'
function bing:init(config) function bing:init(config)
assert(config.bing_api_key, assert(config.bing_api_key,
'bing.lua requires a Bing API key from http://datamarket.azure.com/dataset/bing/search.' 'bing.lua requires a Bing API key from http://datamarket.azure.com/dataset/bing/search.'
) )
bing.headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. config.bing_api_key) } bing.headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. config.bing_api_key) }
bing.triggers = utilities.triggers(self.info.username, config.cmd_pat) bing.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('bing', true):t('g', true):t('google', true).table :t('bing', true):t('g', true):t('google', true).table
bing.doc = [[ bing.doc = [[
/bing <query> /bing <query>
Returns the top web results from Bing. Returns the top web results from Bing.
Aliases: /g, /google Aliases: /g, /google
]] ]]
bing.doc = bing.doc:gsub('/', config.cmd_pat) bing.doc = bing.doc:gsub('/', config.cmd_pat)
end end
function bing:action(msg, config) function bing:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, bing.doc, true) utilities.send_reply(self, msg, bing.doc, true)
return return
end end
local url = bing.search_url:format(URL.escape(input)) local url = bing.search_url:format(URL.escape(input))
local resbody = {} local resbody = {}
local _, code = https.request{ local _, code = https.request{
url = url, url = url,
headers = bing.headers, headers = bing.headers,
sink = ltn12.sink.table(resbody), sink = ltn12.sink.table(resbody),
} }
if code ~= 200 then if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local data = JSON.decode(table.concat(resbody)) local data = JSON.decode(table.concat(resbody))
-- Four results in a group, eight in private. -- Four results in a group, eight in private.
local limit = msg.chat.type == 'private' and 8 or 4 local limit = msg.chat.type == 'private' and 8 or 4
-- No more results than provided. -- No more results than provided.
limit = limit > #data.d.results and #data.d.results or limit limit = limit > #data.d.results and #data.d.results or limit
if limit == 0 then if limit == 0 then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local reslist = {} local reslist = {}
for i = 1, limit do for i = 1, limit do
table.insert(reslist, string.format( table.insert(reslist, string.format(
'• <a href="%s">%s</a>', '• <a href="%s">%s</a>',
utilities.html_escape(data.d.results[i].Url), utilities.html_escape(data.d.results[i].Url),
utilities.html_escape(data.d.results[i].Title) utilities.html_escape(data.d.results[i].Title)
)) ))
end end
local output = string.format( local output = string.format(
'<b>Search results for</b> <i>%s</i><b>:</b>\n%s', '<b>Search results for</b> <i>%s</i><b>:</b>\n%s',
utilities.html_escape(input), utilities.html_escape(input),
table.concat(reslist, '\n') table.concat(reslist, '\n')
) )
utilities.send_message(self, msg.chat.id, output, true, nil, 'html') utilities.send_message(self, msg.chat.id, output, true, nil, 'html')
end end
return bing return bing

View File

@ -3,92 +3,92 @@ local utilities = require('otouto.utilities')
local blacklist = {} local blacklist = {}
function blacklist:init(config) function blacklist:init(config)
blacklist.triggers = utilities.triggers(self.info.username, config.cmd_pat) blacklist.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('blacklist', true):t('unblacklist', true).table :t('blacklist', true):t('unblacklist', true).table
blacklist.error = false blacklist.error = false
end end
function blacklist:action(msg, config) function blacklist:action(msg, config)
if msg.from.id ~= config.admin then return true end if msg.from.id ~= config.admin then return true end
local targets = {} local targets = {}
if msg.reply_to_message then if msg.reply_to_message then
table.insert(targets, { table.insert(targets, {
id = msg.reply_to_message.from.id, id = msg.reply_to_message.from.id,
id_str = tostring(msg.reply_to_message.from.id), id_str = tostring(msg.reply_to_message.from.id),
name = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name) name = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name)
}) })
else else
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if input then if input then
for user in input:gmatch('%g+') do for user in input:gmatch('%g+') do
if self.database.users[user] then if self.database.users[user] then
table.insert(targets, { table.insert(targets, {
id = self.database.users[user].id, id = self.database.users[user].id,
id_str = tostring(self.database.users[user].id), id_str = tostring(self.database.users[user].id),
name = utilities.build_name(self.database.users[user].first_name, self.database.users[user].last_name) name = utilities.build_name(self.database.users[user].first_name, self.database.users[user].last_name)
}) })
elseif tonumber(user) then elseif tonumber(user) then
local t = { local t = {
id_str = user, id_str = user,
id = tonumber(user) id = tonumber(user)
} }
if tonumber(user) < 0 then if tonumber(user) < 0 then
t.name = 'Group (' .. user .. ')' t.name = 'Group (' .. user .. ')'
else else
t.name = 'Unknown (' .. user .. ')' t.name = 'Unknown (' .. user .. ')'
end end
table.insert(targets, t) table.insert(targets, t)
elseif user:match('^@') then elseif user:match('^@') then
local u = utilities.resolve_username(self, user) local u = utilities.resolve_username(self, user)
if u then if u then
table.insert(targets, { table.insert(targets, {
id = u.id, id = u.id,
id_str = tostring(u.id), id_str = tostring(u.id),
name = utilities.build_name(u.first_name, u.last_name) name = utilities.build_name(u.first_name, u.last_name)
}) })
else else
table.insert(targets, { err = 'Sorry, I do not recognize that username ('..user..').' }) table.insert(targets, { err = 'Sorry, I do not recognize that username ('..user..').' })
end end
else else
table.insert(targets, { err = 'Invalid username or ID ('..user..').' }) table.insert(targets, { err = 'Invalid username or ID ('..user..').' })
end end
end end
else else
utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID, or a group or groups via ID.') utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID, or a group or groups via ID.')
return return
end end
end end
local output = '' local output = ''
if msg.text:match('^'..config.cmd_pat..'blacklist') then if msg.text:match('^'..config.cmd_pat..'blacklist') then
for _, target in ipairs(targets) do for _, target in ipairs(targets) do
if target.err then if target.err then
output = output .. target.err .. '\n' output = output .. target.err .. '\n'
elseif self.database.blacklist[target.id_str] then elseif self.database.blacklist[target.id_str] then
output = output .. target.name .. ' is already blacklisted.\n' output = output .. target.name .. ' is already blacklisted.\n'
else else
self.database.blacklist[target.id_str] = true self.database.blacklist[target.id_str] = true
output = output .. target.name .. ' is now blacklisted.\n' output = output .. target.name .. ' is now blacklisted.\n'
if config.drua_block_on_blacklist and target.id > 0 then if config.drua_block_on_blacklist and target.id > 0 then
require('otouto.drua-tg').block(target.id) require('otouto.drua-tg').block(target.id)
end end
end end
end end
elseif msg.text:match('^'..config.cmd_pat..'unblacklist') then elseif msg.text:match('^'..config.cmd_pat..'unblacklist') then
for _, target in ipairs(targets) do for _, target in ipairs(targets) do
if target.err then if target.err then
output = output .. target.err .. '\n' output = output .. target.err .. '\n'
elseif not self.database.blacklist[target.id_str] then elseif not self.database.blacklist[target.id_str] then
output = output .. target.name .. ' is not blacklisted.\n' output = output .. target.name .. ' is not blacklisted.\n'
else else
self.database.blacklist[target.id_str] = nil self.database.blacklist[target.id_str] = nil
output = output .. target.name .. ' is no longer blacklisted.\n' output = output .. target.name .. ' is no longer blacklisted.\n'
if config.drua_block_on_blacklist and target.id > 0 then if config.drua_block_on_blacklist and target.id > 0 then
require('otouto.drua-tg').unblock(target.id) require('otouto.drua-tg').unblock(target.id)
end end
end end
end end
end end
utilities.send_reply(self, msg, output) utilities.send_reply(self, msg, output)
end end
return blacklist return blacklist

View File

@ -7,22 +7,22 @@ local utilities = require('otouto.utilities')
calc.command = 'calc <expression>' calc.command = 'calc <expression>'
function calc:init(config) function calc:init(config)
calc.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('calc', true).table calc.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('calc', true).table
calc.doc = config.cmd_pat .. [[calc <expression> calc.doc = config.cmd_pat .. [[calc <expression>
Returns solutions to mathematical expressions and conversions between common units. Results provided by mathjs.org.]] Returns solutions to mathematical expressions and conversions between common units. Results provided by mathjs.org.]]
end end
function calc:action(msg, config) function calc:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, calc.doc, true) utilities.send_reply(self, msg, calc.doc, true)
return return
end end
local url = 'https://api.mathjs.org/v1/?expr=' .. URL.escape(input) local url = 'https://api.mathjs.org/v1/?expr=' .. URL.escape(input)
local output = HTTPS.request(url) local output = HTTPS.request(url)
output = output and '`'..output..'`' or config.errors.connection output = output and '`'..output..'`' or config.errors.connection
utilities.send_reply(self, msg, output, true) utilities.send_reply(self, msg, output, true)
end end
return calc return calc

View File

@ -7,22 +7,22 @@ local utilities = require('otouto.utilities')
local catfact = {} local catfact = {}
function catfact:init(config) function catfact:init(config)
catfact.triggers = utilities.triggers(self.info.username, config.cmd_pat) catfact.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('catfact', true).table :t('catfact', true).table
catfact.command = 'catfact' catfact.command = 'catfact'
catfact.doc = 'Returns a cat fact.' catfact.doc = 'Returns a cat fact.'
catfact.url = 'http://catfacts-api.appspot.com/api/facts' catfact.url = 'http://catfacts-api.appspot.com/api/facts'
end end
function catfact:action(msg, config) function catfact:action(msg, config)
local jstr, code = HTTP.request(catfact.url) local jstr, code = HTTP.request(catfact.url)
if code ~= 200 then if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
local output = '*Cat Fact*\n_' .. data.facts[1] .. '_' local output = '*Cat Fact*\n_' .. data.facts[1] .. '_'
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end
return catfact return catfact

View File

@ -4,12 +4,12 @@ local HTTP = require('socket.http')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function cats:init(config) function cats:init(config)
if not config.thecatapi_key then if not config.thecatapi_key then
print('Missing config value: thecatapi_key.') print('Missing config value: thecatapi_key.')
print('cats.lua will be enabled, but there are more features with a key.') print('cats.lua will be enabled, but there are more features with a key.')
end end
cats.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cat').table cats.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cat').table
end end
cats.command = 'cat' cats.command = 'cat'
@ -17,21 +17,21 @@ cats.doc = 'Returns a cat!'
function cats:action(msg, config) function cats:action(msg, config)
local url = 'http://thecatapi.com/api/images/get?format=html&type=jpg' local url = 'http://thecatapi.com/api/images/get?format=html&type=jpg'
if config.thecatapi_key then if config.thecatapi_key then
url = url .. '&api_key=' .. config.thecatapi_key url = url .. '&api_key=' .. config.thecatapi_key
end end
local str, res = HTTP.request(url) local str, res = HTTP.request(url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
str = str:match('<img src="(.-)">') str = str:match('<img src="(.-)">')
local output = '[Cat!]('..str..')' local output = '[Cat!]('..str..')'
utilities.send_message(self, msg.chat.id, output, false, nil, true) utilities.send_message(self, msg.chat.id, output, false, nil, true)
end end

View File

@ -4,9 +4,9 @@ local bindings = require('otouto.bindings')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function channel:init(config) function channel:init(config)
channel.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ch', true).table channel.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ch', true).table
channel.command = 'ch <channel> \\n <message>' channel.command = 'ch <channel> \\n <message>'
channel.doc = config.cmd_pat .. [[ch <channel> channel.doc = config.cmd_pat .. [[ch <channel>
<message> <message>
Sends a message to a channel. Channel may be specified via ID or username. Messages are markdown-enabled. Users may only send messages to channels for which they are the owner or an administrator. Sends a message to a channel. Channel may be specified via ID or username. Messages are markdown-enabled. Users may only send messages to channels for which they are the owner or an administrator.
@ -20,41 +20,41 @@ The following markdown syntax is supported:
end end
function channel:action(msg, config) function channel:action(msg, config)
-- An exercise in using zero early returns. :) -- An exercise in using zero early returns. :)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
local output local output
if input then if input then
local chat_id = utilities.get_word(input, 1) local chat_id = utilities.get_word(input, 1)
local admin_list, t = bindings.getChatAdministrators(self, { chat_id = chat_id } ) local admin_list, t = bindings.getChatAdministrators(self, { chat_id = chat_id } )
if admin_list then if admin_list then
local is_admin = false local is_admin = false
for _, admin in ipairs(admin_list.result) do for _, admin in ipairs(admin_list.result) do
if admin.user.id == msg.from.id then if admin.user.id == msg.from.id then
is_admin = true is_admin = true
end end
end end
if is_admin then if is_admin then
local text = input:match('\n(.+)') local text = input:match('\n(.+)')
if text then if text then
local success, result = utilities.send_message(self, chat_id, text, true, nil, true) local success, result = utilities.send_message(self, chat_id, text, true, nil, true)
if success then if success then
output = 'Your message has been sent!' output = 'Your message has been sent!'
else else
output = 'Sorry, I was unable to send your message.\n`' .. result.description .. '`' output = 'Sorry, I was unable to send your message.\n`' .. result.description .. '`'
end end
else else
output = 'Please enter a message to be sent. Markdown is supported.' output = 'Please enter a message to be sent. Markdown is supported.'
end end
else else
output = 'Sorry, you do not appear to be an administrator for that channel.' output = 'Sorry, you do not appear to be an administrator for that channel.'
end end
else else
output = 'Sorry, I was unable to retrieve a list of administrators for that channel.\n`' .. t.description .. '`' output = 'Sorry, I was unable to retrieve a list of administrators for that channel.\n`' .. t.description .. '`'
end end
else else
output = channel.doc output = channel.doc
end end
utilities.send_reply(self, msg, output, true) utilities.send_reply(self, msg, output, true)
end end
return channel return channel

View File

@ -7,22 +7,22 @@ local utilities = require('otouto.utilities')
local chuck = {} local chuck = {}
function chuck:init(config) function chuck:init(config)
chuck.triggers = utilities.triggers(self.info.username, config.cmd_pat) chuck.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('chuck', true):t('cn', true):t('chucknorris', true).table :t('chuck', true):t('cn', true):t('chucknorris', true).table
chuck.command = 'chuck' chuck.command = 'chuck'
chuck.doc = 'Returns a fact about Chuck Norris.' chuck.doc = 'Returns a fact about Chuck Norris.'
chuck.url = 'http://api.icndb.com/jokes/random' chuck.url = 'http://api.icndb.com/jokes/random'
end end
function chuck:action(msg, config) function chuck:action(msg, config)
local jstr, code = HTTP.request(chuck.url) local jstr, code = HTTP.request(chuck.url)
if code ~= 200 then if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
local output = '*Chuck Norris Fact*\n_' .. data.value.joke .. '_' local output = '*Chuck Norris Fact*\n_' .. data.value.joke .. '_'
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end
return chuck return chuck

View File

@ -7,30 +7,30 @@ local bindings = require('otouto.bindings')
local cleverbot = {} local cleverbot = {}
function cleverbot:init(config) function cleverbot:init(config)
cleverbot.name = '^' .. self.info.first_name:lower() .. ', ' cleverbot.name = '^' .. self.info.first_name:lower() .. ', '
cleverbot.username = '^@' .. self.info.username:lower() .. ', ' cleverbot.username = '^@' .. self.info.username:lower() .. ', '
cleverbot.triggers = { cleverbot.triggers = {
'^' .. self.info.first_name:lower() .. ', ', '^' .. self.info.first_name:lower() .. ', ',
'^@' .. self.info.username:lower() .. ', ' '^@' .. self.info.username:lower() .. ', '
} }
cleverbot.url = config.chatter.cleverbot_api cleverbot.url = config.chatter.cleverbot_api
cleverbot.error = false cleverbot.error = false
end end
function cleverbot:action(msg, config) function cleverbot:action(msg, config)
bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' }) bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' })
local input = msg.text_lower:gsub(cleverbot.name, ''):gsub(cleverbot.name, '') local input = msg.text_lower:gsub(cleverbot.name, ''):gsub(cleverbot.name, '')
local jstr, code = HTTPS.request(cleverbot.url .. URL.escape(input)) local jstr, code = HTTPS.request(cleverbot.url .. URL.escape(input))
if code ~= 200 then if code ~= 200 then
utilities.send_message(self, msg.chat.id, config.chatter.connection) utilities.send_message(self, msg.chat.id, config.chatter.connection)
return return
end end
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
if not data.clever then if not data.clever then
utilities.send_message(self, msg.chat.id, config.chatter.response) utilities.send_message(self, msg.chat.id, config.chatter.response)
return return
end end
utilities.send_message(self, msg.chat.id, data.clever) utilities.send_message(self, msg.chat.id, data.clever)
end end
return cleverbot return cleverbot

View File

@ -8,19 +8,19 @@ commit.command = 'commit'
commit.doc = 'Returns a commit message from whatthecommit.com.' commit.doc = 'Returns a commit message from whatthecommit.com.'
function commit:init(config) function commit:init(config)
commit.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('commit').table commit.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('commit').table
end end
function commit:action(msg) function commit:action(msg)
bindings.request( bindings.request(
self, self,
'sendMessage', 'sendMessage',
{ {
chat_id = msg.chat.id, chat_id = msg.chat.id,
text = '```\n' .. (http.request('http://whatthecommit.com/index.txt')) .. '\n```', text = '```\n' .. (http.request('http://whatthecommit.com/index.txt')) .. '\n```',
parse_mode = 'Markdown' parse_mode = 'Markdown'
} }
) )
end end
return commit return commit

View File

@ -6,52 +6,52 @@ local utilities = require('otouto.utilities')
local cmd_pat -- Prevents the command from being uncallable. local cmd_pat -- Prevents the command from being uncallable.
function control:init(config) function control:init(config)
cmd_pat = config.cmd_pat cmd_pat = config.cmd_pat
control.triggers = utilities.triggers(self.info.username, cmd_pat, control.triggers = utilities.triggers(self.info.username, cmd_pat,
{'^'..cmd_pat..'script'}):t('reload', true):t('halt').table {'^'..cmd_pat..'script'}):t('reload', true):t('halt').table
end end
function control:action(msg, config) function control:action(msg, config)
if msg.from.id ~= config.admin then if msg.from.id ~= config.admin then
return return
end end
if msg.date < os.time() - 2 then return end if msg.date < os.time() - 2 then return end
if msg.text_lower:match('^'..cmd_pat..'reload') then if msg.text_lower:match('^'..cmd_pat..'reload') then
for pac, _ in pairs(package.loaded) do for pac, _ in pairs(package.loaded) do
if pac:match('^otouto%.plugins%.') then if pac:match('^otouto%.plugins%.') then
package.loaded[pac] = nil package.loaded[pac] = nil
end end
end end
package.loaded['otouto.bindings'] = nil package.loaded['otouto.bindings'] = nil
package.loaded['otouto.utilities'] = nil package.loaded['otouto.utilities'] = nil
package.loaded['otouto.drua-tg'] = nil package.loaded['otouto.drua-tg'] = nil
package.loaded['config'] = nil package.loaded['config'] = nil
if not msg.text_lower:match('%-config') then if not msg.text_lower:match('%-config') then
for k, v in pairs(require('config')) do for k, v in pairs(require('config')) do
config[k] = v config[k] = v
end end
end end
bot.init(self, config) bot.init(self, config)
utilities.send_reply(self, msg, 'Bot reloaded!') utilities.send_reply(self, msg, 'Bot reloaded!')
elseif msg.text_lower:match('^'..cmd_pat..'halt') then elseif msg.text_lower:match('^'..cmd_pat..'halt') then
self.is_started = false self.is_started = false
utilities.send_reply(self, msg, 'Stopping bot!') utilities.send_reply(self, msg, 'Stopping bot!')
elseif msg.text_lower:match('^'..cmd_pat..'script') then elseif msg.text_lower:match('^'..cmd_pat..'script') then
local input = msg.text_lower:match('^'..cmd_pat..'script\n(.+)') local input = msg.text_lower:match('^'..cmd_pat..'script\n(.+)')
if not input then if not input then
utilities.send_reply(self, msg, 'usage: ```\n'..cmd_pat..'script\n'..cmd_pat..'command <arg>\n...\n```', true) utilities.send_reply(self, msg, 'usage: ```\n'..cmd_pat..'script\n'..cmd_pat..'command <arg>\n...\n```', true)
return return
end end
input = input .. '\n' input = input .. '\n'
for command in input:gmatch('(.-)\n') do for command in input:gmatch('(.-)\n') do
command = utilities.trim(command) command = utilities.trim(command)
msg.text = command msg.text = command
bot.on_msg_receive(self, msg, config) bot.on_msg_receive(self, msg, config)
end end
end end
end end

View File

@ -6,8 +6,8 @@ local utilities = require('otouto.utilities')
currency.command = 'cash [amount] <from> to <to>' currency.command = 'cash [amount] <from> to <to>'
function currency:init(config) function currency:init(config)
currency.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cash', true).table currency.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cash', true).table
currency.doc = config.cmd_pat .. [[cash [amount] <from> to <to> currency.doc = config.cmd_pat .. [[cash [amount] <from> to <to>
Example: ]] .. config.cmd_pat .. [[cash 5 USD to EUR Example: ]] .. config.cmd_pat .. [[cash 5 USD to EUR
Returns exchange rates for various currencies. Returns exchange rates for various currencies.
Source: Google Finance.]] Source: Google Finance.]]
@ -15,44 +15,44 @@ end
function currency:action(msg, config) function currency:action(msg, config)
local input = msg.text:upper() local input = msg.text:upper()
if not input:match('%a%a%a TO %a%a%a') then if not input:match('%a%a%a TO %a%a%a') then
utilities.send_message(self, msg.chat.id, currency.doc, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, currency.doc, true, msg.message_id, true)
return return
end end
local from = input:match('(%a%a%a) TO') local from = input:match('(%a%a%a) TO')
local to = input:match('TO (%a%a%a)') local to = input:match('TO (%a%a%a)')
local amount = utilities.get_word(input, 2) local amount = utilities.get_word(input, 2)
amount = tonumber(amount) or 1 amount = tonumber(amount) or 1
local result = 1 local result = 1
local url = 'https://www.google.com/finance/converter' local url = 'https://www.google.com/finance/converter'
if from ~= to then if from ~= to then
url = url .. '?from=' .. from .. '&to=' .. to .. '&a=' .. amount url = url .. '?from=' .. from .. '&to=' .. to .. '&a=' .. amount
local str, res = HTTPS.request(url) local str, res = HTTPS.request(url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
str = str:match('<span class=bld>(.*) %u+</span>') str = str:match('<span class=bld>(.*) %u+</span>')
if not str then if not str then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
result = string.format('%.2f', str) result = string.format('%.2f', str)
end end
local output = amount .. ' ' .. from .. ' = ' .. result .. ' ' .. to .. '\n\n' local output = amount .. ' ' .. from .. ' = ' .. result .. ' ' .. to .. '\n\n'
output = output .. os.date('!%F %T UTC') .. '\nSource: Google Finance`' output = output .. os.date('!%F %T UTC') .. '\nSource: Google Finance`'
output = '```\n' .. output .. '\n```' output = '```\n' .. output .. '\n```'
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end

View File

@ -5,49 +5,49 @@ local utilities = require('otouto.utilities')
dice.command = 'roll <nDr>' dice.command = 'roll <nDr>'
function dice:init(config) function dice:init(config)
dice.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('roll', true).table dice.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('roll', true).table
dice.doc = config.cmd_pat .. [[roll <nDr> dice.doc = config.cmd_pat .. [[roll <nDr>
Returns a set of dice rolls, where n is the number of rolls and r is the range. If only a range is given, returns only one roll.]] Returns a set of dice rolls, where n is the number of rolls and r is the range. If only a range is given, returns only one roll.]]
end end
function dice:action(msg) function dice:action(msg)
local input = utilities.input(msg.text_lower) local input = utilities.input(msg.text_lower)
if not input then if not input then
utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true)
return return
end end
local count, range local count, range
if input:match('^[%d]+d[%d]+$') then if input:match('^[%d]+d[%d]+$') then
count, range = input:match('([%d]+)d([%d]+)') count, range = input:match('([%d]+)d([%d]+)')
elseif input:match('^d?[%d]+$') then elseif input:match('^d?[%d]+$') then
count = 1 count = 1
range = input:match('^d?([%d]+)$') range = input:match('^d?([%d]+)$')
else else
utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true)
return return
end end
count = tonumber(count) count = tonumber(count)
range = tonumber(range) range = tonumber(range)
if range < 2 then if range < 2 then
utilities.send_reply(self, msg, 'The minimum range is 2.') utilities.send_reply(self, msg, 'The minimum range is 2.')
return return
end end
if range > 1000 or count > 1000 then if range > 1000 or count > 1000 then
utilities.send_reply(self, msg, 'The maximum range and count are 1000.') utilities.send_reply(self, msg, 'The maximum range and count are 1000.')
return return
end end
local output = '*' .. count .. 'd' .. range .. '*\n`' local output = '*' .. count .. 'd' .. range .. '*\n`'
for _ = 1, count do for _ = 1, count do
output = output .. math.random(range) .. '\t' output = output .. math.random(range) .. '\t'
end end
output = output .. '`' output = output .. '`'
utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true)
end end

View File

@ -8,8 +8,8 @@ local utilities = require('otouto.utilities')
dilbert.command = 'dilbert [date]' dilbert.command = 'dilbert [date]'
function dilbert:init(config) function dilbert:init(config)
dilbert.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('dilbert', true).table dilbert.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('dilbert', true).table
dilbert.doc = config.cmd_pat .. [[dilbert [YYYY-MM-DD] dilbert.doc = config.cmd_pat .. [[dilbert [YYYY-MM-DD]
Returns the latest Dilbert strip or that of the provided date. Returns the latest Dilbert strip or that of the provided date.
Dates before the first strip will return the first strip. Dates after the last trip will return the last strip. Dates before the first strip will return the first strip. Dates after the last trip will return the last strip.
Source: dilbert.com]] Source: dilbert.com]]
@ -17,32 +17,32 @@ end
function dilbert:action(msg, config) function dilbert:action(msg, config)
bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'upload_photo' } ) bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'upload_photo' } )
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if not input then input = os.date('%F') end if not input then input = os.date('%F') end
if not input:match('^%d%d%d%d%-%d%d%-%d%d$') then input = os.date('%F') end if not input:match('^%d%d%d%d%-%d%d%-%d%d$') then input = os.date('%F') end
local url = 'http://dilbert.com/strip/' .. URL.escape(input) local url = 'http://dilbert.com/strip/' .. URL.escape(input)
local str, res = HTTP.request(url) local str, res = HTTP.request(url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local strip_filename = '/tmp/' .. input .. '.gif' local strip_filename = '/tmp/' .. input .. '.gif'
local strip_file = io.open(strip_filename) local strip_file = io.open(strip_filename)
if strip_file then if strip_file then
strip_file:close() strip_file:close()
strip_file = strip_filename strip_file = strip_filename
else else
local strip_url = str:match('<meta property="og:image" content="(.-)"/>') local strip_url = str:match('<meta property="og:image" content="(.-)"/>')
strip_file = utilities.download_file(strip_url, '/tmp/' .. input .. '.gif') strip_file = utilities.download_file(strip_url, '/tmp/' .. input .. '.gif')
end end
local strip_title = str:match('<meta property="article:publish_date" content="(.-)"/>') local strip_title = str:match('<meta property="article:publish_date" content="(.-)"/>')
bindings.sendPhoto(self, { chat_id = msg.chat.id, caption = strip_title }, { photo = strip_file } ) bindings.sendPhoto(self, { chat_id = msg.chat.id, caption = strip_title }, { photo = strip_file } )
end end

View File

@ -5,25 +5,25 @@ local utilities = require('otouto.utilities')
echo.command = 'echo <text>' echo.command = 'echo <text>'
function echo:init(config) function echo:init(config)
echo.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('echo', true).table echo.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('echo', true).table
echo.doc = config.cmd_pat .. 'echo <text> \nRepeats a string of text.' echo.doc = config.cmd_pat .. 'echo <text> \nRepeats a string of text.'
end end
function echo:action(msg) function echo:action(msg)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_message(self, msg.chat.id, echo.doc, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, echo.doc, true, msg.message_id, true)
else else
local output local output
if msg.chat.type == 'supergroup' then if msg.chat.type == 'supergroup' then
output = utilities.style.enquote('Echo', input) output = utilities.style.enquote('Echo', input)
else else
output = utilities.md_escape(utilities.char.zwnj..input) output = utilities.md_escape(utilities.char.zwnj..input)
end end
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end
end end

View File

@ -6,52 +6,52 @@ eightball.command = '8ball'
eightball.doc = 'Returns an answer from a magic 8-ball!' eightball.doc = 'Returns an answer from a magic 8-ball!'
function eightball:init(config) function eightball:init(config)
eightball.triggers = utilities.triggers(self.info.username, config.cmd_pat, eightball.triggers = utilities.triggers(self.info.username, config.cmd_pat,
{'[Yy]/[Nn]%p*$'}):t('8ball', true).table {'[Yy]/[Nn]%p*$'}):t('8ball', true).table
end end
local ball_answers = { local ball_answers = {
"It is certain.", "It is certain.",
"It is decidedly so.", "It is decidedly so.",
"Without a doubt.", "Without a doubt.",
"Yes, definitely.", "Yes, definitely.",
"You may rely on it.", "You may rely on it.",
"As I see it, yes.", "As I see it, yes.",
"Most likely.", "Most likely.",
"Outlook: good.", "Outlook: good.",
"Yes.", "Yes.",
"Signs point to yes.", "Signs point to yes.",
"Reply hazy try again.", "Reply hazy try again.",
"Ask again later.", "Ask again later.",
"Better not tell you now.", "Better not tell you now.",
"Cannot predict now.", "Cannot predict now.",
"Concentrate and ask again.", "Concentrate and ask again.",
"Don't count on it.", "Don't count on it.",
"My reply is no.", "My reply is no.",
"My sources say no.", "My sources say no.",
"Outlook: not so good.", "Outlook: not so good.",
"Very doubtful.", "Very doubtful.",
"There is a time and place for everything, but not now." "There is a time and place for everything, but not now."
} }
local yesno_answers = { local yesno_answers = {
'Absolutely.', 'Absolutely.',
'In your dreams.', 'In your dreams.',
'Yes.', 'Yes.',
'No.' 'No.'
} }
function eightball:action(msg) function eightball:action(msg)
local output local output
if msg.text_lower:match('y/n%p?$') then if msg.text_lower:match('y/n%p?$') then
output = yesno_answers[math.random(#yesno_answers)] output = yesno_answers[math.random(#yesno_answers)]
else else
output = ball_answers[math.random(#ball_answers)] output = ball_answers[math.random(#ball_answers)]
end end
utilities.send_reply(self, msg, output) utilities.send_reply(self, msg, output)
end end

View File

@ -5,13 +5,13 @@ local fortune = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function fortune:init(config) function fortune:init(config)
local s = io.popen('fortune'):read('*all') local s = io.popen('fortune'):read('*all')
assert( assert(
not s:match('not found$'), not s:match('not found$'),
'fortune.lua requires the fortune program to be installed.' 'fortune.lua requires the fortune program to be installed.'
) )
fortune.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('fortune').table fortune.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('fortune').table
end end
fortune.command = 'fortune' fortune.command = 'fortune'
@ -19,11 +19,11 @@ fortune.doc = 'Returns a UNIX fortune.'
function fortune:action(msg) function fortune:action(msg)
local fortunef = io.popen('fortune') local fortunef = io.popen('fortune')
local output = fortunef:read('*all') local output = fortunef:read('*all')
output = '```\n' .. output .. '\n```' output = '```\n' .. output .. '\n```'
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
fortunef:close() fortunef:close()
end end

View File

@ -9,58 +9,58 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function gImages:init(config) function gImages:init(config)
assert(config.google_api_key and config.google_cse_key, assert(config.google_api_key and config.google_cse_key,
'gImages.lua requires a Google API key from http://console.developers.google.com and a Google Custom Search Engine key from http://cse.google.com/cse.' 'gImages.lua requires a Google API key from http://console.developers.google.com and a Google Custom Search Engine key from http://cse.google.com/cse.'
) )
gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('image', true):t('i', true):t('insfw', true).table gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('image', true):t('i', true):t('insfw', true).table
gImages.doc = config.cmd_pat .. [[image <query> gImages.doc = config.cmd_pat .. [[image <query>
Returns a randomized top result from Google Images. Safe search is enabled by default; use "]] .. config.cmd_pat .. [[insfw" to disable it. NSFW results will not display an image preview. Returns a randomized top result from Google Images. Safe search is enabled by default; use "]] .. config.cmd_pat .. [[insfw" to disable it. NSFW results will not display an image preview.
Alias: ]] .. config.cmd_pat .. 'i' Alias: ]] .. config.cmd_pat .. 'i'
gImages.search_url = 'https://www.googleapis.com/customsearch/v1?&searchType=image&imgSize=xlarge&alt=json&num=8&start=1&key=' .. config.google_api_key .. '&cx=' .. config.google_cse_key gImages.search_url = 'https://www.googleapis.com/customsearch/v1?&searchType=image&imgSize=xlarge&alt=json&num=8&start=1&key=' .. config.google_api_key .. '&cx=' .. config.google_cse_key
end end
gImages.command = 'image <query>' gImages.command = 'image <query>'
function gImages:action(msg, config) function gImages:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, gImages.doc, true) utilities.send_reply(self, msg, gImages.doc, true)
return return
end end
local url = gImages.search_url local url = gImages.search_url
if not string.match(msg.text, '^'..config.cmd_pat..'i[mage]*nsfw') then if not string.match(msg.text, '^'..config.cmd_pat..'i[mage]*nsfw') then
url = url .. '&safe=high' url = url .. '&safe=high'
end end
url = url .. '&q=' .. URL.escape(input) url = url .. '&q=' .. URL.escape(input)
local jstr, res = HTTPS.request(url) local jstr, res = HTTPS.request(url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local jdat = JSON.decode(jstr) local jdat = JSON.decode(jstr)
if jdat.searchInformation.totalResults == '0' then if jdat.searchInformation.totalResults == '0' then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local i = math.random(jdat.queries.request[1].count) local i = math.random(jdat.queries.request[1].count)
local img_url = jdat.items[i].link local img_url = jdat.items[i].link
local img_title = jdat.items[i].title local img_title = jdat.items[i].title
local output = '[' .. img_title .. '](' .. img_url .. ')' local output = '[' .. img_title .. '](' .. img_url .. ')'
if msg.text:match('nsfw') then if msg.text:match('nsfw') then
utilities.send_reply(self, '*NSFW*\n'..msg, output) utilities.send_reply(self, '*NSFW*\n'..msg, output)
else else
utilities.send_message(self, msg.chat.id, output, false, nil, true) utilities.send_message(self, msg.chat.id, output, false, nil, true)
end end
end end

View File

@ -6,34 +6,34 @@ local utilities = require('otouto.utilities')
gMaps.command = 'location <query>' gMaps.command = 'location <query>'
function gMaps:init(config) function gMaps:init(config)
gMaps.triggers = utilities.triggers(self.info.username, config.cmd_pat) gMaps.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('location', true):t('loc', true).table :t('location', true):t('loc', true).table
gMaps.doc = [[ gMaps.doc = [[
/location <query> /location <query>
Returns a location from Google Maps. Returns a location from Google Maps.
Alias: /loc Alias: /loc
]] ]]
gMaps.doc = gMaps.doc:gsub('/', config.cmd_pat) gMaps.doc = gMaps.doc:gsub('/', config.cmd_pat)
end end
function gMaps:action(msg, config) function gMaps:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, gMaps.doc, true) utilities.send_reply(self, msg, gMaps.doc, true)
return return
end end
local coords = utilities.get_coords(input, config) local coords = utilities.get_coords(input, config)
if type(coords) == 'string' then if type(coords) == 'string' then
utilities.send_reply(self, msg, coords) utilities.send_reply(self, msg, coords)
end end
bindings.sendLocation(self, { bindings.sendLocation(self, {
chat_id = msg.chat.id, chat_id = msg.chat.id,
latitude = coords.lat, latitude = coords.lat,
longitude = coords.lon, longitude = coords.lon,
reply_to_message_id = msg.message_id reply_to_message_id = msg.message_id
} ) } )
end end
return gMaps return gMaps

View File

@ -3,30 +3,30 @@ local utilities = require('otouto.utilities')
local greetings = {} local greetings = {}
function greetings:init(config) function greetings:init(config)
greetings.triggers = {} greetings.triggers = {}
for _, triggers in pairs(config.greetings) do for _, triggers in pairs(config.greetings) do
for i = 1, #triggers do for i = 1, #triggers do
triggers[i] = '^' .. triggers[i] .. ',? ' .. self.info.first_name:lower() .. '%p*$' triggers[i] = '^' .. triggers[i] .. ',? ' .. self.info.first_name:lower() .. '%p*$'
table.insert(greetings.triggers, triggers[i]) table.insert(greetings.triggers, triggers[i])
end end
end end
end end
function greetings:action(msg, config) function greetings:action(msg, config)
local nick local nick
if self.database.userdata[tostring(msg.from.id)] then if self.database.userdata[tostring(msg.from.id)] then
nick = self.database.userdata[tostring(msg.from.id)].nickname nick = self.database.userdata[tostring(msg.from.id)].nickname
end end
nick = nick or utilities.build_name(msg.from.first_name, msg.from.last_name) nick = nick or utilities.build_name(msg.from.first_name, msg.from.last_name)
for response, triggers in pairs(config.greetings) do for response, triggers in pairs(config.greetings) do
for _, trigger in pairs(triggers) do for _, trigger in pairs(triggers) do
if string.match(msg.text_lower, trigger) then if string.match(msg.text_lower, trigger) then
utilities.send_message(self, msg.chat.id, response:gsub('#NAME', nick)) utilities.send_message(self, msg.chat.id, response:gsub('#NAME', nick))
return return
end end
end end
end end
end end
return greetings return greetings

View File

@ -8,68 +8,68 @@ local hackernews = {}
hackernews.command = 'hackernews' hackernews.command = 'hackernews'
local function get_hackernews_results() local function get_hackernews_results()
local results = {} local results = {}
local jstr, code = HTTPS.request(hackernews.topstories_url) local jstr, code = HTTPS.request(hackernews.topstories_url)
if code ~= 200 then return end if code ~= 200 then return end
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
for i = 1, 8 do for i = 1, 8 do
local ijstr, icode = HTTPS.request(hackernews.res_url:format(data[i])) local ijstr, icode = HTTPS.request(hackernews.res_url:format(data[i]))
if icode ~= 200 then return end if icode ~= 200 then return end
local idata = JSON.decode(ijstr) local idata = JSON.decode(ijstr)
local result local result
if idata.url then if idata.url then
result = string.format( result = string.format(
'\n• <code>[</code><a href="%s">%s</a><code>]</code> <a href="%s">%s</a>', '\n• <code>[</code><a href="%s">%s</a><code>]</code> <a href="%s">%s</a>',
utilities.html_escape(hackernews.art_url:format(idata.id)), utilities.html_escape(hackernews.art_url:format(idata.id)),
idata.id, idata.id,
utilities.html_escape(idata.url), utilities.html_escape(idata.url),
utilities.html_escape(idata.title) utilities.html_escape(idata.title)
) )
else else
result = string.format( result = string.format(
'\n• <code>[</code><a href="%s">%s</a><code>]</code> %s', '\n• <code>[</code><a href="%s">%s</a><code>]</code> %s',
utilities.html_escape(hackernews.art_url:format(idata.id)), utilities.html_escape(hackernews.art_url:format(idata.id)),
idata.id, idata.id,
utilities.html_escape(idata.title) utilities.html_escape(idata.title)
) )
end end
table.insert(results, result) table.insert(results, result)
end end
return results return results
end end
function hackernews:init(config) function hackernews:init(config)
hackernews.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hackernews', true):t('hn', true).table hackernews.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hackernews', true):t('hn', true).table
hackernews.doc = [[Returns four (if group) or eight (if private message) top stories from Hacker News. hackernews.doc = [[Returns four (if group) or eight (if private message) top stories from Hacker News.
Alias: ]] .. config.cmd_pat .. 'hn' Alias: ]] .. config.cmd_pat .. 'hn'
hackernews.topstories_url = 'https://hacker-news.firebaseio.com/v0/topstories.json' hackernews.topstories_url = 'https://hacker-news.firebaseio.com/v0/topstories.json'
hackernews.res_url = 'https://hacker-news.firebaseio.com/v0/item/%s.json' hackernews.res_url = 'https://hacker-news.firebaseio.com/v0/item/%s.json'
hackernews.art_url = 'https://news.ycombinator.com/item?id=%s' hackernews.art_url = 'https://news.ycombinator.com/item?id=%s'
hackernews.last_update = 0 hackernews.last_update = 0
if config.hackernews_onstart == true then if config.hackernews_onstart == true then
hackernews.results = get_hackernews_results() hackernews.results = get_hackernews_results()
if hackernews.results then hackernews.last_update = os.time() / 60 end if hackernews.results then hackernews.last_update = os.time() / 60 end
end end
end end
function hackernews:action(msg, config) function hackernews:action(msg, config)
local now = os.time() / 60 local now = os.time() / 60
if not hackernews.results or hackernews.last_update + config.hackernews_interval < now then if not hackernews.results or hackernews.last_update + config.hackernews_interval < now then
bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' }) bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' })
hackernews.results = get_hackernews_results() hackernews.results = get_hackernews_results()
if not hackernews.results then if not hackernews.results then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
hackernews.last_update = now hackernews.last_update = now
end end
-- Four results in a group, eight in private. -- Four results in a group, eight in private.
local res_count = msg.chat.id == msg.from.id and 8 or 4 local res_count = msg.chat.id == msg.from.id and 8 or 4
local output = '<b>Top Stories from Hacker News:</b>' local output = '<b>Top Stories from Hacker News:</b>'
for i = 1, res_count do for i = 1, res_count do
output = output .. hackernews.results[i] output = output .. hackernews.results[i]
end end
utilities.send_message(self, msg.chat.id, output, true, nil, 'html') utilities.send_message(self, msg.chat.id, output, true, nil, 'html')
end end
return hackernews return hackernews

View File

@ -8,111 +8,111 @@ local utilities = require('otouto.utilities')
local HTTPS = require('ssl.https') local HTTPS = require('ssl.https')
function hearthstone:init(config) function hearthstone:init(config)
hearthstone.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hearthstone', true):t('hs').table hearthstone.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hearthstone', true):t('hs').table
hearthstone.command = 'hearthstone <query>' hearthstone.command = 'hearthstone <query>'
if not self.database.hearthstone or os.time() > self.database.hearthstone.expiration then if not self.database.hearthstone or os.time() > self.database.hearthstone.expiration then
print('Downloading Hearthstone database...') print('Downloading Hearthstone database...')
local jstr, res = HTTPS.request('https://api.hearthstonejson.com/v1/latest/enUS/cards.json') local jstr, res = HTTPS.request('https://api.hearthstonejson.com/v1/latest/enUS/cards.json')
if not jstr or res ~= 200 then if not jstr or res ~= 200 then
print('Error connecting to hearthstonejson.com.') print('Error connecting to hearthstonejson.com.')
print('hearthstone.lua will not be enabled.') print('hearthstone.lua will not be enabled.')
hearthstone.command = nil hearthstone.command = nil
hearthstone.triggers = nil hearthstone.triggers = nil
return return
end end
self.database.hearthstone = JSON.decode(jstr) self.database.hearthstone = JSON.decode(jstr)
self.database.hearthstone.expiration = os.time() + 600000 self.database.hearthstone.expiration = os.time() + 600000
print('Download complete! It will be stored for a week.') print('Download complete! It will be stored for a week.')
end end
hearthstone.doc = config.cmd_pat .. [[hearthstone <query> hearthstone.doc = config.cmd_pat .. [[hearthstone <query>
Returns Hearthstone card info. Returns Hearthstone card info.
Alias: ]] .. config.cmd_pat .. 'hs' Alias: ]] .. config.cmd_pat .. 'hs'
end end
local function format_card(card) local function format_card(card)
local ctype = card.type local ctype = card.type
if card.race then if card.race then
ctype = card.race ctype = card.race
end end
if card.rarity then if card.rarity then
ctype = card.rarity .. ' ' .. ctype ctype = card.rarity .. ' ' .. ctype
end end
if card.playerClass then if card.playerClass then
ctype = ctype .. ' (' .. card.playerClass .. ')' ctype = ctype .. ' (' .. card.playerClass .. ')'
elseif card.faction then elseif card.faction then
ctype = ctype .. ' (' .. card.faction .. ')' ctype = ctype .. ' (' .. card.faction .. ')'
end end
local stats local stats
if card.cost then if card.cost then
stats = card.cost .. 'c' stats = card.cost .. 'c'
if card.attack then if card.attack then
stats = stats .. ' | ' .. card.attack .. 'a' stats = stats .. ' | ' .. card.attack .. 'a'
end end
if card.health then if card.health then
stats = stats .. ' | ' .. card.health .. 'h' stats = stats .. ' | ' .. card.health .. 'h'
end end
if card.durability then if card.durability then
stats = stats .. ' | ' .. card.durability .. 'd' stats = stats .. ' | ' .. card.durability .. 'd'
end end
elseif card.health then elseif card.health then
stats = card.health .. 'h' stats = card.health .. 'h'
end end
-- unused? -- unused?
local info local info
if card.text then if card.text then
info = card.text:gsub('</?.->',''):gsub('%$','') info = card.text:gsub('</?.->',''):gsub('%$','')
if card.flavor then if card.flavor then
info = info .. '\n_' .. card.flavor .. '_' info = info .. '\n_' .. card.flavor .. '_'
end end
elseif card.flavor then elseif card.flavor then
info = card.flavor info = card.flavor
else else
info = nil info = nil
end end
local s = '*' .. card.name .. '*\n' .. ctype local s = '*' .. card.name .. '*\n' .. ctype
if stats then if stats then
s = s .. '\n' .. stats s = s .. '\n' .. stats
end end
if info then if info then
s = s .. '\n' .. info s = s .. '\n' .. info
end end
return s return s
end end
function hearthstone:action(msg, config) function hearthstone:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, hearthstone.doc, true) utilities.send_reply(self, msg, hearthstone.doc, true)
return return
end end
local output = '' local output = ''
for _,v in pairs(self.database.hearthstone) do for _,v in pairs(self.database.hearthstone) do
if type(v) == 'table' and string.lower(v.name):match(input) then if type(v) == 'table' and string.lower(v.name):match(input) then
output = output .. format_card(v) .. '\n\n' output = output .. format_card(v) .. '\n\n'
end end
end end
output = utilities.trim(output) output = utilities.trim(output)
if output:len() == 0 then if output:len() == 0 then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true)
end end

View File

@ -3,51 +3,51 @@ local utilities = require('otouto.utilities')
local help = {} local help = {}
function help:init(config) function help:init(config)
help.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('help', true):t('h', true).table help.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('help', true):t('h', true).table
help.command = 'help [command]' help.command = 'help [command]'
help.doc = config.cmd_pat .. 'help [command] \nReturns usage information for a given command.' help.doc = config.cmd_pat .. 'help [command] \nReturns usage information for a given command.'
end end
function help:action(msg, config) function help:action(msg, config)
local input = utilities.input(msg.text_lower) local input = utilities.input(msg.text_lower)
if input then if input then
if not help.help_word then if not help.help_word then
for _, plugin in ipairs(self.plugins) do for _, plugin in ipairs(self.plugins) do
if plugin.command and plugin.doc and not plugin.help_word then if plugin.command and plugin.doc and not plugin.help_word then
plugin.help_word = utilities.get_word(plugin.command, 1) plugin.help_word = utilities.get_word(plugin.command, 1)
end end
end end
end end
for _,plugin in ipairs(self.plugins) do for _,plugin in ipairs(self.plugins) do
if plugin.help_word == input:gsub('^/', '') then if plugin.help_word == input:gsub('^/', '') then
local output = '*Help for* _' .. plugin.help_word .. '_*:*\n' .. plugin.doc local output = '*Help for* _' .. plugin.help_word .. '_*:*\n' .. plugin.doc
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
return return
end end
end end
utilities.send_reply(self, msg, 'Sorry, there is no help for that command.') utilities.send_reply(self, msg, 'Sorry, there is no help for that command.')
else else
-- Generate the help message on first run. -- Generate the help message on first run.
if not help.text then if not help.text then
local commandlist = {} local commandlist = {}
for _, plugin in ipairs(self.plugins) do for _, plugin in ipairs(self.plugins) do
if plugin.command then if plugin.command then
table.insert(commandlist, plugin.command) table.insert(commandlist, plugin.command)
end end
end end
table.sort(commandlist) table.sort(commandlist)
help.text = '*Available commands:*\n' .. config.cmd_pat .. table.concat(commandlist, '\n'..config.cmd_pat) .. '\nArguments: <required> [optional]' help.text = '*Available commands:*\n' .. config.cmd_pat .. table.concat(commandlist, '\n'..config.cmd_pat) .. '\nArguments: <required> [optional]'
help.text = help.text:gsub('%[', '\\[') help.text = help.text:gsub('%[', '\\[')
end end
-- Attempt to send the help message via PM. -- Attempt to send the help message via PM.
-- If msg is from a group, tell the group whether the PM was successful. -- If msg is from a group, tell the group whether the PM was successful.
local res = utilities.send_message(self, msg.from.id, help.text, true, nil, true) local res = utilities.send_message(self, msg.from.id, help.text, true, nil, true)
if not res then if not res then
utilities.send_reply(self, msg, 'Please [message me privately](http://telegram.me/' .. self.info.username .. '?start=help) for a list of commands.', true) utilities.send_reply(self, msg, 'Please [message me privately](http://telegram.me/' .. self.info.username .. '?start=help) for a list of commands.', true)
elseif msg.chat.type ~= 'private' then elseif msg.chat.type ~= 'private' then
utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.')
end end
end end
end end
return help return help

View File

@ -3,60 +3,60 @@ local utilities = require('otouto.utilities')
local id = {} local id = {}
function id:init(config) function id:init(config)
id.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('id', true).table id.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('id', true).table
id.command = 'id <user>' id.command = 'id <user>'
id.doc = config.cmd_pat .. [[id <user> ... id.doc = config.cmd_pat .. [[id <user> ...
Returns the name, ID, and username (if applicable) for the given users. Returns the name, ID, and username (if applicable) for the given users.
Arguments must be usernames and/or IDs. Input is also accepted via reply. If no input is given, returns info for the user. Arguments must be usernames and/or IDs. Input is also accepted via reply. If no input is given, returns info for the user.
]] ]]
end end
function id.format(t) function id.format(t)
if t.username then if t.username then
return string.format( return string.format(
'@%s, AKA <b>%s</b> <code>[%s]</code>.\n', '@%s, AKA <b>%s</b> <code>[%s]</code>.\n',
t.username, t.username,
utilities.build_name(t.first_name, t.last_name), utilities.build_name(t.first_name, t.last_name),
t.id t.id
) )
else else
return string.format( return string.format(
'<b>%s</b> <code>[%s]</code>.\n', '<b>%s</b> <code>[%s]</code>.\n',
utilities.build_name(t.first_name, t.last_name), utilities.build_name(t.first_name, t.last_name),
t.id t.id
) )
end end
end end
function id:action(msg) function id:action(msg)
local output local output
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if msg.reply_to_message then if msg.reply_to_message then
output = id.format(msg.reply_to_message.from) output = id.format(msg.reply_to_message.from)
elseif input then elseif input then
output = '' output = ''
for user in input:gmatch('%g+') do for user in input:gmatch('%g+') do
if tonumber(user) then if tonumber(user) then
if self.database.users[user] then if self.database.users[user] then
output = output .. id.format(self.database.users[user]) output = output .. id.format(self.database.users[user])
else else
output = output .. 'I don\'t recognize that ID (' .. user .. ').\n' output = output .. 'I don\'t recognize that ID (' .. user .. ').\n'
end end
elseif user:match('^@') then elseif user:match('^@') then
local t = utilities.resolve_username(self, user) local t = utilities.resolve_username(self, user)
if t then if t then
output = output .. id.format(t) output = output .. id.format(t)
else else
output = output .. 'I don\'t recognize that username (' .. user .. ').\n' output = output .. 'I don\'t recognize that username (' .. user .. ').\n'
end end
else else
output = output .. 'Invalid username or ID (' .. user .. ').\n' output = output .. 'Invalid username or ID (' .. user .. ').\n'
end end
end end
else else
output = id.format(msg.from) output = id.format(msg.from)
end end
utilities.send_reply(self, msg, output, 'html') utilities.send_reply(self, msg, output, 'html')
end end
return id return id

View File

@ -8,39 +8,39 @@ local utilities = require('otouto.utilities')
imdb.command = 'imdb <query>' imdb.command = 'imdb <query>'
function imdb:init(config) function imdb:init(config)
imdb.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('imdb', true).table imdb.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('imdb', true).table
imdb.doc = config.cmd_pat .. 'imdb <query> \nReturns an IMDb entry.' imdb.doc = config.cmd_pat .. 'imdb <query> \nReturns an IMDb entry.'
end end
function imdb:action(msg, config) function imdb:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, imdb.doc, true) utilities.send_reply(self, msg, imdb.doc, true)
return return
end end
local url = 'http://www.omdbapi.com/?t=' .. URL.escape(input) local url = 'http://www.omdbapi.com/?t=' .. URL.escape(input)
local jstr, res = HTTP.request(url) local jstr, res = HTTP.request(url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local jdat = JSON.decode(jstr) local jdat = JSON.decode(jstr)
if jdat.Response ~= 'True' then if jdat.Response ~= 'True' then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local output = '*' .. jdat.Title .. ' ('.. jdat.Year ..')*\n' local output = '*' .. jdat.Title .. ' ('.. jdat.Year ..')*\n'
output = output .. jdat.imdbRating ..'/10 | '.. jdat.Runtime ..' | '.. jdat.Genre ..'\n' output = output .. jdat.imdbRating ..'/10 | '.. jdat.Runtime ..' | '.. jdat.Genre ..'\n'
output = output .. '_' .. jdat.Plot .. '_\n' output = output .. '_' .. jdat.Plot .. '_\n'
output = output .. '[Read more.](http://imdb.com/title/' .. jdat.imdbID .. ')' output = output .. '[Read more.](http://imdb.com/title/' .. jdat.imdbID .. ')'
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end

View File

@ -7,37 +7,37 @@ local utilities = require('otouto.utilities')
local isup = {} local isup = {}
function isup:init(config) function isup:init(config)
isup.triggers = utilities.triggers(self.info.username, config.cmd_pat) isup.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('websitedown', true):t('isitup', true):t('isup', true).table :t('websitedown', true):t('isitup', true):t('isup', true).table
isup.doc = config.cmd_pat .. [[isup <url> isup.doc = config.cmd_pat .. [[isup <url>
Returns the up or down status of a website.]] Returns the up or down status of a website.]]
isup.command = 'isup <url>' isup.command = 'isup <url>'
end end
function isup:action(msg, config) function isup:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, isup.doc) utilities.send_reply(self, msg, isup.doc)
return return
end end
local protocol = HTTP local protocol = HTTP
local url_lower = input:lower() local url_lower = input:lower()
if url_lower:match('^https') then if url_lower:match('^https') then
protocol = HTTPS protocol = HTTPS
elseif not url_lower:match('^http') then elseif not url_lower:match('^http') then
input = 'http://' .. input input = 'http://' .. input
end end
local _, code = protocol.request(input) local _, code = protocol.request(input)
code = tonumber(code) code = tonumber(code)
local output local output
if not code or code > 399 then if not code or code > 399 then
output = 'This website is down or nonexistent.' output = 'This website is down or nonexistent.'
else else
output = 'This website is up.' output = 'This website is up.'
end end
utilities.send_reply(self, msg, output, true) utilities.send_reply(self, msg, output, true)
end end
return isup return isup

View File

@ -9,12 +9,12 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function lastfm:init(config) function lastfm:init(config)
assert(config.lastfm_api_key, assert(config.lastfm_api_key,
'lastfm.lua requires a last.fm API key from http://last.fm/api.' 'lastfm.lua requires a last.fm API key from http://last.fm/api.'
) )
lastfm.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lastfm', true):t('np', true):t('fmset', true).table lastfm.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lastfm', true):t('np', true):t('fmset', true).table
lastfm.doc = config.cmd_pat .. [[np [username] lastfm.doc = config.cmd_pat .. [[np [username]
Returns what you are or were last listening to. If you specify a username, info will be returned for that username. Returns what you are or were last listening to. If you specify a username, info will be returned for that username.
]] .. config.cmd_pat .. [[fmset <username> ]] .. config.cmd_pat .. [[fmset <username>
@ -25,84 +25,84 @@ lastfm.command = 'lastfm'
function lastfm:action(msg, config) function lastfm:action(msg, config)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
local from_id_str = tostring(msg.from.id) local from_id_str = tostring(msg.from.id)
self.database.userdata[from_id_str] = self.database.userdata[from_id_str] or {} self.database.userdata[from_id_str] = self.database.userdata[from_id_str] or {}
if string.match(msg.text, '^'..config.cmd_pat..'lastfm') then if string.match(msg.text, '^'..config.cmd_pat..'lastfm') then
utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true)
return return
elseif string.match(msg.text, '^'..config.cmd_pat..'fmset') then elseif string.match(msg.text, '^'..config.cmd_pat..'fmset') then
if not input then if not input then
utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true)
elseif input == '--' or input == utilities.char.em_dash then elseif input == '--' or input == utilities.char.em_dash then
self.database.userdata[from_id_str].lastfm = nil self.database.userdata[from_id_str].lastfm = nil
utilities.send_reply(self, msg, 'Your last.fm username has been forgotten.') utilities.send_reply(self, msg, 'Your last.fm username has been forgotten.')
else else
self.database.userdata[from_id_str].lastfm = input self.database.userdata[from_id_str].lastfm = input
utilities.send_reply(self, msg, 'Your last.fm username has been set to "' .. input .. '".') utilities.send_reply(self, msg, 'Your last.fm username has been set to "' .. input .. '".')
end end
return return
end end
local url = 'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&api_key=' .. config.lastfm_api_key .. '&user=' local url = 'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&api_key=' .. config.lastfm_api_key .. '&user='
local username local username
local alert = '' local alert = ''
if input then if input then
username = input username = input
elseif self.database.userdata[from_id_str].lastfm then elseif self.database.userdata[from_id_str].lastfm then
username = self.database.userdata[from_id_str].lastfm username = self.database.userdata[from_id_str].lastfm
elseif msg.from.username then elseif msg.from.username then
username = msg.from.username username = msg.from.username
alert = '\n\nYour username has been set to ' .. username .. '.\nTo change it, use '..config.cmd_pat..'fmset <username>.' alert = '\n\nYour username has been set to ' .. username .. '.\nTo change it, use '..config.cmd_pat..'fmset <username>.'
self.database.userdata[from_id_str].lastfm = username self.database.userdata[from_id_str].lastfm = username
else else
utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.') utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.')
return return
end end
url = url .. URL.escape(username) url = url .. URL.escape(username)
local jstr, res local jstr, res
utilities.with_http_timeout( utilities.with_http_timeout(
1, function () 1, function ()
jstr, res = HTTP.request(url) jstr, res = HTTP.request(url)
end) end)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local jdat = JSON.decode(jstr) local jdat = JSON.decode(jstr)
if jdat.error then if jdat.error then
utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.') utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.')
return return
end end
jdat = jdat.recenttracks.track[1] or jdat.recenttracks.track jdat = jdat.recenttracks.track[1] or jdat.recenttracks.track
if not jdat then if not jdat then
utilities.send_reply(self, msg, 'No history for this user.' .. alert) utilities.send_reply(self, msg, 'No history for this user.' .. alert)
return return
end end
local output = input or msg.from.first_name local output = input or msg.from.first_name
output = '🎵 ' .. output output = '🎵 ' .. output
if jdat['@attr'] and jdat['@attr'].nowplaying then if jdat['@attr'] and jdat['@attr'].nowplaying then
output = output .. ' is currently listening to:\n' output = output .. ' is currently listening to:\n'
else else
output = output .. ' last listened to:\n' output = output .. ' last listened to:\n'
end end
local title = jdat.name or 'Unknown' local title = jdat.name or 'Unknown'
local artist = 'Unknown' local artist = 'Unknown'
if jdat.artist then if jdat.artist then
artist = jdat.artist['#text'] artist = jdat.artist['#text']
end end
output = output .. title .. ' - ' .. artist .. alert output = output .. title .. ' - ' .. artist .. alert
utilities.send_message(self, msg.chat.id, output) utilities.send_message(self, msg.chat.id, output)
end end

View File

@ -5,59 +5,59 @@ local URL = require('socket.url')
local JSON, serpent local JSON, serpent
function luarun:init(config) function luarun:init(config)
luarun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lua', true):t('return', true).table luarun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lua', true):t('return', true).table
if config.luarun_serpent then if config.luarun_serpent then
serpent = require('serpent') serpent = require('serpent')
luarun.serialize = function(t) luarun.serialize = function(t)
return serpent.block(t, {comment=false}) return serpent.block(t, {comment=false})
end end
else else
JSON = require('dkjson') JSON = require('dkjson')
luarun.serialize = function(t) luarun.serialize = function(t)
return JSON.encode(t, {indent=true}) return JSON.encode(t, {indent=true})
end end
end end
end end
function luarun:action(msg, config) function luarun:action(msg, config)
if msg.from.id ~= config.admin then if msg.from.id ~= config.admin then
return true return true
end end
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if not input then if not input then
utilities.send_reply(self, msg, 'Please enter a string to load.') utilities.send_reply(self, msg, 'Please enter a string to load.')
return return
end end
if msg.text_lower:match('^'..config.cmd_pat..'return') then if msg.text_lower:match('^'..config.cmd_pat..'return') then
input = 'return ' .. input input = 'return ' .. input
end end
local output = loadstring( [[ local output = loadstring( [[
local bot = require('otouto.bot') local bot = require('otouto.bot')
local bindings = require('otouto.bindings') local bindings = require('otouto.bindings')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local drua = require('otouto.drua-tg') local drua = require('otouto.drua-tg')
local JSON = require('dkjson') local JSON = require('dkjson')
local URL = require('socket.url') local URL = require('socket.url')
local HTTP = require('socket.http') local HTTP = require('socket.http')
local HTTPS = require('ssl.https') local HTTPS = require('ssl.https')
return function (self, msg, config) ]] .. input .. [[ end return function (self, msg, config) ]] .. input .. [[ end
]] )()(self, msg, config) ]] )()(self, msg, config)
if output == nil then if output == nil then
output = 'Done!' output = 'Done!'
else else
if type(output) == 'table' then if type(output) == 'table' then
local s = luarun.serialize(output) local s = luarun.serialize(output)
if URL.escape(s):len() < 4000 then if URL.escape(s):len() < 4000 then
output = s output = s
end end
end end
output = '```\n' .. tostring(output) .. '\n```' output = '```\n' .. tostring(output) .. '\n```'
end end
utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true)
end end

View File

@ -3,65 +3,65 @@ local me = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function me:init(config) function me:init(config)
me.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('me', true).table me.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('me', true).table
me.command = 'me' me.command = 'me'
me.doc = 'Returns userdata stored by the bot.' me.doc = 'Returns userdata stored by the bot.'
end end
function me:action(msg, config) function me:action(msg, config)
local user local user
if msg.from.id == config.admin then if msg.from.id == config.admin then
if msg.reply_to_message then if msg.reply_to_message then
user = msg.reply_to_message.from user = msg.reply_to_message.from
else else
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if input then if input then
if tonumber(input) then if tonumber(input) then
user = self.database.users[input] user = self.database.users[input]
if not user then if not user then
utilities.send_reply(self, msg, 'Unrecognized ID.') utilities.send_reply(self, msg, 'Unrecognized ID.')
return return
end end
elseif input:match('^@') then elseif input:match('^@') then
user = utilities.resolve_username(self, input) user = utilities.resolve_username(self, input)
if not user then if not user then
utilities.send_reply(self, msg, 'Unrecognized username.') utilities.send_reply(self, msg, 'Unrecognized username.')
return return
end end
else else
utilities.send_reply(self, msg, 'Invalid username or ID.') utilities.send_reply(self, msg, 'Invalid username or ID.')
return return
end end
end end
end end
end end
user = user or msg.from user = user or msg.from
local userdata = self.database.userdata[tostring(user.id)] or {} local userdata = self.database.userdata[tostring(user.id)] or {}
local data = {} local data = {}
for k,v in pairs(userdata) do for k,v in pairs(userdata) do
table.insert(data, string.format( table.insert(data, string.format(
'<b>%s</b> <code>%s</code>\n', '<b>%s</b> <code>%s</code>\n',
utilities.html_escape(k), utilities.html_escape(k),
utilities.html_escape(v) utilities.html_escape(v)
)) ))
end end
local output local output
if #data == 0 then if #data == 0 then
output = 'There is no data stored for this user.' output = 'There is no data stored for this user.'
else else
output = string.format( output = string.format(
'<b>%s</b> <code>[%s]</code><b>:</b>\n', '<b>%s</b> <code>[%s]</code><b>:</b>\n',
utilities.html_escape(utilities.build_name( utilities.html_escape(utilities.build_name(
user.first_name, user.first_name,
user.last_name user.last_name
)), )),
user.id user.id
) .. table.concat(data) ) .. table.concat(data)
end end
utilities.send_message(self, msg.chat.id, output, true, nil, 'html') utilities.send_message(self, msg.chat.id, output, true, nil, 'html')
end end

View File

@ -5,45 +5,45 @@ local utilities = require('otouto.utilities')
nick.command = 'nick <nickname>' nick.command = 'nick <nickname>'
function nick:init(config) function nick:init(config)
nick.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('nick', true).table nick.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('nick', true).table
nick.doc = config.cmd_pat .. [[nick <nickname> nick.doc = config.cmd_pat .. [[nick <nickname>
Set your nickname. Use "]] .. config.cmd_pat .. 'nick --" to delete it.' Set your nickname. Use "]] .. config.cmd_pat .. 'nick --" to delete it.'
end end
function nick:action(msg, config) function nick:action(msg, config)
local id_str, name local id_str, name
if msg.from.id == config.admin and msg.reply_to_message then if msg.from.id == config.admin and msg.reply_to_message then
id_str = tostring(msg.reply_to_message.from.id) id_str = tostring(msg.reply_to_message.from.id)
name = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name) name = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name)
else else
id_str = tostring(msg.from.id) id_str = tostring(msg.from.id)
name = utilities.build_name(msg.from.first_name, msg.from.last_name) name = utilities.build_name(msg.from.first_name, msg.from.last_name)
end end
self.database.userdata[id_str] = self.database.userdata[id_str] or {} self.database.userdata[id_str] = self.database.userdata[id_str] or {}
local output local output
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if not input then if not input then
if self.database.userdata[id_str].nickname then if self.database.userdata[id_str].nickname then
output = name .. '\'s nickname is "' .. self.database.userdata[id_str].nickname .. '".' output = name .. '\'s nickname is "' .. self.database.userdata[id_str].nickname .. '".'
else else
output = name .. ' currently has no nickname.' output = name .. ' currently has no nickname.'
end end
elseif utilities.utf8_len(input) > 32 then elseif utilities.utf8_len(input) > 32 then
output = 'The character limit for nicknames is 32.' output = 'The character limit for nicknames is 32.'
elseif input == '--' or input == utilities.char.em_dash then elseif input == '--' or input == utilities.char.em_dash then
self.database.userdata[id_str].nickname = nil self.database.userdata[id_str].nickname = nil
output = name .. '\'s nickname has been deleted.' output = name .. '\'s nickname has been deleted.'
else else
input = input:gsub('\n', ' ') input = input:gsub('\n', ' ')
self.database.userdata[id_str].nickname = input self.database.userdata[id_str].nickname = input
output = name .. '\'s nickname has been set to "' .. input .. '".' output = name .. '\'s nickname has been set to "' .. input .. '".'
end end
utilities.send_reply(self, msg, output) utilities.send_reply(self, msg, output)
end end

View File

@ -8,34 +8,34 @@ patterns.doc = [[
s/<pattern>/<substitution> s/<pattern>/<substitution>
Replace all matches for the given pattern. Replace all matches for the given pattern.
Uses Lua patterns. Uses Lua patterns.
]] ]]
function patterns:init(config) function patterns:init(config)
patterns.triggers = { config.cmd_pat .. '?s/.-/.-$' } patterns.triggers = { config.cmd_pat .. '?s/.-/.-$' }
end end
function patterns:action(msg) function patterns:action(msg)
if not msg.reply_to_message then return true end if not msg.reply_to_message then return true end
local output = msg.reply_to_message.text local output = msg.reply_to_message.text
if msg.reply_to_message.from.id == self.info.id then if msg.reply_to_message.from.id == self.info.id then
output = output:gsub('Did you mean:\n"', '') output = output:gsub('Did you mean:\n"', '')
output = output:gsub('"$', '') output = output:gsub('"$', '')
end end
local m1, m2 = msg.text:match('^/?s/(.-)/(.-)/?$') local m1, m2 = msg.text:match('^/?s/(.-)/(.-)/?$')
if not m2 then return true end if not m2 then return true end
local res local res
res, output = pcall( res, output = pcall(
function() function()
return output:gsub(m1, m2) return output:gsub(m1, m2)
end end
) )
if res == false then if res == false then
utilities.send_reply(self, msg, 'Malformed pattern!') utilities.send_reply(self, msg, 'Malformed pattern!')
else else
output = utilities.trim(output:sub(1, 4000)) output = utilities.trim(output:sub(1, 4000))
output = utilities.style.enquote('Did you mean', output) output = utilities.style.enquote('Did you mean', output)
utilities.send_reply(self, msg.reply_to_message, output, true) utilities.send_reply(self, msg.reply_to_message, output, true)
end end
end end
return patterns return patterns

View File

@ -5,12 +5,12 @@ local ping = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function ping:init(config) function ping:init(config)
ping.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ping'):t('annyong').table ping.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ping'):t('annyong').table
end end
function ping:action(msg, config) function ping:action(msg, config)
local output = msg.text_lower:match('^'..config.cmd_pat..'ping') and 'Pong!' or 'Annyong.' local output = msg.text_lower:match('^'..config.cmd_pat..'ping') and 'Pong!' or 'Annyong.'
utilities.send_message(self, msg.chat.id, output) utilities.send_message(self, msg.chat.id, output)
end end
return ping return ping

View File

@ -8,8 +8,8 @@ local utilities = require('otouto.utilities')
pokedex.command = 'pokedex <query>' pokedex.command = 'pokedex <query>'
function pokedex:init(config) function pokedex:init(config)
pokedex.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pokedex', true):t('dex', true).table pokedex.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pokedex', true):t('dex', true).table
pokedex.doc = config.cmd_pat .. [[pokedex <query> pokedex.doc = config.cmd_pat .. [[pokedex <query>
Returns a Pokedex entry from pokeapi.co. Returns a Pokedex entry from pokeapi.co.
Queries must be a number of the name of a Pokémon. Queries must be a number of the name of a Pokémon.
Alias: ]] .. config.cmd_pat .. 'dex' Alias: ]] .. config.cmd_pat .. 'dex'
@ -17,54 +17,54 @@ end
function pokedex:action(msg, config) function pokedex:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, pokedex.doc, true) utilities.send_reply(self, msg, pokedex.doc, true)
return return
end end
bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } )
local url = 'http://pokeapi.co' local url = 'http://pokeapi.co'
local dex_url = url .. '/api/v1/pokemon/' .. input local dex_url = url .. '/api/v1/pokemon/' .. input
local dex_jstr, res = HTTP.request(dex_url) local dex_jstr, res = HTTP.request(dex_url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local dex_jdat = JSON.decode(dex_jstr) local dex_jdat = JSON.decode(dex_jstr)
if not dex_jdat.descriptions or not dex_jdat.descriptions[1] then if not dex_jdat.descriptions or not dex_jdat.descriptions[1] then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local desc_url = url .. dex_jdat.descriptions[math.random(#dex_jdat.descriptions)].resource_uri local desc_url = url .. dex_jdat.descriptions[math.random(#dex_jdat.descriptions)].resource_uri
local desc_jstr, _ = HTTP.request(desc_url) local desc_jstr, _ = HTTP.request(desc_url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local desc_jdat = JSON.decode(desc_jstr) local desc_jdat = JSON.decode(desc_jstr)
local poke_type local poke_type
for _,v in ipairs(dex_jdat.types) do for _,v in ipairs(dex_jdat.types) do
local type_name = v.name:gsub("^%l", string.upper) local type_name = v.name:gsub("^%l", string.upper)
if not poke_type then if not poke_type then
poke_type = type_name poke_type = type_name
else else
poke_type = poke_type .. ' / ' .. type_name poke_type = poke_type .. ' / ' .. type_name
end end
end end
poke_type = poke_type .. ' type' poke_type = poke_type .. ' type'
local output = '*' .. dex_jdat.name .. '*\n#' .. dex_jdat.national_id .. ' | ' .. poke_type .. '\n_' .. desc_jdat.description:gsub('POKMON', 'Pokémon'):gsub('Pokmon', 'Pokémon') .. '_' local output = '*' .. dex_jdat.name .. '*\n#' .. dex_jdat.national_id .. ' | ' .. poke_type .. '\n_' .. desc_jdat.description:gsub('POKMON', 'Pokémon'):gsub('Pokmon', 'Pokémon') .. '_'
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end

View File

@ -3,110 +3,110 @@ local utilities = require('otouto.utilities')
local pgc = {} local pgc = {}
function pgc:init(config) function pgc:init(config)
pgc.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('gocalc', true).table pgc.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('gocalc', true).table
pgc.doc = config.cmd_pat .. [[gocalc <required candy> <number of Pokémon> <number of candy> pgc.doc = config.cmd_pat .. [[gocalc <required candy> <number of Pokémon> <number of candy>
Calculates the number of Pokémon that must be transferred before evolving, how many evolutions the user is able to perform, and how many Pokémon and candy will be left over. Calculates the number of Pokémon that must be transferred before evolving, how many evolutions the user is able to perform, and how many Pokémon and candy will be left over.
All arguments must be positive numbers. Batch jobs may be performed by separating valid sets of arguments by lines. All arguments must be positive numbers. Batch jobs may be performed by separating valid sets of arguments by lines.
Example (forty pidgeys and three hundred pidgey candies): Example (forty pidgeys and three hundred pidgey candies):
]] .. config.cmd_pat .. 'gocalc 12 40 300' ]] .. config.cmd_pat .. 'gocalc 12 40 300'
pgc.command = 'gocalc <required candy> <#pokemon> <#candy>' pgc.command = 'gocalc <required candy> <#pokemon> <#candy>'
end end
-- This function written by Juan Potato. MIT-licensed. -- This function written by Juan Potato. MIT-licensed.
local pidgey_calc = function(candies_to_evolve, mons, candies) local pidgey_calc = function(candies_to_evolve, mons, candies)
local transferred = 0; local transferred = 0;
local evolved = 0; local evolved = 0;
while true do while true do
if math.floor(candies / candies_to_evolve) == 0 or mons == 0 then if math.floor(candies / candies_to_evolve) == 0 or mons == 0 then
break break
else else
mons = mons - 1 mons = mons - 1
candies = candies - candies_to_evolve + 1 candies = candies - candies_to_evolve + 1
evolved = evolved + 1 evolved = evolved + 1
if mons == 0 then if mons == 0 then
break break
end end
end end
end end
while true do while true do
if (candies + mons) < (candies_to_evolve + 1) or mons == 0 then if (candies + mons) < (candies_to_evolve + 1) or mons == 0 then
break break
end end
while candies < candies_to_evolve do while candies < candies_to_evolve do
transferred = transferred + 1 transferred = transferred + 1
mons = mons - 1 mons = mons - 1
candies = candies + 1 candies = candies + 1
end end
mons = mons - 1 mons = mons - 1
candies = candies - candies_to_evolve + 1 candies = candies - candies_to_evolve + 1
evolved = evolved + 1 evolved = evolved + 1
end end
return { return {
transfer = transferred, transfer = transferred,
evolve = evolved, evolve = evolved,
leftover_mons = mons, leftover_mons = mons,
leftover_candy = candies leftover_candy = candies
} }
end end
local single_job = function(input) local single_job = function(input)
local req_candy, mons, candies = input:match('^(%d+) (%d+) (%d+)$') local req_candy, mons, candies = input:match('^(%d+) (%d+) (%d+)$')
req_candy = tonumber(req_candy) req_candy = tonumber(req_candy)
mons = tonumber(mons) mons = tonumber(mons)
candies = tonumber(candies) candies = tonumber(candies)
if not (req_candy and mons and candies) then if not (req_candy and mons and candies) then
return { err = 'Invalid input: Three numbers expected.' } return { err = 'Invalid input: Three numbers expected.' }
elseif req_candy > 400 then elseif req_candy > 400 then
return { err = 'Invalid required candy: Maximum is 400.' } return { err = 'Invalid required candy: Maximum is 400.' }
elseif mons > 1000 then elseif mons > 1000 then
return { err = 'Invalid number of Pokémon: Maximum is 1000.' } return { err = 'Invalid number of Pokémon: Maximum is 1000.' }
elseif candies > 10000 then elseif candies > 10000 then
return { err = 'Invalid number of candies: Maximum is 10000.' } return { err = 'Invalid number of candies: Maximum is 10000.' }
else else
return pidgey_calc(req_candy, mons, candies) return pidgey_calc(req_candy, mons, candies)
end end
end end
function pgc:action(msg) function pgc:action(msg)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if not input then if not input then
utilities.send_reply(self, msg, pgc.doc, true) utilities.send_reply(self, msg, pgc.doc, true)
return return
end end
input = input .. '\n' input = input .. '\n'
local output = '' local output = ''
local total_evolutions = 0 local total_evolutions = 0
for line in input:gmatch('(.-)\n') do for line in input:gmatch('(.-)\n') do
local info = single_job(line) local info = single_job(line)
output = output .. '`' .. line .. '`\n' output = output .. '`' .. line .. '`\n'
if info.err then if info.err then
output = output .. info.err .. '\n\n' output = output .. info.err .. '\n\n'
else else
total_evolutions = total_evolutions + info.evolve total_evolutions = total_evolutions + info.evolve
local s = '*Transfer:* %s. \n*Evolve:* %s (%s XP, %s minutes). \n*Leftover:* %s mons, %s candy.\n\n' local s = '*Transfer:* %s. \n*Evolve:* %s (%s XP, %s minutes). \n*Leftover:* %s mons, %s candy.\n\n'
s = s:format(info.transfer, info.evolve, info.evolve..'k', info.evolve*0.5, info.leftover_mons, info.leftover_candy) s = s:format(info.transfer, info.evolve, info.evolve..'k', info.evolve*0.5, info.leftover_mons, info.leftover_candy)
output = output .. s output = output .. s
end end
end end
local s = '*Total evolutions:* %s. \n*Recommendation:* %s' local s = '*Total evolutions:* %s. \n*Recommendation:* %s'
local recommendation local recommendation
local egg_count = math.floor(total_evolutions/60) local egg_count = math.floor(total_evolutions/60)
if egg_count < 1 then if egg_count < 1 then
recommendation = 'Wait until you have atleast sixty Pokémon to evolve before using a lucky egg.' recommendation = 'Wait until you have atleast sixty Pokémon to evolve before using a lucky egg.'
else else
recommendation = string.format( recommendation = string.format(
'Use %s lucky egg%s for %s evolutions.', 'Use %s lucky egg%s for %s evolutions.',
egg_count, egg_count,
egg_count == 1 and '' or 's', egg_count == 1 and '' or 's',
egg_count * 60 egg_count * 60
) )
end end
s = s:format(total_evolutions, recommendation) s = s:format(total_evolutions, recommendation)
output = output .. s output = output .. s
utilities.send_reply(self, msg, output, true) utilities.send_reply(self, msg, output, true)
end end
return pgc return pgc

View File

@ -21,7 +21,7 @@ Set your Pokémon Go team for statistical purposes. The team must be valid, and
db.membership = {} db.membership = {}
end end
for _, set in pairs(db.membership) do for _, set in pairs(db.membership) do
setmetatable(set, utilities.set_meta) setmetatable(set, utilities.set_meta)
end end
end end

View File

@ -6,37 +6,37 @@ local utilities = require('otouto.utilities')
preview.command = 'preview <link>' preview.command = 'preview <link>'
function preview:init(config) function preview:init(config)
preview.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('preview', true).table preview.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('preview', true).table
preview.doc = config.cmd_pat .. 'preview <link> \nReturns a full-message, "unlinked" preview.' preview.doc = config.cmd_pat .. 'preview <link> \nReturns a full-message, "unlinked" preview.'
end end
function preview:action(msg) function preview:action(msg)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, preview.doc, true) utilities.send_reply(self, msg, preview.doc, true)
return return
end end
input = utilities.get_word(input, 1) input = utilities.get_word(input, 1)
if not input:match('^https?://.+') then if not input:match('^https?://.+') then
input = 'http://' .. input input = 'http://' .. input
end end
local res = HTTP.request(input) local res = HTTP.request(input)
if not res then if not res then
utilities.send_reply(self, msg, 'Please provide a valid link.') utilities.send_reply(self, msg, 'Please provide a valid link.')
return return
end end
if res:len() == 0 then if res:len() == 0 then
utilities.send_reply(self, msg, 'Sorry, the link you provided is not letting us make a preview.') utilities.send_reply(self, msg, 'Sorry, the link you provided is not letting us make a preview.')
return return
end end
-- Invisible zero-width, non-joiner. -- Invisible zero-width, non-joiner.
local output = '<a href="' .. input .. '">' .. utilities.char.zwnj .. '</a>' local output = '<a href="' .. input .. '">' .. utilities.char.zwnj .. '</a>'
utilities.send_message(self, msg.chat.id, output, false, nil, 'html') utilities.send_message(self, msg.chat.id, output, false, nil, 'html')
end end

View File

@ -6,138 +6,138 @@ pun.command = 'pun'
pun.doc = 'Returns a pun.' pun.doc = 'Returns a pun.'
function pun:init(config) function pun:init(config)
pun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pun').table pun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pun').table
end end
local puns = { local puns = {
"The person who invented the door-knock won the No-bell prize.", "The person who invented the door-knock won the No-bell prize.",
"I couldn't work out how to fasten my seatbelt. Then it clicked.", "I couldn't work out how to fasten my seatbelt. Then it clicked.",
"Never trust atoms; they make up everything.", "Never trust atoms; they make up everything.",
"Singing in the shower is all fun and games until you get shampoo in your mouth - Then it becomes a soap opera.", "Singing in the shower is all fun and games until you get shampoo in your mouth - Then it becomes a soap opera.",
"I can't believe I got fired from the calendar factory. All I did was take a day off.", "I can't believe I got fired from the calendar factory. All I did was take a day off.",
"To the guy who invented zero: Thanks for nothing!", "To the guy who invented zero: Thanks for nothing!",
"Enough with the cripple jokes! I just can't stand them.", "Enough with the cripple jokes! I just can't stand them.",
"I've accidentally swallowed some Scrabble tiles. My next crap could spell disaster.", "I've accidentally swallowed some Scrabble tiles. My next crap could spell disaster.",
"How does Moses make his tea? Hebrews it.", "How does Moses make his tea? Hebrews it.",
"Did you hear about the guy who got hit in the head with a can of soda? He was lucky it was a soft drink.", "Did you hear about the guy who got hit in the head with a can of soda? He was lucky it was a soft drink.",
"When William joined the army he disliked the phrase 'fire at will'.", "When William joined the army he disliked the phrase 'fire at will'.",
"There was a sign on the lawn at a rehab center that said 'Keep off the Grass'.", "There was a sign on the lawn at a rehab center that said 'Keep off the Grass'.",
"I wondered why the baseball was getting bigger. Then it hit me.", "I wondered why the baseball was getting bigger. Then it hit me.",
"I can hear music coming out of my printer. I think the paper's jamming again.", "I can hear music coming out of my printer. I think the paper's jamming again.",
"I have a few jokes about unemployed people, but none of them work", "I have a few jokes about unemployed people, but none of them work",
"Want to hear a construction joke? I'm working on it", "Want to hear a construction joke? I'm working on it",
"I always take a second pair of pants when I go golfing, in case I get a hole in one.", "I always take a second pair of pants when I go golfing, in case I get a hole in one.",
"I couldn't remember how to throw a boomerang, but then it came back to me.", "I couldn't remember how to throw a boomerang, but then it came back to me.",
"I've decided that my wifi will be my valentine. IDK, we just have this connection.", "I've decided that my wifi will be my valentine. IDK, we just have this connection.",
"A prisoner's favorite punctuation mark is the period. It marks the end of his sentence.", "A prisoner's favorite punctuation mark is the period. It marks the end of his sentence.",
"I used to go fishing with Skrillex, but he kept dropping the bass.", "I used to go fishing with Skrillex, but he kept dropping the bass.",
"Two antennae met on a roof and got married. The wedding was okay, but the reception was incredible.", "Two antennae met on a roof and got married. The wedding was okay, but the reception was incredible.",
"A book just fell on my head. I've only got my shelf to blame.", "A book just fell on my head. I've only got my shelf to blame.",
"I dropped my steak on the floor. Now it's ground beef.", "I dropped my steak on the floor. Now it's ground beef.",
"I used to have a fear of hurdles, but I got over it.", "I used to have a fear of hurdles, but I got over it.",
"The outcome of war does not prove who is right, but only who is left.", "The outcome of war does not prove who is right, but only who is left.",
"Darth Vader tries not to burn his food, but it always comes out a little on the dark side.", "Darth Vader tries not to burn his food, but it always comes out a little on the dark side.",
"The store keeps calling me to buy more furniture, but all I wanted was a one night stand.", "The store keeps calling me to buy more furniture, but all I wanted was a one night stand.",
"This girl said she recognized me from the vegetarian club, but I'd never met herbivore.", "This girl said she recognized me from the vegetarian club, but I'd never met herbivore.",
"Police arrested two kids yesterday, one was drinking battery acid, the other was eating fireworks. They charged one and let the other one off...", "Police arrested two kids yesterday, one was drinking battery acid, the other was eating fireworks. They charged one and let the other one off...",
"No more Harry Potter jokes guys. I'm Sirius.", "No more Harry Potter jokes guys. I'm Sirius.",
"It was hard getting over my addiction to hokey pokey, but I've turned myself around.", "It was hard getting over my addiction to hokey pokey, but I've turned myself around.",
"It takes a lot of balls to golf the way I do.", "It takes a lot of balls to golf the way I do.",
"Why did everyone want to hang out with the mushroom? Because he was a fungi.", "Why did everyone want to hang out with the mushroom? Because he was a fungi.",
"How much does a hipster weigh? An instagram.", "How much does a hipster weigh? An instagram.",
"I used to be addicted to soap, but I'm clean now.", "I used to be addicted to soap, but I'm clean now.",
"When life gives you melons, youre probably dyslexic.", "When life gives you melons, youre probably dyslexic.",
"What's with all the blind jokes? I just don't see the point.", "What's with all the blind jokes? I just don't see the point.",
"If Apple made a car, would it have Windows?", "If Apple made a car, would it have Windows?",
"Need an ark? I Noah guy.", "Need an ark? I Noah guy.",
"The scarecrow won an award because he was outstanding in his field.", "The scarecrow won an award because he was outstanding in his field.",
"What's the difference between a man in a tux on a bicycle, and a man in a sweatsuit on a trycicle? A tire.", "What's the difference between a man in a tux on a bicycle, and a man in a sweatsuit on a trycicle? A tire.",
"What do you do with a sick chemist? If you can't helium, and you can't curium, you'll just have to barium.", "What do you do with a sick chemist? If you can't helium, and you can't curium, you'll just have to barium.",
"I'm reading a book about anti-gravity. It's impossible to put down.", "I'm reading a book about anti-gravity. It's impossible to put down.",
"Trying to write with a broken pencil is pointless.", "Trying to write with a broken pencil is pointless.",
"When TVs go on vacation, they travel to remote islands.", "When TVs go on vacation, they travel to remote islands.",
"I was going to tell a midget joke, but it's too short.", "I was going to tell a midget joke, but it's too short.",
"Jokes about German sausage are the wurst.", "Jokes about German sausage are the wurst.",
"How do you organize a space party? You planet.", "How do you organize a space party? You planet.",
"Sleeping comes so naturally to me, I could do it with my eyes closed.", "Sleeping comes so naturally to me, I could do it with my eyes closed.",
"I'm glad I know sign language; it's pretty handy.", "I'm glad I know sign language; it's pretty handy.",
"Atheism is a non-prophet organization.", "Atheism is a non-prophet organization.",
"Velcro: What a rip-off!", "Velcro: What a rip-off!",
"If they made a Minecraft movie, it would be a blockbuster.", "If they made a Minecraft movie, it would be a blockbuster.",
"I don't trust people with graph paper. They're always plotting something", "I don't trust people with graph paper. They're always plotting something",
"I had a friend who was addicted to brake fluid. He says he can stop anytime.", "I had a friend who was addicted to brake fluid. He says he can stop anytime.",
"The form said I had Type A blood, but it was a Type O.", "The form said I had Type A blood, but it was a Type O.",
"I went to to the shop to buy eight Sprites - I came home and realised I'd picked 7Up.", "I went to to the shop to buy eight Sprites - I came home and realised I'd picked 7Up.",
"There was an explosion at a pie factory. 3.14 people died.", "There was an explosion at a pie factory. 3.14 people died.",
"A man drove his car into a tree and found out how a Mercedes bends.", "A man drove his car into a tree and found out how a Mercedes bends.",
"The experienced carpenter really nailed it, but the new guy screwed everything up.", "The experienced carpenter really nailed it, but the new guy screwed everything up.",
"I didn't like my beard at first, but then it grew on me.", "I didn't like my beard at first, but then it grew on me.",
"Smaller babies may be delivered by stork, but the heavier ones need a crane.", "Smaller babies may be delivered by stork, but the heavier ones need a crane.",
"What's the definition of a will? It's a dead giveaway.", "What's the definition of a will? It's a dead giveaway.",
"I was going to look for my missing watch, but I could never find the time.", "I was going to look for my missing watch, but I could never find the time.",
"I hate elevators, and I often take steps to avoid them.", "I hate elevators, and I often take steps to avoid them.",
"Did you hear about the guy whose whole left side was cut off? He's all right now.", "Did you hear about the guy whose whole left side was cut off? He's all right now.",
"It's not that the man did not know how to juggle, he just didn't have the balls to do it.", "It's not that the man did not know how to juggle, he just didn't have the balls to do it.",
"I used to be a loan shark, but I lost interest", "I used to be a loan shark, but I lost interest",
"I don't trust these stairs; they're always up to something.", "I don't trust these stairs; they're always up to something.",
"My friend's bakery burned down last night. Now his business is toast.", "My friend's bakery burned down last night. Now his business is toast.",
"Don't trust people that do acupuncture; they're back stabbers.", "Don't trust people that do acupuncture; they're back stabbers.",
"The man who survived mustard gas and pepper spray is now a seasoned veteran.", "The man who survived mustard gas and pepper spray is now a seasoned veteran.",
"Police were called to a daycare where a three-year-old was resisting a rest.", "Police were called to a daycare where a three-year-old was resisting a rest.",
"When Peter Pan punches, they Neverland", "When Peter Pan punches, they Neverland",
"The shoemaker did not deny his apprentice anything he needed. He gave him his awl.", "The shoemaker did not deny his apprentice anything he needed. He gave him his awl.",
"I did a theatrical performance about puns. It was a play on words.", "I did a theatrical performance about puns. It was a play on words.",
"Show me a piano falling down a mineshaft and I'll show you A-flat minor.", "Show me a piano falling down a mineshaft and I'll show you A-flat minor.",
"Have you ever tried to eat a clock? It's very time consuming.", "Have you ever tried to eat a clock? It's very time consuming.",
"There was once a cross-eyed teacher who couldn't control his pupils.", "There was once a cross-eyed teacher who couldn't control his pupils.",
"A new type of broom came out and it is sweeping the nation.", "A new type of broom came out and it is sweeping the nation.",
"I relish the fact that you've mustard the strength to ketchup to me.", "I relish the fact that you've mustard the strength to ketchup to me.",
"I knew a woman who owned a taser. Man, was she stunning!", "I knew a woman who owned a taser. Man, was she stunning!",
"What did the grape say when it got stepped on? Nothing - but it let out a little whine.", "What did the grape say when it got stepped on? Nothing - but it let out a little whine.",
"It was an emotional wedding. Even the cake was in tiers.", "It was an emotional wedding. Even the cake was in tiers.",
"When a clock is hungry it goes back four seconds.", "When a clock is hungry it goes back four seconds.",
"The dead batteries were given out free of charge.", "The dead batteries were given out free of charge.",
"Why are there no knock-knock jokes about America? Because freedom rings.", "Why are there no knock-knock jokes about America? Because freedom rings.",
"When the cannibal showed up late to dinner, they gave him the cold shoulder.", "When the cannibal showed up late to dinner, they gave him the cold shoulder.",
"I should have been sad when my flashlight died, but I was delighted.", "I should have been sad when my flashlight died, but I was delighted.",
"Why don't tennis players ever get married? Love means nothing to them.", "Why don't tennis players ever get married? Love means nothing to them.",
"Pterodactyls can't be heard going to the bathroom because the P is silent.", "Pterodactyls can't be heard going to the bathroom because the P is silent.",
"Mermaids make calls on their shell phones.", "Mermaids make calls on their shell phones.",
"What do you call an aardvark with three feet? A yaardvark.", "What do you call an aardvark with three feet? A yaardvark.",
"Captain Kirk has three ears: A right ear, a left ear, and a final front ear.", "Captain Kirk has three ears: A right ear, a left ear, and a final front ear.",
"How do celebrities stay cool? They have a lot of fans.", "How do celebrities stay cool? They have a lot of fans.",
"Without geometry, life is pointless.", "Without geometry, life is pointless.",
"Did you hear about the cow who tried to jump over a barbed-wire fence? It ended in udder destruction.", "Did you hear about the cow who tried to jump over a barbed-wire fence? It ended in udder destruction.",
"The truth may ring like a bell, but it is seldom ever tolled.", "The truth may ring like a bell, but it is seldom ever tolled.",
"I used to work for the IRS, but my job was too taxing.", "I used to work for the IRS, but my job was too taxing.",
"I used to be a programmer, but then I lost my drive.", "I used to be a programmer, but then I lost my drive.",
"Pediatricians are doctors with little patients.", "Pediatricians are doctors with little patients.",
"I finally fired my masseuse today. She always rubbed me the wrong way.", "I finally fired my masseuse today. She always rubbed me the wrong way.",
"I stayed up all night wondering where the sun went. Then it dawned on me.", "I stayed up all night wondering where the sun went. Then it dawned on me.",
"What's the difference between a man and his dog? The man wears a suit; the dog just pants.", "What's the difference between a man and his dog? The man wears a suit; the dog just pants.",
"A psychic midget who escapes from prison is a small medium at large.", "A psychic midget who escapes from prison is a small medium at large.",
"I've been to the dentist several times, so I know the drill.", "I've been to the dentist several times, so I know the drill.",
"The roundest knight at King Arthur's round table was Sir Cumference. He acquired his size from too much pi.", "The roundest knight at King Arthur's round table was Sir Cumference. He acquired his size from too much pi.",
"She was only a whiskey maker, but he loved her still.", "She was only a whiskey maker, but he loved her still.",
"Male deer have buck teeth.", "Male deer have buck teeth.",
"Whiteboards are remarkable.", "Whiteboards are remarkable.",
"Visitors in Cuba are always Havana good time.", "Visitors in Cuba are always Havana good time.",
"Why does electricity shock people? It doesn't know how to conduct itself.", "Why does electricity shock people? It doesn't know how to conduct itself.",
"Lancelot had a scary dream about his horse. It was a knight mare.", "Lancelot had a scary dream about his horse. It was a knight mare.",
"A tribe of cannibals captured a missionary and ate him. Afterward, they all had violent food poisoning. This just goes to show that you can't keep a good man down.", "A tribe of cannibals captured a missionary and ate him. Afterward, they all had violent food poisoning. This just goes to show that you can't keep a good man down.",
"Heaven for gamblers is a paradise.", "Heaven for gamblers is a paradise.",
"Old wheels aren't thrown away, they're just retired.", "Old wheels aren't thrown away, they're just retired.",
"Horses are very stable animals.", "Horses are very stable animals.",
"Banks don't crash, they just lose their balance.", "Banks don't crash, they just lose their balance.",
"The career of a skier can go downhill very fast.", "The career of a skier can go downhill very fast.",
"In democracy, it's your vote that counts. In feudalism, it's your count that votes.", "In democracy, it's your vote that counts. In feudalism, it's your count that votes.",
"A sea lion is nothing but an ionized seal.", "A sea lion is nothing but an ionized seal.",
"The vegetables from my garden aren't that great. I guess you could say they're mediokra." "The vegetables from my garden aren't that great. I guess you could say they're mediokra."
} }
function pun:action(msg) function pun:action(msg)
utilities.send_reply(self, msg, puns[math.random(#puns)]) utilities.send_reply(self, msg, puns[math.random(#puns)])
end end

View File

@ -16,34 +16,34 @@ reactions.doc = 'Returns a list of "reaction" emoticon commands.'
local help local help
function reactions:init(config) function reactions:init(config)
-- Generate a "help" message triggered by "/reactions". -- Generate a "help" message triggered by "/reactions".
help = 'Reactions:\n' help = 'Reactions:\n'
reactions.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('reactions').table reactions.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('reactions').table
local username = self.info.username:lower() local username = self.info.username:lower()
for trigger,reaction in pairs(config.reactions) do for trigger,reaction in pairs(config.reactions) do
help = help .. '' .. config.cmd_pat .. trigger .. ': ' .. reaction .. '\n' help = help .. '' .. config.cmd_pat .. trigger .. ': ' .. reaction .. '\n'
table.insert(reactions.triggers, '^'..config.cmd_pat..trigger) table.insert(reactions.triggers, '^'..config.cmd_pat..trigger)
table.insert(reactions.triggers, '^'..config.cmd_pat..trigger..'@'..username) table.insert(reactions.triggers, '^'..config.cmd_pat..trigger..'@'..username)
table.insert(reactions.triggers, config.cmd_pat..trigger..'$') table.insert(reactions.triggers, config.cmd_pat..trigger..'$')
table.insert(reactions.triggers, config.cmd_pat..trigger..'@'..username..'$') table.insert(reactions.triggers, config.cmd_pat..trigger..'@'..username..'$')
table.insert(reactions.triggers, '\n'..config.cmd_pat..trigger) table.insert(reactions.triggers, '\n'..config.cmd_pat..trigger)
table.insert(reactions.triggers, '\n'..config.cmd_pat..trigger..'@'..username) table.insert(reactions.triggers, '\n'..config.cmd_pat..trigger..'@'..username)
table.insert(reactions.triggers, config.cmd_pat..trigger..'\n') table.insert(reactions.triggers, config.cmd_pat..trigger..'\n')
table.insert(reactions.triggers, config.cmd_pat..trigger..'@'..username..'\n') table.insert(reactions.triggers, config.cmd_pat..trigger..'@'..username..'\n')
end end
end end
function reactions:action(msg, config) function reactions:action(msg, config)
if string.match(msg.text_lower, config.cmd_pat..'reactions') then if string.match(msg.text_lower, config.cmd_pat..'reactions') then
utilities.send_message(self, msg.chat.id, help) utilities.send_message(self, msg.chat.id, help)
return return
end end
for trigger,reaction in pairs(config.reactions) do for trigger,reaction in pairs(config.reactions) do
if string.match(msg.text_lower, config.cmd_pat..trigger) then if string.match(msg.text_lower, config.cmd_pat..trigger) then
utilities.send_message(self, msg.chat.id, reaction) utilities.send_message(self, msg.chat.id, reaction)
return return
end end
end end
end end
return reactions return reactions

View File

@ -8,29 +8,29 @@ local utilities = require('otouto.utilities')
reddit.command = 'reddit [r/subreddit | query]' reddit.command = 'reddit [r/subreddit | query]'
function reddit:init(config) function reddit:init(config)
reddit.triggers = utilities.triggers(self.info.username, config.cmd_pat, {'^/r/'}):t('reddit', true):t('r', true):t('r/', true).table reddit.triggers = utilities.triggers(self.info.username, config.cmd_pat, {'^/r/'}):t('reddit', true):t('r', true):t('r/', true).table
reddit.doc = config.cmd_pat .. [[reddit [r/subreddit | query] reddit.doc = config.cmd_pat .. [[reddit [r/subreddit | query]
Returns the top posts or results for a given subreddit or query. If no argument is given, returns the top posts from r/all. Querying specific subreddits is not supported. Returns the top posts or results for a given subreddit or query. If no argument is given, returns the top posts from r/all. Querying specific subreddits is not supported.
Aliases: ]] .. config.cmd_pat .. 'r, /r/subreddit' Aliases: ]] .. config.cmd_pat .. 'r, /r/subreddit'
end end
local format_results = function(posts) local format_results = function(posts)
local output = '' local output = ''
for _,v in ipairs(posts) do for _,v in ipairs(posts) do
local post = v.data local post = v.data
local title = post.title:gsub('%[', '('):gsub('%]', ')'):gsub('&amp;', '&') local title = post.title:gsub('%[', '('):gsub('%]', ')'):gsub('&amp;', '&')
if title:len() > 256 then if title:len() > 256 then
title = title:sub(1, 253) title = title:sub(1, 253)
title = utilities.trim(title) .. '...' title = utilities.trim(title) .. '...'
end end
local short_url = 'redd.it/' .. post.id local short_url = 'redd.it/' .. post.id
local s = '[' .. title .. '](' .. short_url .. ')' local s = '[' .. title .. '](' .. short_url .. ')'
if post.domain and not post.is_self and not post.over_18 then if post.domain and not post.is_self and not post.over_18 then
s = '`[`[' .. post.domain .. '](' .. post.url:gsub('%)', '\\)') .. ')`]` ' .. s s = '`[`[' .. post.domain .. '](' .. post.url:gsub('%)', '\\)') .. ')`]` ' .. s
end end
output = output .. '' .. s .. '\n' output = output .. '' .. s .. '\n'
end end
return output return output
end end
reddit.subreddit_url = 'http://www.reddit.com/%s/.json?limit=' reddit.subreddit_url = 'http://www.reddit.com/%s/.json?limit='
@ -38,46 +38,46 @@ reddit.search_url = 'http://www.reddit.com/search.json?q=%s&limit='
reddit.rall_url = 'http://www.reddit.com/.json?limit=' reddit.rall_url = 'http://www.reddit.com/.json?limit='
function reddit:action(msg, config) function reddit:action(msg, config)
-- Eight results in PM, four results elsewhere. -- Eight results in PM, four results elsewhere.
local limit = 4 local limit = 4
if msg.chat.type == 'private' then if msg.chat.type == 'private' then
limit = 8 limit = 8
end end
local text = msg.text_lower local text = msg.text_lower
if text:match('^/r/.') then if text:match('^/r/.') then
-- Normalize input so this hack works easily. -- Normalize input so this hack works easily.
text = msg.text_lower:gsub('^/r/', config.cmd_pat..'r r/') text = msg.text_lower:gsub('^/r/', config.cmd_pat..'r r/')
end end
local input = utilities.input(text) local input = utilities.input(text)
local source, url local source, url
if input then if input then
if input:match('^r/.') then if input:match('^r/.') then
input = utilities.get_word(input, 1) input = utilities.get_word(input, 1)
url = reddit.subreddit_url:format(input) .. limit url = reddit.subreddit_url:format(input) .. limit
source = '*/' .. utilities.md_escape(input) .. '*\n' source = '*/' .. utilities.md_escape(input) .. '*\n'
else else
input = utilities.input(msg.text) input = utilities.input(msg.text)
source = '*Results for* _' .. utilities.md_escape(input) .. '_ *:*\n' source = '*Results for* _' .. utilities.md_escape(input) .. '_ *:*\n'
input = URL.escape(input) input = URL.escape(input)
url = reddit.search_url:format(input) .. limit url = reddit.search_url:format(input) .. limit
end end
else else
url = reddit.rall_url .. limit url = reddit.rall_url .. limit
source = '*/r/all*\n' source = '*/r/all*\n'
end end
local jstr, res = HTTP.request(url) local jstr, res = HTTP.request(url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
else else
local jdat = JSON.decode(jstr) local jdat = JSON.decode(jstr)
if #jdat.data.children == 0 then if #jdat.data.children == 0 then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
else else
local output = format_results(jdat.data.children) local output = format_results(jdat.data.children)
output = source .. output output = source .. output
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end
end end
end end
return reddit return reddit

View File

@ -5,87 +5,87 @@ local utilities = require('otouto.utilities')
remind.command = 'remind <duration> <message>' remind.command = 'remind <duration> <message>'
function remind:init(config) function remind:init(config)
self.database.reminders = self.database.reminders or {} self.database.reminders = self.database.reminders or {}
remind.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('remind', true).table remind.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('remind', true).table
config.remind = config.remind or {} config.remind = config.remind or {}
setmetatable(config.remind, { __index = function() return 1000 end }) setmetatable(config.remind, { __index = function() return 1000 end })
remind.doc = config.cmd_pat .. [[remind <duration> <message> remind.doc = config.cmd_pat .. [[remind <duration> <message>
Repeats a message after a duration of time, in minutes. Repeats a message after a duration of time, in minutes.
The maximum length of a reminder is %s characters. The maximum duration of a timer is %s minutes. The maximum number of reminders for a group is %s. The maximum number of reminders in private is %s.]] The maximum length of a reminder is %s characters. The maximum duration of a timer is %s minutes. The maximum number of reminders for a group is %s. The maximum number of reminders in private is %s.]]
remind.doc = remind.doc:format(config.remind.max_length, config.remind.max_duration, config.remind.max_reminders_group, config.remind.max_reminders_private) remind.doc = remind.doc:format(config.remind.max_length, config.remind.max_duration, config.remind.max_reminders_group, config.remind.max_reminders_private)
end end
function remind:action(msg, config) function remind:action(msg, config)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if not input then if not input then
utilities.send_reply(self, msg, remind.doc, true) utilities.send_reply(self, msg, remind.doc, true)
return return
end end
local duration = tonumber(utilities.get_word(input, 1)) local duration = tonumber(utilities.get_word(input, 1))
if not duration then if not duration then
utilities.send_reply(self, msg, remind.doc, true) utilities.send_reply(self, msg, remind.doc, true)
return return
end end
if duration < 1 then if duration < 1 then
duration = 1 duration = 1
elseif duration > config.remind.max_duration then elseif duration > config.remind.max_duration then
duration = config.remind.max_duration duration = config.remind.max_duration
end end
local message = utilities.input(input) local message = utilities.input(input)
if not message then if not message then
utilities.send_reply(self, msg, remind.doc, true) utilities.send_reply(self, msg, remind.doc, true)
return return
end end
if #message > config.remind.max_length then if #message > config.remind.max_length then
utilities.send_reply(self, msg, 'The maximum length of reminders is ' .. config.remind.max_length .. '.') utilities.send_reply(self, msg, 'The maximum length of reminders is ' .. config.remind.max_length .. '.')
return return
end end
local chat_id_str = tostring(msg.chat.id) local chat_id_str = tostring(msg.chat.id)
local output local output
self.database.reminders[chat_id_str] = self.database.reminders[chat_id_str] or {} self.database.reminders[chat_id_str] = self.database.reminders[chat_id_str] or {}
if msg.chat.type == 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_private then if msg.chat.type == 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_private then
output = 'Sorry, you already have the maximum number of reminders.' output = 'Sorry, you already have the maximum number of reminders.'
elseif msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_group then elseif msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_group then
output = 'Sorry, this group already has the maximum number of reminders.' output = 'Sorry, this group already has the maximum number of reminders.'
else else
table.insert(self.database.reminders[chat_id_str], { table.insert(self.database.reminders[chat_id_str], {
time = os.time() + (duration * 60), time = os.time() + (duration * 60),
message = message message = message
}) })
output = string.format( output = string.format(
'I will remind you in %s minute%s!', 'I will remind you in %s minute%s!',
duration, duration,
duration == 1 and '' or 's' duration == 1 and '' or 's'
) )
end end
utilities.send_reply(self, msg, output, true) utilities.send_reply(self, msg, output, true)
end end
function remind:cron(config) function remind:cron(config)
local time = os.time() local time = os.time()
-- Iterate over the group entries in the reminders database. -- Iterate over the group entries in the reminders database.
for chat_id, group in pairs(self.database.reminders) do for chat_id, group in pairs(self.database.reminders) do
-- Iterate over each reminder. -- Iterate over each reminder.
for k, reminder in pairs(group) do for k, reminder in pairs(group) do
-- If the reminder is past-due, send it and nullify it. -- If the reminder is past-due, send it and nullify it.
-- Otherwise, add it to the replacement table. -- Otherwise, add it to the replacement table.
if time > reminder.time then if time > reminder.time then
local output = utilities.style.enquote('Reminder', reminder.message) local output = utilities.style.enquote('Reminder', reminder.message)
local res = utilities.send_message(self, chat_id, output, true, nil, true) local res = utilities.send_message(self, chat_id, output, true, nil, true)
-- If the message fails to send, save it for later (if enabled in config). -- If the message fails to send, save it for later (if enabled in config).
if res or not config.remind.persist then if res or not config.remind.persist then
group[k] = nil group[k] = nil
end end
end end
end end
end end
end end
return remind return remind

View File

@ -5,30 +5,30 @@ local bindings = require('otouto.bindings')
local rms = {} local rms = {}
function rms:init(config) function rms:init(config)
rms.BASE_URL = 'https://rms.sexy/img/' rms.BASE_URL = 'https://rms.sexy/img/'
rms.LIST = {} rms.LIST = {}
local s, r = https.request(rms.BASE_URL) local s, r = https.request(rms.BASE_URL)
if r ~= 200 then if r ~= 200 then
print('Error connecting to rms.sexy.\nrmspic.lua will not be enabled.') print('Error connecting to rms.sexy.\nrmspic.lua will not be enabled.')
return return
end end
for link in s:gmatch('<a href=".-%.%a%a%a">(.-)</a>') do for link in s:gmatch('<a href=".-%.%a%a%a">(.-)</a>') do
table.insert(rms.LIST, link) table.insert(rms.LIST, link)
end end
rms.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('rms').table rms.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('rms').table
end end
function rms:action(msg, config) function rms:action(msg, config)
bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'upload_photo' }) bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'upload_photo' })
local choice = rms.LIST[math.random(#rms.LIST)] local choice = rms.LIST[math.random(#rms.LIST)]
local filename = '/tmp/' .. choice local filename = '/tmp/' .. choice
local image_file = io.open(filename) local image_file = io.open(filename)
if image_file then if image_file then
image_file:close() image_file:close()
else else
utilities.download_file(rms.BASE_URL .. choice, filename) utilities.download_file(rms.BASE_URL .. choice, filename)
end end
bindings.sendPhoto(self, { chat_id = msg.chat.id }, { photo = filename }) bindings.sendPhoto(self, { chat_id = msg.chat.id }, { photo = filename })
end end
return rms return rms

View File

@ -3,9 +3,9 @@ local setandget = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function setandget:init(config) function setandget:init(config)
self.database.setandget = self.database.setandget or {} self.database.setandget = self.database.setandget or {}
setandget.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('set', true):t('get', true).table setandget.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('set', true):t('get', true).table
setandget.doc = config.cmd_pat .. [[set <name> <value> setandget.doc = config.cmd_pat .. [[set <name> <value>
Stores a value with the given name. Use "]] .. config.cmd_pat .. [[set <name> --" to delete the stored value. Stores a value with the given name. Use "]] .. config.cmd_pat .. [[set <name> --" to delete the stored value.
]] .. config.cmd_pat .. [[get [name] ]] .. config.cmd_pat .. [[get [name]
Returns the stored value or a list of stored values.]] Returns the stored value or a list of stored values.]]
@ -15,56 +15,56 @@ setandget.command = 'set <name> <value>'
function setandget:action(msg, config) function setandget:action(msg, config)
local chat_id_str = tostring(msg.chat.id) local chat_id_str = tostring(msg.chat.id)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
self.database.setandget[chat_id_str] = self.database.setandget[chat_id_str] or {} self.database.setandget[chat_id_str] = self.database.setandget[chat_id_str] or {}
if msg.text_lower:match('^'..config.cmd_pat..'set') then if msg.text_lower:match('^'..config.cmd_pat..'set') then
if not input then if not input then
utilities.send_message(self, msg.chat.id, setandget.doc, true, nil, true) utilities.send_message(self, msg.chat.id, setandget.doc, true, nil, true)
return return
end end
local name = utilities.get_word(input:lower(), 1) local name = utilities.get_word(input:lower(), 1)
local value = utilities.input(input) local value = utilities.input(input)
if not name or not value then if not name or not value then
utilities.send_message(self, msg.chat.id, setandget.doc, true, nil, true) utilities.send_message(self, msg.chat.id, setandget.doc, true, nil, true)
elseif value == '--' or value == '' then elseif value == '--' or value == '' then
self.database.setandget[chat_id_str][name] = nil self.database.setandget[chat_id_str][name] = nil
utilities.send_message(self, msg.chat.id, 'That value has been deleted.') utilities.send_message(self, msg.chat.id, 'That value has been deleted.')
else else
self.database.setandget[chat_id_str][name] = value self.database.setandget[chat_id_str][name] = value
utilities.send_message(self, msg.chat.id, '"' .. name .. '" has been set to "' .. value .. '".', true) utilities.send_message(self, msg.chat.id, '"' .. name .. '" has been set to "' .. value .. '".', true)
end end
elseif msg.text_lower:match('^'..config.cmd_pat..'get') then elseif msg.text_lower:match('^'..config.cmd_pat..'get') then
if not input then if not input then
local output local output
if utilities.table_size(self.database.setandget[chat_id_str]) == 0 then if utilities.table_size(self.database.setandget[chat_id_str]) == 0 then
output = 'No values have been stored here.' output = 'No values have been stored here.'
else else
output = '*List of stored values:*\n' output = '*List of stored values:*\n'
for k,v in pairs(self.database.setandget[chat_id_str]) do for k,v in pairs(self.database.setandget[chat_id_str]) do
output = output .. '' .. k .. ': `' .. v .. '`\n' output = output .. '' .. k .. ': `' .. v .. '`\n'
end end
end end
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
return return
end end
local output local output
if self.database.setandget[chat_id_str][input:lower()] then if self.database.setandget[chat_id_str][input:lower()] then
output = '`' .. self.database.setandget[chat_id_str][input:lower()] .. '`' output = '`' .. self.database.setandget[chat_id_str][input:lower()] .. '`'
else else
output = 'There is no value stored by that name.' output = 'There is no value stored by that name.'
end end
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end
end end

View File

@ -3,32 +3,32 @@ local shell = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function shell:init(config) function shell:init(config)
shell.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('run', true).table shell.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('run', true).table
end end
function shell:action(msg, config) function shell:action(msg, config)
if msg.from.id ~= config.admin then if msg.from.id ~= config.admin then
return return
end end
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
input = input:gsub('', '--') input = input:gsub('', '--')
if not input then if not input then
utilities.send_reply(self, msg, 'Please specify a command to run.') utilities.send_reply(self, msg, 'Please specify a command to run.')
return return
end end
local f = io.popen(input) local f = io.popen(input)
local output = f:read('*all') local output = f:read('*all')
f:close() f:close()
if output:len() == 0 then if output:len() == 0 then
output = 'Done!' output = 'Done!'
else else
output = '```\n' .. output .. '\n```' output = '```\n' .. output .. '\n```'
end end
utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true)
end end

View File

@ -6,45 +6,45 @@ shout.command = 'shout <text>'
local utf8 = '('..utilities.char.utf_8..'*)' local utf8 = '('..utilities.char.utf_8..'*)'
function shout:init(config) function shout:init(config)
shout.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('shout', true).table shout.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('shout', true).table
shout.doc = config.cmd_pat .. 'shout <text> \nShouts something. Input may be the replied-to message.' shout.doc = config.cmd_pat .. 'shout <text> \nShouts something. Input may be the replied-to message.'
end end
function shout:action(msg) function shout:action(msg)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, shout.doc, true) utilities.send_reply(self, msg, shout.doc, true)
return return
end end
input = utilities.trim(input) input = utilities.trim(input)
input = input:upper() input = input:upper()
local output = '' local output = ''
local inc = 0 local inc = 0
local ilen = 0 local ilen = 0
for match in input:gmatch(utf8) do for match in input:gmatch(utf8) do
if ilen < 20 then if ilen < 20 then
ilen = ilen + 1 ilen = ilen + 1
output = output .. match .. ' ' output = output .. match .. ' '
end end
end end
ilen = 0 ilen = 0
output = output .. '\n' output = output .. '\n'
for match in input:sub(2):gmatch(utf8) do for match in input:sub(2):gmatch(utf8) do
if ilen < 19 then if ilen < 19 then
local spacing = '' local spacing = ''
for _ = 1, inc do for _ = 1, inc do
spacing = spacing .. ' ' spacing = spacing .. ' '
end end
inc = inc + 1 inc = inc + 1
ilen = ilen + 1 ilen = ilen + 1
output = output .. match .. ' ' .. spacing .. match .. '\n' output = output .. match .. ' ' .. spacing .. match .. '\n'
end end
end end
output = '```\n' .. utilities.trim(output) .. '\n```' output = '```\n' .. utilities.trim(output) .. '\n```'
utilities.send_message(self, msg.chat.id, output, true, false, true) utilities.send_message(self, msg.chat.id, output, true, false, true)
end end

View File

@ -5,160 +5,160 @@ local utilities = require('otouto.utilities')
slap.command = 'slap [target]' slap.command = 'slap [target]'
function slap:init(config) function slap:init(config)
slap.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('slap', true).table slap.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('slap', true).table
slap.doc = config.cmd_pat .. 'slap [target] \nSlap somebody.' slap.doc = config.cmd_pat .. 'slap [target] \nSlap somebody.'
end end
local slaps = { local slaps = {
'VICTIM was shot by VICTOR.', 'VICTIM was shot by VICTOR.',
'VICTIM was pricked to death.', 'VICTIM was pricked to death.',
'VICTIM walked into a cactus while trying to escape VICTOR.', 'VICTIM walked into a cactus while trying to escape VICTOR.',
'VICTIM drowned.', 'VICTIM drowned.',
'VICTIM drowned whilst trying to escape VICTOR.', 'VICTIM drowned whilst trying to escape VICTOR.',
'VICTIM blew up.', 'VICTIM blew up.',
'VICTIM was blown up by VICTOR.', 'VICTIM was blown up by VICTOR.',
'VICTIM hit the ground too hard.', 'VICTIM hit the ground too hard.',
'VICTIM fell from a high place.', 'VICTIM fell from a high place.',
'VICTIM fell off a ladder.', 'VICTIM fell off a ladder.',
'VICTIM fell into a patch of cacti.', 'VICTIM fell into a patch of cacti.',
'VICTIM was doomed to fall by VICTOR.', 'VICTIM was doomed to fall by VICTOR.',
'VICTIM was blown from a high place by VICTOR.', 'VICTIM was blown from a high place by VICTOR.',
'VICTIM was squashed by a falling anvil.', 'VICTIM was squashed by a falling anvil.',
'VICTIM went up in flames.', 'VICTIM went up in flames.',
'VICTIM burned to death.', 'VICTIM burned to death.',
'VICTIM was burnt to a crisp whilst fighting VICTOR.', 'VICTIM was burnt to a crisp whilst fighting VICTOR.',
'VICTIM walked into a fire whilst fighting VICTOR.', 'VICTIM walked into a fire whilst fighting VICTOR.',
'VICTIM tried to swim in lava.', 'VICTIM tried to swim in lava.',
'VICTIM tried to swim in lava while trying to escape VICTOR.', 'VICTIM tried to swim in lava while trying to escape VICTOR.',
'VICTIM was struck by lightning.', 'VICTIM was struck by lightning.',
'VICTIM was slain by VICTOR.', 'VICTIM was slain by VICTOR.',
'VICTIM got finished off by VICTOR.', 'VICTIM got finished off by VICTOR.',
'VICTIM was killed by magic.', 'VICTIM was killed by magic.',
'VICTIM was killed by VICTOR using magic.', 'VICTIM was killed by VICTOR using magic.',
'VICTIM starved to death.', 'VICTIM starved to death.',
'VICTIM suffocated in a wall.', 'VICTIM suffocated in a wall.',
'VICTIM fell out of the world.', 'VICTIM fell out of the world.',
'VICTIM was knocked into the void by VICTOR.', 'VICTIM was knocked into the void by VICTOR.',
'VICTIM withered away.', 'VICTIM withered away.',
'VICTIM was pummeled by VICTOR.', 'VICTIM was pummeled by VICTOR.',
'VICTIM was fragged by VICTOR.', 'VICTIM was fragged by VICTOR.',
'VICTIM was desynchronized.', 'VICTIM was desynchronized.',
'VICTIM was wasted.', 'VICTIM was wasted.',
'VICTIM was busted.', 'VICTIM was busted.',
'VICTIM\'s bones are scraped clean by the desolate wind.', 'VICTIM\'s bones are scraped clean by the desolate wind.',
'VICTIM has died of dysentery.', 'VICTIM has died of dysentery.',
'VICTIM fainted.', 'VICTIM fainted.',
'VICTIM is out of usable Pokemon! VICTIM whited out!', 'VICTIM is out of usable Pokemon! VICTIM whited out!',
'VICTIM is out of usable Pokemon! VICTIM blacked out!', 'VICTIM is out of usable Pokemon! VICTIM blacked out!',
'VICTIM whited out!', 'VICTIM whited out!',
'VICTIM blacked out!', 'VICTIM blacked out!',
'VICTIM says goodbye to this cruel world.', 'VICTIM says goodbye to this cruel world.',
'VICTIM got rekt.', 'VICTIM got rekt.',
'VICTIM was sawn in half by VICTOR.', 'VICTIM was sawn in half by VICTOR.',
'VICTIM died. I blame VICTOR.', 'VICTIM died. I blame VICTOR.',
'VICTIM was axe-murdered by VICTOR.', 'VICTIM was axe-murdered by VICTOR.',
'VICTIM\'s melon was split by VICTOR.', 'VICTIM\'s melon was split by VICTOR.',
'VICTIM was sliced and diced by VICTOR.', 'VICTIM was sliced and diced by VICTOR.',
'VICTIM was split from crotch to sternum by VICTOR.', 'VICTIM was split from crotch to sternum by VICTOR.',
'VICTIM\'s death put another notch in VICTOR\'s axe.', 'VICTIM\'s death put another notch in VICTOR\'s axe.',
'VICTIM died impossibly!', 'VICTIM died impossibly!',
'VICTIM died from VICTOR\'s mysterious tropical disease.', 'VICTIM died from VICTOR\'s mysterious tropical disease.',
'VICTIM escaped infection by dying.', 'VICTIM escaped infection by dying.',
'VICTIM played hot-potato with a grenade.', 'VICTIM played hot-potato with a grenade.',
'VICTIM was knifed by VICTOR.', 'VICTIM was knifed by VICTOR.',
'VICTIM fell on his sword.', 'VICTIM fell on his sword.',
'VICTIM ate a grenade.', 'VICTIM ate a grenade.',
'VICTIM practiced being VICTOR\'s clay pigeon.', 'VICTIM practiced being VICTOR\'s clay pigeon.',
'VICTIM is what\'s for dinner!', 'VICTIM is what\'s for dinner!',
'VICTIM was terminated by VICTOR.', 'VICTIM was terminated by VICTOR.',
'VICTIM was shot before being thrown out of a plane.', 'VICTIM was shot before being thrown out of a plane.',
'VICTIM was not invincible.', 'VICTIM was not invincible.',
'VICTIM has encountered an error.', 'VICTIM has encountered an error.',
'VICTIM died and reincarnated as a goat.', 'VICTIM died and reincarnated as a goat.',
'VICTOR threw VICTIM off a building.', 'VICTOR threw VICTIM off a building.',
'VICTIM is sleeping with the fishes.', 'VICTIM is sleeping with the fishes.',
'VICTIM got a premature burial.', 'VICTIM got a premature burial.',
'VICTOR replaced all of VICTIM\'s music with Nickelback.', 'VICTOR replaced all of VICTIM\'s music with Nickelback.',
'VICTOR spammed VICTIM\'s email.', 'VICTOR spammed VICTIM\'s email.',
'VICTOR made VICTIM a knuckle sandwich.', 'VICTOR made VICTIM a knuckle sandwich.',
'VICTOR slapped VICTIM with pure nothing.', 'VICTOR slapped VICTIM with pure nothing.',
'VICTOR hit VICTIM with a small, interstellar spaceship.', 'VICTOR hit VICTIM with a small, interstellar spaceship.',
'VICTIM was quickscoped by VICTOR.', 'VICTIM was quickscoped by VICTOR.',
'VICTOR put VICTIM in check-mate.', 'VICTOR put VICTIM in check-mate.',
'VICTOR RSA-encrypted VICTIM and deleted the private key.', 'VICTOR RSA-encrypted VICTIM and deleted the private key.',
'VICTOR put VICTIM in the friendzone.', 'VICTOR put VICTIM in the friendzone.',
'VICTOR slaps VICTIM with a DMCA takedown request!', 'VICTOR slaps VICTIM with a DMCA takedown request!',
'VICTIM became a corpse blanket for VICTOR.', 'VICTIM became a corpse blanket for VICTOR.',
'Death is when the monsters get you. Death comes for VICTIM.', 'Death is when the monsters get you. Death comes for VICTIM.',
'Cowards die many times before their death. VICTIM never tasted death but once.', 'Cowards die many times before their death. VICTIM never tasted death but once.',
'VICTIM died of hospital gangrene.', 'VICTIM died of hospital gangrene.',
'VICTIM got a house call from Doctor VICTOR.', 'VICTIM got a house call from Doctor VICTOR.',
'VICTOR beheaded VICTIM.', 'VICTOR beheaded VICTIM.',
'VICTIM got stoned...by an angry mob.', 'VICTIM got stoned...by an angry mob.',
'VICTOR sued the pants off VICTIM.', 'VICTOR sued the pants off VICTIM.',
'VICTIM was impeached.', 'VICTIM was impeached.',
'VICTIM was one-hit KO\'d by VICTOR.', 'VICTIM was one-hit KO\'d by VICTOR.',
'VICTOR sent VICTIM to /dev/null.', 'VICTOR sent VICTIM to /dev/null.',
'VICTOR sent VICTIM down the memory hole.', 'VICTOR sent VICTIM down the memory hole.',
'VICTIM was a mistake.', 'VICTIM was a mistake.',
'"VICTIM was a mistake." - VICTOR', '"VICTIM was a mistake." - VICTOR',
'VICTOR checkmated VICTIM in two moves.' 'VICTOR checkmated VICTIM in two moves.'
} }
-- optimize later -- optimize later
function slap:action(msg) function slap:action(msg)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
local victor_id = msg.from.id local victor_id = msg.from.id
local victim_id local victim_id
if msg.reply_to_message then if msg.reply_to_message then
victim_id = msg.reply_to_message.from.id victim_id = msg.reply_to_message.from.id
else else
if input then if input then
if tonumber(input) then if tonumber(input) then
victim_id = tonumber(input) victim_id = tonumber(input)
elseif input:match('^@') then elseif input:match('^@') then
local t = utilities.resolve_username(self, input) local t = utilities.resolve_username(self, input)
if t then if t then
victim_id = t.id victim_id = t.id
end end
end end
end end
end end
-- IDs -- IDs
if victim_id then if victim_id then
if victim_id == victor_id then if victim_id == victor_id then
victor_id = self.info.id victor_id = self.info.id
end end
else else
if not input then if not input then
victor_id = self.info.id victor_id = self.info.id
victim_id = msg.from.id victim_id = msg.from.id
end end
end end
-- Names -- Names
local victor_name, victim_name local victor_name, victim_name
if input and not victim_id then if input and not victim_id then
victim_name = input victim_name = input
else else
local victim_id_str = tostring(victim_id) local victim_id_str = tostring(victim_id)
if self.database.userdata[victim_id_str] and self.database.userdata[victim_id_str].nickname then if self.database.userdata[victim_id_str] and self.database.userdata[victim_id_str].nickname then
victim_name = self.database.userdata[victim_id_str].nickname victim_name = self.database.userdata[victim_id_str].nickname
elseif self.database.users[victim_id_str] then elseif self.database.users[victim_id_str] then
victim_name = utilities.build_name(self.database.users[victim_id_str].first_name, self.database.users[victim_id_str].last_name) victim_name = utilities.build_name(self.database.users[victim_id_str].first_name, self.database.users[victim_id_str].last_name)
else else
victim_name = victim_id_str victim_name = victim_id_str
end end
end end
local victor_id_str = tostring(victor_id) local victor_id_str = tostring(victor_id)
if self.database.userdata[victor_id_str] and self.database.userdata[victor_id_str].nickname then if self.database.userdata[victor_id_str] and self.database.userdata[victor_id_str].nickname then
victor_name = self.database.userdata[victor_id_str].nickname victor_name = self.database.userdata[victor_id_str].nickname
elseif self.database.users[victor_id_str] then elseif self.database.users[victor_id_str] then
victor_name = utilities.build_name(self.database.users[victor_id_str].first_name, self.database.users[victor_id_str].last_name) victor_name = utilities.build_name(self.database.users[victor_id_str].first_name, self.database.users[victor_id_str].last_name)
else else
victor_name = self.info.first_name victor_name = self.info.first_name
end end
local output = utilities.char.zwnj .. slaps[math.random(#slaps)]:gsub('VICTIM', victim_name):gsub('VICTOR', victor_name) local output = utilities.char.zwnj .. slaps[math.random(#slaps)]:gsub('VICTIM', victim_name):gsub('VICTOR', victor_name)
utilities.send_message(self, msg.chat.id, output) utilities.send_message(self, msg.chat.id, output)
end end
return slap return slap

View File

@ -8,71 +8,71 @@ local utilities = require('otouto.utilities')
local starwars = {} local starwars = {}
function starwars:init(config) function starwars:init(config)
starwars.triggers = utilities.triggers(self.info.username, config.cmd_pat) starwars.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('starwars', true):t('sw', true).table :t('starwars', true):t('sw', true).table
starwars.doc = config.cmd_pat .. [[starwars <query> starwars.doc = config.cmd_pat .. [[starwars <query>
Returns the opening crawl from the specified Star Wars film. Returns the opening crawl from the specified Star Wars film.
Alias: ]] .. config.cmd_pat .. 'sw' Alias: ]] .. config.cmd_pat .. 'sw'
starwars.command = 'starwars <query>' starwars.command = 'starwars <query>'
starwars.base_url = 'http://swapi.co/api/films/' starwars.base_url = 'http://swapi.co/api/films/'
end end
local films_by_number = { local films_by_number = {
['phantom menace'] = 4, ['phantom menace'] = 4,
['attack of the clones'] = 5, ['attack of the clones'] = 5,
['revenge of the sith'] = 6, ['revenge of the sith'] = 6,
['new hope'] = 1, ['new hope'] = 1,
['empire strikes back'] = 2, ['empire strikes back'] = 2,
['return of the jedi'] = 3, ['return of the jedi'] = 3,
['force awakens'] = 7 ['force awakens'] = 7
} }
local corrected_numbers = { local corrected_numbers = {
4, 4,
5, 5,
6, 6,
1, 1,
2, 2,
3, 3,
7 7
} }
function starwars:action(msg, config) function starwars:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, starwars.doc, true) utilities.send_reply(self, msg, starwars.doc, true)
return return
end end
bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } )
local film local film
if tonumber(input) then if tonumber(input) then
input = tonumber(input) input = tonumber(input)
film = corrected_numbers[input] or input film = corrected_numbers[input] or input
else else
for title, number in pairs(films_by_number) do for title, number in pairs(films_by_number) do
if string.match(input, title) then if string.match(input, title) then
film = number film = number
break break
end end
end end
end end
if not film then if not film then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local url = starwars.base_url .. film local url = starwars.base_url .. film
local jstr, code = HTTP.request(url) local jstr, code = HTTP.request(url)
if code ~= 200 then if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local output = '*' .. JSON.decode(jstr).opening_crawl .. '*' local output = '*' .. JSON.decode(jstr).opening_crawl .. '*'
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end
return starwars return starwars

View File

@ -8,52 +8,52 @@ time.command = 'time <location>'
time.base_url = 'https://maps.googleapis.com/maps/api/timezone/json?location=%s,%s&timestamp=%s' time.base_url = 'https://maps.googleapis.com/maps/api/timezone/json?location=%s,%s&timestamp=%s'
function time:init(config) function time:init(config)
time.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('time', true).table time.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('time', true).table
time.doc = config.cmd_pat .. [[time <location> time.doc = config.cmd_pat .. [[time <location>
Returns the time, date, and timezone for the given location.]] Returns the time, date, and timezone for the given location.]]
end end
function time:action(msg, config) function time:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, time.doc, true) utilities.send_reply(self, msg, time.doc, true)
return return
end end
local coords = utilities.get_coords(input, config) local coords = utilities.get_coords(input, config)
if type(coords) == 'string' then if type(coords) == 'string' then
utilities.send_reply(self, msg, coords) utilities.send_reply(self, msg, coords)
return return
end end
local now = os.time() local now = os.time()
local utc = os.time(os.date('!*t', now)) local utc = os.time(os.date('!*t', now))
local url = time.base_url:format(coords.lat, coords.lon, utc) local url = time.base_url:format(coords.lat, coords.lon, utc)
local jstr, code = HTTPS.request(url) local jstr, code = HTTPS.request(url)
if code ~= 200 then if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
if data.status == 'ZERO_RESULTS' then if data.status == 'ZERO_RESULTS' then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local timestamp = now + data.rawOffset + data.dstOffset local timestamp = now + data.rawOffset + data.dstOffset
local utcoff = (data.rawOffset + data.dstOffset) / 3600 local utcoff = (data.rawOffset + data.dstOffset) / 3600
if utcoff == math.abs(utcoff) then if utcoff == math.abs(utcoff) then
utcoff = '+' .. utilities.pretty_float(utcoff) utcoff = '+' .. utilities.pretty_float(utcoff)
else else
utcoff = utilities.pretty_float(utcoff) utcoff = utilities.pretty_float(utcoff)
end end
local output = string.format('```\n%s\n%s (UTC%s)\n```', local output = string.format('```\n%s\n%s (UTC%s)\n```',
os.date('!%I:%M %p\n%A, %B %d, %Y', timestamp), os.date('!%I:%M %p\n%A, %B %d, %Y', timestamp),
data.timeZoneName, data.timeZoneName,
utcoff utcoff
) )
utilities.send_reply(self, msg, output, true) utilities.send_reply(self, msg, output, true)
end end
return time return time

View File

@ -8,38 +8,38 @@ local utilities = require('otouto.utilities')
translate.command = 'translate [text]' translate.command = 'translate [text]'
function translate:init(config) function translate:init(config)
assert(config.yandex_key, assert(config.yandex_key,
'translate.lua requires a Yandex translate API key from http://tech.yandex.com/keys/get.' 'translate.lua requires a Yandex translate API key from http://tech.yandex.com/keys/get.'
) )
translate.triggers = utilities.triggers(self.info.username, config.cmd_pat) translate.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('translate', true):t('tl', true).table :t('translate', true):t('tl', true).table
translate.doc = config.cmd_pat .. [[translate [text] translate.doc = config.cmd_pat .. [[translate [text]
Translates input or the replied-to message into the bot's language.]] Translates input or the replied-to message into the bot's language.]]
translate.base_url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. config.yandex_key .. '&lang=' .. config.lang .. '&text=%s' translate.base_url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. config.yandex_key .. '&lang=' .. config.lang .. '&text=%s'
end end
function translate:action(msg, config) function translate:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, translate.doc, true) utilities.send_reply(self, msg, translate.doc, true)
return return
end end
local url = translate.base_url:format(URL.escape(input)) local url = translate.base_url:format(URL.escape(input))
local jstr, code = HTTPS.request(url) local jstr, code = HTTPS.request(url)
if code ~= 200 then if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
if data.code ~= 200 then if data.code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
utilities.send_reply(self, msg.reply_to_message or msg, utilities.style.enquote('Translation', data.text[1]), true) utilities.send_reply(self, msg.reply_to_message or msg, utilities.style.enquote('Translation', data.text[1]), true)
end end
return translate return translate

View File

@ -9,42 +9,42 @@ urbandictionary.command = 'urbandictionary <query>'
urbandictionary.base_url = 'http://api.urbandictionary.com/v0/define?term=' urbandictionary.base_url = 'http://api.urbandictionary.com/v0/define?term='
function urbandictionary:init(config) function urbandictionary:init(config)
urbandictionary.triggers = utilities.triggers(self.info.username, config.cmd_pat) urbandictionary.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('urbandictionary', true):t('ud', true):t('urban', true).table :t('urbandictionary', true):t('ud', true):t('urban', true).table
urbandictionary.doc = [[ urbandictionary.doc = [[
/urbandictionary <query> /urbandictionary <query>
Returns a definition from Urban Dictionary. Returns a definition from Urban Dictionary.
Aliases: /ud, /urban Aliases: /ud, /urban
]] ]]
urbandictionary.doc = urbandictionary.doc:gsub('/', config.cmd_pat) urbandictionary.doc = urbandictionary.doc:gsub('/', config.cmd_pat)
end end
function urbandictionary:action(msg, config) function urbandictionary:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, urbandictionary.doc, true) utilities.send_reply(self, msg, urbandictionary.doc, true)
return return
end end
local url = urbandictionary.base_url .. URL.escape(input) local url = urbandictionary.base_url .. URL.escape(input)
local jstr, code = HTTP.request(url) local jstr, code = HTTP.request(url)
if code ~= 200 then if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
local output local output
if data.result_type == 'no_results' then if data.result_type == 'no_results' then
output = config.errors.results output = config.errors.results
else else
output = string.format('*%s*\n\n%s\n\n_%s_', output = string.format('*%s*\n\n%s\n\n_%s_',
data.list[1].word:gsub('*', '*\\**'), data.list[1].word:gsub('*', '*\\**'),
utilities.trim(utilities.md_escape(data.list[1].definition)), utilities.trim(utilities.md_escape(data.list[1].definition)),
utilities.trim((data.list[1].example or '')):gsub('_', '_\\__') utilities.trim((data.list[1].example or '')):gsub('_', '_\\__')
) )
end end
utilities.send_reply(self, msg, output, true) utilities.send_reply(self, msg, output, true)
end end
return urbandictionary return urbandictionary

View File

@ -6,12 +6,12 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function weather:init(config) function weather:init(config)
assert(config.owm_api_key, assert(config.owm_api_key,
'weather.lua requires an OpenWeatherMap API key from http://openweathermap.org/API.' 'weather.lua requires an OpenWeatherMap API key from http://openweathermap.org/API.'
) )
weather.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('weather', true).table weather.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('weather', true).table
weather.doc = config.cmd_pat .. [[weather <location> weather.doc = config.cmd_pat .. [[weather <location>
Returns the current weather conditions for a given location.]] Returns the current weather conditions for a given location.]]
end end
@ -19,37 +19,37 @@ weather.command = 'weather <location>'
function weather:action(msg, config) function weather:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, weather.doc, true) utilities.send_reply(self, msg, weather.doc, true)
return return
end end
local coords = utilities.get_coords(input, config) local coords = utilities.get_coords(input, config)
if type(coords) == 'string' then if type(coords) == 'string' then
utilities.send_reply(self, msg, coords) utilities.send_reply(self, msg, coords)
return return
end end
local url = 'http://api.openweathermap.org/data/2.5/weather?APPID=' .. config.owm_api_key .. '&lat=' .. coords.lat .. '&lon=' .. coords.lon local url = 'http://api.openweathermap.org/data/2.5/weather?APPID=' .. config.owm_api_key .. '&lat=' .. coords.lat .. '&lon=' .. coords.lon
local jstr, res = HTTP.request(url) local jstr, res = HTTP.request(url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local jdat = JSON.decode(jstr) local jdat = JSON.decode(jstr)
if jdat.cod ~= 200 then if jdat.cod ~= 200 then
utilities.send_reply(self, msg, 'Error: City not found.') utilities.send_reply(self, msg, 'Error: City not found.')
return return
end end
local celsius = string.format('%.2f', jdat.main.temp - 273.15) local celsius = string.format('%.2f', jdat.main.temp - 273.15)
local fahrenheit = string.format('%.2f', celsius * (9/5) + 32) local fahrenheit = string.format('%.2f', celsius * (9/5) + 32)
local output = '`' .. celsius .. '°C | ' .. fahrenheit .. '°F, ' .. jdat.weather[1].description .. '.`' local output = '`' .. celsius .. '°C | ' .. fahrenheit .. '°F, ' .. jdat.weather[1].description .. '.`'
utilities.send_reply(self, msg, output, true) utilities.send_reply(self, msg, output, true)
end end

View File

@ -6,54 +6,54 @@ local bindings = require('otouto.bindings')
whoami.command = 'whoami' whoami.command = 'whoami'
function whoami:init(config) function whoami:init(config)
whoami.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('who'):t('whoami').table whoami.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('who'):t('whoami').table
whoami.doc = [[ whoami.doc = [[
Returns user and chat info for you or the replied-to message. Returns user and chat info for you or the replied-to message.
Alias: ]] .. config.cmd_pat .. 'who' Alias: ]] .. config.cmd_pat .. 'who'
end end
function whoami:action(msg) function whoami:action(msg)
-- Operate on the replied-to message, if it exists. -- Operate on the replied-to message, if it exists.
msg = msg.reply_to_message or msg msg = msg.reply_to_message or msg
-- If it's a private conversation, bot is chat, unless bot is from. -- If it's a private conversation, bot is chat, unless bot is from.
local chat = msg.from.id == msg.chat.id and self.info or msg.chat local chat = msg.from.id == msg.chat.id and self.info or msg.chat
-- Names for the user and group, respectively. HTML-escaped. -- Names for the user and group, respectively. HTML-escaped.
local from_name = utilities.html_escape( local from_name = utilities.html_escape(
utilities.build_name( utilities.build_name(
msg.from.first_name, msg.from.first_name,
msg.from.last_name msg.from.last_name
) )
) )
local chat_name = utilities.html_escape( local chat_name = utilities.html_escape(
chat.title chat.title
or utilities.build_name(chat.first_name, chat.last_name) or utilities.build_name(chat.first_name, chat.last_name)
) )
-- "Normalize" a group ID so it's not arbitrarily modified by the bot API. -- "Normalize" a group ID so it's not arbitrarily modified by the bot API.
local chat_id = math.abs(chat.id) local chat_id = math.abs(chat.id)
if chat_id > 1000000000000 then chat_id = chat_id - 1000000000000 end if chat_id > 1000000000000 then chat_id = chat_id - 1000000000000 end
-- Do the thing. -- Do the thing.
local output = string.format( local output = string.format(
'You are %s <code>[%s]</code>, and you are messaging %s <code>[%s]</code>.', 'You are %s <code>[%s]</code>, and you are messaging %s <code>[%s]</code>.',
msg.from.username and string.format( msg.from.username and string.format(
'@%s, also known as <b>%s</b>', '@%s, also known as <b>%s</b>',
msg.from.username, msg.from.username,
from_name from_name
) or '<b>' .. from_name .. '</b>', ) or '<b>' .. from_name .. '</b>',
msg.from.id, msg.from.id,
msg.chat.username and string.format( msg.chat.username and string.format(
'@%s, also known as <b>%s</b>', '@%s, also known as <b>%s</b>',
chat.username, chat.username,
chat_name chat_name
) or '<b>' .. chat_name .. '</b>', ) or '<b>' .. chat_name .. '</b>',
chat_id chat_id
) )
bindings.sendMessage(self, { bindings.sendMessage(self, {
chat_id = msg.chat.id, chat_id = msg.chat.id,
reply_to_message_id = msg.message_id, reply_to_message_id = msg.message_id,
disable_web_page_preview = true, disable_web_page_preview = true,
parse_mode = 'HTML', parse_mode = 'HTML',
text = output text = output
}) })
end end
return whoami return whoami

View File

@ -8,83 +8,83 @@ local utilities = require('otouto.utilities')
wikipedia.command = 'wikipedia <query>' wikipedia.command = 'wikipedia <query>'
function wikipedia:init(config) function wikipedia:init(config)
wikipedia.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('wikipedia', true):t('wiki', true):t('w', true).table wikipedia.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('wikipedia', true):t('wiki', true):t('w', true).table
wikipedia.doc = config.cmd_pat .. [[wikipedia <query> wikipedia.doc = config.cmd_pat .. [[wikipedia <query>
Returns an article from Wikipedia. Returns an article from Wikipedia.
Aliases: ]] .. config.cmd_pat .. 'w, ' .. config.cmd_pat .. 'wiki' Aliases: ]] .. config.cmd_pat .. 'w, ' .. config.cmd_pat .. 'wiki'
wikipedia.search_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=' wikipedia.search_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch='
wikipedia.res_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&prop=extracts&format=json&exchars=4000&exsectionformat=plain&titles=' wikipedia.res_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&prop=extracts&format=json&exchars=4000&exsectionformat=plain&titles='
wikipedia.art_url = 'https://' .. config.lang .. '.wikipedia.org/wiki/' wikipedia.art_url = 'https://' .. config.lang .. '.wikipedia.org/wiki/'
end end
function wikipedia:action(msg, config) function wikipedia:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, wikipedia.doc, true) utilities.send_reply(self, msg, wikipedia.doc, true)
return return
end end
local jstr, code = HTTPS.request(wikipedia.search_url .. URL.escape(input)) local jstr, code = HTTPS.request(wikipedia.search_url .. URL.escape(input))
if code ~= 200 then if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
if data.query.searchinfo.totalhits == 0 then if data.query.searchinfo.totalhits == 0 then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local title local title
for _, v in ipairs(data.query.search) do for _, v in ipairs(data.query.search) do
if not v.snippet:match('may refer to:') then if not v.snippet:match('may refer to:') then
title = v.title title = v.title
break break
end end
end end
if not title then if not title then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local res_jstr, res_code = HTTPS.request(wikipedia.res_url .. URL.escape(title)) local res_jstr, res_code = HTTPS.request(wikipedia.res_url .. URL.escape(title))
if res_code ~= 200 then if res_code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local _, text = next(JSON.decode(res_jstr).query.pages) local _, text = next(JSON.decode(res_jstr).query.pages)
if not text then if not text then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
text = text.extract text = text.extract
-- Remove crap and take only the first paragraph. -- Remove crap and take only the first paragraph.
text = text:gsub('</?.->', ''):gsub('%[.+%]', '') text = text:gsub('</?.->', ''):gsub('%[.+%]', '')
local l = text:find('\n') local l = text:find('\n')
if l then if l then
text = text:sub(1, l-1) text = text:sub(1, l-1)
end end
local url = wikipedia.art_url .. URL.escape(title) local url = wikipedia.art_url .. URL.escape(title)
title = utilities.html_escape(title) title = utilities.html_escape(title)
-- If the beginning of the article is the title, embolden that. -- If the beginning of the article is the title, embolden that.
-- Otherwise, we'll add a title in bold. -- Otherwise, we'll add a title in bold.
local short_title = title:gsub('%(.+%)', '') local short_title = title:gsub('%(.+%)', '')
local combined_text, count = text:gsub('^'..short_title, '<b>'..short_title..'</b>') local combined_text, count = text:gsub('^'..short_title, '<b>'..short_title..'</b>')
local body local body
if count == 1 then if count == 1 then
body = combined_text body = combined_text
else else
body = '<b>' .. title .. '</b>\n' .. text body = '<b>' .. title .. '</b>\n' .. text
end end
local output = string.format( local output = string.format(
'%s\n<a href="%s">Read more.</a>', '%s\n<a href="%s">Read more.</a>',
body, body,
utilities.html_escape(url) utilities.html_escape(url)
) )
utilities.send_message(self, msg.chat.id, output, true, nil, 'html') utilities.send_message(self, msg.chat.id, output, true, nil, 'html')
end end
return wikipedia return wikipedia

View File

@ -9,44 +9,44 @@ xkcd.base_url = 'https://xkcd.com/info.0.json'
xkcd.strip_url = 'http://xkcd.com/%s/info.0.json' xkcd.strip_url = 'http://xkcd.com/%s/info.0.json'
function xkcd:init(config) function xkcd:init(config)
xkcd.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('xkcd', true).table xkcd.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('xkcd', true).table
xkcd.doc = config.cmd_pat .. [[xkcd [i] xkcd.doc = config.cmd_pat .. [[xkcd [i]
Returns the latest xkcd strip and its alt text. If a number is given, returns that number strip. If "r" is passed in place of a number, returns a random strip.]] Returns the latest xkcd strip and its alt text. If a number is given, returns that number strip. If "r" is passed in place of a number, returns a random strip.]]
local jstr = HTTP.request(xkcd.base_url) local jstr = HTTP.request(xkcd.base_url)
if jstr then if jstr then
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
if data then if data then
xkcd.latest = data.num xkcd.latest = data.num
end end
end end
xkcd.latest = xkcd.latest or 1700 xkcd.latest = xkcd.latest or 1700
end end
function xkcd:action(msg, config) function xkcd:action(msg, config)
local input = utilities.get_word(msg.text, 2) local input = utilities.get_word(msg.text, 2)
if input == 'r' then if input == 'r' then
input = math.random(xkcd.latest) input = math.random(xkcd.latest)
elseif tonumber(input) then elseif tonumber(input) then
input = tonumber(input) input = tonumber(input)
else else
input = xkcd.latest input = xkcd.latest
end end
local url = xkcd.strip_url:format(input) local url = xkcd.strip_url:format(input)
local jstr, code = HTTP.request(url) local jstr, code = HTTP.request(url)
if code == 404 then if code == 404 then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
elseif code ~= 200 then elseif code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
else else
local data = JSON.decode(jstr) local data = JSON.decode(jstr)
local output = string.format('*%s (*[%s](%s)*)*\n_%s_', local output = string.format('*%s (*[%s](%s)*)*\n_%s_',
data.safe_title:gsub('*', '*\\**'), data.safe_title:gsub('*', '*\\**'),
data.num, data.num,
data.img, data.img,
data.alt:gsub('_', '_\\__') data.alt:gsub('_', '_\\__')
) )
utilities.send_message(self, msg.chat.id, output, false, nil, true) utilities.send_message(self, msg.chat.id, output, false, nil, true)
end end
end end
return xkcd return xkcd

View File

@ -8,12 +8,12 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function youtube:init(config) function youtube:init(config)
assert(config.google_api_key, assert(config.google_api_key,
'youtube.lua requires a Google API key from http://console.developers.google.com.' 'youtube.lua requires a Google API key from http://console.developers.google.com.'
) )
youtube.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('youtube', true):t('yt', true).table youtube.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('youtube', true):t('yt', true).table
youtube.doc = config.cmd_pat .. [[youtube <query> youtube.doc = config.cmd_pat .. [[youtube <query>
Returns the top result from YouTube. Returns the top result from YouTube.
Alias: ]] .. config.cmd_pat .. 'yt' Alias: ]] .. config.cmd_pat .. 'yt'
end end
@ -22,32 +22,32 @@ youtube.command = 'youtube <query>'
function youtube:action(msg, config) function youtube:action(msg, config)
local input = utilities.input_from_msg(msg) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_reply(self, msg, youtube.doc, true) utilities.send_reply(self, msg, youtube.doc, true)
return return
end end
local url = 'https://www.googleapis.com/youtube/v3/search?key=' .. config.google_api_key .. '&type=video&part=snippet&maxResults=4&q=' .. URL.escape(input) local url = 'https://www.googleapis.com/youtube/v3/search?key=' .. config.google_api_key .. '&type=video&part=snippet&maxResults=4&q=' .. URL.escape(input)
local jstr, res = HTTPS.request(url) local jstr, res = HTTPS.request(url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local jdat = JSON.decode(jstr) local jdat = JSON.decode(jstr)
if jdat.pageInfo.totalResults == 0 then if jdat.pageInfo.totalResults == 0 then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local vid_url = 'https://www.youtube.com/watch?v=' .. jdat.items[1].id.videoId local vid_url = 'https://www.youtube.com/watch?v=' .. jdat.items[1].id.videoId
local vid_title = jdat.items[1].snippet.title local vid_title = jdat.items[1].snippet.title
vid_title = vid_title:gsub('%(.+%)',''):gsub('%[.+%]','') vid_title = vid_title:gsub('%(.+%)',''):gsub('%[.+%]','')
local output = '[' .. vid_title .. '](' .. vid_url .. ')' local output = '[' .. vid_title .. '](' .. vid_url .. ')'
utilities.send_message(self, msg.chat.id, output, false, nil, true) utilities.send_message(self, msg.chat.id, output, false, nil, true)
end end

View File

@ -15,47 +15,47 @@ local bindings = require('otouto.bindings')
-- Edit: To keep things working and allow for HTML messages, you can now pass a -- Edit: To keep things working and allow for HTML messages, you can now pass a
-- string for use_markdown and that will be sent as the parse mode. -- string for use_markdown and that will be sent as the parse mode.
function utilities:send_message(chat_id, text, disable_web_page_preview, reply_to_message_id, use_markdown) function utilities:send_message(chat_id, text, disable_web_page_preview, reply_to_message_id, use_markdown)
local parse_mode local parse_mode
if type(use_markdown) == 'string' then if type(use_markdown) == 'string' then
parse_mode = use_markdown parse_mode = use_markdown
elseif use_markdown == true then elseif use_markdown == true then
parse_mode = 'markdown' parse_mode = 'markdown'
end end
return bindings.request(self, 'sendMessage', { return bindings.request(self, 'sendMessage', {
chat_id = chat_id, chat_id = chat_id,
text = text, text = text,
disable_web_page_preview = disable_web_page_preview, disable_web_page_preview = disable_web_page_preview,
reply_to_message_id = reply_to_message_id, reply_to_message_id = reply_to_message_id,
parse_mode = parse_mode parse_mode = parse_mode
} ) } )
end end
function utilities:send_reply(old_msg, text, use_markdown) function utilities:send_reply(old_msg, text, use_markdown)
return utilities.send_message(self, old_msg.chat.id, text, true, old_msg.message_id, use_markdown) return utilities.send_message(self, old_msg.chat.id, text, true, old_msg.message_id, use_markdown)
end end
-- get the indexed word in a string -- get the indexed word in a string
function utilities.get_word(s, i) function utilities.get_word(s, i)
s = s or '' s = s or ''
i = i or 1 i = i or 1
local n = 0 local n = 0
for w in s:gmatch('%g+') do for w in s:gmatch('%g+') do
n = n + 1 n = n + 1
if n == i then return w end if n == i then return w end
end end
return false return false
end end
-- Returns the string after the first space. -- Returns the string after the first space.
function utilities.input(s) function utilities.input(s)
if not s:find(' ') then if not s:find(' ') then
return false return false
end end
return s:sub(s:find(' ')+1) return s:sub(s:find(' ')+1)
end end
function utilities.input_from_msg(msg) function utilities.input_from_msg(msg)
return utilities.input(msg.text) or (msg.reply_to_message and #msg.reply_to_message.text > 0 and msg.reply_to_message.text) or false return utilities.input(msg.text) or (msg.reply_to_message and #msg.reply_to_message.text > 0 and msg.reply_to_message.text) or false
end end
-- Calculates the length of the given string as UTF-8 characters -- Calculates the length of the given string as UTF-8 characters
@ -72,180 +72,180 @@ end
-- Trims whitespace from a string. -- Trims whitespace from a string.
function utilities.trim(str) function utilities.trim(str)
local s = str:gsub('^%s*(.-)%s*$', '%1') local s = str:gsub('^%s*(.-)%s*$', '%1')
return s return s
end end
-- Loads a JSON file as a table. -- Loads a JSON file as a table.
function utilities.load_data(filename) function utilities.load_data(filename)
local f = io.open(filename) local f = io.open(filename)
if f then if f then
local s = f:read('*all') local s = f:read('*all')
f:close() f:close()
return JSON.decode(s) return JSON.decode(s)
else else
return {} return {}
end end
end end
-- Saves a table to a JSON file. -- Saves a table to a JSON file.
function utilities.save_data(filename, data) function utilities.save_data(filename, data)
local s = JSON.encode(data) local s = JSON.encode(data)
local f = io.open(filename, 'w') local f = io.open(filename, 'w')
f:write(s) f:write(s)
f:close() f:close()
end end
-- Gets coordinates for a location. Used by gMaps.lua, time.lua, weather.lua. -- Gets coordinates for a location. Used by gMaps.lua, time.lua, weather.lua.
function utilities.get_coords(input, config) function utilities.get_coords(input, config)
local url = 'http://maps.googleapis.com/maps/api/geocode/json?address=' .. URL.escape(input) local url = 'http://maps.googleapis.com/maps/api/geocode/json?address=' .. URL.escape(input)
local jstr, res = HTTP.request(url) local jstr, res = HTTP.request(url)
if res ~= 200 then if res ~= 200 then
return config.errors.connection return config.errors.connection
end end
local jdat = JSON.decode(jstr) local jdat = JSON.decode(jstr)
if jdat.status == 'ZERO_RESULTS' then if jdat.status == 'ZERO_RESULTS' then
return config.errors.results return config.errors.results
end end
return { return {
lat = jdat.results[1].geometry.location.lat, lat = jdat.results[1].geometry.location.lat,
lon = jdat.results[1].geometry.location.lng lon = jdat.results[1].geometry.location.lng
} }
end end
-- Get the number of values in a key/value table. -- Get the number of values in a key/value table.
function utilities.table_size(tab) function utilities.table_size(tab)
local i = 0 local i = 0
for _,_ in pairs(tab) do for _,_ in pairs(tab) do
i = i + 1 i = i + 1
end end
return i return i
end end
-- Just an easy way to get a user's full name. -- Just an easy way to get a user's full name.
-- Alternatively, abuse it to concat two strings like I do. -- Alternatively, abuse it to concat two strings like I do.
function utilities.build_name(first, last) function utilities.build_name(first, last)
if last then if last then
return first .. ' ' .. last return first .. ' ' .. last
else else
return first return first
end end
end end
function utilities:resolve_username(input) function utilities:resolve_username(input)
input = input:gsub('^@', '') input = input:gsub('^@', '')
for _, user in pairs(self.database.users) do for _, user in pairs(self.database.users) do
if user.username and user.username:lower() == input:lower() then if user.username and user.username:lower() == input:lower() then
local t = {} local t = {}
for key, val in pairs(user) do for key, val in pairs(user) do
t[key] = val t[key] = val
end end
return t return t
end end
end end
end end
function utilities:handle_exception(err, message, config) function utilities:handle_exception(err, message, config)
local output = string.format( local output = string.format(
'\n[%s]\n%s: %s\n%s\n', '\n[%s]\n%s: %s\n%s\n',
os.date('%F %T'), os.date('%F %T'),
self.info.username, self.info.username,
err or '', err or '',
message message
) )
if config.log_chat then if config.log_chat then
output = '```' .. output .. '```' output = '```' .. output .. '```'
utilities.send_message(self, config.log_chat, output, true, nil, true) utilities.send_message(self, config.log_chat, output, true, nil, true)
else else
print(output) print(output)
end end
end end
function utilities.download_file(url, filename) function utilities.download_file(url, filename)
if not filename then if not filename then
filename = url:match('.+/(.-)$') or os.time() filename = url:match('.+/(.-)$') or os.time()
filename = '/tmp/' .. filename filename = '/tmp/' .. filename
end end
local body = {} local body = {}
local doer = HTTP local doer = HTTP
local do_redir = true local do_redir = true
if url:match('^https') then if url:match('^https') then
doer = HTTPS doer = HTTPS
do_redir = false do_redir = false
end end
local _, res = doer.request{ local _, res = doer.request{
url = url, url = url,
sink = ltn12.sink.table(body), sink = ltn12.sink.table(body),
redirect = do_redir redirect = do_redir
} }
if res ~= 200 then return false end if res ~= 200 then return false end
local file = io.open(filename, 'w+') local file = io.open(filename, 'w+')
file:write(table.concat(body)) file:write(table.concat(body))
file:close() file:close()
return filename return filename
end end
function utilities.md_escape(text) function utilities.md_escape(text)
return text:gsub('_', '\\_') return text:gsub('_', '\\_')
:gsub('%[', '\\['):gsub('%]', '\\]') :gsub('%[', '\\['):gsub('%]', '\\]')
:gsub('%*', '\\*'):gsub('`', '\\`') :gsub('%*', '\\*'):gsub('`', '\\`')
end end
function utilities.html_escape(text) function utilities.html_escape(text)
return text:gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;') return text:gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;')
end end
utilities.triggers_meta = {} utilities.triggers_meta = {}
utilities.triggers_meta.__index = utilities.triggers_meta utilities.triggers_meta.__index = utilities.triggers_meta
function utilities.triggers_meta:t(pattern, has_args) function utilities.triggers_meta:t(pattern, has_args)
local username = self.username:lower() local username = self.username:lower()
table.insert(self.table, '^'..self.cmd_pat..pattern..'$') table.insert(self.table, '^'..self.cmd_pat..pattern..'$')
table.insert(self.table, '^'..self.cmd_pat..pattern..'@'..username..'$') table.insert(self.table, '^'..self.cmd_pat..pattern..'@'..username..'$')
if has_args then if has_args then
table.insert(self.table, '^'..self.cmd_pat..pattern..'%s+[^%s]*') table.insert(self.table, '^'..self.cmd_pat..pattern..'%s+[^%s]*')
table.insert(self.table, '^'..self.cmd_pat..pattern..'@'..username..'%s+[^%s]*') table.insert(self.table, '^'..self.cmd_pat..pattern..'@'..username..'%s+[^%s]*')
end end
return self return self
end end
function utilities.triggers(username, cmd_pat, trigger_table) function utilities.triggers(username, cmd_pat, trigger_table)
local self = setmetatable({}, utilities.triggers_meta) local self = setmetatable({}, utilities.triggers_meta)
self.username = username self.username = username
self.cmd_pat = cmd_pat self.cmd_pat = cmd_pat
self.table = trigger_table or {} self.table = trigger_table or {}
return self return self
end end
function utilities.with_http_timeout(timeout, fun) function utilities.with_http_timeout(timeout, fun)
local original = HTTP.TIMEOUT local original = HTTP.TIMEOUT
HTTP.TIMEOUT = timeout HTTP.TIMEOUT = timeout
fun() fun()
HTTP.TIMEOUT = original HTTP.TIMEOUT = original
end end
function utilities.pretty_float(x) function utilities.pretty_float(x)
if x % 1 == 0 then if x % 1 == 0 then
return tostring(math.floor(x)) return tostring(math.floor(x))
else else
return tostring(x) return tostring(x)
end end
end end
-- This table will store unsavory characters that are not properly displayed, -- This table will store unsavory characters that are not properly displayed,
-- or are just not fun to type. -- or are just not fun to type.
utilities.char = { utilities.char = {
zwnj = '', zwnj = '',
arabic = '[\216-\219][\128-\191]', arabic = '[\216-\219][\128-\191]',
rtl_override = '', rtl_override = '',
rtl_mark = '', rtl_mark = '',
em_dash = '', em_dash = '',
utf_8 = '[%z\1-\127\194-\244][\128-\191]', utf_8 = '[%z\1-\127\194-\244][\128-\191]',
} }
utilities.set_meta = {} utilities.set_meta = {}
@ -283,7 +283,7 @@ end
-- More to be added. -- More to be added.
utilities.style = {} utilities.style = {}
utilities.style.enquote = function(title, body) utilities.style.enquote = function(title, body)
return '*' .. title:gsub('*', '\\*') .. ':*\n"' .. utilities.md_escape(body) .. '"' return '*' .. title:gsub('*', '\\*') .. ':*\n"' .. utilities.md_escape(body) .. '"'
end end
return utilities return utilities

View File

@ -4,8 +4,8 @@
# config.lua), delete state file after stop, wait five seconds, and restart. # config.lua), delete state file after stop, wait five seconds, and restart.
while true; do while true; do
tg/bin/telegram-cli -P 4567 -E tg/bin/telegram-cli -P 4567 -E
[ -f ~/.telegram-cli/state ] && rm ~/.telegram-cli/state [ -f ~/.telegram-cli/state ] && rm ~/.telegram-cli/state
echo 'tg has stopped. ^C to exit.' echo 'tg has stopped. ^C to exit.'
sleep 5s sleep 5s
done done