Merge pull request #1 from Brawl345/master

Übernehme änderungen von Brawlbot
This commit is contained in:
Akamaru 2016-06-15 18:25:09 +02:00 committed by GitHub
commit 5c75429586
34 changed files with 2292 additions and 178 deletions

35
.travis.yml Normal file
View File

@ -0,0 +1,35 @@
language: erlang
before_install:
- CURDIR=$(pwd)
- sudo apt-get update -qq
- sudo apt-get install -qq libreadline-dev libconfig-dev libssl-dev lua5.2 liblua5.2-dev libevent-dev make unzip git libjansson-dev
- THIS_DIR=$(cd $(dirname $0); pwd)
- cd $THIS_DIR
- git clone https://github.com/keplerproject/luarocks.git
- cd luarocks
- git checkout tags/v2.3.0
- PREFIX="$THIS_DIR/.luarocks"
- ./configure --prefix=$PREFIX --sysconfdir=$PREFIX/luarocks --force-config
- make build && make install
- cd ..
- rm -rf luarocks
- ./.luarocks/bin/luarocks install luasocket
- ./.luarocks/bin/luarocks install luasec
- ./.luarocks/bin/luarocks install multipart-post
- ./.luarocks/bin/luarocks install dkjson
- ./.luarocks/bin/luarocks install oauth
- ./.luarocks/bin/luarocks install redis-lua
- ./.luarocks/bin/luarocks install lua-cjson
- ./.luarocks/bin/luarocks install fakeredis
- ./.luarocks/bin/luarocks install xml
- ./.luarocks/bin/luarocks install feedparser
- ./.luarocks/bin/luarocks install serpent
- ./.luarocks/bin/luarocks install sha1
- ./.luarocks/bin/luarocks install lpeg
- cd $CURDIR
script:
- luac -p otouto/*.lua
- luac -p otouto/plugins/*.lua

View File

@ -1,4 +1,6 @@
# Brawlbot v2 # Brawlbot v2
[![Build Status](https://travis-ci.org/Brawl345/Brawlbot-v2.svg?branch=master)](https://travis-ci.org/Brawl345/Brawlbot-v2)
The plugin-wielding, multipurpose Telegram bot. The plugin-wielding, multipurpose Telegram bot.
[Public Bot](http://telegram.me/mokubot) | [Official Channel](http://telegram.me/otouto) | [Development Group](http://telegram.me/BotDevelopment) [Public Bot](http://telegram.me/mokubot) | [Official Channel](http://telegram.me/otouto) | [Development Group](http://telegram.me/BotDevelopment)
@ -11,11 +13,10 @@ otouto is free software; you are free to redistribute it and/or modify it under
| For Users | For Coders | | For Users | For Coders |
|:----------------------------------------------|:------------------------------| |:----------------------------------------------|:------------------------------|
| [Setup](#setup) | [Introduction](#introduction) | | [Setup](#setup) | [Plugins](#plugins) |
| [Control plugins](#control-plugins) | [Plugins](#plugins) | | [Control plugins](#control-plugins) | [Bindings](#bindings) |
| [Group Administration](#group-administration) | [Bindings](#bindings) | | [Group Administration](#group-administration) | [Output style](#output-style) |
| [List of plugins](#list-of-plugins) | [Output style](#output-style) | | [List of plugins](#list-of-plugins) | [Contributors](#contributors) |
| | [Contributors](#contributors) |
## Setup ## Setup
You _must_ have Lua (5.2+), luasocket, luasec, multipart-post, and dkjson installed. You should also have lpeg, though it is not required. It is recommended you install these with LuaRocks. You _must_ have Lua (5.2+), luasocket, luasec, multipart-post, and dkjson installed. You should also have lpeg, though it is not required. It is recommended you install these with LuaRocks.
@ -194,9 +195,6 @@ Additionally, antiflood can be configured to automatically ban a user after he h
* * * * * *
## Introduction
####todo
## Plugins ## Plugins
otouto uses a robust plugin system, similar to yagop's [Telegram-Bot](http://github.com/yagop/telegram-bot). otouto uses a robust plugin system, similar to yagop's [Telegram-Bot](http://github.com/yagop/telegram-bot).
@ -205,13 +203,14 @@ Most plugins are intended for public use, but a few are for other purposes, like
A plugin can have five components, and two of them are required: A plugin can have five components, and two of them are required:
| Component | Description | Required? | | Component | Description | Required? |
|:----------------|:---------------------------------------------|:----------| |:------------------|:---------------------------------------------|:----------|
| plugin:action | Main function. Expects `msg` table as an argument. | Y | | `plugin:action` | Main function. Expects `msg` table as an argument. | Y |
| plugin.triggers | Table of triggers for the plugin. Uses Lua patterns. | Y | | `plugin.triggers` | Table of triggers for the plugin. Uses Lua patterns. | Y |
| plugin:init | Optional function run when the plugin is loaded. | N | | `plugin:init` | Optional function run when the plugin is loaded. | N |
| plugin:cron | Optional function to be called every minute. | N | | `plugin:cron` | Optional function to be called every minute. | N |
| plugin.command | Basic command and syntax. Listed in the help text. | N | | `plugin.command` | Basic command and syntax. Listed in the help text. | N |
| plugin.doc | Usage for the plugin. Returned by "/help $command". | N | | `plugin.doc` | Usage for the plugin. Returned by "/help $command". | N |
| `plugin.error` | Plugin-specific error message; false for no message. | N |
The `bot:on_msg_receive` function adds a few variables to the `msg` table for your convenience. These are self-explanatory: `msg.from.id_str`, `msg.to.id_str`, `msg.chat.id_str`, `msg.text_lower`, `msg.from.name`. The `bot:on_msg_receive` function adds a few variables to the `msg` table for your convenience. These are self-explanatory: `msg.from.id_str`, `msg.to.id_str`, `msg.chat.id_str`, `msg.text_lower`, `msg.from.name`.

View File

@ -22,6 +22,7 @@ Sende /hilfe, um zu starten
cmd_pat = '/', cmd_pat = '/',
errors = { -- Generic error messages used in various plugins. errors = { -- Generic error messages used in various plugins.
generic = 'An unexpected error occurred.',
connection = 'Verbindungsfehler.', connection = 'Verbindungsfehler.',
quotaexceeded = 'API-Quota aufgebraucht.', quotaexceeded = 'API-Quota aufgebraucht.',
results = 'Keine Ergebnisse gefunden.', results = 'Keine Ergebnisse gefunden.',
@ -30,7 +31,7 @@ Sende /hilfe, um zu starten
syntax = 'Invalide Syntax.', syntax = 'Invalide Syntax.',
chatter_connection = 'Ich möchte gerade nicht reden', chatter_connection = 'Ich möchte gerade nicht reden',
chatter_response = 'Ich weiß nicht, was ich darauf antworten soll.' chatter_response = 'Ich weiß nicht, was ich darauf antworten soll.'
}, }
plugins = { -- To enable a plugin, add its name to the list. plugins = { -- To enable a plugin, add its name to the list.
'control', 'control',

View File

@ -1,22 +0,0 @@
package = 'otouto'
version = 'dev-1'
source = {
url = 'git://github.com/topkecleon/otouto.git'
}
description = {
summary = 'The plugin-wielding, multipurpose Telegram bot!',
detailed = 'A plugin-wielding, multipurpose bot for the Telegram API.',
homepage = 'http://otou.to',
maintainer = 'Drew <drew@otou.to>',
license = 'GPL-2'
}
dependencies = {
'lua >= 5.2',
'LuaSocket ~> 3.0',
'LuaSec ~> 0.6',
'dkjson ~> 2.5',
'LPeg ~> 1.0'
}

View File

@ -5,7 +5,7 @@ local bindings -- Load Telegram bindings.
local utilities -- Load miscellaneous and cross-plugin functions. local utilities -- Load miscellaneous and cross-plugin functions.
local redis = (loadfile "./otouto/redis.lua")() local redis = (loadfile "./otouto/redis.lua")()
bot.version = '2' bot.version = '2.0'
function bot:init(config) -- The function run when the bot is started or reloaded. function bot:init(config) -- The function run when the bot is started or reloaded.
@ -69,14 +69,28 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec
msg.text_lower = msg.text:lower() msg.text_lower = msg.text:lower()
end end
for _,v in ipairs(self.plugins) do for _, plugin in ipairs(self.plugins) do
for _,w in pairs(v.triggers) do for _, trigger in pairs(plugin.triggers) do
if string.match(msg.text_lower, w) then if string.match(msg.text_lower, trigger) then
local success, result = pcall(function() local success, result = pcall(function()
return v.action(self, msg, config) -- trying to port matches to otouto
for k, pattern in pairs(plugin.triggers) do
matches = match_pattern(pattern, msg.text)
if matches then
break;
end
end
return plugin.action(self, msg, config, matches)
end) end)
if not success then if not success then
utilities.send_reply(self, msg, 'Sorry, an unexpected error occurred.') -- If the plugin has an error message, send it. If it does
-- not, use the generic one specified in config. If it's set
-- to false, do nothing.
if plugin.error then
utilities.send_reply(self, msg, plugin.error)
elseif plugin.error == nil then
utilities.send_reply(self, msg, config.errors.generic, true)
end
utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config)
return return
end end

View File

@ -26,6 +26,7 @@ function ninegag:get_9GAG()
end end
function ninegag:action(msg, config) function ninegag:action(msg, config)
utilities.send_typing(self, msg.chat.id, 'upload_photo')
local url, title = ninegag:get_9GAG() local url, title = ninegag:get_9GAG()
if not url then if not url then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
@ -33,9 +34,7 @@ function ninegag:action(msg, config)
end end
local file = download_to_file(url) local file = download_to_file(url)
bindings.sendPhoto(self, {chat_id = msg.chat.id, caption = title}, {photo = file} ) utilities.send_photo(self, msg.chat.id, file, title)
os.remove(file)
print("Deleted: "..file)
end end
return ninegag return ninegag

View File

@ -71,6 +71,9 @@ function administration:init(config)
administration.doc = '`Returns a list of administrated groups.\nUse '..config.cmd_pat..'ahelp for more administrative commands.`' 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.
administration.error = false
end end
function administration.init_flags(cmd_pat) return { function administration.init_flags(cmd_pat) return {
@ -265,7 +268,7 @@ function administration:kick_user(chat, target, reason, config)
victim = utilities.build_name( victim = utilities.build_name(
self.database.users[tostring(target)].first_name, self.database.users[tostring(target)].first_name,
self.database.users[tostring(target)].last_name self.database.users[tostring(target)].last_name
) ) .. ' [' .. victim .. ']'
end end
local group = self.database.administration.groups[tostring(chat)].name local group = self.database.administration.groups[tostring(chat)].name
utilities.handle_exception(self, victim..' kicked from '..group, reason, config) utilities.handle_exception(self, victim..' kicked from '..group, reason, config)
@ -917,7 +920,9 @@ function administration.init_command(self_, config)
if input then if input then
input = utilities.get_word(input, 1) input = utilities.get_word(input, 1)
input = tonumber(input) input = tonumber(input)
if not input or not administration.flags[input] then input = false end if not input or not administration.flags[input] then
input = false
end
end end
if not input then if not input then
local output = '*Flags for ' .. msg.chat.title .. ':*\n' local output = '*Flags for ' .. msg.chat.title .. ':*\n'
@ -1209,7 +1214,9 @@ function administration.init_command(self_, config)
doc = 'Adds a group to the administration system. Pass numbers as arguments to enable those flags immediately. For example, this would add the group and enable the unlisted flag, antibot, and antiflood:\n/gadd 1 4 5', doc = 'Adds a group to the administration system. Pass numbers as arguments to enable those flags immediately. For example, this would add the group and enable the unlisted flag, antibot, and antiflood:\n/gadd 1 4 5',
action = function(self, msg, group, config) action = function(self, msg, group, config)
if self.database.administration.groups[msg.chat.id_str] then if msg.chat.id == msg.from.id then
utilities.send_message(self, msg.chat.id, 'No.')
elseif self.database.administration.groups[msg.chat.id_str] then
utilities.send_reply(self, msg, 'I am already administrating this group.') utilities.send_reply(self, msg, 'I am already administrating this group.')
else else
local flags = {} local flags = {}
@ -1327,6 +1334,18 @@ function administration.init_command(self_, config)
end end
end end
end end
},
{ -- /buildwall :^)
triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('buildwall').table,
privilege = 3,
interior = true,
action = function(self, msg, group, config)
for i = 2, 5 do
group.flags[i] = true
end
utilities.send_message(self, msg.chat.id, 'antisquig, antisquig++, antibot, and antiflood have been enabled.')
end
} }
} }

View File

@ -0,0 +1,117 @@
local app_store = {}
local https = require('ssl.https')
local json = require('dkjson')
local utilities = require('otouto.utilities')
local redis = (loadfile "./otouto/redis.lua")()
app_store.triggers = {
"itunes.apple.com/(.*)/app/(.*)/id(%d+)",
"^!itunes (%d+)$",
"itunes.apple.com/app/id(%d+)"
}
local BASE_URL = 'https://itunes.apple.com/lookup'
local makeOurDate = function(dateString)
local pattern = "(%d+)%-(%d+)%-(%d+)T"
local year, month, day = dateString:match(pattern)
return day..'.'..month..'.'..year
end
function app_store:get_appstore_data()
local url = BASE_URL..'/?id='..appid..'&country=de'
local res,code = https.request(url)
if code ~= 200 then return "HTTP-FEHLER" end
local data = json.decode(res).results[1]
if data == nil then return 'NOTFOUND' end
if data.wrapperType ~= 'software' then return nil end
return data
end
function app_store:send_appstore_data(data)
-- Header
local name = data.trackName
local author = data.sellerName
local price = data.formattedPrice
local version = data.version
-- Body
local description = string.sub(unescape(data.description), 1, 150) .. '...'
local min_ios_ver = data.minimumOsVersion
local size = string.gsub(round(data.fileSizeBytes / 1000000, 2), "%.", ",") -- wtf Apple, it's 1024, not 1000!
local release = makeOurDate(data.releaseDate)
if data.isGameCenterEnabled then
game_center = '\nUnterstützt Game Center'
else
game_center = ''
end
local category_count = tablelength(data.genres)
if category_count == 1 then
category = '\nKategorie: '..data.genres[1]
else
local category_loop = '\nKategorien: '
for v in pairs(data.genres) do
if v < category_count then
category_loop = category_loop..data.genres[v]..', '
else
category_loop = category_loop..data.genres[v]
end
end
category = category_loop
end
-- Footer
if data.averageUserRating and data.userRatingCount then
avg_rating = 'Bewertung: '..string.gsub(data.averageUserRating, "%.", ",")..' Sterne '
ratings = 'von '..comma_value(data.userRatingCount)..' Bewertungen'
else
avg_rating = ""
ratings = ""
end
local header = '*'..name..'* v'..version..' von *'..author..'* ('..price..'):'
local body = '\n'..description..'\n_Benötigt mind. iOS '..min_ios_ver..'_\nGröße: '..size..' MB\nErstveröffentlicht am '..release..game_center..category
local footer = '\n'..avg_rating..ratings
local text = header..body..footer
-- Picture
if data.screenshotUrls[1] and data.ipadScreenshotUrls[1] then
image_url = data.screenshotUrls[1]
elseif data.screenshotUrls[1] and not data.ipadScreenshotUrls[1] then
image_url = data.screenshotUrls[1]
elseif not data.screenshotUrls[1] and data.ipadScreenshotUrls[1] then
image_url = data.ipadScreenshotUrls[1]
else
image_url = nil
end
return text, image_url
end
function app_store:action(msg, config, matches)
if not matches[3] then
appid = matches[1]
else
appid = matches[3]
end
local data = app_store:get_appstore_data()
if data == nil then print('Das Appstore-Plugin unterstützt nur Apps!') end
if data == 'HTTP-FEHLER' or data == 'NOTFOUND' then
utilities.send_reply(self, msg, '*App nicht gefunden!*', true)
return
else
local output, image_url = app_store:send_appstore_data(data)
utilities.send_reply(self, msg, output, true)
if image_url then
utilities.send_typing(self, msg.chat.id, 'upload_photo')
local file = download_to_file(image_url)
utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id)
end
end
end
return app_store

48
otouto/plugins/bitly.lua Normal file
View File

@ -0,0 +1,48 @@
local bitly = {}
local https = require('ssl.https')
local json = require('dkjson')
local utilities = require('otouto.utilities')
local redis = (loadfile "./otouto/redis.lua")()
function bitly:init(config)
if not cred_data.bitly_access_token then
print('Missing config value: bitly_access_token.')
print('bitly.lua will not be enabled.')
return
end
bitly.triggers = {
"bit.ly/([A-Za-z0-9-_-]+)",
"bitly.com/([A-Za-z0-9-_-]+)",
"j.mp/([A-Za-z0-9-_-]+)",
"andib.tk/([A-Za-z0-9-_-]+)"
}
end
local BASE_URL = 'https://api-ssl.bitly.com/v3/expand'
function bitly:expand_bitly_link (shorturl)
local access_token = cred_data.bitly_access_token
local url = BASE_URL..'?access_token='..access_token..'&shortUrl=https://bit.ly/'..shorturl
local res,code = https.request(url)
if code ~= 200 then return "HTTP-FEHLER" end
local data = json.decode(res).data.expand[1]
cache_data('bitly', shorturl, data)
return data.long_url
end
function bitly:action(msg, config, matches)
local shorturl = matches[1]
local hash = 'telegram:cache:bitly:'..shorturl
if redis:exists(hash) == false then
utilities.send_reply(self, msg, bitly:expand_bitly_link(shorturl))
return
else
local data = redis:hgetall(hash)
utilities.send_reply(self, msg, data.long_url)
return
end
end
return bitly

View File

@ -0,0 +1,142 @@
local bitly_create = {}
local http = require('socket.http')
local https = require('ssl.https')
local URL = require('socket.url')
local json = require('dkjson')
local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
local OAuth = require "OAuth"
local redis = (loadfile "./otouto/redis.lua")()
function bitly_create:init(config)
if not cred_data.bitly_client_id then
print('Missing config value: bitly_client_id.')
print('bitly_create.lua will not be enabled.')
return
elseif not cred_data.bitly_client_secret then
print('Missing config value: bitly_client_secret.')
print('bitly_create.lua will not be enabled.')
return
elseif not cred_data.bitly_redirect_uri then
print('Missing config value: bitly_redirect_uri.')
print('bitly_create.lua will not be enabled.')
return
end
bitly_create.triggers = {
"^/short (auth) (.+)$",
"^/short (auth)$",
"^/short (unauth)$",
"^/short (me)$",
"^/short (j.mp) (https?://[%w-_%.%?%.:/%+=&]+)$",
"^/short (bit.ly) (https?://[%w-_%.%?%.:/%+=&]+)$",
"^/short (bitly.com) (https?://[%w-_%.%?%.:/%+=&]+)$",
"^/short (https?://[%w-_%.%?%.:/%+=&]+)$"
}
bitly_create.doc = [[*
]]..config.cmd_pat..[[short* _<Link>_: Kürzt einen Link mit der Standard Bitly-Adresse
*]]..config.cmd_pat..[[short* _<j.mp|bit.ly|bitly.com>_ _[Link]_: Kürzt einen Link mit der ausgewählten Kurz-URL
*]]..config.cmd_pat..[[short* _auth_: Loggt deinen Account ein und nutzt ihn für deine Links (empfohlen!)
*]]..config.cmd_pat..[[short* _me_: Gibt den eingeloggten Account aus
*]]..config.cmd_pat..[[short* _unauth_: Loggt deinen Account aus
]]
end
bitly_create.command = 'short <URL>'
local BASE_URL = 'https://api-ssl.bitly.com'
local client_id = cred_data.bitly_client_id
local client_secret = cred_data.bitly_client_secret
local redirect_uri = cred_data.bitly_redirect_uri
function bitly_create:get_bitly_access_token(hash, code)
local req = post_petition(BASE_URL..'/oauth/access_token', 'client_id='..client_id..'&client_secret='..client_secret..'&code='..code..'&redirect_uri='..redirect_uri)
if not req.access_token then return '*Fehler beim Einloggen!*' end
local access_token = req.access_token
local login_name = req.login
redis:hset(hash, 'bitly', access_token)
return 'Erfolgreich als `'..login_name..'` eingeloggt!'
end
function bitly_create:get_bitly_user_info(bitly_access_token)
local url = BASE_URL..'/v3/user/info?access_token='..bitly_access_token..'&format=json'
local res,code = https.request(url)
if code == 401 then return 'Login fehlgeschlagen!' end
if code ~= 200 then return 'HTTP-Fehler!' end
local data = json.decode(res).data
if data.full_name then
name = '*'..data.full_name..'* (`'..data.login..'`)'
else
name = '`'..data.login..'`'
end
local text = 'Eingeloggt als '..name
return text
end
function bitly_create:create_bitlink (long_url, domain, bitly_access_atoken)
local url = BASE_URL..'/v3/shorten?access_token='..bitly_access_token..'&domain='..domain..'&longUrl='..long_url..'&format=txt'
local text,code = https.request(url)
if code ~= 200 then return 'FEHLER: '..text end
return text
end
function bitly_create:action(msg, config, matches)
local hash = 'user:'..msg.from.id
bitly_access_token = redis:hget(hash, 'bitly')
if matches[1] == 'auth' and matches[2] then
utilities.send_reply(self, msg, bitly_create:get_bitly_access_token(hash, matches[2]), true)
return
end
if matches[1] == 'auth' then
utilities.send_reply(self, msg, 'Bitte logge dich ein und folge den Anweisungen:\n[Bei Bitly anmelden](https://bitly.com/oauth/authorize?client_id='..client_id..'&redirect_uri='..redirect_uri..')', true)
return
end
if matches[1] == 'unauth' and bitly_access_token then
redis:hdel(hash, 'bitly')
utilities.send_reply(self, msg, '*Erfolgreich ausgeloggt!* Du kannst den Zugriff [in deinen Kontoeinstellungen](https://bitly.com/a/settings/connected) endgültig entziehen.', true)
return
elseif matches[1] == 'unauth' and not bitly_access_token then
utilities.send_reply(self, msg, 'Wie willst du dich ausloggen, wenn du gar nicht eingeloggt bist?', true)
return
end
if matches[1] == 'me' and bitly_access_token then
local text = bitly_create:get_bitly_user_info(bitly_access_token)
if text then
utilities.send_reply(self, msg, text, true)
return
else
return
end
elseif matches[1] == 'me' and not bitly_access_token then
utilities.send_reply(self, msg, 'Du bist nicht eingeloggt! Logge dich ein mit\n/short auth', true)
return
end
if not bitly_access_token then
print('Not signed in, will use global bitly access_token')
bitly_access_token = cred_data.bitly_access_token
end
if matches[2] == nil then
long_url = url_encode(matches[1])
domain = 'bit.ly'
else
long_url = url_encode(matches[2])
domain = matches[1]
end
utilities.send_reply(self, msg, bitly_create:create_bitlink(long_url, domain, bitly_access_token))
return
end
return bitly_create

125
otouto/plugins/creds.lua Normal file
View File

@ -0,0 +1,125 @@
local creds_manager = {}
local utilities = require('otouto.utilities')
local redis = (loadfile "./otouto/redis.lua")()
function creds_manager:init(config)
creds_manager.triggers = {
"^(/creds)$",
"^(/creds add) ([^%s]+) (.+)$",
"^(/creds del) (.+)$",
"^(/creds rename) ([^%s]+) (.+)$"
}
creds_manager.doc = [[*
]]..config.cmd_pat..[[creds*: Zeigt alle Logindaten und API-Keys
*]]..config.cmd_pat..[[creds* _add_ _<Variable>_ _<Schlüssel>_: Speichert Schlüssel mit dieser Variable ein
*]]..config.cmd_pat..[[creds* _del_ _<Variable>_: Löscht Schlüssel mit dieser Variable
*]]..config.cmd_pat..[[creds* _rename_ _<Variable>_ _<Neue Variable>_: Benennt Variable um, behält Schlüssel bei
]]
end
creds_manager.command = 'creds'
local hash = "telegram:credentials"
-- See: http://www.lua.org/pil/19.3.html
function pairsByKeys (t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0 -- iterator variable
local iter = function () -- iterator function
i = i + 1
if a[i] == nil then
return nil
else
return a[i], t[a[i]]
end
end
return iter
end
function creds_manager:reload_creds()
cred_data = redis:hgetall(hash)
end
function creds_manager:list_creds()
creds_manager:reload_creds()
if redis:exists("telegram:credentials") == true then
local text = ""
for var, key in pairsByKeys(cred_data) do
text = text..var..' = '..key..'\n'
end
return text
else
create_cred()
return "Es wurden noch keine Logininformationen gespeichert, lege Tabelle an...\nSpeichere Keys mit /creds add [Variable] [Key] ein!"
end
end
function creds_manager:add_creds(var, key)
print('Saving credential for '..var..' to redis hash '..hash)
redis:hset(hash, var, key)
creds_manager:reload_creds()
return 'Gespeichert!'
end
function creds_manager:del_creds(var)
if redis:hexists(hash, var) == true then
print('Deleting credential for '..var..' from redis hash '..hash)
redis:hdel(hash, var)
creds_manager:reload_creds()
return 'Key von "'..var..'" erfolgreich gelöscht!'
else
return 'Du hast keine Logininformationen für diese Variable eingespeichert.'
end
end
function creds_manager:rename_creds(var, newvar)
if redis:hexists(hash, var) == true then
local key = redis:hget(hash, var)
if redis:hsetnx(hash, newvar, key) == true then
redis:hdel(hash, var)
creds_manager:reload_creds()
return '"'..var..'" erfolgreich zu "'..newvar..'" umbenannt.'
else
return "Variable konnte nicht umbenannt werden: Zielvariable existiert bereits."
end
else
return 'Die zu umbennende Variable existiert nicht.'
end
end
function creds_manager:action(msg, config, matches)
local receiver = msg.from.id
if receiver ~= config.admin then
utilities.send_reply(self, msg, config.errors.sudo)
return
end
if msg.chat.type ~= 'private' then
utilities.send_reply(self, msg, 'Dieses Plugin solltest du nur [privat](http://telegram.me/' .. self.info.username .. '?start=creds) verwenden!', true)
return
end
if matches[1] == "/creds" then
utilities.send_reply(self, msg, creds_manager:list_creds())
return
elseif matches[1] == "/creds add" then
local var = string.lower(string.sub(matches[2], 1, 50))
local key = string.sub(matches[3], 1, 1000)
utilities.send_reply(self, msg, creds_manager:add_creds(var, key))
return
elseif matches[1] == "/creds del" then
local var = string.lower(matches[2])
utilities.send_reply(self, msg, creds_manager:del_creds(var))
return
elseif matches[1] == "/creds rename" then
local var = string.lower(string.sub(matches[2], 1, 50))
local newvar = string.lower(string.sub(matches[3], 1, 1000))
utilities.send_reply(self, msg, creds_manager:rename_creds(var, newvar))
return
end
end
return creds_manager

View File

@ -1,56 +0,0 @@
local dice = {}
local utilities = require('otouto.utilities')
dice.command = 'roll <nDr>'
function dice:init(config)
dice.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('roll', true).table
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.
```]]
end
function dice:action(msg)
local input = utilities.input(msg.text_lower)
if not input then
utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true)
return
end
local count, range
if input:match('^[%d]+d[%d]+$') then
count, range = input:match('([%d]+)d([%d]+)')
elseif input:match('^d?[%d]+$') then
count = 1
range = input:match('^d?([%d]+)$')
else
utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true)
return
end
count = tonumber(count)
range = tonumber(range)
if range < 2 then
utilities.send_reply(self, msg, 'The minimum range is 2.')
return
end
if range > 1000 or count > 1000 then
utilities.send_reply(self, msg, 'The maximum range and count are 1000.')
return
end
local output = '*' .. count .. 'd' .. range .. '*\n`'
for _ = 1, count do
output = output .. math.random(range) .. '\t'
end
output = output .. '`'
utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true)
end
return dice

37
otouto/plugins/expand.lua Normal file
View File

@ -0,0 +1,37 @@
local expand = {}
local http = require('socket.http')
local utilities = require('otouto.utilities')
function expand:init(config)
expand.triggers = {
"^/expand (https?://[%w-_%.%?%.:/%+=&]+)$"
}
expand.doc = [[*
]]..config.cmd_pat..[[expand* _<Kurz-URL>_: Verlängert Kurz-URL (301er/302er)]]
end
expand.command = 'expand <Kurz-URL>'
function expand:action(msg, config, matches)
local response_body = {}
local request_constructor = {
url = matches[1],
method = "HEAD",
sink = ltn12.sink.table(response_body),
headers = {},
redirect = false
}
local ok, response_code, response_headers, response_status_line = http.request(request_constructor)
if ok and response_headers.location then
utilities.send_reply(self, msg, response_headers.location)
return
else
utilities.send_reply(self, msg, "Fehler beim Erweitern der URL.")
return
end
end
return expand

180
otouto/plugins/facebook.lua Normal file
View File

@ -0,0 +1,180 @@
local facebook = {}
local http = require('socket.http')
local https = require('ssl.https')
local URL = require('socket.url')
local json = require('dkjson')
local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
local redis = (loadfile "./otouto/redis.lua")()
function facebook:init(config)
if not cred_data.fb_access_token then
print('Missing config value: fb_access_token.')
print('facebook.lua will not be enabled.')
return
end
facebook.triggers = {
"facebook.com/([A-Za-z0-9-._-]+)/(posts)/(%d+)",
"facebook.com/(permalink).php%?(story_fbid)=(%d+)&id=(%d+)",
"facebook.com/(photo).php%?fbid=(%d+)",
"facebook.com/([A-Za-z0-9-._-]+)/(photos)/a.(%d+[%d%.]*)/(%d+)",
"facebook.com/(video).php%?v=(%d+)",
"facebook.com/([A-Za-z0-9-._-]+)/(videos)/(%d+[%d%.]*)",
"facebook.com/([A-Za-z0-9-._-]+)"
}
end
local BASE_URL = 'https://graph.facebook.com/v2.5'
local fb_access_token = cred_data.fb_access_token
local makeOurDate = function(dateString)
local pattern = "(%d+)%/(%d+)%/(%d+)"
local month, day, year = dateString:match(pattern)
return day..'.'..month..'.'..year
end
function facebook:get_fb_id(name)
local url = BASE_URL..'/'..name..'?access_token='..fb_access_token..'&locale=de_DE'
local res,code = https.request(url)
if code ~= 200 then return nil end
local data = json.decode(res)
return data.id
end
function facebook:fb_post (id, story_id)
local url = BASE_URL..'/'..id..'_'..story_id..'?access_token='..fb_access_token..'&locale=de_DE&fields=from,name,story,message,link'
local res,code = https.request(url)
if code ~= 200 then return nil end
local data = json.decode(res)
local from = data.from.name
local message = data.message
local name = data.name
if data.link then
link = '\n'..data.name..':\n'..data.link
else
link = ""
end
if data.story then
story = ' ('..data.story..')'
else
story = ""
end
local text = '*'..from..'*'..story..':\n'..message..'\n'..link
return text
end
function facebook:send_facebook_photo(photo_id, receiver)
local url = BASE_URL..'/'..photo_id..'?access_token='..fb_access_token..'&locale=de_DE&fields=images,from,name'
local res,code = https.request(url)
if code ~= 200 then return nil end
local data = json.decode(res)
local from = '*'..data.from.name..'*'
if data.name then
text = from..' hat ein Bild gepostet:\n'..data.name
else
text = from..' hat ein Bild gepostet:'
end
local image_url = data.images[1].source
return text, image_url
end
function facebook:send_facebook_video(video_id)
local url = BASE_URL..'/'..video_id..'?access_token='..fb_access_token..'&locale=de_DE&fields=description,from,source,title'
local res,code = https.request(url)
if code ~= 200 then return nil end
local data = json.decode(res)
local from = '*'..data.from.name..'*'
local description = data.description
local source = data.source
if data.title then
text = from..' hat ein Video gepostet:\n'..description..'\n['..data.title..']('..source..')'
else
text = from..' hat ein Video gepostet:\n'..description..'\n'..utilities.md_escape(source)
end
return text
end
function facebook:facebook_info(name)
local url = BASE_URL..'/'..name..'?access_token='..fb_access_token..'&locale=de_DE&fields=about,name,birthday,category,founded,general_info,is_verified'
local res,code = https.request(url)
if code ~= 200 then return nil end
local data = json.decode(res)
local name = data.name
if data.is_verified then
name = name..''
end
local category = data.category
if data.about then
about = '\n'..data.about
else
about = ""
end
if data.general_info then
general_info = '\n'..data.general_info
else
general_info = ""
end
if data.birthday and data.founded then
birth = '\nGeburtstag: '..makeOurDate(data.birthday)
elseif data.birthday and not data.founded then
birth = '\nGeburtstag: '..makeOurDate(data.birthday)
elseif data.founded and not data.birthday then
birth = '\nGegründet: '..data.founded
else
birth = ""
end
local text = '*'..name..'* ('..category..')_'..about..'_'..general_info..birth
return text
end
function facebook:action(msg, config, matches)
if matches[1] == 'permalink' or matches[2] == 'posts' then
story_id = matches[3]
if not matches[4] then
id = facebook:get_fb_id(matches[1])
else
id = matches[4]
end
utilities.send_reply(self, msg, facebook:fb_post(id, story_id), true)
return
elseif matches[1] == 'photo' or matches[2] == 'photos' then
if not matches[4] then
photo_id = matches[2]
else
photo_id = matches[4]
end
utilities.send_typing(self, msg.chat.id, 'upload_photo')
local text, image_url = facebook:send_facebook_photo(photo_id, receiver)
local file = download_to_file(image_url)
utilities.send_reply(self, msg, text, true)
utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id)
return
elseif matches[1] == 'video' or matches[2] == 'videos' then
if not matches[3] then
video_id = matches[2]
else
video_id = matches[3]
end
local output = facebook:send_facebook_video(video_id)
utilities.send_reply(self, msg, output, true)
return
else
utilities.send_reply(self, msg, facebook:facebook_info(matches[1]), true)
return
end
end
return facebook

222
otouto/plugins/forecast.lua Normal file
View File

@ -0,0 +1,222 @@
local forecast = {}
local HTTPS = require('ssl.https')
local URL = require('socket.url')
local JSON = require('dkjson')
local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
local redis = (loadfile "./otouto/redis.lua")()
function forecast:init(config)
if not cred_data.forecastio_apikey then
print('Missing config value: forecastio_apikey.')
print('weather.lua will not be enabled.')
return
elseif not cred_data.google_apikey then
print('Missing config value: google_apikey.')
print('weather.lua will not be enabled.')
return
end
forecast.triggers = {
"^(/f)$",
"^(/f) (.*)$",
"^(/fh)$",
"^(/fh) (.*)$",
"^(/forecast)$",
"^(/forecast) (.*)$",
"^(/forecasth)$",
"^(/forecasth) (.*)$"
}
forecast.doc = [[*
]]..config.cmd_pat..[[f*: Wettervorhersage für deinen Wohnort _(/location set <Ort>)_
*]]..config.cmd_pat..[[f* _<Ort>_: Wettervorhersage für diesen Ort
*]]..config.cmd_pat..[[fh*: 24-Stunden-Wettervorhersage für deine Stadt _(/location set [Ort]_
*]]..config.cmd_pat..[[fh* _<Ort>_: 24-Stunden-Wettervorhersage für diesen Ort
]]
end
forecast.command = 'forecast'
local BASE_URL = "https://api.forecast.io/forecast"
local apikey = cred_data.forecastio_apikey
local google_apikey = cred_data.google_apikey
function get_city_name(lat, lng)
local city = redis:hget('telegram:cache:weather:pretty_names', lat..','..lng)
if city then return city end
local url = 'https://maps.googleapis.com/maps/api/geocode/json?latlng='..lat..','..lng..'&result_type=political&language=de&key='..google_apikey
local res, code = HTTPS.request(url)
if code ~= 200 then return 'Unbekannte Stadt' end
local data = JSON.decode(res).results[1]
local city = data.formatted_address
print('Setting '..lat..','..lng..' in redis hash telegram:cache:weather:pretty_names to "'..city..'"')
redis:hset('telegram:cache:weather:pretty_names', lat..','..lng, city)
return city
end
function get_condition_symbol(weather, n)
if weather.data[n].icon == 'clear-day' then
return '☀️'
elseif weather.data[n].icon == 'clear-night' then
return '🌙'
elseif weather.data[n].icon == 'rain' then
return '☔️'
elseif weather.data[n].icon == 'snow' then
return '❄️'
elseif weather.data[n].icon == 'sleet' then
return '🌨'
elseif weather.data[n].icon == 'wind' then
return '💨'
elseif weather.data[n].icon == 'fog' then
return '🌫'
elseif weather.data[n].icon == 'cloudy' then
return '☁️☁️'
elseif weather.data[n].icon == 'partly-cloudy-day' then
return '🌤'
elseif weather.data[n].icon == 'partly-cloudy-night' then
return '🌙☁️'
else
return ''
end
end
function get_temp(weather, n, hourly)
if hourly then
local temperature = string.gsub(round(weather.data[n].temperature, 1), "%.", ",")
local condition = weather.data[n].summary
return temperature..'°C | '..get_condition_symbol(weather, n)..' '..condition
else
local day = string.gsub(round(weather.data[n].temperatureMax, 1), "%.", ",")
local night = string.gsub(round(weather.data[n].temperatureMin, 1), "%.", ",")
local condition = weather.data[n].summary
return '☀️ '..day..'°C | 🌙 '..night..'°C | '..get_condition_symbol(weather, n)..' '..condition
end
end
function forecast:get_forecast(lat, lng)
print('Finde Wetter in '..lat..', '..lng)
local text = redis:get('telegram:cache:forecast:'..lat..','..lng)
if text then print('...aus dem Cache..') return text end
local url = BASE_URL..'/'..apikey..'/'..lat..','..lng..'?lang=de&units=si&exclude=currently,minutely,hourly,alerts,flags'
local response_body = {}
local request_constructor = {
url = url,
method = "GET",
sink = ltn12.sink.table(response_body)
}
local ok, response_code, response_headers, response_status_line = HTTPS.request(request_constructor)
if not ok then return nil end
local data = JSON.decode(table.concat(response_body))
local ttl = string.sub(response_headers["cache-control"], 9)
local weather = data.daily
local city = get_city_name(lat, lng)
local header = '*Vorhersage für '..city..':*\n_'..weather.summary..'_\n'
local text = '*Heute:* '..get_temp(weather, 1)
local text = text..'\n*Morgen:* '..get_temp(weather, 2)
for day in pairs(weather.data) do
if day > 2 then
text = text..'\n*'..convert_timestamp(weather.data[day].time, '%a, %d.%m')..'*: '..get_temp(weather, day)
end
end
local text = string.gsub(text, "Mon", "Mo")
local text = string.gsub(text, "Tue", "Di")
local text = string.gsub(text, "Wed", "Mi")
local text = string.gsub(text, "Thu", "Do")
local text = string.gsub(text, "Fri", "Fr")
local text = string.gsub(text, "Sat", "Sa")
local text = string.gsub(text, "Sun", "So")
cache_data('forecast', lat..','..lng, header..text, tonumber(ttl), 'key')
return header..text
end
function forecast:get_forecast_hourly(lat, lng)
print('Finde stündliches Wetter in '..lat..', '..lng)
local text = redis:get('telegram:cache:forecast:'..lat..','..lng..':hourly')
if text then print('...aus dem Cache..') return text end
local url = BASE_URL..'/'..apikey..'/'..lat..','..lng..'?lang=de&units=si&exclude=currently,minutely,daily,alerts,flags'
local response_body = {}
local request_constructor = {
url = url,
method = "GET",
sink = ltn12.sink.table(response_body)
}
local ok, response_code, response_headers, response_status_line = HTTPS.request(request_constructor)
if not ok then return nil end
local data = JSON.decode(table.concat(response_body))
local ttl = string.sub(response_headers["cache-control"], 9)
local weather = data.hourly
local city = get_city_name(lat, lng)
local header = '*24-Stunden-Vorhersage für '..city..':*\n_'..weather.summary..'_'
local text = ""
for hour in pairs(weather.data) do
if hour < 26 then
text = text..'\n*'..convert_timestamp(weather.data[hour].time, '%H:%M Uhr')..'* | '..get_temp(weather, hour, true)
end
end
cache_data('forecast', lat..','..lng..':hourly', header..text, tonumber(ttl), 'key')
return header..text
end
function forecast:action(msg, config, matches)
local user_id = msg.from.id
local city = get_location(user_id)
if matches[2] then
city = matches[2]
else
local set_location = get_location(user_id)
if not set_location then
city = 'Berlin, Deutschland'
else
city = set_location
end
end
local lat = redis:hget('telegram:cache:weather:'..string.lower(city), 'lat')
local lng = redis:hget('telegram:cache:weather:'..string.lower(city), 'lng')
if not lat and not lng then
print('Koordinaten nicht eingespeichert, frage Google...')
coords = utilities.get_coords(city, config)
lat = coords.lat
lng = coords.lon
end
if not lat and not lng then
utilities.send_reply(self, msg, '*Diesen Ort gibt es nicht!*', true)
return
end
redis:hset('telegram:cache:weather:'..string.lower(city), 'lat', lat)
redis:hset('telegram:cache:weather:'..string.lower(city), 'lng', lng)
if matches[1] == '/forecasth' or matches[1] == '/fh' then
text = forecast:get_forecast_hourly(lat, lng)
else
text = forecast:get_forecast(lat, lng)
end
if not text then
text = '*Konnte die Wettervorhersage für diese Stadt nicht bekommen.*'
end
utilities.send_reply(self, msg, text, true)
end
return forecast

View File

@ -21,11 +21,10 @@ function gImages:init(config)
end end
gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('img', true):t('i', true):t('insfw', true).table gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('img', true):t('i', true):t('insfw', true).table
gImages.doc = [[``` gImages.doc = [[*
]]..config.cmd_pat..[[img <Suchbegriff> ]]..config.cmd_pat..[[img* _<Suchbegriff>_
Sucht Bild mit Google und versendet es (SafeSearch aktiv) Sucht Bild mit Google und versendet es (SafeSearch aktiv)
Alias: ]]..config.cmd_pat..[[i Alias: *]]..config.cmd_pat..[[i*]]
```]]
end end
gImages.command = 'img <Suchbegriff>' gImages.command = 'img <Suchbegriff>'
@ -47,6 +46,7 @@ function gImages:action(msg, config)
return return
end end
utilities.send_typing(self, msg.chat.id, 'upload_photo')
local apikey = cred_data.google_apikey local apikey = cred_data.google_apikey
local cseid = cred_data.google_cse_id local cseid = cred_data.google_cse_id
local BASE_URL = 'https://www.googleapis.com/customsearch/v1' local BASE_URL = 'https://www.googleapis.com/customsearch/v1'
@ -68,9 +68,7 @@ function gImages:action(msg, config)
local img_url = jdat.items[i].link local img_url = jdat.items[i].link
local file = download_to_file(img_url) local file = download_to_file(img_url)
bindings.sendPhoto(self, {chat_id = msg.chat.id, caption = img_url}, {photo = file} ) utilities.send_photo(self, msg.chat.id, file, img_url)
os.remove(file)
print("Deleted: "..file)
end end
return gImages return gImages

77
otouto/plugins/github.lua Normal file
View File

@ -0,0 +1,77 @@
local github = {}
local http = require('socket.http')
local https = require('ssl.https')
local URL = require('socket.url')
local json = require('dkjson')
local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
local redis = (loadfile "./otouto/redis.lua")()
function github:init(config)
github.triggers = {
"github.com/([A-Za-z0-9-_-.-._.]+)/([A-Za-z0-9-_-.-._.]+)/commit/([a-z0-9-]+)",
"github.com/([A-Za-z0-9-_-.-._.]+)/([A-Za-z0-9-_-.-._.]+)/?$"
}
end
local BASE_URL = 'https://api.github.com'
function github:get_gh_data(gh_code, gh_commit_sha)
if gh_commit_sha == nil then
url = BASE_URL..'/repos/'..gh_code
else
url = BASE_URL..'/repos/'..gh_code..'/git/commits/'..gh_commit_sha
end
local res,code = https.request(url)
if code ~= 200 then return "HTTP-FEHLER" end
local data = json.decode(res)
return data
end
function github:send_github_data(data)
if not data.owner then return nil end
local name = '*'..data.name..'*'
local description = '_'..data.description..'_'
local owner = data.owner.login
local clone_url = data.clone_url
if data.language == nil or data.language == "" then
language = ''
else
language = '\nSprache: '..data.language
end
if data.open_issues_count == 0 then
issues = ''
else
issues = '\nOffene Bugreports: '..data.open_issues_count
end
if data.homepage == nil or data.homepage == "" then
homepage = ''
else
homepage = '\n[Homepage besuchen]('..data.homepage..')'
end
local text = name..' von '..owner..'\n'..description..'\n`git clone '..clone_url..'`'..language..issues..homepage
return text
end
function github:send_gh_commit_data(gh_code, gh_commit_sha, data)
if not data.committer then return nil end
local committer = data.committer.name
local message = data.message
local text = '`'..gh_code..'@'..gh_commit_sha..'` von *'..committer..'*:\n'..message
return text
end
function github:action(msg, config, matches)
local gh_code = matches[1]..'/'..matches[2]
local gh_commit_sha = matches[3]
local data = github:get_gh_data(gh_code, gh_commit_sha)
if not gh_commit_sha then
output = github:send_github_data(data)
else
output = github:send_gh_commit_data(gh_code, gh_commit_sha, data)
end
utilities.send_reply(self, msg, output, true)
end
return github

16
otouto/plugins/images.lua Normal file
View File

@ -0,0 +1,16 @@
local images = {}
local utilities = require('otouto.utilities')
images.triggers = {
"(https?://[%w-_%%%.%?%.:,/%+=~&%[%]]+%.[Pp][Nn][Gg])$",
"(https?://[%w-_%%%.%?%.:,/%+=~&%[%]]+%.[Jj][Pp][Ee]?[Gg])$"
}
function images:action(msg)
utilities.send_typing(self, msg.chat.id, 'upload_photo')
local url = matches[1]
local file = download_to_file(url)
utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id)
end
return images

View File

@ -51,7 +51,7 @@ function imdb:action(msg, config)
if jdat.Poster ~= "N/A" then if jdat.Poster ~= "N/A" then
local file = download_to_file(jdat.Poster) local file = download_to_file(jdat.Poster)
bindings.sendPhoto(self, {chat_id = msg.chat.id}, {photo = file} ) utilities.send_photo(self, msg.chat.id, file)
end end
end end

View File

@ -0,0 +1,69 @@
local loc_manager = {}
local utilities = require('otouto.utilities')
local redis = (loadfile "./otouto/redis.lua")()
function loc_manager:init(config)
loc_manager.triggers = {
"^/location (set) (.*)$",
"^/location (del)$",
"^/location$"
}
loc_manager.doc = [[*
]]..config.cmd_pat..[[location*: Gibt deinen gesetzten Wohnort aus
*]]..config.cmd_pat..[[location* _set_ _<Ort>_: Setzt deinen Wohnort auf diesen Ort
*]]..config.cmd_pat..[[location* _del_: Löscht deinen angegebenen Wohnort
]]
end
loc_manager.command = 'location'
function loc_manager:set_location(user_id, location)
local hash = 'user:'..user_id
local set_location = get_location(user_id)
if set_location == location then
return 'Dieser Ort wurde bereits gesetzt.'
else
print('Setting location in redis hash '..hash..' to location')
redis:hset(hash, 'location', location)
return 'Dein Wohnort wurde auf *'..location..'* festgelegt.'
end
end
function loc_manager:del_location(user_id)
local hash = 'user:'..user_id
local set_location = get_location(user_id)
if not set_location then
return 'Du hast keinen Ort gesetzt'
else
print('Setting location in redis hash '..hash..' to false')
-- We set the location to false, because deleting the value blocks redis for a few milliseconds
redis:hset(hash, 'location', false)
return 'Dein Wohnort *'..set_location..'* wurde gelöscht!'
end
end
function loc_manager:action(msg, config, matches)
local user_id = msg.from.id
if matches[1] == 'set' then
utilities.send_reply(self, msg, loc_manager:set_location(user_id, matches[2]), true)
return
elseif matches[1] == 'del' then
utilities.send_reply(self, msg, loc_manager:del_location(user_id), true)
return
else
local set_location = get_location(user_id)
if not set_location then
utilities.send_reply(self, msg, '*Du hast keinen Ort gesetzt!*', true)
return
else
local coords = utilities.get_coords(set_location, config)
utilities.send_location(self, msg.chat.id, coords.lat, coords.lon, msg.message_id)
utilities.send_reply(self, msg, 'Gesetzter Wohnort: *'..set_location..'*', true)
return
end
end
end
return loc_manager

67
otouto/plugins/media.lua Normal file
View File

@ -0,0 +1,67 @@
local media = {}
local utilities = require('otouto.utilities')
local mimetype = (loadfile "./otouto/mimetype.lua")()
media.triggers = {
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(gif))$",
"^(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(mp4))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(pdf))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(ogg))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(zip))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(tar.gz))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(7z))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(mp3))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(rar))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(wmv))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(doc))$",
"^(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(avi))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(wav))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(apk))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(webm))$",
"^(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(ogv))$",
"(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(webp))$"
}
function media:action(msg)
local url = matches[1]
local ext = matches[2]
local receiver = msg.chat.id
utilities.send_typing(self, receiver, 'upload_document')
local file = download_to_file(url)
local mime_type = mimetype.get_content_type_no_sub(ext)
if ext == 'gif' then
print('send gif')
utilities.send_document(self, receiver, file, nil, msg.message_id)
return
elseif mime_type == 'text' then
print('send_document')
utilities.send_document(self, receiver, file, nil, msg.message_id)
return
elseif mime_type == 'image' then
print('send_photo')
utilities.send_photo(self, receiver, file, nil, msg.message_id)
return
elseif mime_type == 'audio' then
print('send_audio')
utilities.send_audio(self, receiver, file, nil, msg.message_id)
return
elseif mime_type == 'video' then
print('send_video')
utilities.send_video(self, receiver, file, nil, msg.message_id)
return
else
print('send_file')
utilities.send_document(self, receiver, file, nil, msg.message_id)
return
end
end
return media

View File

@ -0,0 +1,45 @@
local pasteee = {}
local bot = require('otouto.bot')
local utilities = require('otouto.utilities')
function pasteee:init(config)
if not cred_data.pasteee_key then
print('Missing config value: pasteee_key.')
print('pasteee.lua will not be enabled, listquotes won\'t be available.')
return
end
pasteee.triggers = {
"^/pasteee (.*)$"
}
pasteee.doc = [[*
]]..config.cmd_pat..[[pasteee* _<Text>_: Postet Text auf Paste.ee]]
end
pasteee.command = 'pasteee <Text>'
local key = cred_data.pasteee_key
function upload(text, noraw)
local url = "https://paste.ee/api"
local pet = post_petition(url, 'key='..key..'&paste='..text..'&format=json')
if pet.status ~= 'success' then return 'Ein Fehler ist aufgetreten: '..pet.error, true end
if noraw then
return pet.paste.link
else
return pet.paste.raw
end
end
function pasteee:action(msg, config, matches)
local text = matches[1]
local link, iserror = upload(text)
if iserror then
utilities.send_reply(self, msg, link)
return
end
utilities.send_reply(self, msg, '[Text auf Paste.ee ansehen]('..link..')', true)
end
return pasteee

View File

@ -7,7 +7,7 @@ patterns.triggers = {
} }
function patterns:action(msg) function patterns:action(msg)
if not msg.reply_to_message then return 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"', '')
@ -22,8 +22,7 @@ function patterns:action(msg)
end end
) )
if res == false then if res == false then
output = 'Malformed pattern!' utilities.send_reply(self, msg, 'Malformed pattern!')
utilities.send_reply(self, msg, output)
else else
output = output:sub(1, 4000) output = output:sub(1, 4000)
output = 'Did you mean:\n"' .. output .. '"' output = 'Did you mean:\n"' .. output .. '"'

111
otouto/plugins/quotes.lua Normal file
View File

@ -0,0 +1,111 @@
local quotes = {}
local bot = require('otouto.bot')
local utilities = require('otouto.utilities')
local redis = (loadfile "./otouto/redis.lua")()
require("./otouto/plugins/pasteee")
function quotes:init(config)
quotes.triggers = {
"^/(delquote) (.+)$",
"^/(addquote) (.+)$",
"^/(quote)$",
"^/(listquotes)$"
}
quotes.doc = [[*
]]..config.cmd_pat..[[addquote* _<Zitat>_: Fügt Zitat hinzu.
*]]..config.cmd_pat..[[delquote* _<Zitat>_: Löscht das Zitat (nur Superuser)
*]]..config.cmd_pat..[[quote*: Gibt zufälliges Zitat aus
*]]..config.cmd_pat..[[listquotes*: Listet alle Zitate auf
]]
end
quotes.command = 'quote'
function quotes:save_quote(msg)
if msg.text:sub(11):isempty() then
return "Benutzung: /addquote [Zitat]"
end
local quote = msg.text:sub(11)
local hash = get_redis_hash(msg, 'quotes')
print('Saving quote to redis set '..hash)
redis:sadd(hash, quote)
return '*Gespeichert!*'
end
function quotes:delete_quote(msg)
if msg.text:sub(11):isempty() then
return "Benutzung: /delquote [Zitat]"
end
local quote = msg.text:sub(11)
local hash = get_redis_hash(msg, 'quotes')
print('Deleting quote from redis set '..hash)
if redis:sismember(hash, quote) == true then
redis:srem(hash, quote)
return '*Zitat erfolgreich gelöscht!*'
else
return 'Dieses Zitat existiert nicht.'
end
end
function quotes:get_quote(msg)
local hash = get_redis_hash(msg, 'quotes')
if hash then
print('Getting quote from redis set '..hash)
local quotes_table = redis:smembers(hash)
if not quotes_table[1] then
return 'Es wurden noch keine Zitate gespeichert.\nSpeichere doch welche mit /addquote [Zitat]'
else
return quotes_table[math.random(1,#quotes_table)]
end
end
end
function quotes:list_quotes(msg)
local hash = get_redis_hash(msg, 'quotes')
if hash then
print('Getting quotes from redis set '..hash)
local quotes_table = redis:smembers(hash)
local text = ""
for num,quote in pairs(quotes_table) do
text = text..num..") "..quote..'\n'
end
if not text or text == "" then
return 'Es wurden noch keine Zitate gespeichert.\nSpeichere doch welche mit !addquote [Zitat]'
else
return upload(text)
end
end
end
function quotes:action(msg, config, matches)
if matches[1] == "quote" then
utilities.send_message(self, msg.chat.id, quotes:get_quote(msg), true)
return
elseif matches[1] == "addquote" and matches[2] then
utilities.send_reply(self, msg, quotes:save_quote(msg), true)
return
elseif matches[1] == "delquote" and matches[2] then
if msg.from.id ~= config.admin then
utilities.send_reply(self, msg, config.errors.sudo)
return
end
utilities.send_reply(self, msg, quotes:delete_quote(msg), true)
return
elseif matches[1] == "listquotes" then
local link, iserror = quotes:list_quotes(msg)
if iserror then
utilities.send_reply(self, msg, link)
return
end
utilities.send_reply(self, msg, '[Lise aller Zitate auf Paste.ee ansehen]('..link..')', true)
return
end
utilities.send_reply(self, msg, quotes.doc, true)
end
return quotes

View File

@ -0,0 +1,86 @@
local respond = {}
local https = require('ssl.https')
local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
function respond:init(config)
respond.triggers = {
"([Ff][Gg][Tt].? [Ss][Ww][Ii][Ff][Tt])",
"([Ee][Ii][Nn][Zz][Ii][Gg][Ss][Tt][Ee][Ss])",
"([Ee][Ii][Nn][Zz][Ii][Gg][Ss][Tt][Ee][Rr])",
"([Ee][Ii][Nn][Zz][Ii][Gg][Ss][Tt][Ee])",
"^[Bb][Oo][Tt]%??$",
"^/([Ll][Oo][Dd])$",
"^/([Ll][Ff])$",
"^/([Kk][Aa])$",
"^/([Ii][Dd][Kk])$",
"^/([Nn][Bb][Cc])$",
"^/([Ii][Dd][Cc])$",
"^%*([Ff][Rr][Oo][Ss][Cc][Hh])%*",
"^/([Ff][Rr][Oo][Ss][Cc][Hh])$",
"^%(([Ii][Nn][Ll][Oo][Vv][Ee])%)$",
"^/[Ww][Aa][Tt]$"
}
end
respond.command = 'lod, /lf, /nbc, /wat'
function respond:action(msg, config, matches)
local user_name = get_name(msg)
local receiver = msg.chat.id
local GDRIVE_URL = 'https://de2319bd4b4b51a5ef2939a7638c1d35646f49f8.googledrive.com/host/0B_mfIlDgPiyqU25vUHZqZE9IUXc'
if user_name == "DefenderX" then user_name = "Deffu" end
if string.match(msg.text, "[Ff][Gg][Tt].? [Ss][Ww][Ii][Ff][Tt]") then
utilities.send_message(self, receiver, 'Dünnes Eis, '..user_name..'!')
return
elseif string.match(msg.text, "([Ee][Ii][Nn][Zz][Ii][Gg][Ss][Tt][Ee][Ss])") then
utilities.send_message(self, receiver, '*einziges')
return
elseif string.match(msg.text, "([Ee][Ii][Nn][Zz][Ii][Gg][Ss][Tt][Ee][Rr])") then
utilities.send_message(self, receiver, '*einziger')
return
elseif string.match(msg.text, "([Ee][Ii][Nn][Zz][Ii][Gg][Ss][Tt][Ee])") then
utilities.send_message(self, receiver, '*einzige')
return
elseif string.match(msg.text, "[Bb][Oo][Tt]%??") then
utilities.send_reply(self, msg, '*Ich bin da, '..user_name..'!*', true)
return
elseif string.match(msg.text, "[Ll][Oo][Dd]") then
utilities.send_message(self, receiver, 'ಠ_ಠ')
return
elseif string.match(msg.text, "[Ll][Ff]") then
utilities.send_message(self, receiver, '( ͡° ͜ʖ ͡°)')
return
elseif string.match(msg.text, "[Nn][Bb][Cc]") or string.match(msg.text, "[Ii][Dd][Cc]") or string.match(msg.text, "[Kk][Aa]") or string.match(msg.text, "[Ii][Dd][Kk]") then
utilities.send_message(self, receiver, [[¯\_(ツ)_/¯]])
return
elseif string.match(msg.text, "[Ff][Rr][Oo][Ss][Cc][Hh]") then
utilities.send_message(self, receiver, '🐸🐸🐸')
return
elseif string.match(msg.text, "[Ii][Nn][Ll][Oo][Vv][Ee]") then
local file = download_to_file(GDRIVE_URL..'/inlove.gif')
utilities.send_document(self, receiver, file)
return
elseif string.match(msg.text, "[Ww][Aa][Tt]") then
local WAT_URL = GDRIVE_URL..'/wat'
local wats = {
"/wat1.jpg",
"/wat2.jpg",
"/wat3.jpg",
"/wat4.jpg",
"/wat5.jpg",
"/wat6.jpg",
"/wat7.jpg",
"/wat8.jpg"
}
local random_wat = math.random(5)
local file = download_to_file(WAT_URL..wats[random_wat])
utilities.send_photo(self, receiver, file)
return
end
end
return respond

31
otouto/plugins/roll.lua Normal file
View File

@ -0,0 +1,31 @@
local roll = {}
local utilities = require('otouto.utilities')
roll.command = 'roll'
function roll:init(config)
roll.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('roll', true).table
roll.doc = [[*
]]..config.cmd_pat..[[roll*: Werfe einen Würfel]]
end
local canroll = {
"1",
"2",
"3",
"4",
"5",
"6"
}
function roll:roll_dice()
local randomroll = math.random(6)
return canroll[randomroll]
end
function roll:action(msg)
utilities.send_reply(self, msg, 'Du hast eine *'..roll:roll_dice()..'* gewürfelt.', true)
end
return roll

View File

@ -0,0 +1,112 @@
local tagesschau_eil = {}
local http = require('socket.http')
local https = require('ssl.https')
local url = require('socket.url')
local json = require('dkjson')
local utilities = require('otouto.utilities')
local redis = (loadfile "./otouto/redis.lua")()
tagesschau_eil.command = 'eil <sub/del>'
function tagesschau_eil:init(config)
tagesschau_eil.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('eil', true).table
tagesschau_eil.doc = [[*
]]..config.cmd_pat..[[eil* _sub_: Eilmeldungen abonnieren
*]]..config.cmd_pat..[[eil* _del_: Eilmeldungen deabonnieren
*]]..config.cmd_pat..[[eil* _sync_: Nach neuen Eilmeldungen prüfen (nur Superuser)]]
end
local makeOurDate = function(dateString)
local pattern = "(%d+)%-(%d+)%-(%d+)T(%d+)%:(%d+)%:(%d+)"
local year, month, day, hours, minutes, seconds = dateString:match(pattern)
return day..'.'..month..'.'..year..' um '..hours..':'..minutes..':'..seconds
end
local url = 'http://www.tagesschau.de/api'
local hash = 'telegram:tagesschau'
function tagesschau_eil:abonnieren(id)
if redis:sismember(hash..':subs', id) == false then
redis:sadd(hash..':subs', id)
return '*Eilmeldungen abonniert.*'
else
return 'Die Eilmeldungen wurden hier bereits abonniert.'
end
end
function tagesschau_eil:deabonnieren(id)
if redis:sismember(hash..':subs', id) == true then
redis:srem(hash..':subs', id)
return '*Eilmeldungen deabonniert.*'
else
return 'Die Eilmeldungen wurden hier noch nicht abonniert.'
end
end
function tagesschau_eil:action(msg, config)
local input = utilities.input(msg.text)
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, tagesschau_eil.doc, true, msg.message_id, true)
return
end
end
local id = "user#id" .. msg.from.id
if msg.chat.type == 'channel' then
print('Kanäle werden momentan nicht unterstützt')
end
if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then
id = 'chat#id'..msg.chat.id
end
if input:match('(sub)$') then
local output = tagesschau_eil:abonnieren(id)
utilities.send_reply(self, msg, output, true)
elseif input:match('(del)$') then
local output = tagesschau_eil:deabonnieren(id)
utilities.send_reply(self, msg, output, true)
elseif input:match('(sync)$') then
if msg.from.id ~= config.admin then
utilities.send_reply(self, msg, config.errors.sudo)
return
end
tagesschau_eil:cron(self)
end
return
end
function tagesschau_eil:cron(self_plz)
if not self.BASE_URL then
self = self_plz
end
-- print('EIL: Prüfe...')
local last_eil = redis:get(hash..':last_entry')
local res,code = http.request(url)
local data = json.decode(res)
if code ~= 200 then return end
if not data then return end
if data.breakingnews[1] then
if data.breakingnews[1].details ~= last_eil then
local title = '#EIL: *'..data.breakingnews[1].headline..'*'
local news = data.breakingnews[1].shorttext
local posted_at = makeOurDate(data.breakingnews[1].date)..' Uhr'
local post_url = string.gsub(data.breakingnews[1].details, '/api/', '/')
local post_url = string.gsub(post_url, '.json', '.html')
local eil = title..'\n_'..posted_at..'_\n'..news..'\n[Artikel aufrufen]('..post_url..')'
redis:set(hash..':last_entry', data.breakingnews[1].details)
for _,user in pairs(redis:smembers(hash..':subs')) do
local user = string.gsub(user, 'chat%#id', '')
local user = string.gsub(user, 'user%#id', '')
utilities.send_message(self, user, eil, true, nil, true)
end
end
end
end
return tagesschau_eil

View File

@ -140,15 +140,11 @@ function twitter:action(msg)
utilities.send_reply(self, msg, header .. "\n" .. text.."\n"..footer) utilities.send_reply(self, msg, header .. "\n" .. text.."\n"..footer)
for k, v in pairs(images) do for k, v in pairs(images) do
local file = download_to_file(v) local file = download_to_file(v)
bindings.sendPhoto(self, {chat_id = msg.chat.id}, {photo = file} ) utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id)
os.remove(file)
print("Deleted: "..file)
end end
for k, v in pairs(videos) do for k, v in pairs(videos) do
local file = download_to_file(v) local file = download_to_file(v)
bindings.sendVideo(self, {chat_id = msg.chat.id}, {video = file} ) utilities.send_video(self, msg.chat.id, file, nil, msg.message_id)
os.remove(file)
print("Deleted: "..file)
end end
end end

View File

@ -0,0 +1,349 @@
local twitter_send = {}
local http = require('socket.http')
local https = require('ssl.https')
local URL = require('socket.url')
local json = require('dkjson')
local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
local OAuth = require "OAuth"
local redis = (loadfile "./otouto/redis.lua")()
function twitter_send:init(config)
if not cred_data.tw_consumer_key then
print('Missing config value: tw_consumer_key.')
print('twitter_send.lua will not be enabled.')
return
elseif not cred_data.tw_consumer_secret then
print('Missing config value: tw_consumer_secret.')
print('twitter_send.lua will not be enabled.')
return
end
twitter_send.triggers = {
"^/tw (auth) (%d+)",
"^/tw (unauth)$",
"^/tw (verify)$",
"^/tw (.+)",
"^/(twwhitelist add) (%d+)",
"^/(twwhitelist del) (%d+)"
}
twitter_send.doc = [[*
]]..config.cmd_pat..[[tw* _<Text>_: Sendet einen Tweet an den Account, der im Chat angemeldet ist
*]]..config.cmd_pat..[[tw* _verify_: Gibt den angemeldeten User aus, inklusive Profilbild
*]]..config.cmd_pat..[[twwitelist* _add_ _<user>_: Schaltet User für die Tweet-Funktion frei
*]]..config.cmd_pat..[[twwitelist* _del_ _<user>_: Entfernt User von der Tweet-Whitelist
*]]..config.cmd_pat..[[tw* _auth_ _<PIN>_: Meldet mit dieser PIN an (Setup)
*]]..config.cmd_pat..[[tw* _unauth_: Meldet Twitter-Account ab
]]
end
twitter_send.command = 'tw <Tweet>'
local consumer_key = cred_data.tw_consumer_key
local consumer_secret = cred_data.tw_consumer_secret
function can_send_tweet(msg)
local hash = 'user:'..msg.from.id
local var = redis:hget(hash, 'can_send_tweet')
if var == "true" then
return true
else
return false
end
end
local client = OAuth.new(consumer_key, consumer_secret, {
RequestToken = "https://api.twitter.com/oauth/request_token",
AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"},
AccessToken = "https://api.twitter.com/oauth/access_token"
})
function twitter_send:do_twitter_authorization_flow(hash, is_chat)
local callback_url = "oob"
local values = client:RequestToken({ oauth_callback = callback_url })
local oauth_token = values.oauth_token
local oauth_token_secret = values.oauth_token_secret
-- save temporary oauth keys
redis:hset(hash, 'oauth_token', oauth_token)
redis:hset(hash, 'oauth_token_secret', oauth_token_secret)
local auth_url = client:BuildAuthorizationUrl({ oauth_callback = callback_url, force_login = true })
if is_chat then
return 'Bitte schließe den Vorgang ab, indem du unten auf den Link klickst und mir die angezeigte PIN per `/tw auth PIN` *im Chat von gerade* übergibst.\n[Bei Twitter anmelden]('..auth_url..')'
else
return 'Bitte schließe den Vorgang ab, indem du unten auf den Link klickst und mir die angezeigte PIN per `/tw auth PIN` übergibst.\n[Bei Twitter anmelden]('..auth_url..')'
end
end
function twitter_send:get_twitter_access_token(hash, oauth_verifier, oauth_token, oauth_token_secret)
local oauth_verifier = tostring(oauth_verifier) -- must be a string
-- now we'll use the tokens we got in the RequestToken call, plus our PIN
local client = OAuth.new(consumer_key, consumer_secret, {
RequestToken = "https://api.twitter.com/oauth/request_token",
AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"},
AccessToken = "https://api.twitter.com/oauth/access_token"
}, {
OAuthToken = oauth_token,
OAuthVerifier = oauth_verifier
})
client:SetTokenSecret(oauth_token_secret)
local values, err, headers, status, body = client:GetAccessToken()
if err then return 'Einloggen fehlgeschlagen!' end
-- save permanent oauth keys
redis:hset(hash, 'oauth_token', values.oauth_token)
redis:hset(hash, 'oauth_token_secret', values.oauth_token_secret)
return 'Erfolgreich eingeloggt als "@'..values.screen_name..'" (User-ID: '..values.user_id..')'
end
function twitter_send:reset_twitter_auth(hash, frominvalid)
redis:hdel(hash, 'oauth_token')
redis:hdel(hash, 'oauth_token_secret')
if frominvalid then
return '*Authentifizierung nicht erfolgreich, wird zurückgesetzt...*'
else
return '*Erfolgreich abgemeldet!* Entziehe den Zugriff endgültig in deinen [Twitter-Einstellungen](https://twitter.com/settings/applications)!'
end
end
function twitter_send:resolve_url(url)
local response_body = {}
local request_constructor = {
url = url,
method = "HEAD",
sink = ltn12.sink.table(response_body),
headers = {},
redirect = false
}
local ok, response_code, response_headers, response_status_line = http.request(request_constructor)
if ok and response_headers.location then
return response_headers.location
else
return url
end
end
function twitter_send:twitter_verify_credentials(oauth_token, oauth_token_secret)
local client = OAuth.new(consumer_key, consumer_secret, {
RequestToken = "https://api.twitter.com/oauth/request_token",
AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"},
AccessToken = "https://api.twitter.com/oauth/access_token"
}, {
OAuthToken = oauth_token,
OAuthTokenSecret = oauth_token_secret
})
local response_code, response_headers, response_status_line, response_body =
client:PerformRequest(
"GET", "https://api.twitter.com/1.1/account/verify_credentials.json", {
include_entities = false,
skip_status = true,
include_email = false
}
)
local response = json.decode(response_body)
if response_code == 401 then
return twitter_send:reset_twitter_auth(hash, true)
end
if response_code ~= 200 then
return 'HTTP-Fehler '..response_code..': '..data.errors[1].message
end
-- TODO: copied straight from the twitter_user plugin, maybe we can do it better?
local full_name = response.name
local user_name = response.screen_name
if response.verified then
user_name = user_name..''
end
if response.protected then
user_name = user_name..' 🔒'
end
local header = full_name.. " (@" ..user_name.. ")\n"
local description = unescape(response.description)
if response.location then
location = response.location
else
location = ''
end
if response.url and response.location ~= '' then
url = ' | '..twitter_send:resolve_url(response.url)..'\n'
elseif response.url and response.location == '' then
url = twitter_send:resolve_url(response.url)..'\n'
else
url = '\n'
end
local body = description..'\n'..location..url
local favorites = comma_value(response.favourites_count)
local follower = comma_value(response.followers_count)
local following = comma_value(response.friends_count)
local statuses = comma_value(response.statuses_count)
local footer = statuses..' Tweets, '..follower..' Follower, '..following..' folge ich, '..favorites..' Tweets favorisiert'
local text = 'Eingeloggter Account:\n'..header..body..footer
local pp_url = string.gsub(response.profile_image_url_https, "normal", "400x400")
return text, pp_url
end
function twitter_send:send_tweet(tweet, oauth_token, oauth_token_secret, hash)
local client = OAuth.new(consumer_key, consumer_secret, {
RequestToken = "https://api.twitter.com/oauth/request_token",
AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"},
AccessToken = "https://api.twitter.com/oauth/access_token"
}, {
OAuthToken = oauth_token,
OAuthTokenSecret = oauth_token_secret
})
local response_code, response_headers, response_status_line, response_body =
client:PerformRequest(
"POST", "https://api.twitter.com/1.1/statuses/update.json", {
status = tweet
}
)
local data = json.decode(response_body)
if response_code == 401 then
return twitter_send:reset_twitter_auth(hash, true)
end
if response_code ~= 200 then
return 'HTTP-Fehler '..response_code..': '..data.errors[1].message
end
local statusnumber = comma_value(data.user.statuses_count)
local screen_name = data.user.screen_name
local status_id = data.id_str
return '*Tweet #'..statusnumber..' gesendet!* [Auf Twitter ansehen](https://twitter.com/statuses/'..status_id..')'
end
function twitter_send:add_to_twitter_whitelist(user_id)
local hash = 'user:'..user_id
local whitelisted = redis:hget(hash, 'can_send_tweet')
if whitelisted ~= 'true' then
print('Setting can_send_tweet in redis hash '..hash..' to true')
redis:hset(hash, 'can_send_tweet', true)
return '*User '..user_id..' kann jetzt Tweets senden!*'
else
return '*User '..user_id..' kann schon Tweets senden.*'
end
end
function twitter_send:del_from_twitter_whitelist(user_id)
local hash = 'user:'..user_id
local whitelisted = redis:hget(hash, 'can_send_tweet')
if whitelisted == 'true' then
print('Setting can_send_tweet in redis hash '..hash..' to false')
redis:hset(hash, 'can_send_tweet', false)
return '*User '..user_id..' kann jetzt keine Tweets mehr senden!*'
else
return '*User '..user_id..' ist nicht whitelisted.*'
end
end
function twitter_send:action(msg, config, matches)
if matches[1] == "twwhitelist add" and matches[2] then
if msg.from.id ~= config.admin then
utilities.send_reply(self, msg, config.errors.sudo)
return
else
utilities.send_reply(self, msg, twitter_send:add_to_twitter_whitelist(matches[2]), true)
return
end
end
if matches[1] == "twwhitelist del" and matches[2] then
if msg.from.id ~= config.admin then
utilities.send_reply(self, msg, config.errors.sudo)
return
else
utilities.send_reply(self, msg, twitter_send:del_from_twitter_whitelist(matches[2]), true)
return
end
end
local hash = get_redis_hash(msg, 'twitter')
local oauth_token = redis:hget(hash, 'oauth_token')
local oauth_token_secret = redis:hget(hash, 'oauth_token_secret')
-- Thanks to the great doc at https://github.com/ignacio/LuaOAuth#a-more-involved-example
if not oauth_token and not oauth_token_secret then
if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then
if msg.from.id ~= config.admin then
utilities.send_reply(self, msg, config.errors.sudo)
return
else
-- maybe we can edit the older message to update it to "logged in!"?
-- this should be interesting: https://core.telegram.org/bots/api#editmessagetext
local text = twitter_send:do_twitter_authorization_flow(hash, true)
local res = utilities.send_message(self, msg.from.id, text, true, nil, true)
if not res then
utilities.send_reply(self, msg, 'Bitte starte mich zuerst [privat](http://telegram.me/' .. self.info.username .. '?start).', true)
elseif msg.chat.type ~= 'private' then
utilities.send_message(self, msg.chat.id, '_Bitte warten, der Administrator meldet sich an..._', true, nil, true)
end
return
end
else
utilities.send_reply(self, msg, twitter_send:do_twitter_authorization_flow(hash), true)
return
end
end
if matches[1] == 'auth' and matches[2] then
if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then
if msg.from.id ~= config.admin then
utilities.send_reply(self, msg, config.errors.sudo)
return
end
end
if string.len(matches[2]) > 7 then utilities.send_reply(self, msg, 'Invalide PIN!') return end
utilities.send_reply(self, msg, twitter_send:get_twitter_access_token(hash, matches[2], oauth_token, oauth_token_secret))
return
end
if matches[1] == 'unauth' then
if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then
if msg.from.id ~= config.admin then
utilities.send_reply(self, msg, config.errors.sudo)
return
end
end
utilities.send_reply(self, msg, twitter_send:reset_twitter_auth(hash), true)
return
end
if matches[1] == 'verify' then
local text, pp_url = twitter_send:twitter_verify_credentials(oauth_token, oauth_token_secret)
local file = download_to_file(pp_url)
utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id)
utilities.send_reply(self, msg, text)
return
end
if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then
if not can_send_tweet(msg) then
utilities.send_reply(self, msg, '*Du darfst keine Tweets senden.* Entweder wurdest du noch gar nicht freigeschaltet oder ausgeschlossen.', true)
return
else
utilities.send_reply(self, msg, twitter_send:send_tweet(matches[1], oauth_token, oauth_token_secret, hash), true)
return
end
else
utilities.send_reply(self, msg, twitter_send:send_tweet(matches[1], oauth_token, oauth_token_secret, hash), true)
return
end
end
return twitter_send

View File

@ -1,63 +1,150 @@
local weather = {} local weather = {}
local HTTP = require('socket.http') local HTTPS = require('ssl.https')
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')
local redis = (loadfile "./otouto/redis.lua")()
function weather:init(config) function weather:init(config)
if not config.owm_api_key then if not cred_data.forecastio_apikey then
print('Missing config value: owm_api_key.') print('Missing config value: forecastio_apikey.')
print('weather.lua will not be enabled.')
return
elseif not cred_data.google_apikey then
print('Missing config value: google_apikey.')
print('weather.lua will not be enabled.') print('weather.lua will not be enabled.')
return return
end end
weather.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('weather', true).table weather.triggers = {
weather.doc = [[``` "^/wetter$",
]]..config.cmd_pat..[[weather <location> "^/wetter (.*)$",
Returns the current weather conditions for a given location. "^/w$",
```]] "^/w (.*)$"
}
weather.doc = [[*
]]..config.cmd_pat..[[wetter*: Wetter für deinen Wohnort _(/location set [Ort])_
*]]..config.cmd_pat..[[wetter* _<Ort>_: Wetter für diesen Ort
]]
end end
weather.command = 'weather <location>' weather.command = 'wetter'
function weather:action(msg, config) local BASE_URL = "https://api.forecast.io/forecast"
local apikey = cred_data.forecastio_apikey
local google_apikey = cred_data.google_apikey
local input = utilities.input(msg.text) function get_city_name(lat, lng)
if not input then local city = redis:hget('telegram:cache:weather:pretty_names', lat..','..lng)
if msg.reply_to_message and msg.reply_to_message.text then if city then return city end
input = msg.reply_to_message.text local url = 'https://maps.googleapis.com/maps/api/geocode/json?latlng='..lat..','..lng..'&result_type=political&language=de&key='..google_apikey
local res, code = HTTPS.request(url)
if code ~= 200 then return 'Unbekannte Stadt' end
local data = JSON.decode(res).results[1]
local city = data.formatted_address
print('Setting '..lat..','..lng..' in redis hash telegram:cache:weather:pretty_names to "'..city..'"')
redis:hset('telegram:cache:weather:pretty_names', lat..','..lng, city)
return city
end
function weather:get_weather(lat, lng)
print('Finde Wetter in '..lat..', '..lng)
local text = redis:get('telegram:cache:weather:'..lat..','..lng)
if text then print('...aus dem Cache') return text end
local url = BASE_URL..'/'..apikey..'/'..lat..','..lng..'?lang=de&units=si&exclude=minutely,hourly,daily,alerts,flags'
local response_body = {}
local request_constructor = {
url = url,
method = "GET",
sink = ltn12.sink.table(response_body)
}
local ok, response_code, response_headers, response_status_line = HTTPS.request(request_constructor)
if not ok then return nil end
local data = JSON.decode(table.concat(response_body))
local ttl = string.sub(response_headers["cache-control"], 9)
local weather = data.currently
local city = get_city_name(lat, lng)
local temperature = string.gsub(round(weather.temperature, 1), "%.", ",")
local feelslike = string.gsub(round(weather.apparentTemperature, 1), "%.", ",")
local temp = '*Wetter in '..city..':*\n'..temperature..' °C'
local conditions = ' | '..weather.summary
if weather.icon == 'clear-day' then
conditions = conditions..' ☀️'
elseif weather.icon == 'clear-night' then
conditions = conditions..' 🌙'
elseif weather.icon == 'rain' then
conditions = conditions..' ☔️'
elseif weather.icon == 'snow' then
conditions = conditions..' ❄️'
elseif weather.icon == 'sleet' then
conditions = conditions..' 🌨'
elseif weather.icon == 'wind' then
conditions = conditions..' 💨'
elseif weather.icon == 'fog' then
conditions = conditions..' 🌫'
elseif weather.icon == 'cloudy' then
conditions = conditions..' ☁️☁️'
elseif weather.icon == 'partly-cloudy-day' then
conditions = conditions..' 🌤'
elseif weather.icon == 'partly-cloudy-night' then
conditions = conditions..' 🌙☁️'
else else
utilities.send_message(self, msg.chat.id, weather.doc, true, msg.message_id, true) conditions = conditions..''
return end
local windspeed = ' | 💨 '..string.gsub(round(weather.windSpeed, 1), "%.", ",")..' m/s'
local text = temp..conditions..windspeed
if temperature ~= feelslike then
text = text..'\n(gefühlt: '..feelslike..' °C)'
end
cache_data('weather', lat..','..lng, text, tonumber(ttl), 'key')
return text
end
function weather:action(msg, config, matches)
local user_id = msg.from.id
if matches[1] ~= '/wetter' and matches[1] ~= '/w' then
city = matches[1]
else
local set_location = get_location(user_id)
if not set_location then
city = 'Berlin, Deutschland'
else
city = set_location
end end
end end
local coords = utilities.get_coords(input, config) local lat = redis:hget('telegram:cache:weather:'..string.lower(city), 'lat')
if type(coords) == 'string' then local lng = redis:hget('telegram:cache:weather:'..string.lower(city), 'lng')
utilities.send_reply(self, msg, coords) if not lat and not lng then
print('Koordinaten nicht eingespeichert, frage Google...')
coords = utilities.get_coords(city, config)
lat = coords.lat
lng = coords.lon
end
if not lat and not lng then
utilities.send_reply(self, msg, '*Diesen Ort gibt es nicht!*', true)
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 redis:hset('telegram:cache:weather:'..string.lower(city), 'lat', lat)
redis:hset('telegram:cache:weather:'..string.lower(city), 'lng', lng)
local jstr, res = HTTP.request(url) local text = weather:get_weather(lat, lng)
if res ~= 200 then if not text then
utilities.send_reply(self, msg, config.errors.connection) text = 'Konnte das Wetter von dieser Stadt nicht bekommen.'
return
end end
utilities.send_reply(self, msg, text, true)
local jdat = JSON.decode(jstr)
if jdat.cod ~= 200 then
utilities.send_reply(self, msg, 'Error: City not found.')
return
end
local celsius = string.format('%.2f', jdat.main.temp - 273.15)
local fahrenheit = string.format('%.2f', celsius * (9/5) + 32)
local output = '`' .. celsius .. '°C | ' .. fahrenheit .. '°F, ' .. jdat.weather[1].description .. '.`'
utilities.send_reply(self, msg, output, true)
end end
return weather return weather

View File

@ -8,10 +8,10 @@ local utilities = require('otouto.utilities')
wikipedia.command = 'wiki <Begriff>' wikipedia.command = 'wiki <Begriff>'
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).table
wikipedia.doc = [[* wikipedia.doc = [[*
]]..config.cmd_pat..[[wiki* _<Begriff>_: Gibt Wikipedia-Artikel aus ]]..config.cmd_pat..[[wiki* _<Begriff>_: Gibt Wikipedia-Artikel aus
Aliase: ]]..config.cmd_pat..[[w, ]]..config.cmd_pat..[[wikipedia]] Alias: ]]..config.cmd_pat..[[wikipedia]]
end end
local get_title = function(search) local get_title = function(search)

View File

@ -142,9 +142,7 @@ function send_youtube_data(data, msg, self, link, sendpic)
text = text..'\nACHTUNG, In Deutschland gesperrt!' text = text..'\nACHTUNG, In Deutschland gesperrt!'
end end
local file = download_to_file(image_url) local file = download_to_file(image_url)
bindings.sendPhoto(self, {chat_id = msg.chat.id, reply_to_message_id = msg.message_id, caption = text }, {photo = file} ) utilities.send_photo(self, msg.chat.id, file, text, msg.message_id)
os.remove(file)
print("Deleted: "..file)
else else
utilities.send_reply(self, msg, text, true) utilities.send_reply(self, msg, text, true)
end end

View File

@ -17,7 +17,8 @@ function yt_search:init(config)
end end
yt_search.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('yt', true):t('youtube', true).table yt_search.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('yt', true):t('youtube', true).table
yt_search.doc = [[*]]..config.cmd_pat..[[yt* _<Suchbegriff>_: Sucht nach einem YouTube-Video]] yt_search.doc = [[*
]]..config.cmd_pat..[[yt* _<Suchbegriff>_: Sucht nach einem YouTube-Video]]
end end
local BASE_URL = 'https://www.googleapis.com/youtube/v3' local BASE_URL = 'https://www.googleapis.com/youtube/v3'

View File

@ -8,6 +8,8 @@ local ltn12 = require('ltn12')
local HTTPS = require('ssl.https') local HTTPS = require('ssl.https')
local URL = require('socket.url') local URL = require('socket.url')
local JSON = require('dkjson') local JSON = require('dkjson')
local http = require('socket.http')
local https = require('ssl.https')
local serpent = require("serpent") local serpent = require("serpent")
local bindings = require('otouto.bindings') local bindings = require('otouto.bindings')
local redis = (loadfile "./otouto/redis.lua")() local redis = (loadfile "./otouto/redis.lua")()
@ -34,6 +36,107 @@ function utilities:send_reply(old_msg, text, use_markdown)
parse_mode = use_markdown and 'Markdown' or nil parse_mode = use_markdown and 'Markdown' or nil
} ) } )
end end
-- NOTE: Telegram currently only allows file uploads up to 50 MB
-- https://core.telegram.org/bots/api#sendphoto
function utilities:send_photo(chat_id, file, text, reply_to_message_id)
local output = bindings.request(self, 'sendPhoto', {
chat_id = chat_id,
caption = text or nil,
reply_to_message_id = reply_to_message_id
}, {photo = file} )
os.remove(file)
print("Deleted: "..file)
return output
end
-- https://core.telegram.org/bots/api#sendaudio
function utilities:send_audio(chat_id, file, text, reply_to_message_id, duration, performer, title)
local output = bindings.request(self, 'sendAudio', {
chat_id = chat_id,
caption = text or nil,
duration = duration or nil,
performer = performer or nil,
title = title or nil,
reply_to_message_id = reply_to_message_id
}, {audio = file} )
os.remove(file)
print("Deleted: "..file)
return output
end
-- https://core.telegram.org/bots/api#senddocument
function utilities:send_document(chat_id, file, text, reply_to_message_id)
local output = bindings.request(self, 'sendDocument', {
chat_id = chat_id,
caption = text or nil,
reply_to_message_id = reply_to_message_id
}, {document = file} )
os.remove(file)
print("Deleted: "..file)
return output
end
-- https://core.telegram.org/bots/api#sendvideo
function utilities:send_video(chat_id, file, text, reply_to_message_id, duration, width, height)
local output = bindings.request(self, 'sendVideo', {
chat_id = chat_id,
caption = text or nil,
duration = duration or nil,
width = width or nil,
height = height or nil,
reply_to_message_id = reply_to_message_id
}, {video = file} )
os.remove(file)
print("Deleted: "..file)
return output
end
-- NOTE: Voice messages are .ogg files encoded with OPUS
-- https://core.telegram.org/bots/api#sendvoice
function utilities:send_voice(chat_id, file, text, reply_to_message_id, duration)
local output = bindings.request(self, 'sendVoice', {
chat_id = chat_id,
duration = duration or nil,
reply_to_message_id = reply_to_message_id
}, {voice = file} )
os.remove(file)
print("Deleted: "..file)
return output
end
-- https://core.telegram.org/bots/api#sendlocation
function utilities:send_location(chat_id, latitude, longitude, reply_to_message_id)
return bindings.request(self, 'sendLocation', {
chat_id = chat_id,
latitude = latitude,
longitude = longitude,
reply_to_message_id = reply_to_message_id
} )
end
-- NOTE: Venue is different from location: it shows information, such as the street adress or
-- title of the location with it.
-- https://core.telegram.org/bots/api#sendvenue
function utilities:send_venue(chat_id, latitude, longitude, reply_to_message_id, title, address)
return bindings.request(self, 'sendVenue', {
chat_id = chat_id,
latitude = latitude,
longitude = longitude,
title = title,
address = address,
reply_to_message_id = reply_to_message_id
} )
end
-- https://core.telegram.org/bots/api#sendchataction
function utilities:send_typing(chat_id, action)
return bindings.request(self, 'sendChatAction', {
chat_id = chat_id,
action = action
} )
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 ''
@ -110,6 +213,25 @@ local lc_list = {
['!'] = 'ǃ' ['!'] = 'ǃ'
} }
-- Retruns true if the string is empty
function string:isempty()
return self == nil or self == ''
end
-- Retruns true if the string is blank
function string:isblank()
self = self:trim()
return self:isempty()
end
function get_name(msg)
local name = msg.from.first_name
if name == nil then
name = msg.from.id
end
return name
end
-- http://www.lua.org/manual/5.2/manual.html#pdf-io.popen -- http://www.lua.org/manual/5.2/manual.html#pdf-io.popen
function run_command(str) function run_command(str)
local cmd = io.popen(str) local cmd = io.popen(str)
@ -118,6 +240,12 @@ function run_command(str)
return result return result
end end
function convert_timestamp(timestamp, format)
local converted_date = run_command('date -d @'..timestamp..' +"'..format..'"')
local converted_date = string.gsub(converted_date, '%\n', '')
return converted_date
end
function string.starts(String, Start) function string.starts(String, Start)
return Start == string.sub(String,1,string.len(Start)) return Start == string.sub(String,1,string.len(Start))
end end
@ -227,9 +355,9 @@ 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 = 'https://maps.googleapis.com/maps/api/geocode/json?address=' .. URL.escape(input)
local jstr, res = HTTP.request(url) local jstr, res = HTTPS.request(url)
if res ~= 200 then if res ~= 200 then
return config.errors.connection return config.errors.connection
end end
@ -461,6 +589,62 @@ utilities.char = {
em_dash = '' em_dash = ''
} }
-- Returns a table with matches or nil
--function match_pattern(pattern, text, lower_case)
function match_pattern(pattern, text)
if text then
local matches = { string.match(text, pattern) }
if next(matches) then
return matches
end
end
-- nil
end
function post_petition(url, arguments, headers)
local url, h = string.gsub(url, "http://", "")
local url, hs = string.gsub(url, "https://", "")
local post_prot = "http"
if hs == 1 then
post_prot = "https"
end
local response_body = {}
local request_constructor = {
url = post_prot..'://'..url,
method = "POST",
sink = ltn12.sink.table(response_body),
headers = headers or {},
redirect = false
}
local source = arguments
if type(arguments) == "table" then
local source = helpers.url_encode_arguments(arguments)
end
if not headers then
request_constructor.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF8"
request_constructor.headers["X-Accept"] = "application/json"
request_constructor.headers["Accept"] = "application/json"
end
request_constructor.headers["Content-Length"] = tostring(#source)
request_constructor.source = ltn12.source.string(source)
if post_prot == "http" then
ok, response_code, response_headers, response_status_line = http.request(request_constructor)
else
ok, response_code, response_headers, response_status_line = https.request(request_constructor)
end
if not ok then
return nil
end
response_body = JSON.decode(table.concat(response_body))
return response_body, response_headers
end
function get_redis_hash(msg, var) function get_redis_hash(msg, var)
if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then
return 'chat:'..msg.chat.id..':'..var return 'chat:'..msg.chat.id..':'..var
@ -481,6 +665,14 @@ function tablelength(T)
return count return count
end end
function round(num, idp)
if idp and idp>0 then
local mult = 10^idp
return math.floor(num * mult + 0.5) / mult
end
return math.floor(num + 0.5)
end
function comma_value(amount) function comma_value(amount)
local formatted = amount local formatted = amount
while true do while true do
@ -497,6 +689,16 @@ function string.ends(str, fin)
return fin=='' or string.sub(str,-string.len(fin)) == fin return fin=='' or string.sub(str,-string.len(fin)) == fin
end end
function get_location(user_id)
local hash = 'user:'..user_id
local set_location = redis:hget(hash, 'location')
if set_location == 'false' or set_location == nil then
return false
else
return set_location
end
end
function cache_data(plugin, query, data, timeout, typ) function cache_data(plugin, query, data, timeout, typ)
-- How to: cache_data(pluginname, query_name, data_to_cache, expire_in_seconds) -- How to: cache_data(pluginname, query_name, data_to_cache, expire_in_seconds)
local hash = 'telegram:cache:'..plugin..':'..query local hash = 'telegram:cache:'..plugin..':'..query
@ -619,4 +821,14 @@ function unescape(str)
return str return str
end end
function url_encode(str)
if (str) then
str = string.gsub (str, "\n", "\r\n")
str = string.gsub (str, "([^%w %-%_%.%~])",
function (c) return string.format ("%%%02X", string.byte(c)) end)
str = string.gsub (str, " ", "+")
end
return str
end
return utilities return utilities