Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F51294
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
32 KB
Subscribers
None
View Options
diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex
index 037b7d6..cff556d 100644
--- a/lib/irc/connection.ex
+++ b/lib/irc/connection.ex
@@ -1,521 +1,521 @@
defmodule IRC.Connection do
require Logger
use Ecto.Schema
@moduledoc """
# IRC Connection
Provides a nicer abstraction over ExIRC's handlers.
## Start connections
```
IRC.Connection.start_link(host: "irc.random.sh", port: 6697, nick: "pouetbot", channels: ["#dev"])
## PubSub topics
* `account` -- accounts change
* {:account_change, old_account_id, new_account_id} # Sent when account merged
* {:accounts, [{:account, network, channel, nick, account_id}] # Sent on bot join
* {:account, network, nick, account_id} # Sent on user join
* `message` -- aill messages (without triggers)
* `message:private` -- all messages without a channel
* `message:#CHANNEL` -- all messages within `#CHANNEL`
* `triggers` -- all triggers
* `trigger:TRIGGER` -- any message with a trigger `TRIGGER`
## Replying to %Nola.Message{}
Each `Nola.Message` comes with a dedicated `replyfun`, to which you only have to pass either:
"""
def irc_doc, do: nil
@min_backoff :timer.seconds(5)
@max_backoff :timer.seconds(2*60)
embedded_schema do
field :network, :string
field :host, :string
field :port, :integer
field :nick, :string
field :user, :string
field :name, :string
field :pass, :string
field :tls, :boolean, default: false
field :channels, {:array, :string}, default: []
end
defmodule Supervisor do
use DynamicSupervisor
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(%IRC.Connection{} = conn) do
spec = %{id: conn.id, start: {IRC.Connection, :start_link, [conn]}, restart: :transient}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_restarts: 10,
max_seconds: 1
)
end
end
def changeset(params) do
import Ecto.Changeset
%__MODULE__{id: EntropyString.large_id()}
|> cast(params, [:network, :host, :port, :nick, :user, :name, :pass, :channels, :tls])
|> validate_required([:host, :port, :nick, :user, :name])
|> apply_action(:insert)
end
def to_tuple(%__MODULE__{} = conn) do
{conn.id, conn.network, conn.host, conn.port, conn.nick, conn.user, conn.name, conn.pass, conn.tls, conn.channels, nil}
end
def from_tuple({id, network, host, port, nick, user, name, pass, tls, channels, _}) do
%__MODULE__{id: id, network: network, host: host, port: port, nick: nick, user: user, name: name, pass: pass, tls: tls, channels: channels}
end
## -- MANAGER API
def setup() do
:dets.open_file(dets(), [])
end
def dets(), do: to_charlist(Nola.data_path("/connections.dets"))
def lookup(id) do
case :dets.lookup(dets(), id) do
[object | _] -> from_tuple(object)
_ -> nil
end
end
def connections() do
:dets.foldl(fn(object, acc) -> [from_tuple(object) | acc] end, [], dets())
end
def start_all() do
for conn <- connections(), do: {conn, IRC.Connection.Supervisor.start_child(conn)}
end
def get_network(network, channel \\ nil) do
spec = [{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[{:==, :"$1", {:const, network}}], [:"$_"]}]
results = Enum.map(:dets.select(dets(), spec), fn(object) -> from_tuple(object) end)
if channel do
Enum.find(results, fn(conn) -> Enum.member?(conn.channels, channel) end)
else
List.first(results)
end
end
def get_host_nick(host, port, nick) do
spec = [{{:_, :_, :"$1", :"$2", :"$3", :_, :_, :_, :_, :_, :_},
[{:andalso,
{:andalso, {:==, :"$1", {:const, host}}, {:==, :"$2", {:const, port}}},
{:==, :"$3", {:const, nick}}}],
[:"$_"]}
]
case :dets.select(dets(), spec) do
[object] -> from_tuple(object)
[] -> nil
end
end
def delete_connection(%__MODULE__{id: id} = conn) do
:dets.delete(dets(), id)
stop_connection(conn)
:ok
end
def start_connection(%__MODULE__{} = conn) do
IRC.Connection.Supervisor.start_child(conn)
end
def stop_connection(%__MODULE__{id: id}) do
case :global.whereis_name(id) do
pid when is_pid(pid) ->
GenServer.stop(pid, :normal)
_ -> :error
end
end
def add_connection(opts) do
case changeset(opts) do
{:ok, conn} ->
if existing = get_host_nick(conn.host, conn.port, conn.nick) do
{:error, {:existing, conn}}
else
:dets.insert(dets(), to_tuple(conn))
IRC.Connection.Supervisor.start_child(conn)
end
error -> error
end
end
def update_connection(connection) do
:dets.insert(dets(), to_tuple(connection))
end
def start_link(conn) do
GenServer.start_link(__MODULE__, [conn], name: {:global, conn.id})
end
def broadcast_message(net, chan, message) do
dispatch("conn", {:broadcast, net, chan, message}, IRC.ConnectionPubSub)
end
def broadcast_message(list, message) when is_list(list) do
for {net, chan} <- list do
broadcast_message(net, chan, message)
end
end
def privmsg(channel, line) do
GenServer.cast(__MODULE__, {:privmsg, channel, line})
end
def init([conn]) do
Logger.metadata(conn: conn.id)
backoff = :backoff.init(@min_backoff, @max_backoff)
|> :backoff.type(:jitter)
{:ok, %{client: nil, backoff: backoff, conn: conn, connected_server: nil, connected_port: nil, network: conn.network}, {:continue, :connect}}
end
@triggers %{
"!" => :bang,
"+" => :plus,
"-" => :minus,
"?" => :query,
"." => :dot,
"~" => :tilde,
"@" => :at,
"++" => :plus_plus,
"--" => :minus_minus,
"!!" => :bang_bang,
"??" => :query_query,
".." => :dot_dot,
"~~" => :tilde_tilde,
"@@" => :at_at
}
def handle_continue(:connect, state) do
client_opts = []
|> Keyword.put(:network, state.conn.network)
{:ok, _} = Registry.register(IRC.ConnectionPubSub, "conn", [])
client = if state.client && Process.alive?(state.client) do
Logger.info("Reconnecting client")
state.client
else
Logger.info("Connecting")
{:ok, client} = ExIRC.Client.start_link(debug: false)
ExIRC.Client.add_handler(client, self())
client
end
opts = [{:nodelay, true}]
conn_fun = if state.conn.tls, do: :connect_ssl!, else: :connect!
apply(ExIRC.Client, conn_fun, [client, to_charlist(state.conn.host), state.conn.port, opts])
{:noreply, %{state | client: client}}
end
def handle_info(:disconnected, state) do
{delay, backoff} = :backoff.fail(state.backoff)
Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
Process.send_after(self(), :connect, delay)
{:noreply, %{state | backoff: backoff}}
end
def handle_info(:connect, state) do
{:noreply, state, {:continue, :connect}}
end
def handle_cast({:privmsg, channel, line}, state) do
irc_reply(state, {channel, nil}, line)
{:noreply, state}
end
# Connection successful
def handle_info({:connected, server, port}, state) do
Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
{_, backoff} = :backoff.succeed(state.backoff)
ExIRC.Client.logon(state.client, state.conn.pass || "", state.conn.nick, state.conn.user, state.conn.name)
{:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
end
# Logon successful
def handle_info(:logged_in, state) do
Logger.info("#{inspect(self())} Logged in")
{_, backoff} = :backoff.succeed(state.backoff)
Enum.map(state.conn.channels, &ExIRC.Client.join(state.client, &1))
{:noreply, %{state | backoff: backoff}}
end
# ISUP
def handle_info({:isup, network}, state) when is_binary(network) do
Nola.UserTrack.clear_network(state.network)
if network != state.network do
Logger.warn("Possibly misconfigured network: #{network} != #{state.network}")
end
{:noreply, state}
end
# Been kicked
def handle_info({:kicked, _sender, chan, _reason}, state) do
ExIRC.Client.join(state.client, chan)
{:noreply, state}
end
# Received something in a channel
def handle_info({:received, text, sender, chan}, state) do
user = if user = Nola.UserTrack.find_by_nick(state.network, sender.nick) do
user
else
Logger.error("Could not lookup user for message: #{inspect {state.network, chan, sender.nick}}")
user = Nola.UserTrack.joined(chan, sender, [])
ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
user
end
if !user do
ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
Logger.error("Could not lookup user nor create it for message: #{inspect {state.network, chan, sender.nick}}")
else
if !Map.get(user.options, :puppet) do
reply_fun = fn(text) -> irc_reply(state, {chan, sender}, text) end
account = Nola.Account.lookup(sender)
message = %Nola.Message{id: FlakeId.get(), transport: :irc, at: NaiveDateTime.utc_now(), text: text, network: state.network,
account: account, sender: sender, channel: chan, replyfun: reply_fun,
trigger: extract_trigger(text)}
message = case Nola.UserTrack.messaged(message) do
:ok -> message
{:ok, message} -> message
end
publish(message, ["#{message.network}/#{chan}:messages"])
end
end
{:noreply, state}
end
# Received a private message
def handle_info({:received, text, sender}, state) do
reply_fun = fn(text) -> irc_reply(state, {sender.nick, sender}, text) end
account = Nola.Account.lookup(sender)
message = %Nola.Message{id: FlakeId.get(), transport: :irc, text: text, network: state.network, at: NaiveDateTime.utc_now(),
account: account, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)}
message = case Nola.UserTrack.messaged(message) do
:ok -> message
{:ok, message} -> message
end
publish(message, ["messages:private", "#{message.network}/#{account.id}:messages"])
{:noreply, state}
end
## -- Broadcast
def handle_info({:broadcast, net, account = %Nola.Account{}, message}, state) do
if net == state.conn.network do
user = Nola.UserTrack.find_by_account(net, account)
if user do
irc_reply(state, {user.nick, nil}, message)
end
end
{:noreply, state}
end
def handle_info({:broadcast, net, chan, message}, state) do
if net == state.conn.network && Enum.member?(state.conn.channels, chan) do
irc_reply(state, {chan, nil}, message)
end
{:noreply, state}
end
## -- UserTrack
def handle_info({:joined, channel}, state) do
ExIRC.Client.who(state.client, channel)
{:noreply, state}
end
def handle_info({:who, channel, whos}, state) do
accounts = Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) ->
priv = if operator, do: [:operator], else: []
# Don't touch -- on WHO the bot joined, not the users.
Nola.UserTrack.joined(channel, who, priv, false)
account = Nola.Account.lookup(who)
if account do
{:account, who.network, channel, who.nick, account.id}
end
end)
|> Enum.filter(fn(x) -> x end)
dispatch("account", {:accounts, accounts})
{:noreply, state}
end
def handle_info({:quit, reason, sender}, state) do
Nola.UserTrack.quitted(sender, reason)
{:noreply, state}
end
def handle_info({:joined, channel, sender}, state) do
Nola.UserTrack.joined(channel, sender, [])
account = Nola.Account.lookup(sender)
if account do
dispatch("account", {:account, sender.network, channel, sender.nick, account.id})
end
{:noreply, state}
end
def handle_info({:kicked, nick, _by, channel, _reason}, state) do
Nola.UserTrack.parted(state.network, channel, nick)
{:noreply, state}
end
def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do
Nola.UserTrack.parted(state.network, channel, nick)
{:noreply, state}
end
def handle_info({:mode, [channel, mode, nick]}, state) do
track_mode(state.network, channel, nick, mode)
{:noreply, state}
end
def handle_info({:nick_changed, old_nick, new_nick}, state) do
Nola.UserTrack.renamed(state.network, old_nick, new_nick)
{:noreply, state}
end
def handle_info(unhandled, client) do
Logger.debug("unhandled: #{inspect unhandled}")
{:noreply, client}
end
def publish(pub), do: publish(pub, [])
def publish(m = %Nola.Message{trigger: nil}, keys) do
dispatch(["messages"] ++ keys, {:irc, :text, m})
end
def publish(m = %Nola.Message{trigger: t = %Nola.Trigger{trigger: trigger}}, keys) do
dispatch(["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m})
end
def publish_event(net, event = %{type: _}) when is_binary(net) do
event = event
|> Map.put(:at, NaiveDateTime.utc_now())
|> Map.put(:network, net)
dispatch("#{net}:events", {:irc, :event, event})
end
def publish_event({net, chan}, event = %{type: type}) do
event = event
|> Map.put(:at, NaiveDateTime.utc_now())
|> Map.put(:network, net)
|> Map.put(:channel, chan)
dispatch("#{net}/#{chan}:events", {:irc, :event, event})
end
def dispatch(keys, content, sub \\ Nola.PubSub)
def dispatch(key, content, sub) when is_binary(key), do: dispatch([key], content, sub)
def dispatch(keys, content, sub) when is_list(keys) do
Logger.debug("dispatch #{inspect keys} = #{inspect content}")
for key <- keys do
spawn(fn() -> Registry.dispatch(sub, key, fn h ->
for {pid, _} <- h, do: send(pid, content)
end) end)
end
end
#
# Triggers
#
def triggers, do: @triggers
for {trigger, name} <- @triggers do
def extract_trigger(unquote(trigger)<>text) do
text = String.strip(text)
[trigger | args] = String.split(text, " ")
%Nola.Trigger{type: unquote(name), trigger: String.downcase(trigger), args: args}
end
end
def extract_trigger(_), do: nil
#
# IRC Replies
#
# irc_reply(ExIRC.Client pid, {channel or nick, ExIRC.Sender}, binary | replies
# replies :: {:kick, reason} | {:kick, nick, reason} | {:mode, mode, nick}
defp irc_reply(state = %{client: client, network: network}, {target, _}, text) when is_binary(text) or is_list(text) do
- lines = IRC.splitlong(text)
+ lines = Nola.Irc.Message.splitlong(text)
|> Enum.map(fn(x) -> if(is_list(x), do: x, else: String.split(x, "\n")) end)
|> List.flatten()
outputs = for line <- lines do
ExIRC.Client.msg(client, :privmsg, target, line)
{:irc, :out, %Nola.Message{id: FlakeId.get(), transport: :irc, network: network,
channel: target, text: line, sender: %ExIRC.SenderInfo{nick: state.conn.nick}, at: NaiveDateTime.utc_now(), meta: %{self: true}}}
end
for f <- outputs, do: dispatch(["irc:outputs", "#{network}/#{target}:outputs"], f)
end
defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:kick, reason}) do
ExIRC.Client.kick(client, target, nick, reason)
end
defp irc_reply(%{client: client}, {target, _}, {:kick, nick, reason}) do
ExIRC.Client.kick(client, target, nick, reason)
end
defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:mode, mode}) do
ExIRC.Client.mode(%{client: client}, target, mode, nick)
end
defp irc_reply(%{client: client}, target, {:mode, mode, nick}) do
ExIRC.Client.mode(client, target, mode, nick)
end
defp irc_reply(%{client: client}, target, {:channel_mode, mode}) do
ExIRC.Client.mode(client, target, mode)
end
defp track_mode(network, channel, nick, "+o") do
Nola.UserTrack.change_privileges(network, channel, nick, {[:operator], []})
:ok
end
defp track_mode(network, channel, nick, "-o") do
Nola.UserTrack.change_privileges(network, channel, nick, {[], [:operator]})
:ok
end
defp track_mode(network, channel, nick, "+v") do
Nola.UserTrack.change_privileges(network, channel, nick, {[:voice], []})
:ok
end
defp track_mode(network, channel, nick, "-v") do
Nola.UserTrack.change_privileges(network, channel, nick, {[], [:voice]})
:ok
end
defp track_mode(network, channel, nick, mode) do
Logger.warn("Unhandled track_mode: #{inspect {nick, mode}}")
:ok
end
defp server(%{conn: %{host: host, port: port}}) do
host <> ":" <> to_string(port)
end
end
diff --git a/lib/irc/irc.ex b/lib/irc/irc.ex
index a1d97a2..dd1a5d2 100644
--- a/lib/irc/irc.ex
+++ b/lib/irc/irc.ex
@@ -1,59 +1,49 @@
-defmodule IRC do
+defmodule Nola.Irc do
+ require Logger
+
+ def env(), do: Nola.env(:irc)
+ def env(key, default \\ nil), do: Keyword.get(env(), key, default)
def send_message_as(account, network, channel, text, force_puppet \\ false) do
connection = IRC.Connection.get_network(network)
if connection && (force_puppet || IRC.PuppetConnection.whereis(account, connection)) do
IRC.PuppetConnection.start_and_send_message(account, connection, channel, text)
else
user = Nola.UserTrack.find_by_account(network, account)
nick = if(user, do: user.nick, else: account.name)
IRC.Connection.broadcast_message(network, channel, "<#{nick}> #{text}")
end
end
- def register(key) do
- case Registry.register(Nola.PubSub, key, []) do
- {:ok, _} -> :ok
- error -> error
- end
- end
-
def admin?(%Message{sender: sender}), do: admin?(sender)
def admin?(%{nick: nick, user: user, host: host}) do
for {n, u, h} <- Nola.IRC.env(:admins, []) do
admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host)
end
|> Enum.any?
end
defp admin_part_match?(:_, _), do: true
defp admin_part_match?(a, a), do: true
defp admin_part_match?(_, _), do: false
- @max_chars 440
-
- def splitlong(string, max_chars \\ 440)
+ def application_childs do
+ import Supervisor.Spec
- def splitlong(string, max_chars) when is_list(string) do
- Enum.map(string, fn(s) -> splitlong(s, max_chars) end)
- |> List.flatten()
- end
+ IRC.Connection.setup()
- def splitlong(string, max_chars) do
- string
- |> String.codepoints
- |> Enum.chunk_every(max_chars)
- |> Enum.map(&Enum.join/1)
+ [
+ worker(Registry, [[keys: :duplicate, name: IRC.ConnectionPubSub]], id: :registr_irc_conn),
+ supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]),
+ supervisor(IRC.PuppetConnection.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]),
+ ]
end
- def splitlong_with_prefix(string, prefix, max_chars \\ 440) do
- prefix = "#{prefix} "
- max_chars = max_chars - (length(String.codepoints(prefix)))
- string
- |> String.codepoints
- |> Enum.chunk_every(max_chars)
- |> Enum.map(fn(line) -> prefix <> Enum.join(line) end)
+ # Start plugins first to let them get on connection events.
+ def after_start() do
+ Logger.info("Starting connections")
+ IRC.Connection.start_all()
end
end
diff --git a/lib/irc/message.ex b/lib/irc/message.ex
new file mode 100644
index 0000000..3927079
--- /dev/null
+++ b/lib/irc/message.ex
@@ -0,0 +1,28 @@
+defmodule Nola.Irc.Message do
+
+ @max_chars 440
+
+ def splitlong(string, max_chars \\ 440)
+
+ def splitlong(string, max_chars) when is_list(string) do
+ Enum.map(string, fn(s) -> splitlong(s, max_chars) end)
+ |> List.flatten()
+ end
+
+ def splitlong(string, max_chars) do
+ string
+ |> String.codepoints
+ |> Enum.chunk_every(max_chars)
+ |> Enum.map(&Enum.join/1)
+ end
+
+ def splitlong_with_prefix(string, prefix, max_chars \\ 440) do
+ prefix = "#{prefix} "
+ max_chars = max_chars - (length(String.codepoints(prefix)))
+ string
+ |> String.codepoints
+ |> Enum.chunk_every(max_chars)
+ |> Enum.map(fn(line) -> prefix <> Enum.join(line) end)
+ end
+
+end
diff --git a/lib/irc/nola_irc.ex b/lib/irc/nola_irc.ex
deleted file mode 100644
index 4ed94d1..0000000
--- a/lib/irc/nola_irc.ex
+++ /dev/null
@@ -1,25 +0,0 @@
-defmodule Nola.IRC do
- require Logger
-
- def env(), do: Nola.env(:irc)
- def env(key, default \\ nil), do: Keyword.get(env(), key, default)
-
- def application_childs do
- import Supervisor.Spec
-
- IRC.Connection.setup()
-
- [
- worker(Registry, [[keys: :duplicate, name: IRC.ConnectionPubSub]], id: :registr_irc_conn),
- supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]),
- supervisor(IRC.PuppetConnection.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]),
- ]
- end
-
- # Start plugins first to let them get on connection events.
- def after_start() do
- Logger.info("Starting connections")
- IRC.Connection.start_all()
- end
-
-end
diff --git a/lib/nola/application.ex b/lib/nola/application.ex
index fa880ea..d56d4cb 100644
--- a/lib/nola/application.ex
+++ b/lib/nola/application.ex
@@ -1,57 +1,57 @@
defmodule Nola.Application do
use Application
def start(_type, _args) do
import Supervisor.Spec
Logger.add_backend(Sentry.LoggerBackend)
Nola.Plugins.setup()
:ok = Nola.Matrix.setup()
:ok = Nola.TelegramRoom.setup()
# Define workers and child supervisors to be supervised
children = [
supervisor(NolaWeb.Endpoint, []),
worker(Registry, [[keys: :duplicate, name: Nola.BroadcastRegistry]], id: :registry_broadcast),
worker(Nola.IcecastAgent, []),
worker(Nola.Token, []),
worker(Nola.AuthToken, []),
Nola.Subnet,
{GenMagic.Pool, [name: Nola.GenMagic, pool_size: 2]},
worker(Registry, [[keys: :duplicate, name: Nola.PubSub]], id: :registry_nola_pubsub),
worker(Nola.Membership, []),
worker(Nola.Account, []),
worker(Nola.UserTrack.Storage, []),
worker(Nola.Plugins.Account, []),
supervisor(Nola.Plugins.Supervisor, [], [name: Nola.Plugins.Supervisor]),
- ] ++ Nola.IRC.application_childs
+ ] ++ Nola.Irc.application_childs
++ Nola.Matrix.application_childs
opts = [strategy: :one_for_one, name: Nola.Supervisor]
sup = Supervisor.start_link(children, opts)
start_telegram()
Nola.Plugins.start_all()
- spawn_link(fn() -> Nola.IRC.after_start() end)
+ spawn_link(fn() -> Nola.Irc.after_start() end)
spawn_link(fn() -> Nola.Matrix.after_start() end)
spawn_link(fn() -> Nola.TelegramRoom.after_start() end)
sup
end
def config_change(changed, _new, removed) do
NolaWeb.Endpoint.config_change(changed, removed)
:ok
end
defp start_telegram() do
token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
options = [
username: Keyword.get(Application.get_env(:nola, :telegram, []), :nick, "beauttebot"),
purge: false
]
telegram = Telegram.Bot.ChatBot.Supervisor.start_link({Nola.Telegram, token, options})
end
end
diff --git a/lib/plugins/link/html.ex b/lib/plugins/link/html.ex
index 9b44319..a941aac 100644
--- a/lib/plugins/link/html.ex
+++ b/lib/plugins/link/html.ex
@@ -1,106 +1,106 @@
defmodule Nola.Plugins.Link.HTML do
@behaviour Nola.Plugins.Link
@impl true
def match(_, _), do: false
@impl true
def post_match(_url, "text/html"<>_, _header, _opts) do
{:body, nil}
end
def post_match(_, _, _, _), do: false
@impl true
def post_expand(url, body, _params, _opts) do
html = Floki.parse(body)
title = collect_title(html)
opengraph = collect_open_graph(html)
itemprops = collect_itemprops(html)
text = if Map.has_key?(opengraph, "title") && Map.has_key?(opengraph, "description") do
sitename = if sn = Map.get(opengraph, "site_name") do
"#{sn}"
else
""
end
paywall? = if Map.get(opengraph, "article:content_tier", Map.get(itemprops, "article:content_tier", "free")) == "free" do
""
else
"[paywall] "
end
section = if section = Map.get(opengraph, "article:section", Map.get(itemprops, "article:section", nil)) do
": #{section}"
else
""
end
date = case DateTime.from_iso8601(Map.get(opengraph, "article:published_time", Map.get(itemprops, "article:published_time", ""))) do
{:ok, date, _} ->
"#{Timex.format!(date, "%d/%m/%y", :strftime)}. "
_ ->
""
end
uri = URI.parse(url)
prefix = "#{paywall?}#{Map.get(opengraph, "site_name", uri.host)}#{section}"
prefix = unless prefix == "" do
"#{prefix} — "
else
""
end
- [clean_text("#{prefix}#{Map.get(opengraph, "title")}")] ++ IRC.splitlong(clean_text("#{date}#{Map.get(opengraph, "description")}"))
+ [clean_text("#{prefix}#{Map.get(opengraph, "title")}")] ++ Nola.Irc.Message.splitlong(clean_text("#{date}#{Map.get(opengraph, "description")}"))
else
clean_text(title)
end
{:ok, text}
end
defp collect_title(html) do
case Floki.find(html, "title") do
[{"title", [], [title]} | _] ->
String.trim(title)
_ ->
nil
end
end
defp collect_open_graph(html) do
Enum.reduce(Floki.find(html, "head meta"), %{}, fn(tag, acc) ->
case tag do
{"meta", values, []} ->
name = List.keyfind(values, "property", 0, {nil, nil}) |> elem(1)
content = List.keyfind(values, "content", 0, {nil, nil}) |> elem(1)
case name do
"og:" <> key ->
Map.put(acc, key, content)
"article:"<>_ ->
Map.put(acc, name, content)
_other -> acc
end
_other -> acc
end
end)
end
defp collect_itemprops(html) do
Enum.reduce(Floki.find(html, "[itemprop]"), %{}, fn(tag, acc) ->
case tag do
{"meta", values, []} ->
name = List.keyfind(values, "itemprop", 0, {nil, nil}) |> elem(1)
content = List.keyfind(values, "content", 0, {nil, nil}) |> elem(1)
case name do
"article:" <> key ->
Map.put(acc, name, content)
_other -> acc
end
_other -> acc
end
end)
end
defp clean_text(text) do
text
|> String.replace("\n", " ")
|> HtmlEntities.decode()
end
end
diff --git a/lib/plugins/link/twitter.ex b/lib/plugins/link/twitter.ex
index e7f3e63..48e6bae 100644
--- a/lib/plugins/link/twitter.ex
+++ b/lib/plugins/link/twitter.ex
@@ -1,158 +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)
+ text = Nola.Irc.Message.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(">")
+ |> Nola.Irc.Message.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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Mar 14, 5:11 PM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33636
Default Alt Text
(32 KB)
Attached To
rNOLA Nola
Event Timeline
Log In to Comment