diff --git a/otouto/bot.lua b/otouto/bot.lua index f6456bb..350ab85 100644 --- a/otouto/bot.lua +++ b/otouto/bot.lua @@ -124,9 +124,9 @@ end -- Apply plugin.pre_process function function pre_process_msg(self, msg, config) - for number,plugin in ipairs(self.plugins) do + for _,plugin in ipairs(self.plugins) do if plugin.pre_process and msg then - -- print('Preprocess #'..number) -- remove comment to restore old behaviour + -- print('Preprocess '..plugin.name) -- remove comment to restore old behaviour new_msg = plugin:pre_process(msg, self, config) end end diff --git a/otouto/plugins/id.lua b/otouto/plugins/id.lua new file mode 100644 index 0000000..df429fd --- /dev/null +++ b/otouto/plugins/id.lua @@ -0,0 +1,114 @@ +local id = {} + +local redis = (loadfile "./otouto/redis.lua")() +local bindings = require('otouto.bindings') +local utilities = require('otouto.utilities') + +id.command = 'id' + +function id:init(config) + id.triggers = { + "^/id$", + "^/ids? (chat)$" + } + id.doc = [[``` +Returns user and chat info for you or the replied-to message. +Alias: ]]..config.cmd_pat..[[who +```]] +end + +function id:get_member_count(self, msg, chat_id) + return bindings.request(self, 'getChatMembersCount', { + chat_id = chat_id + } ) +end + +function id:user_print_name(user) -- Yes, copied from stats plugin + if user.name then + return user.name + end + + local text = '' + if user.first_name then + text = user.last_name..' ' + end + if user.lastname then + text = text..user.last_name + end + + return text +end + +function id:get_user(user_id, chat_id) + local user_info = {} + local uhash = 'user:'..user_id + local user = redis:hgetall(uhash) + user_info.name = id:user_print_name(user) + user_info.id = user_id + return user_info +end + +function id:action(msg) + + if matches[1] == "/id" then + if msg.reply_to_message then + msg = msg.reply_to_message + msg.from.name = utilities.build_name(msg.from.first_name, msg.from.last_name) + end + + local chat_id = msg.chat.id + local user = 'Du bist @%s, auch bekannt als *%s* `[%s]`' + if msg.from.username then + user = user:format(utilities.markdown_escape(msg.from.username), msg.from.name, msg.from.id) + else + user = 'Du bist *%s* `[%s]`,' + user = user:format(msg.from.name, msg.from.id) + end + + local group = '@%s, auch bekannt als *%s* `[%s]`.' + if msg.chat.type == 'private' then + group = group:format(utilities.markdown_escape(self.info.username), self.info.first_name, self.info.id) + elseif msg.chat.username then + group = group:format(utilities.markdown_escape(msg.chat.username), msg.chat.title, chat_id) + else + group = '*%s* `[%s]`.' + group = group:format(msg.chat.title, chat_id) + end + + local output = user .. ', und du bist in der Gruppe ' .. group + + utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) + elseif matches[1] == "chat" then + if msg.chat.type ~= 'group' and msg.chat.type ~= 'supergroup' then + utilities.send_reply(self, msg, 'Das hier ist keine Gruppe!') + return + end + local chat_name = msg.chat.title + local chat_id = msg.chat.id + -- Users on chat + local hash = 'chat:'..chat_id..':users' + local users = redis:smembers(hash) + local users_info = {} + -- Get user info + for i = 1, #users do + local user_id = users[i] + local user_info = id:get_user(user_id, chat_id) + table.insert(users_info, user_info) + end + local result = id:get_member_count(self, msg, chat_id) + local member_count = result.result - 1 -- minus the bot + if member_count == 1 then + member_count = 'ist *1 Mitglied' + else + member_count = 'sind *'..member_count..' Mitglieder' + end + local text = 'IDs für *'..chat_name..'* `['..chat_id..']`\nHier '..member_count..':*\n---------\n' + for k,user in pairs(users_info) do + text = text..'*'..user.name..'* `['..user.id..']`\n' + text = string.gsub(text, "%_", " ") + end + utilities.send_reply(self, msg, text, true) + end +end + +return id diff --git a/otouto/plugins/stats.lua b/otouto/plugins/stats.lua index 45ed444..4fbef53 100644 --- a/otouto/plugins/stats.lua +++ b/otouto/plugins/stats.lua @@ -80,6 +80,15 @@ function stats:pre_process(msg, self) return end + if msg.left_chat_member then + -- delete user from redis set, but keep message count + local hash = 'chat:'..msg.chat.id..':users' + local user_id_left = msg.left_chat_member.id + print('User '..user_id_left..' was kicked, deleting him/her from redis set '..hash) + redis:srem(hash, user_id_left) + return msg + end + -- Save user on Redis local hash = 'user:'..msg.from.id -- print('Saving user', hash) -- remove comment to restore old behaviour diff --git a/otouto/plugins/streamable.lua b/otouto/plugins/streamable.lua new file mode 100644 index 0000000..84c30b6 --- /dev/null +++ b/otouto/plugins/streamable.lua @@ -0,0 +1,53 @@ +local streamable = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('otouto.utilities') + +streamable.triggers = { + "streamable.com/([A-Za-z0-9-_-]+)", +} + +function streamable:send_streamable_video(shortcode, self, msg) + local BASE_URL = "https://api.streamable.com" + local url = BASE_URL..'/videos/'..shortcode + local res,code = https.request(url) + if code ~= 200 then return 'HTTP-Fehler' end + local data = json.decode(res) + if data.status ~= 2 then utilities.send_reply(self, msg, "Video ist (noch) nicht verfügbar.") return end + + if data.files.webm then + if data.title == "" then title = shortcode..'.webm' else title = data.title..'.webm' end + url = 'https:'..data.files.webm.url + width = data.files.webm.width + height = data.files.webm.height + if data.files.webm.size > 50000000 then + local size = math.floor(data.files.webm.size / 1000000) + utilities.send_reply(self, msg, '*Video ist größer als 50 MB* ('..size..' MB)!\n[Direktlink]('..url..')', true) + return + end + elseif data.files.mp4 then + if data.title == "" then title = shortcode..'.mp4' else title = data.title..'.mp4' end + url = 'https:'..data.files.mp4.url + width = data.files.mp4.width + height = data.files.mp4.height + if data.files.mp4.size > 50000000 then + local size = math.floor(data.files.mp4.size / 1000000) + utilities.send_reply(self, msg, '*Video ist größer als 50 MB* ('..size..' MB)!\n[Direktlink]('..url..')', true) + return + end + end + + utilities.send_typing(self, msg.chat.id, 'upload_video') + local file = download_to_file(url, title) + utilities.send_video(self, msg.chat.id, file, nil, msg.message_id, nil, width, height) + return +end + +function streamable:action(msg, config, matches) + local shortcode = matches[1] + streamable:send_streamable_video(shortcode, self, msg) + return +end + +return streamable diff --git a/otouto/plugins/tweet.lua b/otouto/plugins/tweet.lua new file mode 100644 index 0000000..25abd99 --- /dev/null +++ b/otouto/plugins/tweet.lua @@ -0,0 +1,204 @@ +local tweet = {} + +local utilities = require('otouto.utilities') +local HTTPS = require('ssl.https') +local JSON = require('dkjson') +local redis = (loadfile "./otouto/redis.lua")() +local OAuth = (require "OAuth") +local bindings = require('otouto.bindings') + +function tweet:init(config) + if not cred_data.tw_consumer_key then + print('Missing config value: tw_consumer_key.') + print('tweet.lua will not be enabled.') + return + elseif not cred_data.tw_consumer_secret then + print('Missing config value: tw_consumer_secret.') + print('tweet.lua will not be enabled.') + return + elseif not cred_data.tw_access_token then + print('Missing config value: tw_access_token.') + print('tweet.lua will not be enabled.') + return + elseif not cred_data.tw_access_token_secret then + print('Missing config value: tw_access_token_secret.') + print('tweet.lua will not be enabled.') + return + end + + tweet.triggers = { + "^/tweet (id) ([%w_%.%-]+)$", + "^/tweet (id) ([%w_%.%-]+) (last)$", + "^/tweet (id) ([%w_%.%-]+) (last) ([%d]+)$", + "^/tweet (name) ([%w_%.%-]+)$", + "^/tweet (name) ([%w_%.%-]+) (last)$", + "^/tweet (name) ([%w_%.%-]+) (last) ([%d]+)$" + } + tweet.doc = [[* +]]..config.cmd_pat..[[tweet* id _[id]_: Zufälliger Tweet vom User mit dieser ID +*]]..config.cmd_pat..[[tweet* id _[id]_ last: Aktuellster Tweet vom User mit dieser ID +*]]..config.cmd_pat..[[tweet* name _[Name]_: Zufälliger Tweet vom User mit diesem Namen +*]]..config.cmd_pat..[[tweet* name _[Name]_ last: Aktuellster Tweet vom User mit diesem Namen]] +end + +tweet.command = 'tweet name ' + +local consumer_key = cred_data.tw_consumer_key +local consumer_secret = cred_data.tw_consumer_secret +local access_token = cred_data.tw_access_token +local access_token_secret = cred_data.tw_access_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 = access_token, + OAuthTokenSecret = access_token_secret +}) + +local twitter_url = "https://api.twitter.com/1.1/statuses/user_timeline.json" + +function tweet:analyze_tweet(tweet) + local header = "Tweet von " .. tweet.user.name .. " (@" .. tweet.user.screen_name .. ")\nhttps://twitter.com/statuses/" .. tweet.id_str + local text = tweet.text + + -- replace short URLs + if tweet.entities.urls then + for k, v in pairs(tweet.entities.urls) do + local short = v.url + local long = v.expanded_url + text = text:gsub(short, long) + end + end + + -- remove urls + local urls = {} + if tweet.extended_entities and tweet.extended_entities.media then + for k, v in pairs(tweet.extended_entities.media) do + if v.video_info and v.video_info.variants then -- If it's a video! + table.insert(urls, v.video_info.variants[1].url) + else -- If not, is an image + table.insert(urls, v.media_url) + end + text = text:gsub(v.url, "") -- Replace the URL in text + text = unescape(text) + end + end + + return header, text, urls +end + +function tweet:send_all_files(self, msg, urls) + local data = { + images = { + func = send_photos_from_url, + urls = {} + }, + gifs = { + func = send_gifs_from_url, + urls = {} + }, + videos = { + func = send_videos_from_url, + urls = {} + } + } + + local table_to_insert = nil + for i,url in pairs(urls) do + local _, _, extension = string.match(url, "(https?)://([^\\]-([^\\%.]+))$") + local mime_type = mimetype.get_content_type_no_sub(extension) + if extension == 'gif' then + table_to_insert = data.gifs.urls + elseif mime_type == 'image' then + table_to_insert = data.images.urls + elseif mime_type == 'video' then + table_to_insert = data.videos.urls + else + table_to_insert = nil + end + if table_to_insert then + table.insert(table_to_insert, url) + end + end + for k, v in pairs(data) do + if #v.urls > 0 then + end + v.func(receiver, v.urls) + end +end + +function tweet:sendTweet(self, msg, tweet) + local header, text, urls = tweet:analyze_tweet(tweet) + -- send the parts + local text = unescape(text) + send_reply(self, msg, header .. "\n" .. text) + tweet:send_all_files(self, msg, urls) + return nil +end + +function tweet:getTweet(self, msg, base, all) + local response_code, response_headers, response_status_line, response_body = client:PerformRequest("GET", twitter_url, base) + + if response_code ~= 200 then + return "Konnte nicht verbinden, evtl. existiert der User nicht?" + end + + local response = json:decode(response_body) + if #response == 0 then + return "Konnte keinen Tweet bekommen, sorry" + end + if all then + for i,tweet in pairs(response) do + tweet:sendTweet(self, msg, tweet) + end + else + local i = math.random(#response) + local tweet = response[i] + tweet:sendTweet(self, msg, tweet) + end + + return nil +end + +function tweet:isint(n) + return n==math.floor(n) +end + +function tweet:action(msg, config, matches) + local base = {include_rts = 1} + + if matches[1] == 'id' then + local userid = tonumber(matches[2]) + if userid == nil or not tweet:isint(userid) then + utilities.send_reply(self, msg, "Die ID eines Users ist eine Zahl, du findest sie, indem du den Namen [auf dieser Webseite](http://gettwitterid.com/) eingibst.", true) + return + end + base.user_id = userid + elseif matches[1] == 'name' then + base.screen_name = matches[2] + else + return "" + end + + local count = 200 + local all = false + if #matches > 2 and matches[3] == 'last' then + count = 1 + if #matches == 4 then + local n = tonumber(matches[4]) + if n > 10 then + utilities.send_reply(self, msg, "Du kannst nur 10 Tweets auf einmal abfragen!") + return + end + count = matches[4] + all = true + end + end + base.count = count + + utilities.send_reply(self, msg, tweet:getTweet(self, msg, base, all)) +end + +return tweet diff --git a/otouto/plugins/twitter_user.lua b/otouto/plugins/twitter_user.lua new file mode 100644 index 0000000..4425100 --- /dev/null +++ b/otouto/plugins/twitter_user.lua @@ -0,0 +1,118 @@ +local twitter_user = {} + +local utilities = require('otouto.utilities') +local http = require('socket.http') +local https = require('ssl.https') +local json = require('dkjson') +local OAuth = (require "OAuth") +local bindings = require('otouto.bindings') + +function twitter_user:init(config) + if not cred_data.tw_consumer_key then + print('Missing config value: tw_consumer_key.') + print('twitter_user.lua will not be enabled.') + return + elseif not cred_data.tw_consumer_secret then + print('Missing config value: tw_consumer_secret.') + print('twitter_user.lua will not be enabled.') + return + elseif not cred_data.tw_access_token then + print('Missing config value: tw_access_token.') + print('twitter_user.lua will not be enabled.') + return + elseif not cred_data.tw_access_token_secret then + print('Missing config value: tw_access_token_secret.') + print('twitter_user.lua will not be enabled.') + return + end + + twitter_user.triggers = { + "twitter.com/([A-Za-z0-9-_-.-_-]+)$" + } +end + +local consumer_key = cred_data.tw_consumer_key +local consumer_secret = cred_data.tw_consumer_secret +local access_token = cred_data.tw_access_token +local access_token_secret = cred_data.tw_access_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 = access_token, + OAuthTokenSecret = access_token_secret +}) + +function twitter_user: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_user:action(msg) + local twitter_url = "https://api.twitter.com/1.1/users/show/"..matches[1]..".json" + local response_code, response_headers, response_status_line, response_body = client:PerformRequest("GET", twitter_url) + local response = json.decode(response_body) + + 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_user:resolve_url(response.url)..'\n' + elseif response.url and response.location == '' then + url = twitter_user: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 pic_url = string.gsub(response.profile_image_url_https, "normal", "400x400") + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(pic_url) + + local text = header..body..footer + if string.len(text) > 199 then -- can only send captions with < 200 characters + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + utilities.send_reply(self, msg, text) + return + else + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id) + return + end +end + +return twitter_user diff --git a/otouto/plugins/venue.lua b/otouto/plugins/venue.lua new file mode 100644 index 0000000..0d2f104 --- /dev/null +++ b/otouto/plugins/venue.lua @@ -0,0 +1,31 @@ +local venue = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('otouto.utilities') + +venue.triggers = { + '/nil' +} + +local apikey = cred_data.google_apikey + +function venue:pre_process(msg, self) + if not msg.venue then return end -- Ignore + + local lat = msg.venue.location.latitude + local lng = msg.venue.location.longitude + local url = 'https://maps.googleapis.com/maps/api/geocode/json?latlng='..lat..','..lng..'&result_type=street_address&language=de&key='..apikey + local res, code = https.request(url) + if code ~= 200 then return msg end + local data = json.decode(res).results[1] + local city = data.formatted_address + utilities.send_reply(self, msg, city) + + return msg +end + +function venue:action(msg) +end + +return venue