Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F51239
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
79 KB
Subscribers
None
View Options
diff --git a/lib/irc.ex b/lib/irc.ex
index 3898068..0fa1b79 100644
--- a/lib/irc.ex
+++ b/lib/irc.ex
@@ -1,49 +1,49 @@
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 = Nola.Irc.Connection.get_network(network)
if connection && (force_puppet || Nola.Irc.PuppetConnection.whereis(account, connection)) do
Nola.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)
Nola.Irc.Connection.broadcast_message(network, channel, "<#{nick}> #{text}")
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
+ 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
def application_childs do
import Supervisor.Spec
Nola.Irc.Connection.setup()
[
worker(Registry, [[keys: :duplicate, name: Nola.Irc.ConnectionPubSub]], id: :registr_irc_conn),
supervisor(Nola.Irc.Connection.Supervisor, [], [name: Nola.Irc.Connection.Supervisor]),
supervisor(Nola.Irc.PuppetConnection.Supervisor, [], [name: Nola.Irc.PuppetConnection.Supervisor]),
]
end
# Start plugins first to let them get on connection events.
def after_start() do
Logger.info("Starting connections")
Nola.Irc.Connection.start_all()
end
end
diff --git a/lib/irc/admin_handler.ex b/lib/irc/admin_handler.ex
index a462789..cb953db 100644
--- a/lib/irc/admin_handler.ex
+++ b/lib/irc/admin_handler.ex
@@ -1,41 +1,41 @@
defmodule Nola.Irc.AdminHandler do
@moduledoc """
# admin
!op
op; requiert admin
"""
def irc_doc, do: nil
def start_link(client) do
GenServer.start_link(__MODULE__, [client])
end
def init([client]) do
ExIRC.Client.add_handler client, self
- :ok = IRC.register("op")
+ {:ok, _} = Registry.register(Nola.PubSub, "op", [])
{:ok, client}
end
def handle_info({:irc, :trigger, "op", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}, sender: sender}}, client) do
- if IRC.admin?(sender) do
+ if Nola.Irc.admin?(sender) do
m.replyfun.({:mode, "+o"})
else
m.replyfun.({:kick, "non"})
end
{:noreply, client}
end
def handle_info({:joined, chan, sender}, client) do
- if IRC.admin?(sender) do
+ if Nola.Irc.admin?(sender) do
ExIRC.Client.mode(client, chan, "+o", sender.nick)
end
{:noreply, client}
end
def handle_info(msg, client) do
{:noreply, client}
end
end
diff --git a/lib/matrix/room.ex b/lib/matrix/room.ex
index 299d7cc..e2965a5 100644
--- a/lib/matrix/room.ex
+++ b/lib/matrix/room.ex
@@ -1,196 +1,196 @@
defmodule Nola.Matrix.Room do
require Logger
alias Nola.Matrix
alias Polyjuice.Client
import Matrix, only: [client: 0, client: 1, user_name: 1, myself?: 1]
defmodule Supervisor do
use DynamicSupervisor
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(room_id) do
spec = %{id: room_id, start: {Nola.Matrix.Room, :start_link, [room_id]}, 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 start(room_id) do
__MODULE__.Supervisor.start_child(room_id)
end
def start_link(room_id) do
GenServer.start_link(__MODULE__, [room_id], name: name(room_id))
end
def start_and_send_matrix_event(room_id, event) do
pid = if pid = whereis(room_id) do
pid
else
case __MODULE__.start(room_id) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
:ignore -> nil
end
end
if(pid, do: send(pid, {:matrix_event, event}))
end
def whereis(room_id) do
{:global, name} = name(room_id)
case :global.whereis_name(name) do
:undefined -> nil
pid -> pid
end
end
def name(room_id) do
{:global, {__MODULE__, room_id}}
end
def init([room_id]) do
case Matrix.lookup_room(room_id) do
{:ok, state} ->
Logger.metadata(matrix_room: room_id)
{:ok, _} = Registry.register(Nola.PubSub, "#{state.network}:events", plugin: __MODULE__)
for t <- ["messages", "triggers", "outputs", "events"] do
{:ok, _} = Registry.register(Nola.PubSub, "#{state.network}/#{state.channel}:#{t}", plugin: __MODULE__)
end
state = state
|> Map.put(:id, room_id)
Logger.info("Started Matrix room #{room_id}")
{:ok, state, {:continue, :update_state}}
error ->
Logger.info("Received event for nonexistent room #{inspect room_id}: #{inspect error}")
:ignore
end
end
def handle_continue(:update_state, state) do
{:ok, s} = Client.Room.get_state(client(), state.id)
members = Enum.reduce(s, [], fn(s, acc) ->
if s["type"] == "m.room.member" do
if s["content"]["membership"] == "join" do
[s["user_id"] | acc]
else
# XXX: The user left, remove from Nola.Memberships ?
acc
end
else
acc
end
end)
|> Enum.filter(& &1)
for m <- members, do: Nola.UserTrack.joined(state.id, %{network: "matrix", nick: m, user: m, host: "matrix."}, [], true)
accounts = Nola.UserTrack.channel(state.network, state.channel)
|> Enum.filter(& &1)
|> Enum.map(fn(tuple) -> Nola.UserTrack.User.from_tuple(tuple).account end)
|> Enum.uniq()
|> Enum.each(fn(account_id) ->
introduce_irc_account(account_id, state)
end)
{:noreply, state}
end
def handle_info({:irc, :text, message}, state), do: handle_irc(message, state)
def handle_info({:irc, :out, message}, state), do: handle_irc(message, state)
def handle_info({:irc, :trigger, _, message}, state), do: handle_irc(message, state)
def handle_info({:irc, :event, event}, state), do: handle_irc(event, state)
def handle_info({:matrix_event, event}, state) do
if myself?(event.user_id) do
{:noreply, state}
else
handle_matrix(event, state)
end
end
def handle_irc(message = %Nola.Message{account: account}, state) do
unless Map.get(message.meta, :puppet) && Map.get(message.meta, :from) == self() do
opts = if Map.get(message.meta, :self) || is_nil(account) do
[]
else
mxid = Matrix.get_or_create_matrix_user(account.id)
[user_id: mxid]
end
Client.Room.send_message(client(opts),state.id, message.text)
end
{:noreply, state}
end
def handle_irc(%{type: :join, account_id: account_id}, state) do
introduce_irc_account(account_id, state)
{:noreply, state}
end
def handle_irc(%{type: quit_or_part, account_id: account_id}, state) when quit_or_part in [:quit, :part] do
mxid = Matrix.get_or_create_matrix_user(account_id)
Client.Room.leave(client(user_id: mxid), state.id)
{:noreply, state}
end
def handle_irc(event, state) do
Logger.warn("Skipped irc event #{inspect event}")
{:noreply, state}
end
def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "join"}}, state) do
_account = get_account(event, state)
Nola.UserTrack.joined(state.id, %{network: "matrix", nick: user_id, user: user_id, host: "matrix."}, [], true)
{:noreply, state}
end
def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "leave"}}, state) do
Nola.UserTrack.parted(state.id, %{network: "matrix", nick: user_id})
{:noreply, state}
end
def handle_matrix(event = %{type: "m.room.message", user_id: user_id, content: %{"msgtype" => "m.text", "body" => text}}, state) do
- IRC.send_message_as(get_account(event, state), state.network, state.channel, text, true)
+ Nola.Irc.send_message_as(get_account(event, state), state.network, state.channel, text, true)
{:noreply, state}
end
def handle_matrix(event, state) do
Logger.warn("Skipped matrix event #{inspect event}")
{:noreply, state}
end
def get_account(%{user_id: user_id}, %{id: id}) do
Nola.Account.find_by_nick("matrix", user_id)
end
defp introduce_irc_account(account_id, state) do
mxid = Matrix.get_or_create_matrix_user(account_id)
account = Nola.Account.get(account_id)
user = Nola.UserTrack.find_by_account(state.network, account)
base_nick = if(user, do: user.nick, else: account.name)
case Client.Profile.put_displayname(client(user_id: mxid), base_nick) do
:ok -> :ok
error ->
Logger.warn("Failed to update profile for #{mxid}: #{inspect error}")
end
case Client.Room.join(client(user_id: mxid), state.id) do
{:ok, _} -> :ok
error ->
Logger.warn("Failed to join room for #{mxid}: #{inspect error}")
end
:ok
end
end
diff --git a/lib/nola/user_track.ex b/lib/nola/user_track.ex
index f6a02ae..c1218b0 100644
--- a/lib/nola/user_track.ex
+++ b/lib/nola/user_track.ex
@@ -1,329 +1,329 @@
defmodule Nola.UserTrack do
@moduledoc """
User Track DB & Utilities
"""
@ets Nola.UserTrack.Storage
# {uuid, network, nick, nicks, privilege_map}
# Privilege map:
# %{"#channel" => [:operator, :voice]
defmodule Storage do
def delete(id) do
op(fn(ets) -> :ets.delete(ets, id) end)
end
def insert(tuple) do
op(fn(ets) -> :ets.insert(ets, tuple) end)
end
def clear_network(network) do
op(fn(ets) ->
spec = [
{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:==, :"$1", {:const, network}}
], [:"$_"]}
]
:ets.match_delete(ets, spec)
end)
end
def op(fun) do
GenServer.call(__MODULE__, {:op, fun})
end
def start_link do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def init([]) do
ets = :ets.new(__MODULE__, [:set, :named_table, :protected, {:read_concurrency, true}])
{:ok, ets}
end
def handle_call({:op, fun}, _from, ets) do
returned = try do
{:ok, fun.(ets)}
rescue
rescued -> {:error, rescued}
catch
rescued -> {:error, rescued}
end
{:reply, returned, ets}
end
def terminate(_reason, ets) do
:ok
end
end
defmodule Id, do: use EntropyString
defmodule User do
defstruct [:id, :account, :network, :nick, {:nicks, []}, :username, :host, :realname, {:privileges, %{}}, {:last_active, %{}}, {:options, %{}}]
def to_tuple(u = %__MODULE__{}) do
{u.id || Nola.UserTrack.Id.large_id, u.network, u.account, String.downcase(u.nick), u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges, u.last_active, u.options}
end
#tuple size: 11
def from_tuple({id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs, last_active, opts}) do
struct = %__MODULE__{id: id, account: account, network: network, nick: nick, nicks: nicks, username: username, host: host, realname: realname, privileges: privs, last_active: last_active, options: opts}
end
end
def find_by_account(%Nola.Account{id: id}) do
#iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
{{:_, :_, :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:==, :"$2", {:const, id}}
], [:"$_"]}
]
results = :ets.select(@ets, spec)
|> Enum.filter(& &1)
for obj <- results, do: User.from_tuple(obj)
end
def find_by_account(network, nil) do
nil
end
def find_by_account(network, %Nola.Account{id: id}) do
#iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
{{:_, :"$1", :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:andalso, {:==, :"$1", {:const, network}},
{:==, :"$2", {:const, id}}}
], [:"$_"]}
]
case :ets.select(@ets, spec) do
results = [_r | _] ->
result = results
|> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "matrix" && net == "matrix" end)
|> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "telegram" && net == "telegram" end)
|> Enum.reject(fn({_, _, _, _, _, _, _, _, _, _, actives, opts}) -> network not in ["matrix", "telegram"] && Map.get(opts, :puppet) end)
|> Enum.sort_by(fn({_, _, _, _, _, _, _, _, _, _, actives, _}) ->
Map.get(actives, nil)
end, {:desc, NaiveDateTime})
|> List.first
if result, do: User.from_tuple(result)
_ -> nil
end
end
def clear_network(network) do
Storage.clear_network(network)
end
def merge_account(old_id, new_id) do
#iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
{{:_, :_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:==, :"$1", {:const, old_id}}
], [:"$_"]}
]
Enum.each(:ets.select(@ets, spec), fn({id, net, _, downcased_nick, nick, nicks, username, host, realname, privs, active, opts}) ->
Storage.op(fn(ets) ->
:ets.insert(@ets, {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active, opts})
end)
end)
end
def find_by_nick(%ExIRC.Who{network: network, nick: nick}) do
find_by_nick(network, nick)
end
def find_by_nick(%ExIRC.SenderInfo{network: network, nick: nick}) do
find_by_nick(network, nick)
end
def find_by_nick(network, nick) do
case :ets.match(@ets, {:"$1", network, :_, String.downcase(nick), :_, :_, :_, :_, :_, :_, :_, :_}) do
[[id] | _] -> lookup(id)
_ ->
nil
end
end
def to_list, do: :ets.tab2list(@ets)
def lookup(id) do
case :ets.lookup(@ets, id) do
[] -> nil
[tuple] -> User.from_tuple(tuple)
end
end
def operator?(network, channel, nick) do
if user = find_by_nick(network, nick) do
privs = Map.get(user.privileges, channel, [])
Enum.member?(privs, :admin) || Enum.member?(privs, :operator)
else
false
end
end
def channel(network, channel) do
Enum.filter(to_list(), fn({_, network, _, _, _, _, _, _, _, channels, _, _}) ->
Map.get(channels, channel)
end)
end
# TODO
def connected(network, nick, user, host, account_id, opts \\ %{}) do
if account = Nola.Account.get(account_id) do
user = if user = find_by_nick(network, nick) do
user
else
user = %User{id: Nola.UserTrack.Id.large_id, account: account_id, network: network, nick: nick, username: user, host: host, privileges: %{}, options: opts}
Storage.op(fn(ets) ->
:ets.insert(ets, User.to_tuple(user))
end)
user
end
Nola.Irc.Connection.publish_event(network, %{type: :connect, user_id: user.id, account_id: user.account})
:ok
else
:error
end
end
def joined(c, s), do: joined(c,s,[])
def joined(channel, sender=%{nick: nick, user: uname, host: host}, privileges, touch \\ true) do
- privileges = if IRC.admin?(sender) do
+ privileges = if Nola.Irc.admin?(sender) do
privileges ++ [:admin]
else privileges end
user = if user = find_by_nick(sender.network, nick) do
%User{user | username: uname, host: host, privileges: Map.put(user.privileges || %{}, channel, privileges)}
else
user = %User{id: Nola.UserTrack.Id.large_id, network: sender.network, nick: nick, username: uname, host: host, privileges: %{channel => privileges}}
account = Nola.Account.lookup(user).id
user = %User{user | account: account}
end
user = touch_struct(user, channel)
if touch && user.account do
Nola.Membership.touch(user.account, sender.network, channel)
end
Storage.op(fn(ets) ->
:ets.insert(ets, User.to_tuple(user))
end)
Nola.Irc.Connection.publish_event({sender.network, channel}, %{type: :join, user_id: user.id, account_id: user.account})
user
end
#def joined(network, channel, nick, privileges) do
# user = if user = find_by_nick(network, nick) do
# %User{user | privileges: Map.put(user.privileges, channel, privileges)}
# else
# %User{nick: nick, privileges: %{channel => privileges}}
# end
#
# Storage.op(fn(ets) ->
# :ets.insert(ets, User.to_tuple(user))
# end)
#end
def messaged(%Nola.Message{network: network, account: account, channel: chan, sender: %{nick: nick}} = m) do
{user, account} = if user = find_by_nick(network, nick) do
{touch_struct(user, chan), account || Nola.Account.lookup(user)}
else
user = %User{network: network, nick: nick, privileges: %{}}
account = Nola.Account.lookup(user)
{%User{user | account: account.id}, account}
end
Storage.insert(User.to_tuple(user))
if chan, do: Nola.Membership.touch(account, network, chan)
if !m.account do
{:ok, %Nola.Message{m | account: account}}
else
:ok
end
end
def renamed(network, old_nick, new_nick) do
if user = find_by_nick(network, old_nick) do
old_account = Nola.Account.lookup(user)
user = %User{user | nick: new_nick, nicks: [old_nick|user.nicks]}
account = Nola.Account.lookup(user, false) || old_account
user = %User{user | nick: new_nick, account: account.id, nicks: [old_nick|user.nicks]}
Storage.insert(User.to_tuple(user))
channels = for {channel, _} <- user.privileges, do: channel
Nola.Irc.Connection.publish_event(network, %{type: :nick, user_id: user.id, account_id: account.id, nick: new_nick, old_nick: old_nick})
end
end
def change_privileges(network, channel, nick, {add, remove}) do
if user = find_by_nick(network, nick) do
privs = Map.get(user.privileges, channel)
privs = Enum.reduce(add, privs, fn(priv, acc) -> [priv|acc] end)
privs = Enum.reduce(remove, privs, fn(priv, acc) -> List.delete(acc, priv) end)
user = %User{user | privileges: Map.put(user.privileges, channel, privs)}
Storage.insert(User.to_tuple(user))
Nola.Irc.Connection.publish_event({network, channel}, %{type: :privileges, user_id: user.id, account_id: user.account, added: add, removed: remove})
end
end
# XXX: Reason
def parted(channel, %{network: network, nick: nick}) do
parted(network, channel, nick)
end
def parted(network, channel, nick) do
if user = find_by_nick(network, nick) do
if user.account do
Nola.Membership.touch(user.account, network, channel)
end
privs = Map.delete(user.privileges, channel)
lasts = Map.delete(user.last_active, channel)
if Enum.count(privs) > 0 do
user = %User{user | privileges: privs}
Storage.insert(User.to_tuple(user))
Nola.Irc.Connection.publish_event({network, channel}, %{type: :part, user_id: user.id, account_id: user.account, reason: nil})
else
Nola.Irc.Connection.publish_event(network, %{type: :quit, user_id: user.id, account_id: user.account, reason: "Left all known channels"})
Storage.delete(user.id)
end
end
end
def quitted(sender, reason) do
if user = find_by_nick(sender.network, sender.nick) do
if user.account do
for {channel, _} <- user.privileges do
Nola.Membership.touch(user.account, sender.network, channel)
end
Nola.Irc.Connection.publish_event(sender.network, %{type: :quit, user_id: user.id, account_id: user.account, reason: reason})
end
Storage.delete(user.id)
end
end
defp touch_struct(user = %User{last_active: last_active}, channel) do
now = NaiveDateTime.utc_now()
last_active = last_active
|> Map.put(channel, now)
|> Map.put(nil, now)
%User{user | last_active: last_active}
end
defp userchans(%{privileges: privileges}) do
for({chan, _} <- privileges, do: chan)
end
end
diff --git a/lib/plugins/account.ex b/lib/plugins/account.ex
index 20abab7..242b290 100644
--- a/lib/plugins/account.ex
+++ b/lib/plugins/account.ex
@@ -1,188 +1,188 @@
defmodule Nola.Plugins.Account do
@moduledoc """
# Account
* **account** Get current account id and token
* **auth `<account-id>` `<token>`** Authenticate and link the current nickname to an account
* **auth** list authentications methods
* **whoami** list currently authenticated users
* **web** get a one-time login link to web
* **enable-telegram** Link a Telegram account
* **enable-sms** Link a SMS number
* **enable-untappd** Link a Untappd account
* **set-name** set account name
* **setusermeta puppet-nick `<nick>`** Set puppet IRC nickname
"""
def irc_doc, do: @moduledoc
def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def init(_) do
{:ok, _} = Registry.register(Nola.PubSub, "messages:private", [])
{:ok, nil}
end
def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "help"}}, state) do
text = [
"account: show current account and auth token",
"auth: show authentications methods",
"whoami: list authenticated users",
"set-name <name>: set account name",
"web: login to web",
"enable-sms | disable-sms: enable/change or disable sms",
"enable-telegram: link/change telegram",
"enable-untappd: link untappd account",
"getmeta: show meta datas",
"setusermeta: set user meta",
]
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "auth"}}, state) do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
predicates = :dets.select(Nola.Account.file("predicates"), spec)
text = for {net, {key, value}} <- predicates, do: "#{net}: #{to_string(key)}: #{value}"
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "whoami"}}, state) do
users = for user <- Nola.UserTrack.find_by_account(m.account) do
chans = Enum.map(user.privileges, fn({chan, _}) -> chan end)
|> Enum.join(" ")
"#{user.network} - #{user.nick}!#{user.username}@#{user.host} - #{chans}"
end
m.replyfun.(users)
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "account"}}, state) do
account = Nola.Account.lookup(m.sender)
text = ["Account Id: #{account.id}",
"Authenticate to this account from another network: \"auth #{account.id} #{account.token}\" to the other bot!"]
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{sender: sender, text: "auth"<>_}}, state) do
#account = Nola.Account.lookup(m.sender)
case String.split(m.text, " ") do
["auth", id, token] ->
join_account(m, id, token)
_ ->
m.replyfun.("Invalid parameters")
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "set-name "<>name}}, state) do
Nola.Account.update_account_name(account, name)
m.replyfun.("Name changed: #{name}")
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{text: "disable-sms"}}, state) do
if Nola.Account.get_meta(m.account, "sms-number") do
Nola.Account.delete_meta(m.account, "sms-number")
m.replfyun.("SMS disabled.")
else
m.replyfun.("SMS already disabled.")
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{text: "web"}}, state) do
auth_url = Untappd.auth_url()
login_url = Nola.AuthToken.new_url(m.account.id, nil)
m.replyfun.("-> " <> login_url)
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{text: "enable-sms"}}, state) do
code = String.downcase(EntropyString.small_id())
Nola.Account.put_meta(m.account, "sms-validation-code", code)
Nola.Account.put_meta(m.account, "sms-validation-target", m.network)
- number = Nola.IRC.Sms.my_number()
+ number = Nola.Plugin.Sms.my_number()
text = "To enable or change your number for SMS messaging, please send:"
<> " \"enable #{code}\" to #{number}"
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{text: "enable-telegram"}}, state) do
code = String.downcase(EntropyString.small_id())
Nola.Account.delete_meta(m.account, "telegram-id")
Nola.Account.put_meta(m.account, "telegram-validation-code", code)
Nola.Account.put_meta(m.account, "telegram-validation-target", m.network)
text = "To enable or change your number for telegram messaging, please open #{Nola.Telegram.my_path()} and send:"
<> " \"/enable #{code}\""
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{text: "enable-untappd"}}, state) do
auth_url = Untappd.auth_url()
login_url = Nola.AuthToken.new_url(m.account.id, {:external_redirect, auth_url})
m.replyfun.(["To link your Untappd account, open this URL:", login_url])
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{text: "getmeta"<>_}}, state) do
result = case String.split(m.text, " ") do
["getmeta"] ->
for {k, v} <- Nola.Account.get_all_meta(m.account) do
case k do
"u:"<>key -> "(user) #{key}: #{v}"
key -> "#{key}: #{v}"
end
end
["getmeta", key] ->
value = Nola.Account.get_meta(m.account, key)
text = if value do
"#{key}: #{value}"
else
"#{key} is not defined"
end
_ ->
"usage: getmeta [key]"
end
m.replyfun.(result)
{:noreply, state}
end
def handle_info({:irc, :text, m = %Nola.Message{text: "setusermeta"<>_}}, state) do
result = case String.split(m.text, " ") do
["setusermeta", key, value] ->
Nola.Account.put_user_meta(m.account, key, value)
"ok"
_ ->
"usage: setusermeta <key> <value>"
end
m.replyfun.(result)
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
defp join_account(m, id, token) do
old_account = Nola.Account.lookup(m.sender)
new_account = Nola.Account.get(id)
if new_account && token == new_account.token do
case Nola.Account.merge_account(old_account.id, new_account.id) do
:ok ->
if old_account.id == new_account.id do
m.replyfun.("Already authenticated, but hello")
else
m.replyfun.("Accounts merged!")
end
_ -> m.replyfun.("Something failed :(")
end
else
m.replyfun.("Invalid token")
end
end
end
diff --git a/lib/plugins/base.ex b/lib/plugins/base.ex
index 3188495..0f2c7e5 100644
--- a/lib/plugins/base.ex
+++ b/lib/plugins/base.ex
@@ -1,132 +1,132 @@
defmodule Nola.Plugins.Base do
def irc_doc, do: nil
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(Nola.PubSub, "trigger:version", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:help", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:liquidrender", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:plugin", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:plugins", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "plugins", msg = %{trigger: %{type: :bang, args: []}}}, _) do
enabled_string = Nola.Plugins.enabled()
|> Enum.map(fn(mod) ->
mod
|> Macro.underscore()
|> String.split("/", parts: :infinity)
|> List.last()
|> Enum.sort()
end)
|> Enum.join(", ")
msg.replyfun.("Enabled plugins: #{enabled_string}")
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :query, args: [plugin]}} = m}, _) do
module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module)
do
m.replyfun.("loaded, active: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil ->
msg = case Nola.Plugins.get(module) do
:disabled -> "disabled"
{_, false, _} -> "disabled"
_ -> "not active"
end
m.replyfun.(msg)
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :plus, args: [plugin]}} = m}, _) do
module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
Nola.Plugins.switch(module, true),
{:ok, pid} <- Nola.Plugins.start(module)
do
m.replyfun.("started: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
:ignore -> m.replyfun.("disabled or throttled")
{:error, _} -> m.replyfun.("start error")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :tilde, args: [plugin]}} = m}, _) do
module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module),
:ok <- GenServer.stop(pid),
{:ok, pid} <- Nola.Plugins.start(module)
do
m.replyfun.("restarted: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil -> m.replyfun.("not active")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :minus, args: [plugin]}} = m}, _) do
module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module),
:ok <- GenServer.stop(pid)
do
- IRC.Plugin.switch(module, false)
+ Nola.Plugins.switch(module, false)
m.replyfun.("stopped: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil -> m.replyfun.("not active")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "liquidrender", m = %{trigger: %{args: args}}}, _) do
template = Enum.join(args, " ")
m.replyfun.(Tmpl.render(template, m))
{:noreply, nil}
end
def handle_info({:irc, :trigger, "help", m = %{trigger: %{type: :bang}}}, _) do
url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel))
m.replyfun.("-> #{url}")
{:noreply, nil}
end
def handle_info({:irc, :trigger, "version", message = %{trigger: %{type: :bang}}}, _) do
{:ok, vsn} = :application.get_key(:nola, :vsn)
ver = List.to_string(vsn)
url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index)
elixir_ver = Application.started_applications() |> List.keyfind(:elixir, 0) |> elem(2) |> to_string()
otp_ver = :erlang.system_info(:system_version) |> to_string() |> String.trim()
system = :erlang.system_info(:system_architecture) |> to_string()
brand = Nola.brand(:name)
owner = "#{Nola.brand(:owner)} <#{Nola.brand(:owner_email)}>"
message.replyfun.([
<<"🤖 I am a robot running", 2, "#{brand}, version #{ver}", 2, " — source: #{Nola.source_url()}">>,
"🦾 Elixir #{elixir_ver} #{otp_ver} on #{system}",
"👷♀️ Owner: h#{owner}",
"🌍 Web interface: #{url}"
])
{:noreply, nil}
end
def handle_info(msg, _) do
{:noreply, nil}
end
end
diff --git a/lib/plugins/say.ex b/lib/plugins/say.ex
index 3df3ac8..114ca64 100644
--- a/lib/plugins/say.ex
+++ b/lib/plugins/say.ex
@@ -1,73 +1,73 @@
defmodule Nola.Plugins.Say do
def irc_doc do
"""
# say
Say something...
* **!say `<channel>` `<text>`** say something on `channel`
* **!asay `<channel>` `<text>`** same but anonymously
You must be a member of the channel.
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [type: __MODULE__]
{:ok, _} = Registry.register(Nola.PubSub, "trigger:say", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:asay", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "messages:private", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "say", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
text = Enum.join(text, " ")
say_for(m.account, target, text, true)
{:noreply, state}
end
def handle_info({:irc, :trigger, "asay", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
text = Enum.join(text, " ")
say_for(m.account, target, text, false)
{:noreply, state}
end
def handle_info({:irc, :text, m = %{text: "say "<>rest}}, state) do
case String.split(rest, " ", parts: 2) do
[target, text] -> say_for(m.account, target, text, true)
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %{text: "asay "<>rest}}, state) do
case String.split(rest, " ", parts: 2) do
[target, text] -> say_for(m.account, target, text, false)
_ -> nil
end
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
defp say_for(account, target, text, with_nick?) do
for {net, chan} <- Nola.Membership.of_account(account) do
chan2 = String.replace(chan, "#", "")
if (target == "#{net}/#{chan}" || target == "#{net}/#{chan2}" || target == chan || target == chan2) do
if with_nick? do
- IRC.send_message_as(account, net, chan, text)
+ Nola.Irc.send_message_as(account, net, chan, text)
else
Nola.Irc.Connection.broadcast_message(net, chan, text)
end
end
end
end
end
diff --git a/lib/plugins/txt.ex b/lib/plugins/txt.ex
index 4f9a803..3b431e9 100644
--- a/lib/plugins/txt.ex
+++ b/lib/plugins/txt.ex
@@ -1,556 +1,556 @@
defmodule Nola.Plugins.Txt do
alias Nola.UserTrack
require Logger
@moduledoc """
# [txt]({{context_path}}/txt)
* **.txt**: liste des fichiers et statistiques.
Les fichiers avec une `*` sont vérrouillés.
[Voir sur le web]({{context_path}}/txt).
* **!txt**: lis aléatoirement une ligne dans tous les fichiers.
* **!txt `<recherche>`**: recherche une ligne dans tous les fichiers.
* **~txt**: essaie de générer une phrase (markov).
* **~txt `<début>`**: essaie de générer une phrase commencant par `<debut>`.
* **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`.
* **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`.
* **!`FICHIER` `<recherche>`**: recherche une ligne contenant `<recherche>` dans `FICHIER`.
* **+txt `<file`>**: crée le fichier `<file>`.
* **+`FICHIER` `<texte>`**: ajoute une ligne `<texte>` dans le fichier `FICHIER`.
* **-`FICHIER` `<chiffre>`**: supprime la ligne `<chiffre>` du fichier `FICHIER`.
* **-txtrw, +txtrw**. op seulement. active/désactive le mode lecture seule.
* **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier.
Insérez `\\\\` pour faire un saut de ligne.
"""
def short_irc_doc, do: "!txt https://sys.115ans.net/irc/txt "
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
defstruct triggers: %{}, rw: true, locks: nil, markov_handler: nil, markov: nil
def random(file) do
GenServer.call(__MODULE__, {:random, file})
end
def reply_random(message, file) do
if line = random(file) do
line
|> format_line(nil, message)
|> message.replyfun.()
line
end
end
def init([]) do
dets_locks_filename = (Nola.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
{:ok, locks} = :dets.open_file(dets_locks_filename, [])
markov_handler = Keyword.get(Application.get_env(:nola, __MODULE__, []), :markov_handler, Nola.Plugins.Txt.Markov.Native)
{:ok, markov} = markov_handler.start_link()
{:ok, _} = Registry.register(Nola.PubSub, "triggers", [plugin: __MODULE__])
{:ok, %__MODULE__{locks: locks, markov_handler: markov_handler, markov: markov, triggers: load()}}
end
def handle_info({:received, "!reload", _, chan}, state) do
{:noreply, %__MODULE__{state | triggers: load()}}
end
#
# ADMIN: RW/RO
#
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :plus}}}, state = %{rw: false}) do
if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture réactivée")
{:noreply, %__MODULE__{state | rw: true}}
else
{:noreply, state}
end
end
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :minus}}}, state = %{rw: true}) do
if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture désactivée")
{:noreply, %__MODULE__{state | rw: false}}
else
{:noreply, state}
end
end
#
# ADMIN: LOCKS
#
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick)
do
:dets.insert(state.locks, {trigger})
msg.replyfun.("txt: #{trigger} verrouillé")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :minus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick),
true <- :dets.member(state.locks, trigger)
do
:dets.delete(state.locks, trigger)
msg.replyfun.("txt: #{trigger} déverrouillé")
end
{:noreply, state}
end
#
# FILE LIST
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :dot}}}, state) do
map = Enum.map(state.triggers, fn({key, data}) ->
ignore? = String.contains?(key, ".")
locked? = case :dets.lookup(state.locks, key) do
[{trigger}] -> "*"
_ -> ""
end
unless ignore?, do: "#{key}: #{to_string(Enum.count(data))}#{locked?}"
end)
|> Enum.filter(& &1)
total = Enum.reduce(state.triggers, 0, fn({_, data}, acc) ->
acc + Enum.count(data)
end)
detail = Enum.join(map, ", ")
total = ". total: #{Enum.count(state.triggers)} fichiers, #{to_string(total)} lignes. Détail: https://sys.115ans.net/irc/txt"
ro = if !state.rw, do: " (lecture seule activée)", else: ""
(detail<>total<>ro)
|> msg.replyfun.()
{:noreply, state}
end
#
# GLOBAL: RANDOM
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: []}}}, state) do
result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
end)
|> Enum.shuffle()
if !Enum.empty?(result) do
{source, line} = Enum.random(result)
msg.replyfun.(format_line(line, "#{source}: ", msg))
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: args}}}, state) do
grep = Enum.join(args, " ")
|> String.downcase
|> :unicode.characters_to_nfd_binary()
result = with_stateful_results(msg, {:bang,"txt",msg.network,msg.channel,grep}, fn() ->
Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
if !String.contains?(trigger, ".") do
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
else
acc
end
end)
|> Enum.filter(fn({_, line}) ->
line
|> String.downcase()
|> :unicode.characters_to_nfd_binary()
|> String.contains?(grep)
end)
|> Enum.shuffle()
end)
if result do
{source, line} = result
msg.replyfun.(["#{source}: " | line])
end
{:noreply, state}
end
def with_stateful_results(msg, key, initfun) do
me = self()
scope = {msg.network, msg.channel || msg.sender.nick}
key = {__MODULE__, me, scope, key}
with_stateful_results(key, initfun)
end
def with_stateful_results(key, initfun) do
pid = case :global.whereis_name(key) do
:undefined ->
start_stateful_results(key, initfun.())
pid -> pid
end
if pid, do: wait_stateful_results(key, initfun, pid)
end
def start_stateful_results(key, []) do
nil
end
def start_stateful_results(key, list) do
me = self()
{pid, _} = spawn_monitor(fn() ->
Process.monitor(me)
stateful_results(me, list)
end)
:yes = :global.register_name(key, pid)
pid
end
def wait_stateful_results(key, initfun, pid) do
send(pid, :get)
receive do
{:stateful_results, line} ->
line
{:DOWN, _ref, :process, ^pid, reason} ->
with_stateful_results(key, initfun)
after
5000 ->
nil
end
end
defp stateful_results(owner, []) do
send(owner, :empty)
:ok
end
@stateful_results_expire :timer.minutes(30)
defp stateful_results(owner, [line | rest] = acc) do
receive do
:get ->
send(owner, {:stateful_results, line})
stateful_results(owner, rest)
{:DOWN, _ref, :process, ^owner, _} ->
:ok
after
@stateful_results_expire -> :ok
end
end
#
# GLOBAL: MARKOV
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: []}}}, state) do
case state.markov_handler.sentence(state.markov) do
{:ok, line} ->
msg.replyfun.(line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: complete}}}, state) do
complete = Enum.join(complete, " ")
case state.markov_handler.complete_sentence(complete, state.markov) do
{:ok, line} ->
msg.replyfun.(line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
#
# TXT CREATE
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- can_write?(state, msg, trigger),
:ok <- create_file(trigger)
do
msg.replyfun.("#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`")
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ -> {:noreply, state}
end
end
#
# TXT: RANDOM
#
def handle_info({:irc, :trigger, trigger, m = %{trigger: %{type: :query, args: opts}}}, state) do
{trigger, _} = clean_trigger(trigger)
if Map.get(state.triggers, trigger) do
url = if m.channel do
NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, m.network, NolaWeb.format_chan(m.channel), trigger)
else
NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, trigger)
end
m.replyfun.("-> #{url}")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: opts}}}, state) do
{trigger, _} = clean_trigger(trigger)
line = get_random(msg, state.triggers, trigger, String.trim(Enum.join(opts, " ")))
if line do
msg.replyfun.(format_line(line, nil, msg))
end
{:noreply, state}
end
#
# TXT: ADD
#
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :plus, args: content}}}, state) do
with \
true <- can_write?(state, msg, trigger),
{:ok, idx} <- add(state.triggers, msg.text)
do
msg.replyfun.("#{msg.sender.nick}: ajouté à #{trigger}. (#{idx})")
{:noreply, %__MODULE__{state | triggers: load()}}
else
{:error, {:jaro, string, idx}} ->
msg.replyfun.("#{msg.sender.nick}: doublon #{trigger}##{idx}: #{string}")
error ->
Logger.debug("txt add failed: #{inspect error}")
{:noreply, state}
end
end
#
# TXT: DELETE
#
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :minus, args: [id]}}}, state) do
with \
true <- can_write?(state, msg, trigger),
data <- Map.get(state.triggers, trigger),
{id, ""} <- Integer.parse(id),
{text, _id} <- Enum.find(data, fn({_, idx}) -> id-1 == idx end)
do
data = data |> Enum.into(Map.new)
data = Map.delete(data, text)
msg.replyfun.("#{msg.sender.nick}: #{trigger}.txt##{id} supprimée: #{text}")
dump(trigger, data)
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ ->
{:noreply, state}
end
end
def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do
state.markov_handler.reload(state.triggers, state.markov)
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
def handle_call({:random, file}, _from, state) do
random = get_random(nil, state.triggers, file, [])
{:reply, random, state}
end
def terminate(_reason, state) do
if state.locks do
:dets.sync(state.locks)
:dets.close(state.locks)
end
:ok
end
# Load/Reloads text files from disk
defp load() do
triggers = Path.wildcard(directory() <> "/*.txt")
|> Enum.reduce(%{}, fn(path, m) ->
file = Path.basename(path)
key = String.replace(file, ".txt", "")
data = directory() <> file
|> File.read!
|> String.split("\n")
|> Enum.reject(fn(line) ->
cond do
line == "" -> true
!line -> true
true -> false
end
end)
|> Enum.with_index
Map.put(m, key, data)
end)
|> Enum.sort
|> Enum.into(Map.new)
send(self(), :reload_markov)
triggers
end
defp dump(trigger, data) do
data = data
|> Enum.sort_by(fn({_, idx}) -> idx end)
|> Enum.map(fn({text, _}) -> text end)
|> Enum.join("\n")
File.write!(directory() <> "/" <> trigger <> ".txt", data<>"\n", [])
end
defp get_random(msg, triggers, trigger, []) do
if data = Map.get(triggers, trigger) do
{data, _idx} = Enum.random(data)
data
else
nil
end
end
defp get_random(msg, triggers, trigger, opt) do
arg = case Integer.parse(opt) do
{pos, ""} -> {:index, pos}
{_pos, _some_string} -> {:grep, opt}
_error -> {:grep, opt}
end
get_with_param(msg, triggers, trigger, arg)
end
defp get_with_param(msg, triggers, trigger, {:index, pos}) do
data = Map.get(triggers, trigger, %{})
case Enum.find(data, fn({_, index}) -> index+1 == pos end) do
{text, _} -> text
_ -> nil
end
end
defp get_with_param(msg, triggers, trigger, {:grep, query}) do
out = with_stateful_results(msg, {:grep, trigger, query}, fn() ->
data = Map.get(triggers, trigger, %{})
regex = Regex.compile!("#{query}", "i")
Enum.filter(data, fn({txt, _}) -> Regex.match?(regex, txt) end)
|> Enum.map(fn({txt, _}) -> txt end)
|> Enum.shuffle()
end)
if out, do: out
end
defp create_file(name) do
File.touch!(directory() <> "/" <> name <> ".txt")
:ok
end
defp add(triggers, trigger_and_content) do
case String.split(trigger_and_content, " ", parts: 2) do
[trigger, content] ->
{trigger, _} = clean_trigger(trigger)
if Map.has_key?(triggers, trigger) do
jaro = Enum.find(triggers[trigger], fn({string, idx}) -> String.jaro_distance(content, string) > 0.9 end)
if jaro do
{string, idx} = jaro
{:error, {:jaro, string, idx}}
else
File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append])
idx = Enum.count(triggers[trigger])+1
{:ok, idx}
end
else
{:error, :notxt}
end
_ -> {:error, :badarg}
end
end
# fixme: this is definitely the ugliest thing i've ever done
defp clean_trigger(trigger) do
[trigger | opts] = trigger
|> String.strip
|> String.split(" ", parts: 2)
trigger = trigger
|> String.downcase
|> :unicode.characters_to_nfd_binary()
|> String.replace(~r/[^a-z0-9._]/, "")
|> String.trim(".")
|> String.trim("_")
{trigger, opts}
end
def format_line(line, prefix, msg) do
prefix = unless(prefix, do: "", else: prefix)
prefix <> line
|> String.split("\\\\")
|> Enum.map(fn(line) ->
String.split(line, "\\\\\\\\")
end)
|> List.flatten()
|> Enum.map(fn(line) ->
String.trim(line)
|> Tmpl.render(msg)
end)
end
def directory() do
Application.get_env(:nola, :data_path) <> "/irc.txt/"
end
defp can_write?(%{rw: rw?, locks: locks}, msg = %{channel: nil, sender: sender}, trigger) do
- admin? = IRC.admin?(sender)
+ admin? = Nola.Irc.admin?(sender)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
end
unlocked? = if rw? == false, do: false, else: !locked?
can? = unlocked? || admin?
if !can? do
reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
end
can?
end
defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, msg = %{channel: channel, sender: sender}, trigger) do
- admin? = IRC.admin?(sender)
+ admin? = Nola.Irc.admin?(sender)
operator? = Nola.UserTrack.operator?(msg.network, channel, sender.nick)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
end
unlocked? = if rw? == false, do: false, else: !locked?
can? = admin? || operator? || unlocked?
if !can? do
reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
end
can?
end
end
diff --git a/lib/telegram.ex b/lib/telegram.ex
index dcd8b1f..f2c9eca 100644
--- a/lib/telegram.ex
+++ b/lib/telegram.ex
@@ -1,233 +1,233 @@
defmodule Nola.Telegram do
require Logger
@behaviour Telegram.ChatBot
def my_path() do
"https://t.me/beauttebot"
end
def send_message(id, text, md2 \\ false) do
md = if md2, do: "MarkdownV2", else: "Markdown"
token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(Nola.Telegram, id)
Telegram.Api.request(token, "sendMessage", chat_id: id, text: text, parse_mode: "Markdown")
end
@impl Telegram.ChatBot
def init(chat_id) when chat_id < 0 do
{:ok, state} = Nola.TelegramRoom.init(chat_id)
{:ok, %{room_state: state}}
end
def init(chat_id) do
Logger.info("Telegram session starting: #{chat_id}")
account = Nola.Account.find_meta_account("telegram-id", chat_id)
account_id = if account, do: account.id
{:ok, %{account: account_id}}
end
@impl Telegram.ChatBot
def handle_update(update, token, %{room_state: room_state}) do
{:ok, room_state} = Nola.TelegramRoom.handle_update(update, token, room_state)
{:ok, %{room_state: room_state}}
end
def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/start"<>_}}, _token, state) do
text = "*Welcome to beautte!*\n\nQuery the bot on IRC and say \"enable-telegram\" to continue."
send_message(m["chat"]["id"], text)
{:ok, %{account: nil}}
end
def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/enable"<>_}}, _token, state) do
key = case String.split(text, " ") do
["/enable", key | _] -> key
_ -> "nil"
end
#Handled message "1247435154:AAGnSSCnySn0RuVxy_SUcDEoOX_rbF6vdq0" %{"message" =>
# %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
# "date" => 1591027272, "entities" =>
# [%{"length" => 7, "offset" => 0, "type" => "bot_command"}],
# "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
# "message_id" => 11, "text" => "/enable salope"}, "update_id" => 764148578}
account = Nola.Account.find_meta_account("telegram-validation-code", String.downcase(key))
text = if account do
net = Nola.Account.get_meta(account, "telegram-validation-target")
Nola.Account.put_meta(account, "telegram-id", m["chat"]["id"])
Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
Nola.Account.delete_meta(account, "telegram-validation-code")
Nola.Account.delete_meta(account, "telegram-validation-target")
Nola.Irc.Connection.broadcast_message(net, account, "Telegram #{m["chat"]["username"]} account added!")
"Yay! Linked to account **#{account.name}**."
else
"Token invalid"
end
send_message(m["chat"]["id"], text)
{:ok, %{account: account.id}}
end
#[debug] Unhandled update: %{"message" =>
# %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
# "date" => 1591096015,
# "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
# "message_id" => 29,
# "photo" => [
# %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADbQADZVMBAAEaBA",
# "file_size" => 9544, "file_unique_id" => "AQADRv09JF0AA2VTAQAB", "height" => 95, "width" => 320},
# %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADeAADZFMBAAEaBA",
# "file_size" => 21420, "file_unique_id" => "AQADRv09JF0AA2RTAQAB", "height" => 148, "width" => 501}]},
# "update_id" => 218161546}
for type <- ~w(photo voice video document animation) do
def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do
start_upload(unquote(type), data, token, state)
end
end
#[debug] Unhandled update: %{"callback_query" =>
# %{
# "chat_instance" => "-7948978714441865930", "data" => "evolu.net/#dmz",
# "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
# "id" => "8913804780149600",
# "message" => %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
# "date" => 1591098553, "from" => %{"first_name" => "devbeautte", "id" => 1293058838, "is_bot" => true, "username" => "devbeauttebot"},
# "message_id" => 62,
# "reply_markup" => %{"inline_keyboard" => [[%{"callback_data" => "random/#", "text" => "random/#"},
# %{"callback_data" => "evolu.net/#dmz", "text" => "evolu.net/#dmz"}]]},
# "text" => "Where should I send the file?"}
# }
# , "update_id" => 218161568}
#def handle_update(t, %{"callback_query" => cb = %{"data" => "resend", "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}) do
#end
def handle_update(%{"callback_query" => cb = %{"data" => "start-upload:"<>target, "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}, t, state) do
account = Nola.Account.find_meta_account("telegram-id", chat_id)
if account do
target = case String.split(target, "/") do
["everywhere"] -> Nola.Membership.of_account(account)
[net, chan] -> [{net, chan}]
end
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Processing...", reply_markup: %{})
{content, type} = cond do
op["photo"] -> {op["photo"], ""}
op["voice"] -> {op["voice"], " a voice message"}
op["video"] -> {op["video"], ""}
op["document"] -> {op["document"], ""}
op["animation"] -> {op["animation"], ""}
end
file = if is_list(content) && Enum.count(content) > 1 do
Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2)
|> List.first()
else
content
end
file_id = file["file_id"]
file_unique_id = file["file_unique_id"]
text = if(op["caption"], do: ": "<> op["caption"] <> "", else: "")
resend = %{"inline_keyboard" => [ [%{"text" => "re-share", "callback_data" => "resend"}] ]}
spawn(fn() ->
with \
{:ok, file} <- Telegram.Api.request(t, "getFile", file_id: file_id),
path = "https://api.telegram.org/file/bot#{t}/#{file["file_path"]}",
{:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
<<smol_body::binary-size(20), _::binary>> = body,
{:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
ext = Path.extname(file["file_path"]),
s3path = "#{account.id}/#{file_unique_id}#{ext}",
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "*Uploading...*", reply_markup: %{}, parse_mode: "MarkdownV2"),
s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type),
{:ok, _} <- ExAws.request(s3req)
do
path = NolaWeb.Router.Helpers.url(NolaWeb.Endpoint) <> "/files/#{s3path}"
sent = for {net, chan} <- target do
txt = "sent#{type}#{text} #{path}"
- IRC.send_message_as(account, net, chan, txt)
+ Nola.Irc.send_message_as(account, net, chan, txt)
"#{net}/#{chan}"
end
if caption = op["caption"], do: as_irc_message(chat_id, caption, account)
text = "Sent on " <> Enum.join(sent, ", ") <> " !"
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "_Sent!_", reply_markup: %{}, parse_mode: "MarkdownV2")
else
error ->
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Something failed.", reply_markup: %{}, parse_mode: "MarkdownV2")
Logger.error("Failed upload from Telegram: #{inspect error}")
end
end)
end
{:ok, state}
end
def handle_update(%{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}, "text" => text}}, _, state) do
account = Nola.Account.find_meta_account("telegram-id", id)
if account do
as_irc_message(id, text, account)
end
{:ok, state}
end
def handle_update(m, _, state) do
Logger.debug("Unhandled update: #{inspect m}")
{:ok, state}
end
@impl Telegram.ChatBot
def handle_info(info, %{room_state: room_state}) do
{:ok, room_state} = Nola.TelegramRoom.handle_info(info, room_state)
{:ok, %{room_state: room_state}}
end
def handle_info(_info, state) do
{:ok, state}
end
defp as_irc_message(id, text, account) do
reply_fun = fn(text) -> send_message(id, text) end
trigger_text = cond do
String.starts_with?(text, "/") ->
"/"<>text = text
"!"<>text
Enum.any?(Nola.Irc.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(text, trigger) end) ->
text
true ->
"!"<>text
end
message = %Nola.Message{
id: FlakeId.get(),
transport: :telegram,
network: "telegram",
channel: nil,
text: text,
account: account,
sender: %ExIRC.SenderInfo{nick: account.name},
replyfun: reply_fun,
trigger: Nola.Irc.Connection.extract_trigger(trigger_text),
at: nil
}
Nola.Irc.Connection.publish(message, ["messages:private", "messages:telegram", "telegram/#{account.id}:messages"])
message
end
defp start_upload(_type, %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}}}, token, state) do
account = Nola.Account.find_meta_account("telegram-id", id)
if account do
text = if(m["text"], do: m["text"], else: nil)
targets = Nola.Membership.of_account(account)
|> Enum.map(fn({net, chan}) -> "#{net}/#{chan}" end)
|> Enum.map(fn(i) -> %{"text" => i, "callback_data" => "start-upload:#{i}"} end)
kb = if Enum.count(targets) > 1 do
[%{"text" => "everywhere", "callback_data" => "start-upload:everywhere"}] ++ targets
else
targets
end
|> Enum.chunk_every(2)
keyboard = %{"inline_keyboard" => kb}
Telegram.Api.request(token, "sendMessage", chat_id: id, text: "Where should I send this file?", reply_markup: keyboard, reply_to_message_id: m["message_id"], parse_mode: "MarkdownV2")
end
{:ok, state}
end
end
diff --git a/lib/telegram/room.ex b/lib/telegram/room.ex
index f3c3715..ede939e 100644
--- a/lib/telegram/room.ex
+++ b/lib/telegram/room.ex
@@ -1,188 +1,188 @@
defmodule Nola.TelegramRoom do
require Logger
@behaviour Telegram.ChatBot
alias Telegram.Api
@couch "bot-telegram-rooms"
def rooms(), do: rooms(:with_docs)
@spec rooms(:with_docs | :ids) :: [Map.t | integer( )]
def rooms(:with_docs) do
case Couch.get(@couch, :all_docs, include_docs: true) do
{:ok, %{"rows" => rows}} -> {:ok, for(%{"doc" => doc} <- rows, do: doc)}
error = {:error, _} -> error
end
end
def rooms(:ids) do
case Couch.get(@couch, :all_docs) do
{:ok, %{"rows" => rows}} -> {:ok, for(%{"id" => id} <- rows, do: id)}
error = {:error, _} -> error
end
end
def room(id, opts \\ []) do
Couch.get(@couch, id, opts)
end
# TODO: Create couch
def setup() do
:ok
end
def after_start() do
for id <- room(:ids), do: Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(Nola.Telegram, Integer.parse(id) |> elem(0))
end
@impl Telegram.ChatBot
def init(id) when is_integer(id) and id < 0 do
token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
{:ok, chat} = Api.request(token, "getChat", chat_id: id)
Logger.metadata(transport: :telegram, id: id, telegram_room_id: id)
tg_room = case room(id) do
{:ok, tg_room = %{"network" => _net, "channel" => _chan}} -> tg_room
{:error, :not_found} ->
[net, chan] = String.split(chat["title"], "/", parts: 2)
{net, chan} = case Nola.Irc.Connection.get_network(net, chan) do
%Nola.Irc.Connection{} -> {net, chan}
_ -> {nil, nil}
end
{:ok, _id, _rev} = Couch.post(@couch, %{"_id" => id, "network" => net, "channel" => nil})
{:ok, tg_room} = room(id)
tg_room
end
%{"network" => net, "channel" => chan} = tg_room
Logger.info("Starting ChatBot for room #{id} \"#{chat["title"]}\" #{inspect tg_room}")
irc_plumbed = if net && chan do
{:ok, _} = Registry.register(Nola.PubSub, "#{net}/#{chan}:messages", plugin: __MODULE__)
{:ok, _} = Registry.register(Nola.PubSub, "#{net}/#{chan}:triggers", plugin: __MODULE__)
{:ok, _} = Registry.register(Nola.PubSub, "#{net}/#{chan}:outputs", plugin: __MODULE__)
true
else
Logger.warn("Did not found telegram match for #{id} \"#{chat["title"]}\"")
false
end
{:ok, %{id: id, net: net, chan: chan, irc: irc_plumbed}}
end
def init(id) do
Logger.error("telegram_room: bad id (not room id)", transport: :telegram, id: id, telegram_room_id: id)
:ignoree
end
defp find_or_create_meta_account(from = %{"id" => user_id}, state) do
if account = Nola.Account.find_meta_account("telegram-id", user_id) do
account
else
first_name = Map.get(from, "first_name")
last_name = Map.get(from, "last_name")
name = [first_name, last_name]
|> Enum.filter(& &1)
|> Enum.join(" ")
username = Map.get(from, "username", first_name)
account = username
|> Nola.Account.new_account()
|> Nola.Account.update_account_name(name)
|> Nola.Account.put_meta("telegram-id", user_id)
Logger.info("telegram_room: created account #{account.id} for telegram user #{user_id}")
account
end
end
def handle_update(%{"message" => %{"from" => from = %{"id" => user_id}, "text" => text}}, _token, state) do
account = find_or_create_meta_account(from, state)
connection = Nola.Irc.Connection.get_network(state.net)
- IRC.send_message_as(account, state.net, state.chan, text, true)
+ Nola.Irc.send_message_as(account, state.net, state.chan, text, true)
{:ok, state}
end
def handle_update(data = %{"message" => %{"from" => from = %{"id" => user_id}, "location" => %{"latitude" => lat, "longitude" => lon}}}, _token, state) do
account = find_or_create_meta_account(from, state)
connection = Nola.Irc.Connection.get_network(state.net)
- IRC.send_message_as(account, state.net, state.chan, "@ #{lat}, #{lon}", true)
+ Nola.Irc.send_message_as(account, state.net, state.chan, "@ #{lat}, #{lon}", true)
{:ok, state}
end
for type <- ~w(photo voice video document animation) do
def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do
upload(unquote(type), data, token, state)
end
end
def handle_update(update, token, state) do
{:ok, state}
end
def handle_info({:irc, _, _, message}, state) do
handle_info({:irc, nil, message}, state)
end
def handle_info({:irc, _, message = %Nola.Message{sender: %{nick: nick}, text: text}}, state) do
if Map.get(message.meta, :from) == self() do
else
body = if Map.get(message.meta, :self), do: text, else: "<#{nick}> #{text}"
Nola.Telegram.send_message(state.id, body)
end
{:ok, state}
end
def handle_info(info, state) do
Logger.info("UNhandled #{inspect info}")
{:ok, state}
end
defp upload(_type, %{"message" => m = %{"chat" => %{"id" => chat_id}, "from" => from = %{"id" => user_id}}}, token, state) do
account = find_or_create_meta_account(from, state)
if account do
{content, type} = cond do
m["photo"] -> {m["photo"], "photo"}
m["voice"] -> {m["voice"], "voice message"}
m["video"] -> {m["video"], "video"}
m["document"] -> {m["document"], "file"}
m["animation"] -> {m["animation"], "gif"}
end
file = if is_list(content) && Enum.count(content) > 1 do
Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2)
|> List.first()
else
content
end
file_id = file["file_id"]
file_unique_id = file["file_unique_id"]
text = if(m["caption"], do: m["caption"] <> " ", else: "")
spawn(fn() ->
with \
{:ok, file} <- Telegram.Api.request(token, "getFile", file_id: file_id),
path = "https://api.telegram.org/file/bot#{token}/#{file["file_path"]}",
{:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
<<smol_body::binary-size(20), _::binary>> = body,
{:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
ext = Path.extname(file["file_path"]),
s3path = "#{account.id}/#{file_unique_id}#{ext}",
s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type),
{:ok, _} <- ExAws.request(s3req)
do
path = NolaWeb.Router.Helpers.url(NolaWeb.Endpoint) <> "/files/#{s3path}"
txt = "#{type}: #{text}#{path}"
connection = Nola.Irc.Connection.get_network(state.net)
- IRC.send_message_as(account, state.net, state.chan, txt, true)
+ Nola.Irc.send_message_as(account, state.net, state.chan, txt, true)
else
error ->
Telegram.Api.request(token, "sendMessage", chat_id: chat_id, text: "File upload failed, sorry.")
Logger.error("Failed upload from Telegram: #{inspect error}")
end
end)
{:ok, state}
end
end
end
diff --git a/lib/web/controllers/irc_controller.ex b/lib/web/controllers/irc_controller.ex
index d2ba04a..e87382b 100644
--- a/lib/web/controllers/irc_controller.ex
+++ b/lib/web/controllers/irc_controller.ex
@@ -1,101 +1,101 @@
defmodule NolaWeb.IrcController do
use NolaWeb, :controller
plug NolaWeb.ContextPlug
def index(conn, params) do
network = Map.get(params, "network")
channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c)
commands = for mod <- Enum.uniq([Nola.Plugins.Account] ++ Nola.Plugins.enabled()) do
if is_atom(mod) do
identifier = Module.split(mod) |> List.last |> Macro.underscore
{identifier, mod.irc_doc()}
end
end
|> Enum.filter(& &1)
|> Enum.filter(fn({_, doc}) -> doc end)
members = cond do
network && channel -> Enum.map(Nola.UserTrack.channel(network, channel), fn(tuple) -> Nola.UserTrack.User.from_tuple(tuple) end)
true ->
Nola.Membership.of_account(conn.assigns.account)
end
render conn, "index.html", network: network, commands: commands, channel: channel, members: members
end
def txt(conn, %{"name" => name}) do
if String.contains?(name, ".txt") do
name = String.replace(name, ".txt", "")
data = data()
if Map.has_key?(data, name) do
lines = Enum.join(data[name], "\n")
text(conn, lines)
else
conn
|> put_status(404)
|> text("Not found")
end
else
do_txt(conn, name)
end
end
def txt(conn, _), do: do_txt(conn, nil)
defp do_txt(conn, nil) do
- doc = Nola.IRC.Txt.irc_doc()
+ doc = Nola.Plugins.Txt.irc_doc()
data = data()
main = Enum.filter(data, fn({trigger, _}) -> !String.contains?(trigger, ".") end) |> Enum.into(Map.new)
system = Enum.filter(data, fn({trigger, _}) -> String.contains?(trigger, ".") end) |> Enum.into(Map.new)
lines = Enum.reduce(main, 0, fn({_, lines}, acc) -> acc + Enum.count(lines) end)
conn
|> assign(:title, "txt")
|> render("txts.html", data: main, doc: doc, files: Enum.count(main), lines: lines, system: system)
end
defp do_txt(conn, txt) do
data = data()
base_url = cond do
conn.assigns[:chan] -> "/#{conn.assigns.network}/#{NolaWeb.format_chan(conn.assigns.chan)}"
true -> "/-"
end
if lines = Map.get(data, txt) do
lines = Enum.map(lines, fn(line) ->
line
|> String.split("\\\\")
|> Enum.intersperse(Phoenix.HTML.Tag.tag(:br))
end)
conn
|> assign(:breadcrumbs, [{"txt", "#{base_url}/txt"}])
|> assign(:title, "#{txt}.txt")
|> render("txt.html", name: txt, data: lines, doc: nil)
else
conn
|> put_status(404)
|> text("Not found")
end
end
defp data() do
dir = Application.get_env(:nola, :data_path) <> "/irc.txt/"
Path.wildcard(dir <> "/*.txt")
|> Enum.reduce(%{}, fn(path, m) ->
path = String.split(path, "/")
file = List.last(path)
key = String.replace(file, ".txt", "")
data = dir <> file
|> File.read!
|> String.split("\n")
|> Enum.reject(fn(line) ->
cond do
line == "" -> true
!line -> true
true -> false
end
end)
Map.put(m, key, data)
end)
|> Enum.sort
|> Enum.into(Map.new)
end
end
diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex
index bf79413..cb46b8f 100644
--- a/lib/web/controllers/page_controller.ex
+++ b/lib/web/controllers/page_controller.ex
@@ -1,53 +1,53 @@
defmodule NolaWeb.PageController do
use NolaWeb, :controller
plug NolaWeb.ContextPlug when action not in [:token]
plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token]
def token(conn, %{"token" => token}) do
with \
{:ok, account, perks} <- Nola.AuthToken.lookup(token)
do
IO.puts("Authenticated account #{inspect account}")
conn = put_session(conn, :account, account)
case perks do
nil -> redirect(conn, to: "/")
{:redirect, path} -> redirect(conn, to: path)
{:external_redirect, url} -> redirect(conn, external: url)
end
else
z ->
IO.inspect(z)
text(conn, "Error: invalid or expired token")
end
end
def index(conn = %{assigns: %{account: account}}, _) do
memberships = Nola.Membership.of_account(account)
users = Nola.UserTrack.find_by_account(account)
metas = Nola.Account.get_all_meta(account)
predicates = Nola.Account.get_predicates(account)
conn
|> assign(:title, account.name)
|> render("user.html", users: users, memberships: memberships, metas: metas, predicates: predicates)
end
def irc(conn, _) do
- bot_helps = for mod <- Nola.IRC.env(:handlers) do
+ bot_helps = for mod <- Nola.Irc.env(:handlers) do
mod.irc_doc()
end
render conn, "irc.html", bot_helps: bot_helps
end
def authenticate(conn, _) do
with \
{:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)},
{:account, account} when not is_nil(account) <- {:account, Nola.Account.get(account_id)}
do
assign(conn, :account, account)
else
_ -> conn
end
end
end
diff --git a/lib/web/live/chat_live.ex b/lib/web/live/chat_live.ex
index f8bab7d..8a9f6f2 100644
--- a/lib/web/live/chat_live.ex
+++ b/lib/web/live/chat_live.ex
@@ -1,120 +1,120 @@
defmodule NolaWeb.ChatLive do
use Phoenix.LiveView
use Phoenix.HTML
require Logger
def mount(%{"network" => network, "chan" => chan}, %{"account" => account_id}, socket) do
chan = NolaWeb.reformat_chan(chan)
connection = Nola.Irc.Connection.get_network(network, chan)
account = Nola.Account.get(account_id)
membership = Nola.Membership.of_account(Nola.Account.get("DRgpD4fLf8PDJMLp8Dtb"))
if account && connection && Enum.member?(membership, {connection.network, chan}) do
{:ok, _} = Registry.register(Nola.PubSub, "#{connection.network}:events", plugin: __MODULE__)
for t <- ["messages", "triggers", "outputs", "events"] do
{:ok, _} = Registry.register(Nola.PubSub, "#{connection.network}/#{chan}:#{t}", plugin: __MODULE__)
end
Nola.Irc.PuppetConnection.start(account, connection)
users = Nola.UserTrack.channel(connection.network, chan)
|> Enum.map(fn(tuple) -> Nola.UserTrack.User.from_tuple(tuple) end)
|> Enum.reduce(Map.new, fn(user = %{id: id}, acc) ->
Map.put(acc, id, user)
end)
- backlog = case Nola.IRC.Buffer.select_buffer(connection.network, chan) do
+ backlog = case Nola.Plugins.Buffer.select_buffer(connection.network, chan) do
{backlog, _} ->
{backlog, _} = Enum.reduce(backlog, {backlog, nil}, &reduce_contextual_event/2)
Enum.reverse(backlog)
_ -> []
end
socket = socket
|> assign(:connection_id, connection.id)
|> assign(:network, connection.network)
|> assign(:chan, chan)
|> assign(:title, "live")
|> assign(:channel, chan)
|> assign(:account_id, account.id)
|> assign(:backlog, backlog)
|> assign(:users, users)
|> assign(:counter, 0)
{:ok, socket}
else
{:ok, redirect(socket, to: "/")}
end
end
def handle_event("send", %{"message" => %{"text" => text}}, socket) do
account = Nola.Account.get(socket.assigns.account_id)
- IRC.send_message_as(account, socket.assigns.network, socket.assigns.channel, text, true)
+ Nola.Irc.send_message_as(account, socket.assigns.network, socket.assigns.channel, text, true)
{:noreply, assign(socket, :counter, socket.assigns.counter + 1)}
end
def handle_info({:irc, :event, event = %{type: :join, user_id: id}}, socket) do
if user = Nola.UserTrack.lookup(id) do
socket = socket
|> assign(:users, Map.put(socket.assigns.users, id, user))
|> append_to_backlog(event)
{:noreply, socket}
else
{:noreply, socket}
end
end
def handle_info({:irc, :event, event = %{type: :nick, user_id: id, nick: nick}}, socket) do
socket = socket
|> assign(:users, update_in(socket.assigns.users, [id, :nick], nick))
|> append_to_backlog(event)
{:noreply, socket}
end
def handle_info({:irc, :event, event = %{type: :quit, user_id: id}}, socket) do
socket = socket
|> assign(:users, Map.delete(socket.assigns.users, id))
|> append_to_backlog(event)
{:noreply, socket}
end
def handle_info({:irc, :event, event = %{type: :part, user_id: id}}, socket) do
socket = socket
|> assign(:users, Map.delete(socket.assigns.users, id))
|> append_to_backlog(event)
{:noreply, socket}
end
def handle_info({:irc, :trigger, _, message}, socket) do
handle_info({:irc, nil, message}, socket)
end
def handle_info({:irc, :text, message}, socket) do
IO.inspect({:live_message, message})
socket = socket
|> append_to_backlog(message)
{:noreply, socket}
end
def handle_info(info, socket) do
Logger.debug("Unhandled info: #{inspect info}")
{:noreply, socket}
end
defp append_to_backlog(socket, line) do
{add, _} = reduce_contextual_event(line, {[], List.last(socket.assigns.backlog)})
assign(socket, :backlog, socket.assigns.backlog ++ add)
end
defp reduce_contextual_event(line, {acc, nil}) do
{[line | acc], line}
end
defp reduce_contextual_event(line, {acc, last}) do
if NaiveDateTime.to_date(last.at) != NaiveDateTime.to_date(line.at) do
{[%{type: :day_changed, date: NaiveDateTime.to_date(line.at), at: nil}, line | acc], line}
else
{[line | acc], line}
end
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Mar 14, 4:20 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33597
Default Alt Text
(79 KB)
Attached To
rNOLA Nola
Event Timeline
Log In to Comment