This commit is contained in:
Andreas Bielawski 2016-08-14 16:30:06 +02:00
commit 72262be90c
18 changed files with 191 additions and 326 deletions

View File

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

View File

@ -188,5 +188,3 @@ Das ist die Datenbank-Struktur:
`database.userdata` speichert Daten von verschiedenen Plugins, hierzu wird aber für Brawlbot-Plugins Redis verwendet.
`database.version` speichert die Bot-Version.
* * *

View File

@ -35,44 +35,20 @@ Sende /hilfe, um zu starten
syntax = 'Invalide Syntax.',
chatter_connection = 'Ich möchte gerade nicht reden',
chatter_response = 'Ich weiß nicht, was ich darauf antworten soll.'
}
},
plugins = { -- To enable a plugin, add its name to the list.
'control',
'blacklist',
'about',
'ping',
'whoami',
'nick',
'echo',
'imgblacklist',
'gImages',
'gSearch',
'gMaps',
'wikipedia',
'hackernews',
'imdb',
'calc',
'urbandictionary',
'time',
'dice',
'reddit',
'xkcd',
'slap',
'commit',
'pun',
'currency',
'shout',
'set',
'get',
'patterns',
'9gag',
'shell',
'adfly',
'twitter',
-- Put new plugins above this line.
'help',
'greetings'
remind = {
persist = true,
max_length = 1000,
max_duration = 526000,
max_reminders_group = 10,
max_reminders_private = 50
},
chatter = {
cleverbot_api = 'https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text=',
connection = 'I don\'t feel like talking right now.',
response = 'I don\'t know what to say to that.'
}
}

View File

@ -48,7 +48,7 @@ function bindings:request(method, parameters, file)
end
local response = {}
local body, boundary = MP_ENCODE(parameters)
local success = HTTPS.request{
local success, code = HTTPS.request{
url = self.BASE_URL .. method,
method = 'POST',
headers = {
@ -60,7 +60,7 @@ function bindings:request(method, parameters, file)
}
local data = table.concat(response)
if not success then
print(method .. ': Connection error.')
print(method .. ': Connection error. [' .. code .. ']')
return false, false
else
local result = JSON.decode(data)

View File

@ -3,13 +3,13 @@ local bot = {}
bindings = require('otouto.bindings')
utilities = require('otouto.utilities')
bot.version = '2.2.6'
bot.version = '2.2.6.1'
function bot:init(config) -- The function run when the bot is started or reloaded.
cred_data = load_cred()
assert(
config.bot_api_key and config.bot_api_key ~= '',
config.bot_api_key,
'You did not set your bot token in the config!'
)
self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/'
@ -28,19 +28,22 @@ function bot:init(config) -- The function run when the bot is started or reloade
self.plugins = {} -- Load plugins.
enabled_plugins = load_plugins()
t = {}
for k,v in pairs(enabled_plugins) do
local p = require('otouto.plugins.'..v)
-- print('loading plugin',v)
self.plugins[k] = p
self.plugins[k].name = v
if p.init then p.init(self, config) end
if not p.triggers then p.triggers = t end
end
print('Bot started successfully as:\n@' .. self.info.username .. ', AKA ' .. self.info.first_name ..' ('..self.info.id..')')
self.last_update = self.last_update or 0 -- Set loop variables: Update offset,
self.last_cron = self.last_cron or os.date('%M') -- the time of the last cron job,
self.last_database_save = self.last_database_save or os.date('%H') -- the time of the last database save,
-- Set loop variables
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_database_save = self.last_database_save or os.date('%H') -- Last db save.
self.is_started = true -- and whether or not the bot should be running.
end
@ -179,13 +182,15 @@ function bot:process_inline_query(inline_query, config) -- When an inline query
utilities.answer_inline_query(self, inline_query, nil, 0, true)
end
-- main
function bot:run(config)
bot.init(self, config) -- Actually start the script.
while self.is_started do -- Start a loop while the bot should be running.
local res = bindings.getUpdates(self, { timeout=20, offset = self.last_update+1 } )
bot.init(self, config)
while self.is_started do
-- Update loop
local res = bindings.getUpdates(self, { timeout = 20, offset = self.last_update + 1 } )
if res then
for n=1, #res.result do -- Go through every new message.
-- Iterate over every new message.
for n=1, #res.result do
local v = res.result[n]
self.last_update = v.update_id
if v.inline_query then
@ -200,7 +205,8 @@ function bot:run(config)
print('Connection error while fetching updates.')
end
if self.last_cron ~= os.date('%M') then -- Run cron jobs every minute.
-- Run cron jobs every minute.
if self.last_cron ~= os.date('%M') then
self.last_cron = os.date('%M')
utilities.save_data(self.info.username..'.db', self.database) -- Save the database.
for n=1, #self.plugins do
@ -256,7 +262,7 @@ function match_inline_plugins(self, inline_query, config, plugin)
end
function match_plugins(self, msg, config, plugin)
local match_table = plugin.triggers or {}
local match_table = plugin.triggers
for n=1, #match_table do
local trigger = plugin.triggers[n]
if string.match(msg.text_lower, trigger) then

View File

@ -5,33 +5,16 @@ local bot = require('otouto.bot')
about.command = 'about'
about.doc = '`Sendet Informationen über den Bot.`'
about.triggers = {
function about:init(config)
about.text = config.about_text..'\n[Brawlbot](https://github.com/Brawl345/Brawlbot-v2) v'..bot.version..', basierend auf [Otouto](http://github.com/topkecleon/otouto) von topkecleon.'
about.triggers = {
'/about',
'/start'
}
}
end
function about:action(msg, config)
-- Filthy hack, but here is where we'll stop forwarded messages from hitting
-- other plugins.
-- disabled to restore old behaviour
-- if msg.forward_from then return end
local output = config.about_text .. '\nBrawlbot v'..bot.version..', basierend auf Otouto von topkecleon.'
if
(msg.new_chat_member and msg.new_chat_member.id == self.info.id)
or msg.text_lower:match('^'..config.cmd_pat..'about$')
or msg.text_lower:match('^'..config.cmd_pat..'about@'..self.info.username:lower()..'$')
or msg.text_lower:match('^'..config.cmd_pat..'start$')
or msg.text_lower:match('^'..config.cmd_pat..'start@'..self.info.username:lower()..'$')
then
utilities.send_message(self, msg.chat.id, output, true, nil, true)
return
end
return true
utilities.send_message(self, msg.chat.id, about.text, true, nil, true)
end
return about

View File

@ -5,21 +5,27 @@ function cleverbot:init(config)
"^/cbot (.+)$",
"^[Bb]rawlbot, (.+)$",
}
cleverbot.doc = [[*
]]..config.cmd_pat..[[cbot* _<Text>_*: Befragt den Cleverbot]]
cleverbot.url = config.chatter.cleverbot_api
end
cleverbot.command = 'cbot <Text>'
function cleverbot:action(msg, config)
local text = msg.text
local url = "https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text="..URL.escape(text)
function cleverbot:action(msg, config, matches)
utilities.send_typing(self, msg.chat.id, 'typing')
local query = https.request(url)
if query == nil then utilities.send_reply(self, msg, 'Ein Fehler ist aufgetreten :(') return end
local decode = json.decode(query)
local answer = string.gsub(decode.clever, "&Auml;", "Ä")
local text = matches[1]
local query, code = https.request(cleverbot.url..URL.escape(text))
if code ~= 200 then
utilities.send_reply(self, msg, 'Ich möchte jetzt nicht reden...')
return
end
local data = json.decode(query)
if not data.clever then
utilities.send_reply(self, msg, 'Ich möchte jetzt nicht reden...')
return
end
local answer = string.gsub(data.clever, "&Auml;", "Ä")
local answer = string.gsub(answer, "&auml;", "ä")
local answer = string.gsub(answer, "&Ouml;", "Ö")
local answer = string.gsub(answer, "&ouml;", "ö")

View File

@ -26,7 +26,7 @@ function echo:inline_callback(inline_query, config, matches)
end
function echo:action(msg)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
utilities.send_message(self, msg.chat.id, echo.doc, true, msg.message_id, true)
else

View File

@ -30,15 +30,11 @@ function gMaps:inline_callback(inline_query, config, matches)
end
function gMaps:action(msg, config)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
if msg.reply_to_message and msg.reply_to_message.text then
input = msg.reply_to_message.text
else
utilities.send_message(self, msg.chat.id, gMaps.doc, true, msg.message_id, true)
utilities.send_reply(self, msg, gMaps.doc, true)
return
end
end
utilities.send_typing(self, msg.chat.id, 'find_location')
local coords = utilities.get_coords(input, config)

View File

@ -51,15 +51,11 @@ function gSearch:stringlinks(results, stats)
end
function gSearch:action(msg, config)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
if msg.reply_to_message and msg.reply_to_message.text then
input = msg.reply_to_message.text
else
utilities.send_message(self, msg.chat.id, gSearch.doc, true, msg.message_id, true)
utilities.send_reply(self, msg, gImages.doc, true)
return
end
end
local results, stats = gSearch:googlethat(input, onfig)
if results == '403' then

View File

@ -67,15 +67,11 @@ function imdb:inline_callback(inline_query, config, matches)
end
function imdb:action(msg, config)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
if msg.reply_to_message and msg.reply_to_message.text then
input = msg.reply_to_message.text
else
utilities.send_message(self, msg.chat.id, imdb.doc, true, msg.message_id, true)
utilities.send_reply(self, msg, imdb.doc, true)
return
end
end
local url = BASE_URL..'/?t='..URL.escape(input)
local jstr, res = https.request(url)

View File

@ -2,11 +2,22 @@ local luarun = {}
function luarun:init(config)
luarun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lua', true):t('return', true).table
if config.luarun_serpent then
serpent = require('serpent')
luarun.serialize = function(t)
return serpent.block(t, {comment=false})
end
else
JSON = require('dkjson')
luarun.serialize = function(t)
return JSON.encode(t, {indent=true})
end
end
end
function luarun:action(msg, config)
if msg.from.id ~= config.admin then
if not is_sudo(msg, config) then
return true
end
@ -24,17 +35,18 @@ function luarun:action(msg, config)
local bot = require('otouto.bot')
local bindings = require('otouto.bindings')
local utilities = require('otouto.utilities')
local json = require('dkjson')
local drua = require('otouto.drua-tg')
local JSON = require('dkjson')
local URL = require('socket.url')
local http = require('socket.http')
local https = require('ssl.https')
local HTTP = require('socket.http')
local HTTPS = require('ssl.https')
return function (self, msg, config) ]] .. input .. [[ end
]] )()(self, msg, config)
if output == nil then
output = 'Done!'
else
if type(output) == 'table' then
local s = json.encode(output, {indent=true})
local s = luarun.serialize(output)
if URL.escape(s):len() < 4000 then
output = s
end

View File

@ -1,8 +1,11 @@
local patterns = {}
patterns.triggers = {
'^/?s/.-/.-$'
}
function patterns:init(config)
patterns.command = 's/<Pattern>/<Ersetzung>'
patterns.triggers = {
config.cmd_pat .. '?s/.-/.-$'
}
end
function patterns:action(msg)
if not msg.reply_to_message then return true end

View File

@ -11,11 +11,9 @@ Returns a full-message, "unlinked" preview.
end
function preview:action(msg)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
utilities.send_message(self, msg.chat.id, preview.doc, true, nil, true)
utilities.send_reply(self, msg, preview.doc, true)
return
end
@ -26,19 +24,18 @@ function preview:action(msg)
local res = http.request(input)
if not res then
utilities.send_reply(self, msg, 'Please provide a valid link.')
utilities.send_reply(self, msg, 'Bitte gebe einen validen Link an.')
return
end
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, dieser Link lässt uns keine Vorschau erstellen.')
return
end
-- Invisible zero-width, non-joiner.
local output = '[](' .. input .. ')'
utilities.send_message(self, msg.chat.id, output, false, nil, true)
local output = '<a href="' .. input .. '">' .. utilities.char.zwnj .. '</a>'
utilities.send_message(self, msg.chat.id, output, false, nil, 'HTML')
end
return preview

View File

@ -7,87 +7,78 @@ function remind:init(config)
remind.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('remind', true).table
remind.doc = [[*
]]..config.cmd_pat..[[remind* _<Länge>_ _<Nachricht>_: Erinnert dich in X Minuten an die Nachricht]]
]]..config.cmd_pat..[[remind* _<Länge>_ _<Nachricht>_
Erinnert dich in der angegeben Länge in Minuten an eine Nachricht.
Die maximale Länge einer Erinnerung beträgt %s Buchstaben, die maximale Zeit beträgt %s Minuten, die maximale Anzahl an Erinnerung für eine Gruppe ist %s und für private Chats %s.]]
remind.doc = remind.doc:format(config.remind.max_length, config.remind.max_duration, config.remind.max_reminders_group, config.remind.max_reminders_private)
end
function remind:action(msg)
-- Ensure there are arguments. If not, send doc.
function remind:action(msg, config)
local input = utilities.input(msg.text)
if not input then
utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true)
utilities.send_reply(self, msg, remind.doc, true)
return
end
-- Ensure first arg is a number. If not, send doc.
local duration = utilities.get_word(input, 1)
if not tonumber(duration) then
utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true)
local duration = tonumber(utilities.get_word(input, 1))
if not duration then
utilities.send_reply(self, msg, remind.doc, true)
return
end
-- Duration must be between one minute and one day (approximately).
duration = tonumber(duration)
if duration < 1 then
duration = 1
elseif duration > 1440 then
duration = 1440
elseif duration > config.remind.max_duration then
duration = config.remind.max_duration
end
-- Ensure there is a second arg.
local message = utilities.input(input)
if not message then
utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true)
utilities.send_reply(self, msg, remind.doc, true)
return
end
-- Make a database entry for the group/user if one does not exist.
self.database.reminders[msg.chat.id_str] = self.database.reminders[msg.chat.id_str] or {}
-- Limit group reminders to 10 and private reminders to 50.
if msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[msg.chat.id_str]) > 9 then
utilities.send_reply(self, msg, 'Diese Gruppe hat schon zehn Erinnerungen!')
return
elseif msg.chat.type == 'private' and utilities.table_size(self.database.reminders[msg.chat.id_str]) > 49 then
utilities.send_reply(msg, 'Du hast schon 50 Erinnerungen!')
if #message > config.remind.max_length then
utilities.send_reply(self, msg, 'Die maximale Länge einer Erinnerung ist ' .. config.remind.max_length .. '.')
return
end
local chat_id_str = tostring(msg.chat.id)
local output
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
output = 'Sorry, du kannst keine Erinnerungen mehr hinzufügen.'
elseif msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_group then
output = 'Sorry, diese Gruppe kann keine Erinnerungen mehr hinzufügen.'
else
-- Put together the reminder with the expiration, message, and message to reply to.
local timestamp = os.time() + duration * 60
local reminder = {
time = timestamp,
message = message
}
table.insert(self.database.reminders[msg.chat.id_str], reminder)
table.insert(self.database.reminders[chat_id_str], reminder)
local human_readable_time = convert_timestamp(timestamp, '%H:%M:%S')
local output = 'Ich werde dich um *'..human_readable_time..' Uhr* erinnern.'
output = 'Ich werde dich um *'..human_readable_time..' Uhr* erinnern.'
end
utilities.send_reply(self, msg, output, true)
end
function remind:cron()
function remind:cron(config)
local time = os.time()
-- Iterate over the group entries in the reminders database.
for chat_id, group in pairs(self.database.reminders) do
local new_group = {}
-- Iterate over each reminder.
for _, reminder in ipairs(group) do
for k, reminder in pairs(group) do
-- If the reminder is past-due, send it and nullify it.
-- Otherwise, add it to the replacement table.
if time > reminder.time then
local output = '*ERINNERUNG:*\n"' .. utilities.md_escape(reminder.message) .. '"'
local res = utilities.send_message(self, chat_id, output, true, nil, true)
-- If the message fails to send, save it for later.
if not res then
table.insert(new_group, reminder)
end
else
table.insert(new_group, reminder)
-- If the message fails to send, save it for later (if enabled in config).
if res or not config.remind.persist then
group[k] = nil
end
end
-- Nullify the original table and replace it with the new one.
self.database.reminders[chat_id] = new_group
-- Nullify the table if it is empty.
if #new_group == 0 then
self.database.reminders[chat_id] = nil
end
end
end

View File

@ -91,7 +91,7 @@ function time:inline_callback(inline_query, config, matches)
end
function time:action(msg, config)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
local output = os.date("%A, %d. %B %Y, *%H:%M:%S Uhr*")
utilities.send_reply(self, msg, time:localize(output), true)

View File

@ -22,52 +22,42 @@ https.timeout = 5
-- For the sake of ease to new contributors and familiarity to old contributors,
-- we'll provide a couple of aliases to real bindings here.
function utilities:send_message(chat_id, text, disable_web_page_preview, reply_to_message_id, use_markdown, reply_markup)
if use_markdown == true then
use_markdown = 'Markdown'
elseif not use_markdown then
use_markdown = nil
local parse_mode
if type(use_markdown) == 'string' then
parse_mode = use_markdown
elseif use_markdown == true then
parse_mode = 'Markdown'
end
return bindings.request(self, 'sendMessage', {
chat_id = chat_id,
text = text,
disable_web_page_preview = disable_web_page_preview,
reply_to_message_id = reply_to_message_id,
parse_mode = use_markdown,
parse_mode = parse_mode,
reply_markup = reply_markup
} )
end
-- https://core.telegram.org/bots/api#editmessagetext
function utilities:edit_message(chat_id, message_id, text, disable_web_page_preview, use_markdown, reply_markup)
if use_markdown == true then
use_markdown = 'Markdown'
elseif not use_markdown then
use_markdown = nil
local parse_mode
if type(use_markdown) == 'string' then
parse_mode = use_markdown
elseif use_markdown == true then
parse_mode = 'Markdown'
end
return bindings.request(self, 'editMessageText', {
chat_id = chat_id,
message_id = message_id,
text = text,
disable_web_page_preview = disable_web_page_preview,
parse_mode = use_markdown,
parse_mode = parse_mode,
reply_markup = reply_markup
} )
end
function utilities:send_reply(old_msg, text, use_markdown, reply_markup)
if use_markdown == true then
use_markdown = 'Markdown'
elseif not use_markdown then
use_markdown = nil
end
return bindings.request(self, 'sendMessage', {
chat_id = old_msg.chat.id,
text = text,
disable_web_page_preview = true,
reply_to_message_id = old_msg.message_id,
parse_mode = use_markdown,
reply_markup = reply_markup
} )
return utilities.send_message(self, old_msg.chat.id, text, true, old_msg.message_id, use_markdown, reply_markup)
end
-- NOTE: Telegram currently only allows file uploads up to 50 MB
@ -222,27 +212,6 @@ function utilities:answer_inline_query(inline_query, results, cache_time, is_per
} )
end
-- get the indexed word in a string
function utilities.get_word(s, i)
s = s or ''
i = i or 1
local t = {}
for w in s:gmatch('%g+') do
table.insert(t, w)
end
return t[i] or false
end
-- Like get_word(), but better.
-- Returns the actual index.
function utilities.index(s)
local t = {}
for w in s:gmatch('%g+') do
table.insert(t, w)
end
return t
end
-- Returns the string after the first space.
function utilities.input(s)
if not s:find(' ') then
@ -251,6 +220,10 @@ function utilities.input(s)
return s:sub(s:find(' ')+1)
end
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
end
-- Calculates the length of the given string as UTF-8 characters
function utilities.utf8_len(s)
local chars = 0
@ -343,13 +316,13 @@ end
-- Loads a JSON file as a table.
function utilities.load_data(filename)
local f = io.open(filename)
if not f then
return {}
end
if f then
local s = f:read('*all')
f:close()
local data = json.decode(s)
return data
return json.decode(s)
else
return {}
end
end
-- Saves a table to a JSON file.
@ -412,78 +385,6 @@ function utilities:resolve_username(input)
end
end
-- Simpler than above function; only returns an ID.
-- Returns nil if no ID is available.
function utilities:id_from_username(input)
input = input:gsub('^@', '')
for _, user in pairs(self.database.users) do
if user.username and user.username:lower() == input:lower() then
return user.id
end
end
end
-- Simpler than below function; only returns an ID.
-- Returns nil if no ID is available.
function utilities:id_from_message(msg)
if msg.reply_to_message then
return msg.reply_to_message.from.id
else
local input = utilities.input(msg.text)
if input then
if tonumber(input) then
return tonumber(input)
elseif input:match('^@') then
return utilities.id_from_username(self, input)
end
end
end
end
function utilities:user_from_message(msg, no_extra)
local input = utilities.input(msg.text_lower)
local target = {}
if msg.reply_to_message then
for k,v in pairs(self.database.users[msg.reply_to_message.from.id_str]) do
target[k] = v
end
elseif input and tonumber(input) then
target.id = tonumber(input)
if self.database.users[input] then
for k,v in pairs(self.database.users[input]) do
target[k] = v
end
end
elseif input and input:match('^@') then
local uname = input:gsub('^@', '')
for _,v in pairs(self.database.users) do
if v.username and uname == v.username:lower() then
for key, val in pairs(v) do
target[key] = val
end
end
end
if not target.id then
target.err = 'Sorry, I don\'t recognize that username.'
end
else
target.err = 'Please specify a user via reply, ID, or username.'
end
if not no_extra then
if target.id then
target.id_str = tostring(target.id)
end
if not target.first_name then
target.first_name = 'User'
end
target.name = utilities.build_name(target.first_name, target.last_name)
end
return target
end
function utilities:handle_exception(err, message, config)
if not err then err = '' end
local output = '\n[' .. os.date('%F %T', os.time()) .. ']\n' .. self.info.username .. ': ' .. err .. '\n' .. message .. '\n'
@ -500,15 +401,17 @@ function utilities.download_file(url, filename)
return download_to_file(url, filename)
end
function utilities.markdown_escape(text)
text = text:gsub('_', '\\_')
text = text:gsub('%[', '\\[')
text = text:gsub('%*', '\\*')
text = text:gsub('`', '\\`')
return text
function utilities.md_escape(text)
return text:gsub('_', '\\_')
:gsub('%[', '\\['):gsub('%]', '\\]')
:gsub('%*', '\\*'):gsub('`', '\\`')
end
utilities.md_escape = utilities.markdown_escape
utilities.markdown_escape = utilities.md_escape
function utilities.html_escape(text)
return text:gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;')
end
utilities.triggers_meta = {}
utilities.triggers_meta.__index = utilities.triggers_meta
@ -584,7 +487,8 @@ utilities.char = {
arabic = '[\216-\219][\128-\191]',
rtl_override = '',
rtl_mark = '',
em_dash = ''
em_dash = '',
utf_8 = '[%z\1-\127\194-\244][\128-\191]',
}
-- taken from http://stackoverflow.com/a/11130774/3163199