From 306782496e9b94d96d7fb4da519959f36a6393f8 Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Sun, 12 Jun 2016 20:53:20 +0200 Subject: [PATCH 01/18] Integriere Telegram-Funktionen, wie sendPhoto, sendVideo, sendDocument, etc. in utilites.lua und update alle Plugins, damit sie diese Funktion nutzen --- otouto/plugins/9gag.lua | 5 +- otouto/plugins/gImages.lua | 5 +- otouto/plugins/imdb.lua | 2 +- otouto/plugins/twitter.lua | 8 +-- otouto/plugins/youtube.lua | 4 +- otouto/utilities.lua | 101 +++++++++++++++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 16 deletions(-) diff --git a/otouto/plugins/9gag.lua b/otouto/plugins/9gag.lua index 9577cac..e32fda6 100644 --- a/otouto/plugins/9gag.lua +++ b/otouto/plugins/9gag.lua @@ -26,6 +26,7 @@ function ninegag:get_9GAG() end function ninegag:action(msg, config) + utilities.send_typing(self, msg.chat.id, 'upload_photo') local url, title = ninegag:get_9GAG() if not url then utilities.send_reply(self, msg, config.errors.connection) @@ -33,9 +34,7 @@ function ninegag:action(msg, config) end local file = download_to_file(url) - bindings.sendPhoto(self, {chat_id = msg.chat.id, caption = title}, {photo = file} ) - os.remove(file) - print("Deleted: "..file) + utilities.send_photo(self, msg.chat.id, file, title) end return ninegag diff --git a/otouto/plugins/gImages.lua b/otouto/plugins/gImages.lua index 36bc7f8..a73b85b 100644 --- a/otouto/plugins/gImages.lua +++ b/otouto/plugins/gImages.lua @@ -47,6 +47,7 @@ function gImages:action(msg, config) return end + utilities.send_typing(self, msg.chat.id, 'upload_photo') local apikey = cred_data.google_apikey local cseid = cred_data.google_cse_id local BASE_URL = 'https://www.googleapis.com/customsearch/v1' @@ -68,9 +69,7 @@ function gImages:action(msg, config) local img_url = jdat.items[i].link local file = download_to_file(img_url) - bindings.sendPhoto(self, {chat_id = msg.chat.id, caption = img_url}, {photo = file} ) - os.remove(file) - print("Deleted: "..file) + utilities.send_photo(self, msg.chat.id, file, img_url) end return gImages diff --git a/otouto/plugins/imdb.lua b/otouto/plugins/imdb.lua index caa3897..b82d803 100644 --- a/otouto/plugins/imdb.lua +++ b/otouto/plugins/imdb.lua @@ -51,7 +51,7 @@ function imdb:action(msg, config) if jdat.Poster ~= "N/A" then 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 diff --git a/otouto/plugins/twitter.lua b/otouto/plugins/twitter.lua index 98546a1..92175cd 100644 --- a/otouto/plugins/twitter.lua +++ b/otouto/plugins/twitter.lua @@ -140,15 +140,11 @@ function twitter:action(msg) utilities.send_reply(self, msg, header .. "\n" .. text.."\n"..footer) for k, v in pairs(images) do local file = download_to_file(v) - bindings.sendPhoto(self, {chat_id = msg.chat.id}, {photo = file} ) - os.remove(file) - print("Deleted: "..file) + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) end for k, v in pairs(videos) do local file = download_to_file(v) - bindings.sendVideo(self, {chat_id = msg.chat.id}, {video = file} ) - os.remove(file) - print("Deleted: "..file) + utilities.send_video(self, msg.chat.id, file, nil, msg.message_id) end end diff --git a/otouto/plugins/youtube.lua b/otouto/plugins/youtube.lua index b41a82e..cc48638 100644 --- a/otouto/plugins/youtube.lua +++ b/otouto/plugins/youtube.lua @@ -142,9 +142,7 @@ function send_youtube_data(data, msg, self, link, sendpic) text = text..'\nACHTUNG, In Deutschland gesperrt!' end 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} ) - os.remove(file) - print("Deleted: "..file) + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id) else utilities.send_reply(self, msg, text, true) end diff --git a/otouto/utilities.lua b/otouto/utilities.lua index ef6ab91..51fbe9d 100644 --- a/otouto/utilities.lua +++ b/otouto/utilities.lua @@ -34,6 +34,107 @@ function utilities:send_reply(old_msg, text, use_markdown) parse_mode = use_markdown and 'Markdown' or nil } ) 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 function utilities.get_word(s, i) s = s or '' From 340ae9e9dab8331db48c50280700fcb0bcd0a9bb Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Sun, 12 Jun 2016 22:16:43 +0200 Subject: [PATCH 02/18] - Tagesschau-Eilmeldungen portiert - kleinere Fixes in Google-Images und YouTube-Search --- otouto/plugins/gImages.lua | 7 +- otouto/plugins/tagesschau_eil.lua | 112 ++++++++++++++++++++++++++++++ otouto/plugins/youtube_search.lua | 3 +- 3 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 otouto/plugins/tagesschau_eil.lua diff --git a/otouto/plugins/gImages.lua b/otouto/plugins/gImages.lua index a73b85b..3fab5b3 100644 --- a/otouto/plugins/gImages.lua +++ b/otouto/plugins/gImages.lua @@ -21,11 +21,10 @@ function gImages:init(config) end gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('img', true):t('i', true):t('insfw', true).table - gImages.doc = [[``` -]]..config.cmd_pat..[[img + gImages.doc = [[* +]]..config.cmd_pat..[[img* __ Sucht Bild mit Google und versendet es (SafeSearch aktiv) -Alias: ]]..config.cmd_pat..[[i -```]] +Alias: *]]..config.cmd_pat..[[i*]] end gImages.command = 'img ' diff --git a/otouto/plugins/tagesschau_eil.lua b/otouto/plugins/tagesschau_eil.lua new file mode 100644 index 0000000..07cf81c --- /dev/null +++ b/otouto/plugins/tagesschau_eil.lua @@ -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 ' + +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 diff --git a/otouto/plugins/youtube_search.lua b/otouto/plugins/youtube_search.lua index 276f96f..f94a392 100644 --- a/otouto/plugins/youtube_search.lua +++ b/otouto/plugins/youtube_search.lua @@ -17,7 +17,8 @@ function yt_search:init(config) end 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* __: Sucht nach einem YouTube-Video]] + yt_search.doc = [[* +]]..config.cmd_pat..[[yt* __: Sucht nach einem YouTube-Video]] end local BASE_URL = 'https://www.googleapis.com/youtube/v3' From 05e1b497b13e1b3e323e1c21cf8ed8000ad9b87e Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Mon, 13 Jun 2016 22:18:36 +0200 Subject: [PATCH 03/18] =?UTF-8?q?-=20Experimenteller=20Support=20f=C3=BCr?= =?UTF-8?q?=20matches()=20in=20Plugins=20(wie=20in=20yagops=20Bot)=20-=20T?= =?UTF-8?q?witter-Send-Plugin=20portiert=20-=20Utilites=20modifiziert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- otouto/bot.lua | 13 +- otouto/plugins/twitter_send.lua | 349 ++++++++++++++++++++++++++++++++ otouto/utilities.lua | 12 ++ 3 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 otouto/plugins/twitter_send.lua diff --git a/otouto/bot.lua b/otouto/bot.lua index d0bd39b..ad8810c 100644 --- a/otouto/bot.lua +++ b/otouto/bot.lua @@ -5,7 +5,7 @@ local bindings -- Load Telegram bindings. local utilities -- Load miscellaneous and cross-plugin functions. 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. @@ -73,10 +73,17 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec for _,w in pairs(v.triggers) do if string.match(msg.text_lower, w) then local success, result = pcall(function() - return v.action(self, msg, config) + -- trying to port matches to otouto + for k, pattern in pairs(v.triggers) do + matches = match_pattern(pattern, msg.text) + if matches then + break; + end + end + return v.action(self, msg, config, matches) end) if not success then - utilities.send_reply(self, msg, 'Sorry, an unexpected error occurred.') + utilities.send_reply(self, msg, 'Ein unbekannter Fehler ist aufgetreten, bitte [melde diesen Bug](https://github.com/Brawl345/Brawlbot-v2/issues).', true) utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) return end diff --git a/otouto/plugins/twitter_send.lua b/otouto/plugins/twitter_send.lua new file mode 100644 index 0000000..67207be --- /dev/null +++ b/otouto/plugins/twitter_send.lua @@ -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* __: 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_ __: Schaltet User für die Tweet-Funktion frei +*]]..config.cmd_pat..[[twwitelist* _del_ __: Entfernt User von der Tweet-Whitelist +*]]..config.cmd_pat..[[tw* _auth_ __: Meldet mit dieser PIN an (Setup) +*]]..config.cmd_pat..[[tw* _unauth_: Meldet Twitter-Account ab +]] +end + +twitter_send.command = 'tw ' + +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 \ No newline at end of file diff --git a/otouto/utilities.lua b/otouto/utilities.lua index 51fbe9d..bf86ae4 100644 --- a/otouto/utilities.lua +++ b/otouto/utilities.lua @@ -562,6 +562,18 @@ utilities.char = { 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 get_redis_hash(msg, var) if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then return 'chat:'..msg.chat.id..':'..var From b3bf7f7c42e6508c40331f6c88ce7b46cea6b09f Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Tue, 14 Jun 2016 12:21:16 +0200 Subject: [PATCH 04/18] - Respond-Plugin portiert - get_name() in utilites --- otouto/plugins/respond.lua | 86 ++++++++++++++++++++++++++++++++++++++ otouto/utilities.lua | 8 ++++ 2 files changed, 94 insertions(+) create mode 100644 otouto/plugins/respond.lua diff --git a/otouto/plugins/respond.lua b/otouto/plugins/respond.lua new file mode 100644 index 0000000..04052c7 --- /dev/null +++ b/otouto/plugins/respond.lua @@ -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 \ No newline at end of file diff --git a/otouto/utilities.lua b/otouto/utilities.lua index bf86ae4..95471a6 100644 --- a/otouto/utilities.lua +++ b/otouto/utilities.lua @@ -211,6 +211,14 @@ local lc_list = { ['!'] = 'ǃ' } +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 function run_command(str) local cmd = io.popen(str) From 0ab125d8b78b6a2e1e8504786f5d8d441278bdfe Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Tue, 14 Jun 2016 13:14:09 +0200 Subject: [PATCH 05/18] =?UTF-8?q?-=20Quotes=20portiert=20-=20Pasteee=20por?= =?UTF-8?q?tiert=20-=20Roll=20portiert=20-=20post=5Fpetition()=20verf?= =?UTF-8?q?=C3=BCgbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- otouto/plugins/dice.lua | 56 ------------------- otouto/plugins/pasteee.lua | 45 +++++++++++++++ otouto/plugins/quotes.lua | 111 +++++++++++++++++++++++++++++++++++++ otouto/plugins/roll.lua | 32 +++++++++++ otouto/utilities.lua | 57 +++++++++++++++++++ 5 files changed, 245 insertions(+), 56 deletions(-) delete mode 100644 otouto/plugins/dice.lua create mode 100644 otouto/plugins/pasteee.lua create mode 100644 otouto/plugins/quotes.lua create mode 100644 otouto/plugins/roll.lua diff --git a/otouto/plugins/dice.lua b/otouto/plugins/dice.lua deleted file mode 100644 index 032d3bf..0000000 --- a/otouto/plugins/dice.lua +++ /dev/null @@ -1,56 +0,0 @@ -local dice = {} - -local utilities = require('otouto.utilities') - -dice.command = 'roll ' - -function dice:init(config) - dice.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('roll', true).table - dice.doc = [[``` -]]..config.cmd_pat..[[roll -Returns a set of dice rolls, where n is the number of rolls and r is the range. If only a range is given, returns only one roll. -```]] -end - -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 diff --git a/otouto/plugins/pasteee.lua b/otouto/plugins/pasteee.lua new file mode 100644 index 0000000..3d7d45e --- /dev/null +++ b/otouto/plugins/pasteee.lua @@ -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* __: Postet Text auf Paste.ee]] +end + +pasteee.command = 'pasteee ' + +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 \ No newline at end of file diff --git a/otouto/plugins/quotes.lua b/otouto/plugins/quotes.lua new file mode 100644 index 0000000..f8aa1cc --- /dev/null +++ b/otouto/plugins/quotes.lua @@ -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* __: Fügt Zitat hinzu. +*]]..config.cmd_pat..[[delquote* __: 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 diff --git a/otouto/plugins/roll.lua b/otouto/plugins/roll.lua new file mode 100644 index 0000000..f8bbf11 --- /dev/null +++ b/otouto/plugins/roll.lua @@ -0,0 +1,32 @@ +local roll = {} + +local utilities = require('otouto.utilities') + +roll.command = 'roll' +roll.doc = '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 diff --git a/otouto/utilities.lua b/otouto/utilities.lua index 95471a6..a7dd8ba 100644 --- a/otouto/utilities.lua +++ b/otouto/utilities.lua @@ -8,6 +8,8 @@ local ltn12 = require('ltn12') local HTTPS = require('ssl.https') local URL = require('socket.url') local JSON = require('dkjson') +local http = require('socket.http') +local https = require('ssl.https') local serpent = require("serpent") local bindings = require('otouto.bindings') local redis = (loadfile "./otouto/redis.lua")() @@ -211,6 +213,17 @@ 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 @@ -582,6 +595,50 @@ function match_pattern(pattern, text) -- 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) if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then return 'chat:'..msg.chat.id..':'..var From 5df5d13e7ae91112b883ac48effb6230fdb091e8 Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Tue, 14 Jun 2016 15:25:53 +0200 Subject: [PATCH 06/18] - Media- und Images-Plugin geportet --- otouto/plugins/images.lua | 15 +++++++++ otouto/plugins/media.lua | 66 +++++++++++++++++++++++++++++++++++++++ otouto/plugins/roll.lua | 3 +- 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 otouto/plugins/images.lua create mode 100644 otouto/plugins/media.lua diff --git a/otouto/plugins/images.lua b/otouto/plugins/images.lua new file mode 100644 index 0000000..58fc019 --- /dev/null +++ b/otouto/plugins/images.lua @@ -0,0 +1,15 @@ +local images = {} + +local utilities = require('otouto.utilities') +images.triggers = { + "(https?://[%w-_%%%.%?%.:,/%+=~&%[%]]+%.[Pp][Nn][Gg])$", + "(https?://[%w-_%%%.%?%.:,/%+=~&%[%]]+%.[Jj][Pp][Ee]?[Gg])$" +} + +function images:action(msg) + 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 diff --git a/otouto/plugins/media.lua b/otouto/plugins/media.lua new file mode 100644 index 0000000..fa9afad --- /dev/null +++ b/otouto/plugins/media.lua @@ -0,0 +1,66 @@ +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 + + 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 diff --git a/otouto/plugins/roll.lua b/otouto/plugins/roll.lua index f8bbf11..f445ed3 100644 --- a/otouto/plugins/roll.lua +++ b/otouto/plugins/roll.lua @@ -3,12 +3,11 @@ local roll = {} local utilities = require('otouto.utilities') roll.command = 'roll' -roll.doc = '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*]] +]]..config.cmd_pat..[[roll*: Werfe einen Würfel]] end local canroll = { From 5362d40b3fbd2b6fa287ad2f817e2dc2bc6d3c0a Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Tue, 14 Jun 2016 16:17:13 +0200 Subject: [PATCH 07/18] - Portiere Credentials_Manager - Portiere Location_Manager - get_location() in utilites - send_typing() in images und media --- otouto/plugins/creds.lua | 125 ++++++++++++++++++++++++++++ otouto/plugins/images.lua | 1 + otouto/plugins/location_manager.lua | 69 +++++++++++++++ otouto/plugins/media.lua | 1 + otouto/utilities.lua | 14 +++- 5 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 otouto/plugins/creds.lua create mode 100644 otouto/plugins/location_manager.lua diff --git a/otouto/plugins/creds.lua b/otouto/plugins/creds.lua new file mode 100644 index 0000000..6118fd5 --- /dev/null +++ b/otouto/plugins/creds.lua @@ -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_ __ __: Speichert Schlüssel mit dieser Variable ein +*]]..config.cmd_pat..[[creds* _del_ __: Löscht Schlüssel mit dieser Variable +*]]..config.cmd_pat..[[creds* _rename_ __ __: 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) + 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) + 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) + 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) nutzen!', 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 diff --git a/otouto/plugins/images.lua b/otouto/plugins/images.lua index 58fc019..a546ba4 100644 --- a/otouto/plugins/images.lua +++ b/otouto/plugins/images.lua @@ -7,6 +7,7 @@ images.triggers = { } 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) diff --git a/otouto/plugins/location_manager.lua b/otouto/plugins/location_manager.lua new file mode 100644 index 0000000..aece496 --- /dev/null +++ b/otouto/plugins/location_manager.lua @@ -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_ __: 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 diff --git a/otouto/plugins/media.lua b/otouto/plugins/media.lua index fa9afad..bd1ba4a 100644 --- a/otouto/plugins/media.lua +++ b/otouto/plugins/media.lua @@ -28,6 +28,7 @@ function media:action(msg) 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) diff --git a/otouto/utilities.lua b/otouto/utilities.lua index a7dd8ba..5306af7 100644 --- a/otouto/utilities.lua +++ b/otouto/utilities.lua @@ -349,9 +349,9 @@ end -- Gets coordinates for a location. Used by gMaps.lua, time.lua, weather.lua. 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 return config.errors.connection end @@ -675,6 +675,16 @@ function string.ends(str, fin) return fin=='' or string.sub(str,-string.len(fin)) == fin 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) -- How to: cache_data(pluginname, query_name, data_to_cache, expire_in_seconds) local hash = 'telegram:cache:'..plugin..':'..query From 55237006155a3e419c4a552fd8355035f83aaf49 Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Tue, 14 Jun 2016 16:22:47 +0200 Subject: [PATCH 08/18] I totally forgot how to deusch --- otouto/plugins/creds.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otouto/plugins/creds.lua b/otouto/plugins/creds.lua index 6118fd5..42c6092 100644 --- a/otouto/plugins/creds.lua +++ b/otouto/plugins/creds.lua @@ -98,7 +98,7 @@ function creds_manager:action(msg, config, matches) 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) nutzen!', true) + utilities.send_reply(self, msg, 'Dieses Plugin solltest du nur [privat](http://telegram.me/' .. self.info.username .. '?start=creds) verwenden!', true) return end From 101eb70eaefaf8711ef49e5a3a89c79e601ba1f4 Mon Sep 17 00:00:00 2001 From: topkecleon Date: Tue, 14 Jun 2016 17:57:36 -0400 Subject: [PATCH 09/18] otouto 3.10 Plugins can now silence error messages or set their own. administration.lua now silences error messages. administration kick notifications now include user IDs. --- config.lua | 3 ++- otouto/bot.lua | 19 +++++++++++++------ otouto/plugins/administration.lua | 29 ++++++++++++++++++++++++----- otouto/plugins/patterns.lua | 5 ++--- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/config.lua b/config.lua index 5ec7a2c..b2be581 100755 --- a/config.lua +++ b/config.lua @@ -43,7 +43,8 @@ Send /help to get started. simsimi_key = '', simsimi_trial = true, - errors = { -- Generic error messages used in various plugins. + errors = { -- Generic error messages. + generic = 'An unexpected error occurred.', connection = 'Connection error.', results = 'No results found.', argument = 'Invalid argument.', diff --git a/otouto/bot.lua b/otouto/bot.lua index 4fe8937..4986ecf 100755 --- a/otouto/bot.lua +++ b/otouto/bot.lua @@ -4,7 +4,7 @@ local bot = {} local bindings -- Load Telegram bindings. local utilities -- Load miscellaneous and cross-plugin functions. -bot.version = '3.9' +bot.version = '3.10' function bot:init(config) -- The function run when the bot is started or reloaded. @@ -66,14 +66,21 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec msg.text_lower = msg.text:lower() end - for _,v in ipairs(self.plugins) do - for _,w in pairs(v.triggers) do - if string.match(msg.text_lower, w) then + for _, plugin in ipairs(self.plugins) do + for _, trigger in pairs(plugin.triggers) do + if string.match(msg.text_lower, trigger) then local success, result = pcall(function() - return v.action(self, msg, config) + return plugin.action(self, msg, config) end) 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) + end utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) return end diff --git a/otouto/plugins/administration.lua b/otouto/plugins/administration.lua index f7dca82..62dbb3f 100644 --- a/otouto/plugins/administration.lua +++ b/otouto/plugins/administration.lua @@ -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.`' + -- In the worst case, don't send errors in reply to random messages. + administration.error = false + end function administration.init_flags(cmd_pat) return { @@ -263,9 +266,9 @@ function administration:kick_user(chat, target, reason, config) local victim = target if self.database.users[tostring(target)] then victim = utilities.build_name( - self.database.users[tostring(target)].first_name, - self.database.users[tostring(target)].last_name - ) + self.database.users[tostring(target)].first_name, + self.database.users[tostring(target)].last_name + ) .. ' [' .. victim .. ']' end local group = self.database.administration.groups[tostring(chat)].name utilities.handle_exception(self, victim..' kicked from '..group, reason, config) @@ -917,7 +920,9 @@ function administration.init_command(self_, config) if input then input = utilities.get_word(input, 1) 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 if not input then 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', 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.') else local flags = {} @@ -1327,6 +1334,18 @@ function administration.init_command(self_, config) 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 } } diff --git a/otouto/plugins/patterns.lua b/otouto/plugins/patterns.lua index bfffcd4..82eb8fd 100644 --- a/otouto/plugins/patterns.lua +++ b/otouto/plugins/patterns.lua @@ -7,7 +7,7 @@ patterns.triggers = { } 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 if msg.reply_to_message.from.id == self.info.id then output = output:gsub('Did you mean:\n"', '') @@ -22,8 +22,7 @@ function patterns:action(msg) end ) if res == false then - output = 'Malformed pattern!' - utilities.send_reply(self, msg, output) + utilities.send_reply(self, msg, 'Malformed pattern!') else output = output:sub(1, 4000) output = 'Did you mean:\n"' .. output .. '"' From b176c2099bcfffd870650e5ddd73023063965d23 Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Wed, 15 Jun 2016 01:16:27 +0200 Subject: [PATCH 10/18] =?UTF-8?q?-=20Portiere=20Weather-=20und=20Forecast-?= =?UTF-8?q?Plugins=20-=20convert=5Ftimestamp()=20und=20round()=20in=20util?= =?UTF-8?q?ites=20-=20Bugfixes=20und=20kleinere=20=C3=84nderungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- otouto/plugins/creds.lua | 6 +- otouto/plugins/forecast.lua | 222 +++++++++++++++++++++++++++++++++++ otouto/plugins/weather.lua | 175 ++++++++++++++++++++------- otouto/plugins/wikipedia.lua | 4 +- otouto/utilities.lua | 14 +++ 5 files changed, 372 insertions(+), 49 deletions(-) create mode 100644 otouto/plugins/forecast.lua diff --git a/otouto/plugins/creds.lua b/otouto/plugins/creds.lua index 42c6092..ff32f46 100644 --- a/otouto/plugins/creds.lua +++ b/otouto/plugins/creds.lua @@ -60,7 +60,7 @@ end function creds_manager:add_creds(var, key) print('Saving credential for '..var..' to redis hash '..hash) redis:hset(hash, var, key) - reload_creds() + creds_manager:reload_creds() return 'Gespeichert!' end @@ -68,7 +68,7 @@ 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) - reload_creds() + creds_manager:reload_creds() return 'Key von "'..var..'" erfolgreich gelöscht!' else return 'Du hast keine Logininformationen für diese Variable eingespeichert.' @@ -80,7 +80,7 @@ function creds_manager:rename_creds(var, newvar) local key = redis:hget(hash, var) if redis:hsetnx(hash, newvar, key) == true then redis:hdel(hash, var) - reload_creds() + creds_manager:reload_creds() return '"'..var..'" erfolgreich zu "'..newvar..'" umbenannt.' else return "Variable konnte nicht umbenannt werden: Zielvariable existiert bereits." diff --git a/otouto/plugins/forecast.lua b/otouto/plugins/forecast.lua new file mode 100644 index 0000000..47fcc3c --- /dev/null +++ b/otouto/plugins/forecast.lua @@ -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 )_ +*]]..config.cmd_pat..[[f* __: Wettervorhersage für diesen Ort +*]]..config.cmd_pat..[[fh*: 24-Stunden-Wettervorhersage für deine Stadt _(/location set [Ort]_ +*]]..config.cmd_pat..[[fh* __: 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 diff --git a/otouto/plugins/weather.lua b/otouto/plugins/weather.lua index d437c46..ee7399c 100644 --- a/otouto/plugins/weather.lua +++ b/otouto/plugins/weather.lua @@ -1,63 +1,150 @@ local weather = {} -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 weather:init(config) - if not config.owm_api_key then - print('Missing config value: owm_api_key.') + 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 - weather.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('weather', true).table - weather.doc = [[``` -]]..config.cmd_pat..[[weather -Returns the current weather conditions for a given location. -```]] + weather.triggers = { + "^/wetter$", + "^/wetter (.*)$", + "^/w$", + "^/w (.*)$" + } + weather.doc = [[* +]]..config.cmd_pat..[[wetter*: Wetter für deinen Wohnort _(/location set [Ort])_ +*]]..config.cmd_pat..[[wetter* __: Wetter für diesen Ort +]] end -weather.command = 'weather ' +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) - 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, weather.doc, true, msg.message_id, true) - return - end +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 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 + conditions = conditions..'' + 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 + + 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 - local coords = utilities.get_coords(input, config) - if type(coords) == 'string' then - utilities.send_reply(self, msg, coords) - return - end - - local url = 'http://api.openweathermap.org/data/2.5/weather?APPID=' .. config.owm_api_key .. '&lat=' .. coords.lat .. '&lon=' .. coords.lon - - local jstr, res = HTTP.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - 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) - + redis:hset('telegram:cache:weather:'..string.lower(city), 'lat', lat) + redis:hset('telegram:cache:weather:'..string.lower(city), 'lng', lng) + + local text = weather:get_weather(lat, lng) + if not text then + text = 'Konnte das Wetter von dieser Stadt nicht bekommen.' + end + utilities.send_reply(self, msg, text, true) end return weather diff --git a/otouto/plugins/wikipedia.lua b/otouto/plugins/wikipedia.lua index 777b87d..b15fdbc 100644 --- a/otouto/plugins/wikipedia.lua +++ b/otouto/plugins/wikipedia.lua @@ -8,10 +8,10 @@ local utilities = require('otouto.utilities') wikipedia.command = 'wiki ' 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 = [[* ]]..config.cmd_pat..[[wiki* __: Gibt Wikipedia-Artikel aus -Aliase: ]]..config.cmd_pat..[[w, ]]..config.cmd_pat..[[wikipedia]] +Alias: ]]..config.cmd_pat..[[wikipedia]] end local get_title = function(search) diff --git a/otouto/utilities.lua b/otouto/utilities.lua index 5306af7..cec8851 100644 --- a/otouto/utilities.lua +++ b/otouto/utilities.lua @@ -240,6 +240,12 @@ function run_command(str) return result 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) return Start == string.sub(String,1,string.len(Start)) end @@ -659,6 +665,14 @@ function tablelength(T) return count 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) local formatted = amount while true do From 8c641e8ba7bf76cdffa611791dab1f5614f3297d Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Wed, 15 Jun 2016 01:31:53 +0200 Subject: [PATCH 11/18] Merging funktioniert nie beim ersten Mal --- config.lua.example | 1 - 1 file changed, 1 deletion(-) diff --git a/config.lua.example b/config.lua.example index 8ee023f..a4e4eb7 100644 --- a/config.lua.example +++ b/config.lua.example @@ -21,7 +21,6 @@ Sende /hilfe, um zu starten -- The symbol that starts a command. Usually noted as '/' in documentation. cmd_pat = '/', -<<<<<<< HEAD:config.lua.example errors = { -- Generic error messages used in various plugins. generic = 'An unexpected error occurred.', connection = 'Verbindungsfehler.', From 52566f490ea31fe5726880e1aa1ebc2761b07228 Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Wed, 15 Jun 2016 01:44:21 +0200 Subject: [PATCH 12/18] Travis --- travis.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 travis.yml diff --git a/travis.yml b/travis.yml new file mode 100644 index 0000000..17e62ee --- /dev/null +++ b/travis.yml @@ -0,0 +1,33 @@ +language: erlang + +before_install: + - 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 + + +script: + - luac -p otouto/*.lua + - luac -p otouto/plugins/*.lua \ No newline at end of file From f07f8dcfdb5fe133689d6ce92a6cfcd1b0ef833e Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Wed, 15 Jun 2016 01:45:35 +0200 Subject: [PATCH 13/18] Huch --- travis.yml => .travis.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename travis.yml => .travis.yml (100%) diff --git a/travis.yml b/.travis.yml similarity index 100% rename from travis.yml rename to .travis.yml From c31150e8cfa46f05349c1825b8711a8d7327a4ca Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Wed, 15 Jun 2016 01:52:26 +0200 Subject: [PATCH 14/18] Versuche, Travis zu fixen --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 17e62ee..f0eb1da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,8 @@ before_install: - ./.luarocks/bin/luarocks install serpent - ./.luarocks/bin/luarocks install sha1 - ./.luarocks/bin/luarocks install lpeg + - cd $THIS_DIR + - ls script: From 4ebe0c38d3b77ed1d9b1881c2a1b7b2224ef7baf Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Wed, 15 Jun 2016 01:56:20 +0200 Subject: [PATCH 15/18] again --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f0eb1da..27eb83a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ 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) @@ -26,8 +27,9 @@ before_install: - ./.luarocks/bin/luarocks install serpent - ./.luarocks/bin/luarocks install sha1 - ./.luarocks/bin/luarocks install lpeg - - cd $THIS_DIR + - cd $CURDIR - ls + - pwd script: From 5600cb7b0b953b59a6fda75d56757e5135927253 Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Wed, 15 Jun 2016 02:02:12 +0200 Subject: [PATCH 16/18] - Travis funkt - Rockspec entfernt --- .travis.yml | 2 -- README.md | 2 ++ otouto-dev-1.rockspec | 22 ---------------------- 3 files changed, 2 insertions(+), 24 deletions(-) delete mode 100644 otouto-dev-1.rockspec diff --git a/.travis.yml b/.travis.yml index 27eb83a..50c956e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,8 +28,6 @@ before_install: - ./.luarocks/bin/luarocks install sha1 - ./.luarocks/bin/luarocks install lpeg - cd $CURDIR - - ls - - pwd script: diff --git a/README.md b/README.md index 7cc94bb..d9128d9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # 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. [Public Bot](http://telegram.me/mokubot) | [Official Channel](http://telegram.me/otouto) | [Development Group](http://telegram.me/BotDevelopment) diff --git a/otouto-dev-1.rockspec b/otouto-dev-1.rockspec deleted file mode 100644 index 86acac1..0000000 --- a/otouto-dev-1.rockspec +++ /dev/null @@ -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 ', - license = 'GPL-2' -} - -dependencies = { - 'lua >= 5.2', - 'LuaSocket ~> 3.0', - 'LuaSec ~> 0.6', - 'dkjson ~> 2.5', - 'LPeg ~> 1.0' -} From f17455a505894ab1a08fcf95f1f13fa6d44a8f0b Mon Sep 17 00:00:00 2001 From: topkecleon Date: Wed, 15 Jun 2016 08:35:39 -0400 Subject: [PATCH 17/18] Updated readme, slightly modified help behavior. --- README.md | 29 +++++++++++++---------------- otouto/plugins/help.lua | 2 +- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0a2feb1..968769f 100755 --- a/README.md +++ b/README.md @@ -11,11 +11,10 @@ otouto is free software; you are free to redistribute it and/or modify it under | For Users | For Coders | |:----------------------------------------------|:------------------------------| -| [Setup](#setup) | [Introduction](#introduction) | -| [Control plugins](#control-plugins) | [Plugins](#plugins) | -| [Group Administration](#group-administration) | [Bindings](#bindings) | -| [List of plugins](#list-of-plugins) | [Output style](#output-style) | -| | [Contributors](#contributors) | +| [Setup](#setup) | [Plugins](#plugins) | +| [Control plugins](#control-plugins) | [Bindings](#bindings) | +| [Group Administration](#group-administration) | [Output style](#output-style) | +| [List of plugins](#list-of-plugins) | [Contributors](#contributors) | ## 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. @@ -194,9 +193,6 @@ Additionally, antiflood can be configured to automatically ban a user after he h * * * -## Introduction -####todo - ## Plugins otouto uses a robust plugin system, similar to yagop's [Telegram-Bot](http://github.com/yagop/telegram-bot). @@ -204,14 +200,15 @@ 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: -| Component | Description | Required? | -|:----------------|:---------------------------------------------|:----------| -| plugin:action | Main function. Expects `msg` table as an argument. | 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:cron | Optional function to be called every minute. | N | -| plugin.command | Basic command and syntax. Listed in the help text. | N | -| plugin.doc | Usage for the plugin. Returned by "/help $command". | N | +| Component | Description | Required? | +|:------------------|:---------------------------------------------|:----------| +| `plugin:action` | Main function. Expects `msg` table as an argument. | 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:cron` | Optional function to be called every minute. | 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.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`. diff --git a/otouto/plugins/help.lua b/otouto/plugins/help.lua index 9d9ffd4..83d4f08 100755 --- a/otouto/plugins/help.lua +++ b/otouto/plugins/help.lua @@ -39,7 +39,7 @@ function help:action(msg) if not input then local res = utilities.send_message(self, msg.from.id, help_text, true, nil, true) if not res then - utilities.send_reply(self, msg, 'Please message me privately or [click here](http://telegram.me/' .. self.info.username .. '?start=help) for a list of commands.', true) + utilities.send_reply(self, msg, 'Please [message me privately](http://telegram.me/' .. self.info.username .. '?start=help) for a list of commands.', true) elseif msg.chat.type ~= 'private' then utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') end From 4a6b9b1816d8784693a5e81c19be687aff8b4bdc Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Wed, 15 Jun 2016 18:00:59 +0200 Subject: [PATCH 18/18] Portierung folgender Plugins: - App Store - Bitly - Bitly_create - Expand - Facebook - GitHub --- otouto/plugins/app_store.lua | 117 +++++++++++++++++++++ otouto/plugins/bitly.lua | 48 +++++++++ otouto/plugins/bitly_create.lua | 142 +++++++++++++++++++++++++ otouto/plugins/expand.lua | 37 +++++++ otouto/plugins/facebook.lua | 180 ++++++++++++++++++++++++++++++++ otouto/plugins/github.lua | 77 ++++++++++++++ otouto/utilities.lua | 10 ++ 7 files changed, 611 insertions(+) create mode 100644 otouto/plugins/app_store.lua create mode 100644 otouto/plugins/bitly.lua create mode 100644 otouto/plugins/bitly_create.lua create mode 100644 otouto/plugins/expand.lua create mode 100644 otouto/plugins/facebook.lua create mode 100644 otouto/plugins/github.lua diff --git a/otouto/plugins/app_store.lua b/otouto/plugins/app_store.lua new file mode 100644 index 0000000..9be7eb1 --- /dev/null +++ b/otouto/plugins/app_store.lua @@ -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 diff --git a/otouto/plugins/bitly.lua b/otouto/plugins/bitly.lua new file mode 100644 index 0000000..d1f9653 --- /dev/null +++ b/otouto/plugins/bitly.lua @@ -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 diff --git a/otouto/plugins/bitly_create.lua b/otouto/plugins/bitly_create.lua new file mode 100644 index 0000000..1b34ff1 --- /dev/null +++ b/otouto/plugins/bitly_create.lua @@ -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* __: Kürzt einen Link mit der Standard Bitly-Adresse +*]]..config.cmd_pat..[[short* __ _[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 ' + +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 \ No newline at end of file diff --git a/otouto/plugins/expand.lua b/otouto/plugins/expand.lua new file mode 100644 index 0000000..60b056e --- /dev/null +++ b/otouto/plugins/expand.lua @@ -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* __: Verlängert Kurz-URL (301er/302er)]] +end + +expand.command = 'expand ' + +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 diff --git a/otouto/plugins/facebook.lua b/otouto/plugins/facebook.lua new file mode 100644 index 0000000..2bbd215 --- /dev/null +++ b/otouto/plugins/facebook.lua @@ -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 \ No newline at end of file diff --git a/otouto/plugins/github.lua b/otouto/plugins/github.lua new file mode 100644 index 0000000..a29328e --- /dev/null +++ b/otouto/plugins/github.lua @@ -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 \ No newline at end of file diff --git a/otouto/utilities.lua b/otouto/utilities.lua index cec8851..27fd9f8 100644 --- a/otouto/utilities.lua +++ b/otouto/utilities.lua @@ -821,4 +821,14 @@ function unescape(str) return str 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