diff options
Diffstat (limited to 'lib/nola_plugins/link/twitter.ex')
-rw-r--r-- | lib/nola_plugins/link/twitter.ex | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/lib/nola_plugins/link/twitter.ex b/lib/nola_plugins/link/twitter.ex new file mode 100644 index 0000000..e7f3e63 --- /dev/null +++ b/lib/nola_plugins/link/twitter.ex @@ -0,0 +1,158 @@ +defmodule Nola.Plugins.Link.Twitter do + @behaviour Nola.Plugins.Link + + @moduledoc """ + # Twitter Link Preview + + Configuration: + + needs an API key and auth tokens: + + ``` + config :extwitter, :oauth, [ + consumer_key: "zzzzz", + consumer_secret: "xxxxxxx", + access_token: "yyyyyy", + access_token_secret: "ssshhhhhh" + ] + ``` + + options: + + * `expand_quoted`: Add the quoted tweet instead of its URL. Default: true. + """ + + def match(uri = %URI{host: twitter, path: path}, _opts) when twitter in ["twitter.com", "m.twitter.com", "mobile.twitter.com"] do + case String.split(path, "/", parts: 4) do + ["", _username, "status", status_id] -> + {status_id, _} = Integer.parse(status_id) + {true, %{status_id: status_id}} + _ -> false + end + end + + def match(_, _), do: false + + @impl true + def post_match(_, _, _, _), do: false + + def expand(_uri, %{status_id: status_id}, opts) do + expand_tweet(ExTwitter.show(status_id, tweet_mode: "extended"), opts) + end + + defp expand_tweet(nil, _opts) do + :error + end + + defp link_tweet(tweet_or_screen_id_tuple, opts, force_twitter_com \\ false) + + defp link_tweet({screen_name, id}, opts, force_twitter_com) do + path = "/#{screen_name}/status/#{id}" + nitter = Keyword.get(opts, :nitter) + host = if !force_twitter_com && nitter, do: nitter, else: "twitter.com" + "https://#{host}/#{screen_name}/status/#{id}" + end + + defp link_tweet(tweet, opts, force_twitter_com) do + link_tweet({tweet.user.screen_name, tweet.id}, opts, force_twitter_com) + end + + defp expand_tweet(tweet, opts) do + head = format_tweet_header(tweet, opts) + + # Format tweet text + text = expand_twitter_text(tweet, opts) + text = if tweet.quoted_status do + quote_url = link_tweet(tweet.quoted_status, opts, true) + String.replace(text, quote_url, "") + else + text + end + text = IRC.splitlong(text) + + reply_to = if tweet.in_reply_to_status_id do + reply_url = link_tweet({tweet.in_reply_to_screen_name, tweet.in_reply_to_status_id}, opts) + text = if tweet.in_reply_to_screen_name == tweet.user.screen_name, do: "continued from", else: "replying to" + <<3, 15, " ↪ ", text::binary, " ", reply_url::binary, 3>> + end + + quoted = if tweet.quoted_status do + full_text = tweet.quoted_status + |> expand_twitter_text(opts) + |> IRC.splitlong_with_prefix(">") + + head = format_tweet_header(tweet.quoted_status, opts, details: false, prefix: "↓ quoting") + + [head | full_text] + else + [] + end + + #<<2, "#{tweet.user.name} (@#{tweet.user.screen_name})", 2, " ", 3, 61, "#{foot} #{nitter_link}", 3>>, reply_to] ++ text ++ quoted + + text = [head, reply_to | text] ++ quoted + |> Enum.filter(& &1) + {:ok, text} + end + + defp expand_twitter_text(tweet, _opts) do + text = Enum.reduce(tweet.entities.urls, tweet.full_text, fn(entity, text) -> + String.replace(text, entity.url, entity.expanded_url) + end) + extended = tweet.extended_entities || %{media: []} + text = Enum.reduce(extended.media, text, fn(entity, text) -> + url = Enum.filter(extended.media, fn(e) -> entity.url == e.url end) + |> Enum.map(fn(e) -> + cond do + e.type == "video" -> e.expanded_url + true -> e.media_url_https + end + end) + |> Enum.join(" ") + String.replace(text, entity.url, url) + end) + |> HtmlEntities.decode() + end + + defp format_tweet_header(tweet, opts, format_opts \\ []) do + prefix = Keyword.get(format_opts, :prefix, nil) + details = Keyword.get(format_opts, :details, true) + + padded_prefix = if prefix, do: "#{prefix} ", else: "" + author = <<padded_prefix::binary, 2, "#{tweet.user.name} (@#{tweet.user.screen_name})", 2>> + + link = link_tweet(tweet, opts) + + {:ok, at} = Timex.parse(tweet.created_at, "%a %b %e %H:%M:%S %z %Y", :strftime) + {:ok, formatted_time} = Timex.format(at, "{relative}", :relative) + + nsfw = if tweet.possibly_sensitive, do: <<3, 52, "NSFW", 3>> + + rts = if tweet.retweet_count && tweet.retweet_count > 0, do: "#{tweet.retweet_count} RT" + likes = if tweet.favorite_count && tweet.favorite_count > 0, do: "#{tweet.favorite_count} ❤︎" + qrts = if tweet.quote_count && tweet.quote_count > 0, do: "#{tweet.quote_count} QRT" + replies = if tweet.reply_count && tweet.reply_count > 0, do: "#{tweet.reply_count} Reps" + + dmcad = if tweet.withheld_copyright, do: <<3, 52, "DMCA", 3>> + withheld_local = if tweet.withheld_in_countries && length(tweet.withheld_in_countries) > 0 do + "Withheld in #{length(tweet.withheld_in_countries)} countries" + end + + verified = if tweet.user.verified, do: <<3, 51, "✔", 3>> + + meta = if details do + [verified, nsfw, formatted_time, dmcad, withheld_local, rts, qrts, likes, replies] + else + [verified, nsfw, formatted_time, dmcad, withheld_local] + end + + meta = meta + |> Enum.filter(& &1) + |> Enum.join(" - ") + + meta = <<3, 15, meta::binary, " → #{link}", 3>> + + <<author::binary, " — ", meta::binary>> + end + +end |