summaryrefslogtreecommitdiff
path: root/lib/nola_plugins/link/twitter.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/nola_plugins/link/twitter.ex')
-rw-r--r--lib/nola_plugins/link/twitter.ex158
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