otouto 3.14

All help messages and many other things moved from markdown to html.
Eventually, I'd like only things made from user input to use markdown.

cats.lua, rmspic.lua, and dilbert.lua moved to sendPhoto with URL.
xkcd.lua and apod.lua not moved to retain formatting.

Probably a lot of other stuff that I forget about. I should commit more often.
This commit is contained in:
topkecleon 2016-10-04 10:07:15 -04:00
parent bc6727275c
commit 9f760114bd
43 changed files with 324 additions and 356 deletions

View File

@ -67,7 +67,7 @@ Send /help to get started.
max_reminders_private = 50 max_reminders_private = 50
}, },
chatter = { cleverbot = {
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.'
@ -113,6 +113,9 @@ Send /help to get started.
-- Conversation, group, or channel for kick/ban notifications. -- Conversation, group, or channel for kick/ban notifications.
-- Defaults to config.log_chat if left empty. -- Defaults to config.log_chat if left empty.
log_chat = nil, log_chat = nil,
-- Default autoban setting.
-- A user is banned after being autokicked this many times in a day.
autoban = 3,
-- Default antiflood values. -- Default antiflood values.
antiflood = { antiflood = {
text = 5, text = 5,

View File

@ -5,19 +5,7 @@
See the "Bindings" section of README.md for usage information. See the "Bindings" section of README.md for usage information.
Copyright 2016 topkecleon <drew@otou.to> Copyright 2016 topkecleon <drew@otou.to>
This code is licensed under the GNU AGPLv3. See /LICENSE for details.
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
]]-- ]]--
local bindings = {} local bindings = {}
@ -25,7 +13,7 @@ local bindings = {}
local https = require('ssl.https') local https = require('ssl.https')
local json = require('dkjson') local json = require('dkjson')
local ltn12 = require('ltn12') local ltn12 = require('ltn12')
local mp_encode = require('multipart-post').encode local mp = require('multipart-post')
function bindings.init(token) function bindings.init(token)
bindings.BASE_URL = 'https://api.telegram.org/bot' .. token .. '/' bindings.BASE_URL = 'https://api.telegram.org/bot' .. token .. '/'
@ -58,7 +46,7 @@ function bindings.request(method, parameters, file)
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 = bindings.BASE_URL .. method, url = bindings.BASE_URL .. method,
method = 'POST', method = 'POST',
@ -79,8 +67,9 @@ function bindings.request(method, parameters, file)
return false, false return false, false
elseif result.ok then elseif result.ok then
return result return result
elseif result.description == 'Method not found' then
error(method .. ': Method not found.')
else else
assert(result.description ~= 'Method not found', method .. ': Method not found.')
return false, result return false, result
end end
end end

View File

@ -3,26 +3,14 @@
The heart and sole of otouto, ie the init and main loop. The heart and sole of otouto, ie the init and main loop.
Copyright 2016 topkecleon <drew@otou.to> Copyright 2016 topkecleon <drew@otou.to>
This code is licensed under the GNU AGPLv3. See /LICENSE for details.
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
]]-- ]]--
local bot = {} local bot = {}
local bindings -- Bot API bindings. local bindings -- Bot API bindings.
local utilities -- Miscellaneous and shared plugins. local utilities -- Miscellaneous and shared plugins.
bot.version = '3.13' bot.version = '3.14'
-- Function to be run on start and reload. -- Function to be run on start and reload.
function bot:init(config) function bot:init(config)
@ -45,14 +33,11 @@ function bot:init(config)
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.13 -> 1.14
-- Back to administration global ban list; copy over current blacklist. -- "database.reminders" -> "database.remind"
if self.database.version ~= '3.13' then if self.database.version ~= '3.14' then
if self.database.administration then self.database.remind = self.database.reminders
self.database.administration.globalbans = self.database.administration.globalbans or self.database.blacklist or {} self.database.reminders = nil
utilities.save_data(self.database_name, self.database)
self.database = utilities.load_data(self.database_name)
end
end end
-- End migration code. -- End migration code.
@ -76,7 +61,9 @@ function bot:init(config)
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 = '<pre>'..utilities.html_escape(plugin.doc)..'</pre>'
end
if not plugin.triggers then plugin.triggers = {} end if not plugin.triggers then plugin.triggers = {} end
end end
@ -129,13 +116,11 @@ function bot:on_msg_receive(msg, config)
end end
-- Support deep linking. -- Support deep linking.
if msg.text:match('^'..config.cmd_pat..'start .+') then if msg.text:match('^/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,
-- 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
@ -153,18 +138,14 @@ function bot:on_msg_receive(msg, config)
utilities.send_reply(msg, config.errors.generic) utilities.send_reply(msg, config.errors.generic)
end end
utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config.log_chat) utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config.log_chat)
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
return return
end end
end end
end end
end end
msg = nil
end end
-- main -- main

View File

@ -5,19 +5,7 @@
For more documentation, read the the manual (otou.to/rtfm). For more documentation, read the the manual (otou.to/rtfm).
Copyright 2016 topkecleon <drew@otou.to> Copyright 2016 topkecleon <drew@otou.to>
This code is licensed under the GNU AGPLv3. See /LICENSE for details.
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
]]-- ]]--
--[[ --[[
@ -36,6 +24,10 @@
target's autokick counter. Added configuration for default flag settings. target's autokick counter. Added configuration for default flag settings.
1.13.2 - /desc can now be used with a query. 1.13.2 - /desc can now be used with a query.
1.13.3 - Removed activity array. Group listings sorted alphabetically.
Removed unneeded "founded" and "grouptype" variables from group data. Added
default autoban setting to config.
]]-- ]]--
local drua = require('otouto.drua-tg') local drua = require('otouto.drua-tg')
@ -50,7 +42,6 @@ function administration:init(config)
self.database.administration = { self.database.administration = {
admins = {}, admins = {},
groups = {}, groups = {},
activity = {},
autokick_timer = os.date('%d'), autokick_timer = os.date('%d'),
globalbans = {} globalbans = {}
} }
@ -66,22 +57,8 @@ function administration:init(config)
administration.flags = administration.init_flags(config.cmd_pat) administration.flags = administration.init_flags(config.cmd_pat)
administration.init_command(self, config) administration.init_command(self, config)
-- Migration 3.13 -> 3.13.1
for _, group in pairs(self.database.administration.groups) do
for i = 7, 9 do
if group.flags[i] == nil then
group.flags[i] = config.administration.flags[i]
end
end
group.antiflood = group.antiflood or {}
for k, v in pairs(config.administration.antiflood) do
group.antiflood[k] = group.antiflood[k] or config.administration.antiflood[k]
end
end
-- End migration
administration.doc = 'Returns a list of administrated groups.\nUse '..config.cmd_pat..'ahelp for more administrative commands.'
administration.command = 'groups [query]' administration.command = 'groups [query]'
administration.doc = 'Returns a list of administrated groups.\nUse '..config.cmd_pat..'ahelp for more administrative commands.'
-- In the worst case, don't send errors in reply to random messages. -- In the worst case, don't send errors in reply to random messages.
administration.error = false administration.error = false
@ -385,16 +362,15 @@ function administration.init_command(self_, config_)
local from_id_str = tostring(msg.from.id) local from_id_str = tostring(msg.from.id)
local chat_id_str = tostring(msg.chat.id) local chat_id_str = tostring(msg.chat.id)
if rank < 2 then if rank == 0 then -- banned
local from_name = utilities.build_name(msg.from.first_name, msg.from.last_name)
-- banned
if rank == 0 then
user.do_kick = true user.do_kick = true
user.dont_unban = true user.dont_unban = true
user.reason = 'banned' user.reason = 'banned'
user.output = 'Sorry, you are banned from ' .. msg.chat.title .. '.' user.output = 'Sorry, you are banned from ' .. msg.chat.title .. '.'
elseif group.flags[2] and ( -- antisquig elseif rank == 1 then
local from_name = utilities.build_name(msg.from.first_name, msg.from.last_name)
if group.flags[2] and ( -- antisquig
msg.text:match(utilities.char.arabic) msg.text:match(utilities.char.arabic)
or msg.text:match(utilities.char.rtl_override) or msg.text:match(utilities.char.rtl_override)
or msg.text:match(utilities.char.rtl_mark) or msg.text:match(utilities.char.rtl_mark)
@ -526,12 +502,12 @@ function administration.init_command(self_, config_)
drua.rename_chat(msg.chat.id, group.name) drua.rename_chat(msg.chat.id, group.name)
else else
group.name = msg.new_chat_title group.name = msg.new_chat_title
if group.grouptype == 'supergroup' then if msg.chat.type == 'supergroup' then
administration.update_desc(self, msg.chat.id, config) administration.update_desc(self, msg.chat.id, config)
end end
end end
elseif msg.new_chat_photo then elseif msg.new_chat_photo then
if group.grouptype == 'group' then if msg.chat.type == 'group' then
if rank < (group.flags[9] and 2 or 3) then if rank < (group.flags[9] and 2 or 3) then
drua.set_photo(msg.chat.id, group.photo) drua.set_photo(msg.chat.id, group.photo)
else else
@ -541,7 +517,7 @@ function administration.init_command(self_, config_)
group.photo = drua.get_photo(msg.chat.id) group.photo = drua.get_photo(msg.chat.id)
end end
elseif msg.delete_chat_photo then elseif msg.delete_chat_photo then
if group.grouptype == 'group' then if msg.chat.type == 'group' then
if rank < (group.flags[9] and 2 or 3) then if rank < (group.flags[9] and 2 or 3) then
drua.set_photo(msg.chat.id, group.photo) drua.set_photo(msg.chat.id, group.photo)
else else
@ -592,16 +568,6 @@ function administration.init_command(self_, config_)
utilities.send_message(msg.new_chat_member.id, output, true, nil, true) utilities.send_message(msg.new_chat_member.id, output, true, nil, true)
end end
-- Last active time for group listing.
if msg.text:len() > 0 then
for i,v in pairs(self.database.administration.activity) do
if v == chat_id_str then
table.remove(self.database.administration.activity, i)
table.insert(self.database.administration.activity, 1, chat_id_str)
end
end
end
return true return true
end end
@ -613,26 +579,28 @@ function administration.init_command(self_, config_)
command = 'groups \\[query]', command = 'groups \\[query]',
privilege = 1, privilege = 1,
interior = false, interior = false,
doc = 'Returns a list of groups matching the query, or a list of all administrated groups.', doc = 'Returns a list of groups matching a query, or a list of all administrated groups.',
action = function(self, msg, _, config) action = function(self, msg, _, config)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
local search_res = '' local group_list = {}
local grouplist = '' local result_list = {}
for _, chat_id_str in ipairs(self.database.administration.activity) do for _, group in pairs(self.database.administration.groups) do
local group = self.database.administration.groups[chat_id_str]
if (not group.flags[1]) and group.link then -- no unlisted or unlinked groups if (not group.flags[1]) and group.link then -- no unlisted or unlinked groups
grouplist = grouplist .. '• [' .. utilities.md_escape(group.name) .. '](' .. group.link .. ')\n' local line = '• [' .. utilities.md_escape(group.name) .. '](' .. group.link .. ')'
table.insert(group_list, line)
if input and string.match(group.name:lower(), input:lower()) then if input and string.match(group.name:lower(), input:lower()) then
search_res = search_res .. '• [' .. utilities.md_escape(group.name) .. '](' .. group.link .. ')\n' table.insert(result_list, line)
end end
end end
end end
local output local output
if search_res ~= '' then if #result_list > 0 then
output = '*Groups matching* _' .. input .. '_ *:*\n' .. search_res table.sort(result_list)
elseif grouplist ~= '' then output = '*Groups matching* _' .. input:gsub('_', '_\\__') .. '_*:*\n' .. table.concat(result_list, '\n')
output = '*Groups:*\n' .. grouplist elseif #group_list > 0 then
table.sort(group_list)
output = '*Groups:*\n' .. table.concat(group_list, '\n')
else else
output = 'There are currently no listed groups.' output = 'There are currently no listed groups.'
end end
@ -764,13 +732,17 @@ function administration.init_command(self_, config_)
doc = 'Returns the group\'s list of rules, or a specific rule.', doc = 'Returns the group\'s list of rules, or a specific rule.',
action = function(self, msg, group, config) action = function(self, msg, group, config)
local output local output = ''
local input = utilities.get_word(msg.text_lower, 2) local input = utilities.input(msg.text)
input = tonumber(input)
if #group.rules > 0 then if #group.rules > 0 then
if input and group.rules[input] then if input then
output = '*' .. input .. '.* ' .. group.rules[input] for i in input:gmatch('%g+') do
else if group.rules[tonumber(i)] then
output = output .. '*' .. i .. '.* ' .. group.rules[tonumber(i)] .. '\n'
end
end
end
if output == '' or not input then
output = '*Rules for ' .. msg.chat.title .. ':*\n' output = '*Rules for ' .. msg.chat.title .. ':*\n'
for i,v in ipairs(group.rules) do for i,v in ipairs(group.rules) do
output = output .. '*' .. i .. '.* ' .. v .. '\n' output = output .. '*' .. i .. '.* ' .. v .. '\n'
@ -958,7 +930,7 @@ function administration.init_command(self_, config_)
local output = '*MOTD for ' .. msg.chat.title .. ':*\n' .. input local output = '*MOTD for ' .. msg.chat.title .. ':*\n' .. input
utilities.send_message(msg.chat.id, output, true, nil, true) utilities.send_message(msg.chat.id, output, true, nil, true)
end end
if group.grouptype == 'supergroup' then if msg.chat.type == 'supergroup' then
administration.update_desc(self, msg.chat.id, config) administration.update_desc(self, msg.chat.id, config)
end end
else else
@ -1195,7 +1167,7 @@ Use this command to configure the point values for each message type. When a use
group.mods[target.id_str] = true group.mods[target.id_str] = true
group.bans[target.id_str] = nil group.bans[target.id_str] = nil
end end
if group.grouptype == 'supergroup' then if msg.chat.type == 'supergroup' then
local chat_member = bindings.getChatMember{ chat_id = msg.chat.id, user_id = target.id } local chat_member = bindings.getChatMember{ chat_id = msg.chat.id, user_id = target.id }
if chat_member and chat_member.result.status == 'member' then if chat_member and chat_member.result.status == 'member' then
drua.channel_set_admin(msg.chat.id, target.id, 2, s) drua.channel_set_admin(msg.chat.id, target.id, 2, s)
@ -1234,7 +1206,7 @@ Use this command to configure the point values for each message type. When a use
output = output .. target.name .. ' is no longer a moderator.\n' output = output .. target.name .. ' is no longer a moderator.\n'
group.mods[target.id_str] = nil group.mods[target.id_str] = nil
end end
if group.grouptype == 'supergroup' then if msg.chat.type == 'supergroup' then
drua.channel_set_admin(msg.chat.id, target.id, 0, s) drua.channel_set_admin(msg.chat.id, target.id, 0, s)
end end
end end
@ -1270,7 +1242,7 @@ Use this command to configure the point values for each message type. When a use
group.governor = target.id group.governor = target.id
utilities.send_reply(msg, target.name .. ' is the new governor.') utilities.send_reply(msg, target.name .. ' is the new governor.')
end end
if group.grouptype == 'supergroup' then if msg.chat.type == 'supergroup' then
local chat_member = bindings.getChatMember{ chat_id = msg.chat.id, user_id = target.id } local chat_member = bindings.getChatMember{ chat_id = msg.chat.id, user_id = target.id }
if chat_member and chat_member.result.status == 'member' then if chat_member and chat_member.result.status == 'member' then
drua.channel_set_admin(msg.chat.id, target.id, 2) drua.channel_set_admin(msg.chat.id, target.id, 2)
@ -1305,7 +1277,7 @@ Use this command to configure the point values for each message type. When a use
group.governor = msg.from.id group.governor = msg.from.id
utilities.send_reply(msg, target.name .. ' is no longer the governor.') utilities.send_reply(msg, target.name .. ' is no longer the governor.')
end end
if group.grouptype == 'supergroup' then if msg.chat.type == 'supergroup' then
drua.channel_set_admin(msg.chat.id, target.id, 0) drua.channel_set_admin(msg.chat.id, target.id, 0)
administration.update_desc(self, msg.chat.id, config) administration.update_desc(self, msg.chat.id, config)
end end
@ -1447,7 +1419,7 @@ Use this command to configure the point values for each message type. When a use
output = output .. target.name .. ' is not an administrator.\n' output = output .. target.name .. ' is not an administrator.\n'
else else
for chat_id, group in pairs(self.database.administration.groups) do for chat_id, group in pairs(self.database.administration.groups) do
if group.grouptype == 'supergroup' then if tonumber(chat_id) < -1000000000000 then
drua.channel_set_admin(chat_id, target.id, 0, s) drua.channel_set_admin(chat_id, target.id, 0, s)
end end
end end
@ -1482,43 +1454,41 @@ This would add a group and enable the unlisted flag, antibot, and antiflood.
elseif group then elseif group then
utilities.send_reply(msg, 'I am already administrating this group.') utilities.send_reply(msg, 'I am already administrating this group.')
else else
local output = 'I am now administrating this group.' self.database.administration.groups[tostring(msg.chat.id)] = {
local flags = {} name = msg.chat.title,
photo = drua.get_photo(msg.chat.id),
link = drua.export_link(msg.chat.id),
governor = msg.from.id,
mods = {},
bans = {},
autoban = config.administration.autoban,
autokicks = {},
antiflood = {},
flags = {},
rules = {},
tempbans = {}
}
group = self.database.administration.groups[tostring(msg.chat.id)]
for k, v in pairs(config.administration.antiflood) do
group.antiflood[k] = config.administration.antiflood[k]
end
for i = 1, #administration.flags do for i = 1, #administration.flags do
flags[i] = config.administration.flags[i] group.flags[i] = config.administration.flags[i]
end end
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
local output = 'I am now administrating this group.'
if input then if input then
for i in input:gmatch('%g+') do for i in input:gmatch('%g+') do
local n = tonumber(i) local n = tonumber(i)
if n and administration.flags[n] and flags[n] ~= true then if n and administration.flags[n] and group.flags[n] ~= true then
flags[n] = true group.flags[n] = true
output = output .. '\n' .. administration.flags[n].short output = output .. '\n' .. administration.flags[n].short
end end
end end
end end
self.database.administration.groups[tostring(msg.chat.id)] = {
mods = {},
governor = msg.from.id,
bans = {},
flags = flags,
rules = {},
grouptype = msg.chat.type,
name = msg.chat.title,
link = drua.export_link(msg.chat.id),
photo = drua.get_photo(msg.chat.id),
founded = os.time(),
autokicks = {},
autoban = 3,
antiflood = {}
}
for k, v in pairs(config.administration.antiflood) do
self.database.administration.groups[tostring(msg.chat.id)].antiflood[k] = config.administration.antiflood[k]
end
administration.update_desc(self, msg.chat.id, config) administration.update_desc(self, msg.chat.id, config)
table.insert(self.database.administration.activity, tostring(msg.chat.id))
utilities.send_reply(msg, output)
drua.channel_set_admin(msg.chat.id, self.info.id, 2) drua.channel_set_admin(msg.chat.id, self.info.id, 2)
utilities.send_reply(msg, output)
end end
end end
}, },
@ -1537,11 +1507,6 @@ This would add a group and enable the unlisted flag, antibot, and antiflood.
if self.database.administration.groups[input] then if self.database.administration.groups[input] then
local chat_name = self.database.administration.groups[input].name local chat_name = self.database.administration.groups[input].name
self.database.administration.groups[input] = nil self.database.administration.groups[input] = nil
for i,v in ipairs(self.database.administration.activity) do
if v == input then
table.remove(self.database.administration.activity, i)
end
end
output = 'I am no longer administrating _' .. utilities.md_escape(chat_name) .. '_.' output = 'I am no longer administrating _' .. utilities.md_escape(chat_name) .. '_.'
else else
if input == tostring(msg.chat.id) then if input == tostring(msg.chat.id) then

View File

@ -11,12 +11,9 @@ 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 = config.cmd_pat .. [[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.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

View File

@ -21,7 +21,7 @@ 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(msg, bible.doc, true) utilities.send_reply(msg, bible.doc, 'html')
return return
end end

View File

@ -21,11 +21,9 @@ function bing:init(config)
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
@ -33,7 +31,7 @@ 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(msg, bing.doc, true) utilities.send_reply(msg, bing.doc, 'html')
return return
end end

View File

@ -15,7 +15,7 @@ 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(msg, calc.doc, true) utilities.send_reply(msg, calc.doc, 'html')
return return
end end

View File

@ -2,6 +2,7 @@ local cats = {}
local HTTP = require('socket.http') local HTTP = require('socket.http')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
function cats:init(config) function cats:init(config)
if not config.thecatapi_key then if not config.thecatapi_key then
@ -29,9 +30,7 @@ function cats:action(msg, config)
end end
str = str:match('<img src="(.-)">') str = str:match('<img src="(.-)">')
local output = '[Cat!]('..str..')' bindings.sendPhoto{chat_id = msg.chat.id, photo = str}
utilities.send_message(msg.chat.id, output, false, nil, true)
end end

View File

@ -16,45 +16,55 @@ The following markdown syntax is supported:
_italic text_ _italic text_
[text](URL) [text](URL)
`inline fixed-width code` `inline fixed-width code`
```pre-formatted fixed-width code block```]] ```pre-formatted fixed-width code block```]]
end end
function channel:action(msg, config) function channel:action(msg, config)
-- An exercise in using zero early returns. :)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
local output if not input then
if input then utilities.send_reply(msg, channel.doc, 'html')
return
end
local chat_id = utilities.get_word(input, 1) local chat_id = utilities.get_word(input, 1)
local chat, t = bindings.getChat{chat_id = chat_id}
if not chat then
utilities.send_reply(msg, 'Sorry, I was unable to retrieve information for that channel.\n`' .. t.description .. '`', true)
return
elseif chat.result.type ~= 'channel' then
utilities.send_reply(msg, 'Sorry, that group does not appear to be a channel.')
return
end
local admin_list, t = bindings.getChatAdministrators{ chat_id = chat_id } local admin_list, t = bindings.getChatAdministrators{ chat_id = chat_id }
if admin_list then if not admin_list then
utilities.send_reply(msg, 'Sorry, I was unable to retrieve a list of administrators for that channel.\n`' .. t.description .. '`', true)
return
end
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 not is_admin then
utilities.send_reply(msg, 'Sorry, you do not appear to be an administrator for that channel.')
return
end
local text = input:match('\n(.+)') local text = input:match('\n(.+)')
if text then if not text then
utilities.send_reply(msg, 'Please enter a message to be sent on a new line. Markdown is supported.')
return
end
local success, result = utilities.send_message(chat_id, text, true, nil, true) local success, result = utilities.send_message(chat_id, text, true, nil, true)
if success then if success then
output = 'Your message has been sent!' utilities.send_reply(msg, 'Your message has been sent!')
else else
output = 'Sorry, I was unable to send your message.\n`' .. result.description .. '`' utilities.send_reply(msg, 'Sorry, I was unable to send your message.\n`' .. result.description .. '`', true)
end end
else
output = 'Please enter a message to be sent. Markdown is supported.'
end
else
output = 'Sorry, you do not appear to be an administrator for that channel.'
end
else
output = 'Sorry, I was unable to retrieve a list of administrators for that channel.\n`' .. t.description .. '`'
end
else
output = channel.doc
end
utilities.send_reply(msg, output, true)
end end
return channel return channel

View File

@ -13,7 +13,7 @@ function cleverbot:init(config)
'^' .. 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.cleverbot.cleverbot_api
cleverbot.error = false cleverbot.error = false
end end
@ -22,12 +22,12 @@ function cleverbot:action(msg, config)
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(msg.chat.id, config.chatter.connection) utilities.send_message(msg.chat.id, config.cleverbot.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(msg.chat.id, config.chatter.response) utilities.send_message(msg.chat.id, config.cleverbot.response)
return return
end end
utilities.send_message(msg.chat.id, data.clever) utilities.send_message(msg.chat.id, data.clever)

View File

@ -12,9 +12,10 @@ function commit:init(config)
end end
function commit:action(msg) function commit:action(msg)
local output = http.request('http://whatthecommit.com/index.txt') or 'Minor text fixes'
bindings.sendMessage{ bindings.sendMessage{
chat_id = msg.chat.id, chat_id = msg.chat.id,
text = '```\n' .. (http.request('http://whatthecommit.com/index.txt')) .. '\n```', text = '```\n' .. output .. '\n```',
parse_mode = 'Markdown' parse_mode = 'Markdown'
} }
end end

View File

@ -17,7 +17,7 @@ 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(msg.chat.id, currency.doc, true, msg.message_id, true) utilities.send_message(msg.chat.id, currency.doc, true, msg.message_id, 'html')
return return
end end

View File

@ -14,7 +14,7 @@ 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(msg.chat.id, dice.doc, true, msg.message_id, true) utilities.send_message(msg.chat.id, dice.doc, true, msg.message_id, 'html')
return return
end end
@ -25,7 +25,7 @@ function dice:action(msg)
count = 1 count = 1
range = input:match('^d?([%d]+)$') range = input:match('^d?([%d]+)$')
else else
utilities.send_message(msg.chat.id, dice.doc, true, msg.message_id, true) utilities.send_message(msg.chat.id, dice.doc, true, msg.message_id, 'html')
return return
end end

View File

@ -30,19 +30,10 @@ function dilbert:action(msg, config)
return return
end end
local strip_filename = '/tmp/' .. input .. '.gif'
local strip_file = io.open(strip_filename)
if strip_file then
strip_file:close()
strip_file = strip_filename
else
local strip_url = str:match('<meta property="og:image" content="(.-)"/>')
strip_file = utilities.download_file(strip_url, '/tmp/' .. input .. '.gif')
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({ chat_id = msg.chat.id, caption = strip_title }, { photo = strip_file }) local strip_url = str:match('<meta property="og:image" content="(.-)"/>')
bindings.sendPhoto{chat_id = msg.chat.id, photo = strip_url, caption = strip_title}
end end

View File

@ -10,22 +10,18 @@ function echo:init(config)
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(msg.chat.id, echo.doc, true, msg.message_id, true) utilities.send_message(msg.chat.id, echo.doc, true, msg.message_id, 'html')
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 = '<b>Echo:</b>\n"' .. utilities.html_escape(input) .. '"'
else else
output = utilities.md_escape(utilities.char.zwnj..input) output = utilities.html_escape(utilities.char.zwnj..input)
end end
utilities.send_message(msg.chat.id, output, true, nil, true) utilities.send_message(msg.chat.id, output, true, nil, 'html')
end end
end end
return echo return echo

View File

@ -10,7 +10,6 @@ function fortune:init(config)
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

View File

@ -11,6 +11,7 @@ local HTTPS = require('ssl.https')
local URL = require('socket.url') local URL = require('socket.url')
local JSON = require('dkjson') local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
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,
@ -32,7 +33,7 @@ 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(msg, gImages.doc, true) utilities.send_reply(msg, gImages.doc, 'html')
return return
end end
@ -59,13 +60,12 @@ function gImages:action(msg, config)
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 .. ')'
if msg.text_lower:match(gImages.nsfw_trigger) then if msg.text_lower:match(gImages.nsfw_trigger) then
local output = '[' .. img_title .. '](' .. img_url .. ')'
utilities.send_message(msg.chat.id, '*NSFW*\n'..output, true, msg.message_id, true) utilities.send_message(msg.chat.id, '*NSFW*\n'..output, true, msg.message_id, true)
else else
utilities.send_message(msg.chat.id, output, false, msg.message_id, true) bindings.sendPhoto{chat_id = msg.chat.id, photo = img_url, caption = img_title}
end end
end end

View File

@ -19,7 +19,7 @@ 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(msg, gMaps.doc, true) utilities.send_reply(msg, gMaps.doc, 'html')
return return
end end

View File

@ -95,7 +95,7 @@ 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(msg, hearthstone.doc, true) utilities.send_reply(msg, hearthstone.doc, 'html')
return return
end end

View File

@ -20,8 +20,8 @@ function help:action(msg, config)
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 = '<b>Help for</b> <i>' .. plugin.help_word .. '</i><b>:</b>\n' .. plugin.doc
utilities.send_message(msg.chat.id, output, true, nil, true) utilities.send_message(msg.chat.id, output, true, nil, 'html')
return return
end end
end end
@ -36,14 +36,14 @@ function help:action(msg, config)
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]' local comlist = '\n' .. config.cmd_pat .. table.concat(commandlist, '\n' .. config.cmd_pat) .. '\nArguments: <required> [optional]'
help.text = help.text:gsub('%[', '\\[') help.text = '<b>Available commands:</b>' .. utilities.html_escape(comlist)
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(msg.from.id, help.text, true, nil, true) local res = utilities.send_message(msg.from.id, help.text, true, nil, 'html')
if not res then if not res then
utilities.send_reply(msg, 'Please [message me privately](http://telegram.me/' .. self.info.username .. '?start=help) for a list of commands.', true) utilities.send_reply(msg, 'Please <a href="http://telegram.me/' .. self.info.username .. '?start=help">message me privately</a> for a list of commands.', 'html')
elseif msg.chat.type ~= 'private' then elseif msg.chat.type ~= 'private' then
utilities.send_reply(msg, 'I have sent you the requested information in a private message.') utilities.send_reply(msg, 'I have sent you the requested information in a private message.')
end end

View File

@ -16,7 +16,7 @@ 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(msg, imdb.doc, true) utilities.send_reply(msg, imdb.doc, 'html')
return return
end end

View File

@ -18,7 +18,7 @@ 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(msg, isup.doc, true) utilities.send_reply(msg, isup.doc, 'html')
return return
end end

View File

@ -9,19 +9,25 @@ 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('npfull', 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 .. [[npfull [username]
Works like ]] .. config.cmd_pat .. [[np, but returns more info, differently formatted and including album art, if available.
]] .. config.cmd_pat .. [[fmset <username> ]] .. config.cmd_pat .. [[fmset <username>
Sets your last.fm username. Otherwise, ]] .. config.cmd_pat .. [[np will use your Telegram username. Use "]] .. config.cmd_pat .. [[fmset --" to delete it.]] Sets your last.fm username. Otherwise, ]] .. config.cmd_pat .. [[np will use your Telegram username. Use "]] .. config.cmd_pat .. [[fmset --" to delete it.]]
end
lastfm.command = 'lastfm' lastfm.command = 'lastfm'
lastfm.base_url = 'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&api_key=' .. config.lastfm_api_key .. '&user='
end
function lastfm:action(msg, config) function lastfm:action(msg, config)
@ -29,12 +35,12 @@ function lastfm:action(msg, config)
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_lower, '^'..config.cmd_pat..'lastfm') then
utilities.send_message(msg.chat.id, lastfm.doc, true, msg.message_id, true) utilities.send_message(msg.chat.id, lastfm.doc, true, msg.message_id, 'html')
return return
elseif string.match(msg.text, '^'..config.cmd_pat..'fmset') then elseif string.match(msg.text_lower, '^'..config.cmd_pat..'fmset') then
if not input then if not input then
utilities.send_message(msg.chat.id, lastfm.doc, true, msg.message_id, true) utilities.send_message(msg.chat.id, lastfm.doc, true, msg.message_id, 'html')
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(msg, 'Your last.fm username has been forgotten.') utilities.send_reply(msg, 'Your last.fm username has been forgotten.')
@ -45,8 +51,6 @@ function lastfm:action(msg, config)
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 username local username
local alert = '' local alert = ''
if input then if input then
@ -62,13 +66,11 @@ function lastfm:action(msg, config)
return return
end end
url = url .. URL.escape(username) local orig = HTTP.TIMEOUT
HTTP.TIMEOUT = 1
local jstr, res = HTTP.request(lastfm.base_url .. URL.escape(username))
HTTP.TIMEOUT = orig
local jstr, res
utilities.with_http_timeout(
1, function ()
jstr, res = HTTP.request(url)
end)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(msg, config.errors.connection) utilities.send_reply(msg, config.errors.connection)
return return
@ -80,29 +82,51 @@ function lastfm:action(msg, config)
return return
end end
jdat = jdat.recenttracks.track[1] or jdat.recenttracks.track local track = jdat.recenttracks.track[1] or jdat.recenttracks.track
if not jdat then if not track then
utilities.send_reply(msg, 'No history for this user.' .. alert) utilities.send_reply(msg, 'No history for this user.' .. alert)
return return
end end
local output = input or msg.from.first_name local output = utilities.html_escape(input or msg.from.first_name)
output = '🎵 ' .. output if track['@attr'] and track['@attr'].nowplaying then
output = output .. ' is currently listening to:'
if jdat['@attr'] and jdat['@attr'].nowplaying then
output = output .. ' is currently listening to:\n'
else else
output = output .. ' last listened to:\n' output = output .. ' last listened to:'
end end
local title = jdat.name or 'Unknown' if msg.text_lower:match('^' .. config.cmd_pat .. 'npfull') then
local artist = 'Unknown'
if jdat.artist then output = '<b>' .. utilities.html_escape(output) .. '</b>'
artist = jdat.artist['#text'] if track.name and #track.name > 0 then
output = output .. '\n🎵 ' .. utilities.html_escape(track.name)
else
output = output .. '\n🎵 Unknown'
end
if track.artist and track.artist['#text'] and #track.artist['#text'] > 0 then
output = output .. '\n👤 ' .. utilities.html_escape(track.artist['#text'])
end
if track.album and track.album['#text'] and #track.album['#text'] > 0 then
output = output .. '\n💿 ' .. utilities.html_escape(track.album['#text'])
end
-- album art
if track.image and track.image[3] and #track.image[3]['#text'] > 0 then
output = '<a href="' .. utilities.html_escape(track.image[3]['#text']) .. '">' .. utilities.char.zwnj .. '</a>' .. output
end end
output = output .. title .. ' - ' .. artist .. alert else
utilities.send_message(msg.chat.id, output)
output = output .. '\n'
if track.artist and track.artist['#text'] and #track.artist['#text'] > 0 then
output = output .. utilities.html_escape(track.artist['#text']) .. ' - '
end
output = output .. utilities.html_escape((track.name or 'Unknown'))
end
output = output .. alert
utilities.send_message(msg.chat.id, output, nil, nil, 'html')
end end

View File

@ -8,7 +8,7 @@ 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/.-/.-$' }
@ -18,8 +18,7 @@ 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:match('^Did you mean:\n"(.+)"$') or output
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
@ -33,8 +32,8 @@ function patterns:action(msg)
utilities.send_reply(msg, 'Malformed pattern!') utilities.send_reply(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 = '<b>Did you mean:</b>\n"' .. utilities.html_escape(output) .. '"'
utilities.send_reply(msg.reply_to_message, output, true) utilities.send_reply(msg.reply_to_message, output, 'html')
end end
end end

View File

@ -19,7 +19,7 @@ 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(msg, pokedex.doc, true) utilities.send_reply(msg, pokedex.doc, 'html')
return return
end end

View File

@ -73,7 +73,7 @@ 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(msg, pgc.doc, true) utilities.send_reply(msg, pgc.doc, 'html')
return return
end end
input = input .. '\n' input = input .. '\n'

View File

@ -14,7 +14,7 @@ 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(msg, preview.doc, true) utilities.send_reply(msg, preview.doc, 'html')
return return
end end

View File

@ -132,7 +132,8 @@ local puns = {
"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.",
"Did you hear about the restaurant on the International Space Station? It lacks atmosphere, but the food is out of this world."
} }
function pun:action(msg) function pun:action(msg)

View File

@ -1,9 +1,9 @@
local reddit = {} local reddit = {}
local HTTP = require('socket.http')
local URL = require('socket.url') local URL = require('socket.url')
local JSON = require('dkjson') local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local HTTPS = require('ssl.https')
reddit.command = 'reddit [r/subreddit | query]' reddit.command = 'reddit [r/subreddit | query]'
@ -65,7 +65,7 @@ function reddit:action(msg, config)
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 = HTTPS.request(url)
if res ~= 200 then if res ~= 200 then
utilities.send_reply(msg, config.errors.connection) utilities.send_reply(msg, config.errors.connection)
else else

View File

@ -5,26 +5,31 @@ 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.remind = self.database.remind 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
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(msg, remind.doc, true) utilities.send_reply(msg, remind.doc, 'html')
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(msg, remind.doc, true) utilities.send_reply(msg, remind.doc, 'html')
return return
end end
@ -40,7 +45,7 @@ function remind:action(msg, config)
elseif utilities.input(input) then elseif utilities.input(input) then
message = utilities.input(input) message = utilities.input(input)
else else
utilities.send_reply(msg, remind.doc, true) utilities.send_reply(msg, remind.doc, 'html')
return return
end end
@ -51,13 +56,13 @@ function remind:action(msg, config)
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.remind[chat_id_str] = self.database.remind[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.remind[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.remind[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.remind[chat_id_str], {
time = os.time() + (duration * 60), time = os.time() + (duration * 60),
message = message message = message
}) })
@ -73,14 +78,14 @@ 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.remind) 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 = '<b>Reminder:</b>\n"' .. utilities.html_escape(reminder.message) .. '"'
local res = utilities.send_message(chat_id, output, true, nil, true) local res = utilities.send_message(chat_id, output, true, nil, 'html')
-- 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

View File

@ -5,30 +5,22 @@ local bindings = require('otouto.bindings')
local rms = {} local rms = {}
function rms:init(config) function rms:init(config)
rms.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('rms').table
rms.command = 'rms'
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 rms.triggers = {}
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, rms.BASE_URL .. link)
end end
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{ chat_id = msg.chat.id, action = 'upload_photo' } bindings.sendPhoto{chat_id = msg.chat.id, photo = rms.LIST[math.random(#rms.LIST)]}
local choice = rms.LIST[math.random(#rms.LIST)]
local filename = '/tmp/' .. choice
local image_file = io.open(filename)
if image_file then
image_file:close()
else
utilities.download_file(rms.BASE_URL .. choice, filename)
end
bindings.sendPhoto({ chat_id = msg.chat.id }, { photo = filename })
end end
return rms return rms

View File

@ -22,7 +22,7 @@ function setandget:action(msg, config)
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(msg.chat.id, setandget.doc, true, nil, true) utilities.send_message(msg.chat.id, setandget.doc, true, nil, 'html')
return return
end end
@ -30,7 +30,7 @@ function setandget:action(msg, config)
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(msg.chat.id, setandget.doc, true, nil, true) utilities.send_message(msg.chat.id, setandget.doc, true, nil, 'html')
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(msg.chat.id, 'That value has been deleted.') utilities.send_message(msg.chat.id, 'That value has been deleted.')

View File

@ -14,7 +14,7 @@ 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(msg, shout.doc, true) utilities.send_reply(msg, shout.doc, 'html')
return return
end end

View File

@ -40,7 +40,7 @@ local corrected_numbers = {
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(msg, starwars.doc, true) utilities.send_reply(msg, starwars.doc, 'html')
return return
end end

View File

@ -16,7 +16,7 @@ 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(msg, time.doc, true) utilities.send_reply(msg, time.doc, 'html')
return return
end end

View File

@ -22,7 +22,7 @@ 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(msg, translate.doc, true) utilities.send_reply(msg, translate.doc, 'html')
return return
end end
@ -39,7 +39,9 @@ function translate:action(msg, config)
return return
end end
utilities.send_reply(msg.reply_to_message or msg, utilities.style.enquote('Translation', data.text[1]), true) local output = '<b>Translation:</b>\n"' .. utilities.html_escape(data.text[1]) .. '"'
utilities.send_reply(msg.reply_to_message or msg, output, 'html')
end end
return translate return translate

View File

@ -11,18 +11,16 @@ 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(msg, urbandictionary.doc, true) utilities.send_reply(msg, urbandictionary.doc, 'html')
return return
end end
@ -38,13 +36,13 @@ function urbandictionary:action(msg, config)
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('<b>%s</b>\n\n%s\n\n<i>%s</i>',
data.list[1].word:gsub('*', '*\\**'), utilities.html_escape(data.list[1].word),
utilities.trim(utilities.md_escape(data.list[1].definition)), utilities.trim(utilities.html_escape(data.list[1].definition)),
utilities.trim((data.list[1].example or '')):gsub('_', '_\\__') utilities.trim(utilities.html_escape(data.list[1].example or ''))
) )
end end
utilities.send_reply(msg, output, true) utilities.send_reply(msg, output, 'html')
end end
return urbandictionary return urbandictionary

44
otouto/plugins/wait.lua Normal file
View File

@ -0,0 +1,44 @@
local utilities = require('otouto.utilities')
local bot = require('otouto.bot')
local wait = {}
function wait:init(config)
self.database.wait = self.database.wait or {}
wait.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('wait', true).table
wait.command = '/wait <duration> <command> [args]'
wait.doc = config.cmd_pat .. [[wait <duration> <command> [args]
Postpone a command for a given duration, in minutes.
Max duration is 10000.]]
end
-- ex: /wait 15 /calc 5 * 10
function wait:action(msg, config)
local duration = utilities.get_word(msg.text, 2)
duration = tonumber(duration)
local input = msg.text
repeat
input = input:gsub('^' .. config.cmd_pat .. '[Ww][Aa][Ii][Tt] %g+ ', '')
until not input:match('^' .. config.cmd_pat .. '[Ww][Aa][Ii][Tt] %g+ ')
if not input or not duration or duration > 10000 then
utilities.send_reply(msg, wait.doc, 'html')
return
end
msg.date = msg.date + ( duration * 60 )
msg.text = input
table.insert(self.database.wait, msg)
utilities.send_reply(msg, 'Queued.')
end
function wait:cron(config)
local now = os.time() + 1
for k, msg in pairs(self.database.wait) do
if msg.date < now then
msg.date = os.time()
bot.on_msg_receive(self, msg, config)
self.database.wait[k] = nil
end
end
end
return wait

View File

@ -21,7 +21,7 @@ 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(msg, weather.doc, true) utilities.send_reply(msg, weather.doc, 'html')
return return
end end

View File

@ -20,7 +20,7 @@ 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(msg, wikipedia.doc, true) utilities.send_reply(msg, wikipedia.doc, 'html')
return return
end end

View File

@ -24,7 +24,7 @@ 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(msg, youtube.doc, true) utilities.send_reply(msg, youtube.doc, 'html')
return return
end end

View File

@ -3,23 +3,9 @@
Functions shared among otouto plugins. Functions shared among otouto plugins.
Copyright 2016 topkecleon <drew@otou.to> Copyright 2016 topkecleon <drew@otou.to>
This code is licensed under the GNU AGPLv3. See /LICENSE for details.
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
]]-- ]]--
local utilities = {}
local HTTP = require('socket.http') local HTTP = require('socket.http')
local ltn12 = require('ltn12') local ltn12 = require('ltn12')
local HTTPS = require('ssl.https') local HTTPS = require('ssl.https')
@ -30,6 +16,8 @@ local bindings = require('otouto.bindings')
-- If no built-in utf8 is available, load the library. -- If no built-in utf8 is available, load the library.
local utf8 = utf8 or require('lua-utf8') local utf8 = utf8 or require('lua-utf8')
local utilities = {}
-- For the sake of ease to new contributors and familiarity to old contributors, -- For the sake of ease to new contributors and familiarity to old contributors,
-- we'll provide a couple of aliases to real bindings here. -- we'll provide a couple of aliases to real bindings here.
-- 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
@ -159,7 +147,7 @@ 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
@ -260,13 +248,6 @@ function utilities.triggers(username, cmd_pat, trigger_table)
return self return self
end end
function utilities.with_http_timeout(timeout, fun)
local original = HTTP.TIMEOUT
HTTP.TIMEOUT = timeout
fun()
HTTP.TIMEOUT = original
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))
@ -317,13 +298,6 @@ function utilities.set_meta:__len()
return self.__count return self.__count
end end
-- Styling functions to keep things consistent and easily changeable across plugins.
-- More to be added.
utilities.style = {}
utilities.style.enquote = function(title, body)
return '*' .. title:gsub('*', '\\*') .. ':*\n"' .. utilities.md_escape(body) .. '"'
end
-- Converts a gross string back into proper UTF-8. -- Converts a gross string back into proper UTF-8.
-- Useful for fixing improper encoding caused by bad JSON escaping. -- Useful for fixing improper encoding caused by bad JSON escaping.
function utilities.fix_utf8(str) function utilities.fix_utf8(str)