Page MenuHomePhabricator

No OneTemporary

diff --git a/lib/couch.ex b/lib/couch.ex
index 71f86a3..9cafd39 100644
--- a/lib/couch.ex
+++ b/lib/couch.ex
@@ -1,96 +1,96 @@
defmodule Couch do
@moduledoc """
Simple, no-frills, CouchDB client
"""
@type base_error :: {:error, :bad_request} | {:error, :unauthorized} |
{:error, :server_error} | {:error, :service_unavailable} |
{:error, :bad_response} | {:error, HTTPoison.Error.t()}
def new(db, params \\ []) do
{url, headers, options} = prepare_request([db], [], params)
case HTTPoison.put(url, headers, options) do
{:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] ->
:ok
{:ok, %HTTPoison.Response{status_code: 412}} ->
{:error, :exists}
error ->
handle_generic_response(error)
end
end
@doc """
Retrieve a document `doc` from `db`.
`params` are [documented here](https://docs.couchdb.org/en/3.2.2-docs/api/document/common.html)
"""
@spec get(String.t(), String.t() | :all_docs, Keyword.t()) :: {:ok, Map.t()} | {:error, :not_found} | {:error, any()}
def get(db, doc, params \\ [])
def get(db, :all_docs, params), do: get(db, "_all_docs", params)
def get(db, doc, params) do
{url, headers, options} = prepare_request([db, doc], [], params)
case HTTPoison.get(url, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Poison.decode!(body)}
error -> handle_generic_response(error)
end
end
@spec post(String.t(), Map.t(), Keyword.t()) :: {:error, :operation_failed} | {:error, :not_found} | {:error, :exists} | {:error, any()}
def post(db, data, params \\ []) do
{url, headers, options} = prepare_request([db], [{"content-type", "application/json"}], params)
with \
{:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] <- HTTPoison.post(url, Poison.encode!(data), headers, options),
{:json, {:ok, %{"ok" => true, "id" => id, "rev" => rev}}} <- {:json, Poison.decode(body)} do
{:ok, id, rev}
else
{:ok, %HTTPoison.Response{status_code: 409}} -> {:error, :exists}
{:json, {:ok, body}} ->
Logger.error("couch: operation failed: #{inspect body}")
{:error, :operation_failed}
error -> handle_generic_response(error)
end
end
@spec put(String.t(), Map.t(), Keyword.t()) :: {:error, :operation_failed} | {:error, :not_found} | {:error, :conflict} | {:error, any()}
def put(db, doc = %{"_id" => id, "_rev" => _}, params \\ []) do
{url, headers, options} = prepare_request([db, id], [{"content-type", "application/json"}], params)
case HTTPoison.put(url, Poison.encode!(doc), headers, options) do
{:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] ->
body = Poison.decode!(body)
if Map.get(body, "ok") do
{:ok, Map.get(body, "id"), Map.get(body, "rev")}
else
{:error, :operation_failed}
end
{:ok, %HTTPoison.Response{status_code: 209}} ->
{:error, :conflict}
error -> handle_generic_response(error)
end
end
defp prepare_request(path, headers \\ [], params \\ [], options \\ []) do
- config = Application.get_env(:lsg, :couch)
+ config = Application.get_env(:nola, :couch)
base_url = Keyword.get(config, :url, "http://localhost:5984")
url = URI.merge(base_url, Path.join(path)) |> to_string()
headers = headers ++ [{"accept", "application/json"}, {"user-agent", "beautte"}]
params = Enum.map(params, fn({k, v}) -> {to_string(k), v} end)
client_options = Keyword.get(config, :client_options, [])
options = [params: params] ++ options ++ client_options
user = Keyword.get(config, :user)
pass = Keyword.get(config, :pass)
hackney_options = Keyword.get(options, :hackney, [])
hackney_options = if user, do: [{:basic_auth, {user, pass}} | hackney_options], else: []
options = [{:hackney, [:insecure | hackney_options]} | options]
{url, headers, options}
end
defp handle_generic_response(%HTTPoison.Response{status_code: code}), do: {:error, Plug.Conn.Status.reason_atom(code)}
defp handle_generic_response(%HTTPoison.Error{reason: reason}), do: {:error, reason}
end
diff --git a/lib/lsg/application.ex b/lib/lsg/application.ex
index 1782053..4f3d1da 100644
--- a/lib/lsg/application.ex
+++ b/lib/lsg/application.ex
@@ -1,56 +1,56 @@
defmodule Nola.Application do
use Application
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
Logger.add_backend(Sentry.LoggerBackend)
:ok = Nola.Matrix.setup()
:ok = Nola.TelegramRoom.setup()
# Define workers and child supervisors to be supervised
children = [
# Start the endpoint when the application starts
supervisor(NolaWeb.Endpoint, []),
# Start your own worker by calling: Nola.Worker.start_link(arg1, arg2, arg3)
# worker(Nola.Worker, [arg1, arg2, arg3]),
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(Nola.Icecast, []),
] ++ Nola.IRC.application_childs
++ Nola.Matrix.application_childs
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Nola.Supervisor]
sup = Supervisor.start_link(children, opts)
start_telegram()
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
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
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(:lsg, :telegram, []), :key)
+ token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
options = [
- username: Keyword.get(Application.get_env(:lsg, :telegram, []), :nick, "beauttebot"),
+ 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/lsg/icecast.ex b/lib/lsg/icecast.ex
index 60fb45a..5a53192 100644
--- a/lib/lsg/icecast.ex
+++ b/lib/lsg/icecast.ex
@@ -1,117 +1,117 @@
defmodule Nola.Icecast do
use GenServer
require Logger
@hackney_pool :default
@httpoison_opts [hackney: [pool: @hackney_pool]]
@fuse __MODULE__
def start_link, do: GenServer.start_link(__MODULE__, [], [])
def init(_) do
GenServer.cast(self(), :poll)
{:ok, nil}
end
def handle_cast(:poll, state) do
state = poll(state)
{:noreply, state}
end
def handle_info(:poll, state) do
state = poll(state)
{:noreply, state}
end
defp poll(state) do
state = case request(base_url(), :get) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
#update_json_stats(Jason.decode(body))
stats = update_stats(body)
if state != stats do
Logger.info "Icecast Update: " <> inspect(stats)
Nola.IcecastAgent.update(stats)
Registry.dispatch(Nola.BroadcastRegistry, "icecast", fn ws ->
for {pid, _} <- ws, do: send(pid, {:icecast, stats})
end)
stats
else
state
end
error ->
Logger.error "Icecast HTTP Error: #{inspect error}"
state
end
- interval = Application.get_env(:lsg, :icecast_poll_interval, 60_000)
+ interval = Application.get_env(:nola, :icecast_poll_interval, 60_000)
:timer.send_after(interval, :poll)
state
end
defp update_stats(html) do
raw = Floki.find(html, "div.roundbox")
|> Enum.map(fn(html) ->
html = Floki.raw_html(html)
[{"h3", _, ["Mount Point /"<>mount]}] = Floki.find(html, "h3.mount")
stats = Floki.find(html, "tr")
|> Enum.map(fn({"tr", _, tds}) ->
[{"td", _, keys}, {"td", _, values}] = tds
key = List.first(keys)
value = List.first(values)
{key, value}
end)
|> Enum.into(Map.new)
{mount, stats}
end)
|> Enum.into(Map.new)
live? = if Map.get(raw["live"], "Content Type:", false), do: true, else: false
np = if live? do
raw["live"]["Currently playing:"]
else
raw["autodj"]["Currently playing:"]
end
genre = raw["live"]["Genre:"] || nil
%{np: np || "", live: live? || false, genre: genre}
end
defp update_json_stats({:ok, body}) do
Logger.debug "JSON STATS: #{inspect body}"
end
defp update_json_stats(error) do
Logger.error "Failed to decode JSON Stats: #{inspect error}"
end
defp request(uri, method, body \\ [], headers \\ []) do
headers = [{"user-agent", "Nola-API[115ans.net, sys.115ans.net] href@random.sh"}] ++ headers
options = @httpoison_opts
case :ok do #:fuse.ask(@fuse, :sync) do
:ok -> run_request(method, uri, body, headers, options)
:blown -> :blown
end
end
# This is to work around hackney's behaviour of returning `{:error, :closed}` when a pool connection has been closed
# (keep-alive expired). We just retry the request immediatly up to five times.
defp run_request(method, uri, body, headers, options), do: run_request(method, uri, body, headers, options, 0)
defp run_request(method, uri, body, headers, options, retries) when retries < 4 do
case HTTPoison.request(method, uri, body, headers, options) do
{:error, :closed} -> run_request(method, uri, body, headers, options, retries + 1)
other -> other
end
end
defp run_request(method, uri, body, headers, options, _exceeded_retries), do: {:error, :unavailable}
#
# -- URIs
#
defp stats_json_url do
base_url() <> "/status-json.xsl"
end
defp base_url do
"http://91.121.59.45:8089"
end
end
diff --git a/lib/lsg/lsg.ex b/lib/lsg/lsg.ex
index 11d0e24..0acb76e 100644
--- a/lib/lsg/lsg.ex
+++ b/lib/lsg/lsg.ex
@@ -1,30 +1,30 @@
defmodule Nola do
@default_brand [
name: "Nola,
source_url: "https://phab.random.sh/source/Bot/",
owner: "Ashamed owner",
owner_email: "contact@my.nola.bot"
]
- def env(), do: Application.get_env(:lsg)
- def env(key, default \\ nil), do: Application.get_env(:lsg, key, default)
+ def env(), do: Application.get_env(:nola)
+ def env(key, default \\ nil), do: Application.get_env(:nola, key, default)
def brand(), do: env(:brand, @default_brand)
def brand(key), do: Keyword.get(brand(), key)
def name(), do: brand(:name)
def source_url(), do: brand(:source_url)
def data_path(suffix) do
Path.join(data_path(), suffix)
end
def data_path do
- Application.get_env(:lsg, :data_path)
+ Application.get_env(:nola, :data_path)
end
def version do
- Application.spec(:lsg)[:vsn]
+ Application.spec(:nola)[:vsn]
end
end
diff --git a/lib/lsg_irc/base_plugin.ex b/lib/lsg_irc/base_plugin.ex
index ee0352e..a2b9ffb 100644
--- a/lib/lsg_irc/base_plugin.ex
+++ b/lib/lsg_irc/base_plugin.ex
@@ -1,131 +1,131 @@
defmodule Nola.IRC.BasePlugin 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(IRC.PubSub, "trigger:version", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:help", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:liquidrender", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:plugin", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:plugins", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "plugins", msg = %{trigger: %{type: :bang, args: []}}}, _) do
enabled_string = IRC.Plugin.enabled()
|> Enum.map(fn(mod) ->
mod
|> Macro.underscore()
|> String.split("/", parts: :infinity)
|> List.last()
|> String.replace("_plugin", "")
|> 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.IRC, Macro.camelize(plugin<>"_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 IRC.Plugin.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.IRC, Macro.camelize(plugin<>"_plugin")])
with true <- Code.ensure_loaded?(module),
IRC.Plugin.switch(module, true),
{:ok, pid} <- IRC.Plugin.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.IRC, Macro.camelize(plugin<>"_plugin")])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module),
:ok <- GenServer.stop(pid),
{:ok, pid} <- IRC.Plugin.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.IRC, Macro.camelize(plugin<>"_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)
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(:lsg, :vsn)
+ {: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()
message.replyfun.([
<<"🤖 I am a robot running", 2, "beautte, version #{ver}", 2, " — source: #{Nola.source_url()}">>,
"🦾 Elixir #{elixir_ver} #{otp_ver} on #{system}",
"👷‍♀️ Owner: href <href@random.sh>",
"🌍 Web interface: #{url}"
])
{:noreply, nil}
end
def handle_info(msg, _) do
{:noreply, nil}
end
end
diff --git a/lib/lsg_irc/finance_plugin.ex b/lib/lsg_irc/finance_plugin.ex
index c1a1771..16d06ee 100644
--- a/lib/lsg_irc/finance_plugin.ex
+++ b/lib/lsg_irc/finance_plugin.ex
@@ -1,190 +1,190 @@
defmodule Nola.IRC.FinancePlugin do
require Logger
@moduledoc """
# finance
Données de [alphavantage.co](https://alphavantage.co).
## forex / monnaies / crypto-monnaies
* **`!forex <MONNAIE1> [MONNAIE2]`**: taux de change entre deux monnaies.
* **`!forex <MONTANT> <MONNAIE1> <MONNAIE2>`**: converti `montant` entre deux monnaies
* **`?currency <recherche>`**: recherche une monnaie
Utiliser le symbole des monnaies (EUR, USD, ...).
## bourses
* **`!stocks <SYMBOLE>`**
* **`?stocks <recherche>`** cherche un symbole
Pour les symboles non-US, ajouter le suffixe (RNO Paris: RNO.PAR).
"""
@currency_list "http://www.alphavantage.co/physical_currency_list/"
@crypto_list "http://www.alphavantage.co/digital_currency_list/"
HTTPoison.start()
load_currency = fn(url) ->
resp = HTTPoison.get!(url)
resp.body
|> String.strip()
|> String.split("\n")
|> Enum.drop(1)
|> Enum.map(fn(line) ->
[symbol, name] = line
|> String.strip()
|> String.split(",", parts: 2)
{symbol, name}
end)
|> Enum.into(Map.new)
end
fiat = load_currency.(@currency_list)
crypto = load_currency.(@crypto_list)
@currencies Map.merge(fiat, crypto)
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:forex", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:currency", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:stocks", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "stocks", message = %{trigger: %{type: :query, args: args = search}}}, state) do
search = Enum.join(search, "%20")
url = "https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords=#{search}&apikey=#{api_key()}"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
data = Poison.decode!(data)
if error = Map.get(data, "Error Message") do
Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
message.replyfun.("stocks: requête invalide")
else
items = for item <- Map.get(data, "bestMatches") do
symbol = Map.get(item, "1. symbol")
name = Map.get(item, "2. name")
type = Map.get(item, "3. type")
region = Map.get(item, "4. region")
currency = Map.get(item, "8. currency")
"#{symbol}: #{name} (#{region}; #{currency}; #{type})"
end
|> Enum.join(", ")
items = if items == "" do
"no results!"
else
items
end
message.replyfun.(items)
end
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
message.replyfun.("forex: erreur (api #{code})")
{:error, %HTTPoison.Error{reason: error}} ->
Logger.error "AlphaVantage HTTP error: #{inspect error}"
message.replyfun.("forex: erreur (http #{inspect error})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "stocks", message = %{trigger: %{type: :bang, args: args = [symbol]}}}, state) do
url = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=#{symbol}&apikey=#{api_key()}"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
data = Poison.decode!(data)
if error = Map.get(data, "Error Message") do
Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
message.replyfun.("stocks: requête invalide")
else
data = Map.get(data, "Global Quote")
open = Map.get(data, "02. open")
high = Map.get(data, "03. high")
low = Map.get(data, "04. low")
price = Map.get(data, "05. price")
volume = Map.get(data, "06. volume")
prev_close = Map.get(data, "08. previous close")
change = Map.get(data, "09. change")
change_pct = Map.get(data, "10. change percent")
msg = "#{symbol}: #{price} #{change} [#{change_pct}] (high: #{high}, low: #{low}, open: #{open}, prev close: #{prev_close}) (volume: #{volume})"
message.replyfun.(msg)
end
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
message.replyfun.("stocks: erreur (api #{code})")
{:error, %HTTPoison.Error{reason: error}} ->
Logger.error "AlphaVantage HTTP error: #{inspect error}"
message.replyfun.("stocks: erreur (http #{inspect error})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "forex", message = %{trigger: %{type: :bang, args: args = [_ | _]}}}, state) do
{amount, from, to} = case args do
[amount, from, to] ->
{amount, _} = Float.parse(amount)
{amount, from, to}
[from, to] ->
{1, from, to}
[from] ->
{1, from, "EUR"}
end
url = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=#{from}&to_currency=#{to}&apikey=#{api_key()}"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
data = Poison.decode!(data)
if error = Map.get(data, "Error Message") do
Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
message.replyfun.("forex: requête invalide")
else
data = Map.get(data, "Realtime Currency Exchange Rate")
from_name = Map.get(data, "2. From_Currency Name")
to_name = Map.get(data, "4. To_Currency Name")
rate = Map.get(data, "5. Exchange Rate")
{rate, _} = Float.parse(rate)
value = amount*rate
message.replyfun.("#{amount} #{from} (#{from_name}) -> #{value} #{to} (#{to_name}) (#{rate})")
end
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
message.replyfun.("forex: erreur (api #{code})")
{:error, %HTTPoison.Error{reason: error}} ->
Logger.error "AlphaVantage HTTP error: #{inspect error}"
message.replyfun.("forex: erreur (http #{inspect error})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "currency", message = %{trigger: %{type: :query, args: args = search}}}, state) do
search = Enum.join(search, " ")
results = Enum.filter(@currencies, fn({symbol, name}) ->
String.contains?(String.downcase(name), String.downcase(search)) || String.contains?(String.downcase(symbol), String.downcase(search))
end)
|> Enum.map(fn({symbol, name}) ->
"#{symbol}: #{name}"
end)
|> Enum.join(", ")
if results == "" do
message.replyfun.("no results!")
else
message.replyfun.(results)
end
{:noreply, state}
end
defp api_key() do
- Application.get_env(:lsg, :alphavantage, [])
+ Application.get_env(:nola, :alphavantage, [])
|> Keyword.get(:api_key, "demo")
end
end
diff --git a/lib/lsg_irc/last_fm_plugin.ex b/lib/lsg_irc/last_fm_plugin.ex
index f29982c..03df675 100644
--- a/lib/lsg_irc/last_fm_plugin.ex
+++ b/lib/lsg_irc/last_fm_plugin.ex
@@ -1,187 +1,187 @@
defmodule Nola.IRC.LastFmPlugin do
require Logger
@moduledoc """
# last.fm
* **!lastfm|np `[nick|username]`**
* **.lastfm|np**
* **+lastfm, -lastfm `<username last.fm>; ?lastfm`** Configurer un nom d'utilisateur last.fm
"""
@single_trigger ~w(lastfm np)
@pubsub_topics ~w(trigger:lastfm trigger:np)
defstruct dets: nil
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [type: __MODULE__]
for t <- @pubsub_topics, do: {:ok, _} = Registry.register(IRC.PubSub, t, type: __MODULE__)
dets_filename = (Nola.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [])
{:ok, %__MODULE__{dets: dets}}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :plus, args: [username]}}}, state) do
username = String.strip(username)
:ok = :dets.insert(state.dets, {message.account.id, username})
message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm configuré: \"#{username}\".")
{:noreply, state}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :minus, args: []}}}, state) do
text = case :dets.lookup(state.dets, message.account.id) do
[{_nick, _username}] ->
:dets.delete(state.dets, message.account.id)
message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm enlevé.")
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :query, args: []}}}, state) do
text = case :dets.lookup(state.dets, message.account.id) do
[{_nick, username}] ->
message.replyfun.("#{message.sender.nick}: #{username}.")
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: []}}}, state) do
irc_now_playing(message.account.id, message, state)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: [nick_or_user]}}}, state) do
irc_now_playing(nick_or_user, message, state)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :dot}}}, state) do
members = IRC.Membership.members(message.network, message.channel)
foldfun = fn({nick, user}, acc) -> [{nick,user}|acc] end
usernames = :dets.foldl(foldfun, [], state.dets)
|> Enum.uniq()
|> Enum.filter(fn({acct,_}) -> Enum.member?(members, acct) end)
|> Enum.map(fn({_, u}) -> u end)
for u <- usernames, do: irc_now_playing(u, message, state)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
def terminate(_reason, state) do
if state.dets do
:dets.sync(state.dets)
:dets.close(state.dets)
end
:ok
end
defp irc_now_playing(nick_or_user, message, state) do
nick_or_user = String.strip(nick_or_user)
id_or_user = if account = IRC.Account.get(nick_or_user) || IRC.Account.find_always_by_nick(message.network, message.channel, nick_or_user) do
account.id
else
nick_or_user
end
username = case :dets.lookup(state.dets, id_or_user) do
[{_, username}] -> username
_ -> id_or_user
end
case now_playing(username) do
{:error, text} when is_binary(text) ->
message.replyfun.(text)
{:ok, map} when is_map(map) ->
track = fetch_track(username, map)
text = format_now_playing(map, track)
user = if account = IRC.Account.get(id_or_user) do
user = IRC.UserTrack.find_by_account(message.network, account)
if(user, do: user.nick, else: account.name)
else
username
end
if user && text do
message.replyfun.("#{user} #{text}")
else
message.replyfun.("#{username}: pas de résultat")
end
other ->
message.replyfun.("erreur :(")
end
end
defp now_playing(user) do
- api = Application.get_env(:lsg, :lastfm)[:api_key]
+ api = Application.get_env(:nola, :lastfm)[:api_key]
url = "http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&extended=1" <> "&api_key=" <> api <> "&user="<> user
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body)
{:ok, %HTTPoison.Response{status_code: 404}} -> {:error, "last.fm: utilisateur \"#{user}\" inexistant"}
{:ok, %HTTPoison.Response{status_code: code}} -> {:error, "last.fm: erreur #{to_string(code)}"}
error ->
Logger.error "Lastfm http error: #{inspect error}"
:error
end
end
defp fetch_track(user, %{"recenttracks" => %{"track" => [ t = %{"name" => name, "artist" => %{"name" => artist}} | _]}}) do
- api = Application.get_env(:lsg, :lastfm)[:api_key]
+ api = Application.get_env(:nola, :lastfm)[:api_key]
url = "http://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json" <> "&api_key=" <> api <> "&username="<> user <> "&artist="<>URI.encode(artist)<>"&track="<>URI.encode(name)
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
case Jason.decode(body) do
{:ok, body} -> body["track"] || %{}
_ -> %{}
end
error ->
Logger.error "Lastfm http error: #{inspect error}"
:error
end
end
defp format_now_playing(%{"recenttracks" => %{"track" => [track = %{"@attr" => %{"nowplaying" => "true"}} | _]}}, et) do
format_track(true, track, et)
end
defp format_now_playing(%{"recenttracks" => %{"track" => [track | _]}}, et) do
format_track(false, track, et)
end
defp format_now_playing(%{"error" => err, "message" => message}, _) do
"last.fm error #{err}: #{message}"
end
defp format_now_playing(miss) do
nil
end
defp format_track(np, track, extended) do
artist = track["artist"]["name"]
album = if track["album"]["#text"], do: " (" <> track["album"]["#text"] <> ")", else: ""
name = track["name"] <> album
action = if np, do: "écoute ", else: "a écouté"
love = if track["loved"] != "0", do: "❤️"
count = if x = extended["userplaycount"], do: "x#{x} #{love}"
tags = (get_in(extended, ["toptags", "tag"]) || [])
|> Enum.map(fn(tag) -> tag["name"] end)
|> Enum.filter(& &1)
|> Enum.join(", ")
[action, artist, name, count, tags, track["url"]]
|> Enum.filter(& &1)
|> Enum.map(&String.trim(&1))
|> Enum.join(" - ")
end
end
diff --git a/lib/lsg_irc/link_plugin.ex b/lib/lsg_irc/link_plugin.ex
index 28e537a..dee78e8 100644
--- a/lib/lsg_irc/link_plugin.ex
+++ b/lib/lsg_irc/link_plugin.ex
@@ -1,271 +1,271 @@
defmodule Nola.IRC.LinkPlugin do
@moduledoc """
# Link Previewer
An extensible link previewer for IRC.
To extend the supported sites, create a new handler implementing the callbacks.
See `link_plugin/` directory for examples. The first in list handler that returns true to the `match/2` callback will be used,
and if the handler returns `:error` or crashes, will fallback to the default preview.
Unsupported websites will use the default link preview method, which is for html document the title, otherwise it'll use
the mimetype and size.
## Configuration:
```
- config :lsg, Nola.IRC.LinkPlugin,
+ config :nola, Nola.IRC.LinkPlugin,
handlers: [
Nola.IRC.LinkPlugin.Youtube: [
invidious: true
],
Nola.IRC.LinkPlugin.Twitter: [],
Nola.IRC.LinkPlugin.Imgur: [],
]
```
"""
@ircdoc """
# Link preview
Previews links (just post a link!).
Announces real URL after redirections and provides extended support for YouTube, Twitter and Imgur.
"""
def short_irc_doc, do: false
def irc_doc, do: @ircdoc
require Logger
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@callback match(uri :: URI.t, options :: Keyword.t) :: {true, params :: Map.t} | false
@callback expand(uri :: URI.t, params :: Map.t, options :: Keyword.t) :: {:ok, lines :: [] | String.t} | :error
@callback post_match(uri :: URI.t, content_type :: binary, headers :: [], opts :: Keyword.t) :: {:body | :file, params :: Map.t} | false
@callback post_expand(uri :: URI.t, body :: binary() | Path.t, params :: Map.t, options :: Keyword.t) :: {:ok, lines :: [] | String.t} | :error
@optional_callbacks [expand: 3, post_expand: 4]
defstruct [:client]
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "messages", [plugin: __MODULE__])
#{:ok, _} = Registry.register(IRC.PubSub, "messages:telegram", [plugin: __MODULE__])
Logger.info("Link handler started")
{:ok, %__MODULE__{}}
end
def handle_info({:irc, :text, message = %{text: text}}, state) do
String.split(text)
|> Enum.map(fn(word) ->
if String.starts_with?(word, "http://") || String.starts_with?(word, "https://") do
uri = URI.parse(word)
if uri.scheme && uri.host do
spawn(fn() ->
:timer.kill_after(:timer.seconds(30))
case expand_link([uri]) do
{:ok, uris, text} ->
text = case uris do
[uri] -> text
[luri | _] ->
if luri.host == uri.host && luri.path == luri.path do
text
else
["-> #{URI.to_string(luri)}", text]
end
end
if is_list(text) do
for line <- text, do: message.replyfun.(line)
else
message.replyfun.(text)
end
_ -> nil
end
end)
end
end
end)
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
def terminate(_reason, state) do
:ok
end
# 1. Match the first valid handler
# 2. Try to run the handler
# 3. If :error or crash, default link.
# If :skip, nothing
# 4. ?
# Over five redirections: cancel.
def expand_link(acc = [_, _, _, _, _ | _]) do
{:ok, acc, "link redirects more than five times"}
end
def expand_link(acc=[uri | _]) do
Logger.debug("link: expanding: #{inspect uri}")
- handlers = Keyword.get(Application.get_env(:lsg, __MODULE__, [handlers: []]), :handlers)
+ handlers = Keyword.get(Application.get_env(:nola, __MODULE__, [handlers: []]), :handlers)
handler = Enum.reduce_while(handlers, nil, fn({module, opts}, acc) ->
Logger.debug("link: attempt expanding: #{inspect module} for #{inspect uri}")
module = Module.concat([module])
case module.match(uri, opts) do
{true, params} -> {:halt, {module, params, opts}}
false -> {:cont, acc}
end
end)
run_expand(acc, handler)
end
def run_expand(acc, nil) do
expand_default(acc)
end
def run_expand(acc=[uri|_], {module, params, opts}) do
Logger.debug("link: expanding #{inspect uri} with #{inspect module}")
case module.expand(uri, params, opts) do
{:ok, data} -> {:ok, acc, data}
:error -> expand_default(acc)
:skip -> nil
end
rescue
e ->
Logger.error("link: rescued #{inspect uri} with #{inspect module}: #{inspect e}")
Logger.error(Exception.format(:error, e, __STACKTRACE__))
expand_default(acc)
catch
e, b ->
Logger.error("link: catched #{inspect uri} with #{inspect module}: #{inspect {e, b}}")
expand_default(acc)
end
defp get(url, headers \\ [], options \\ []) do
get_req(url, :hackney.get(url, headers, <<>>, options))
end
defp get_req(_, {:error, reason}) do
{:error, reason}
end
defp get_req(url, {:ok, 200, headers, client}) do
headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
Map.put(acc, String.downcase(key), value)
end)
content_type = Map.get(headers, "content-type", "application/octect-stream")
length = Map.get(headers, "content-length", "0")
{length, _} = Integer.parse(length)
- handlers = Keyword.get(Application.get_env(:lsg, __MODULE__, [handlers: []]), :handlers)
+ handlers = Keyword.get(Application.get_env(:nola, __MODULE__, [handlers: []]), :handlers)
handler = Enum.reduce_while(handlers, false, fn({module, opts}, acc) ->
module = Module.concat([module])
try do
case module.post_match(url, content_type, headers, opts) do
{mode, params} when mode in [:body, :file] -> {:halt, {module, params, opts, mode}}
false -> {:cont, acc}
end
rescue
e ->
Logger.error(inspect(e))
{:cont, false}
catch
e, b ->
Logger.error(inspect({b}))
{:cont, false}
end
end)
cond do
handler != false and length <= 30_000_000 ->
case get_body(url, 30_000_000, client, handler, <<>>) do
{:ok, _} = ok -> ok
:error ->
{:ok, "file: #{content_type}, size: #{human_size(length)}"}
end
#String.starts_with?(content_type, "text/html") && length <= 30_000_000 ->
# get_body(url, 30_000_000, client, <<>>)
true ->
:hackney.close(client)
{:ok, "file: #{content_type}, size: #{human_size(length)}"}
end
end
defp get_req(_, {:ok, redirect, headers, client}) when redirect in 300..399 do
headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
Map.put(acc, String.downcase(key), value)
end)
location = Map.get(headers, "location")
:hackney.close(client)
{:redirect, location}
end
defp get_req(_, {:ok, status, headers, client}) do
:hackney.close(client)
{:error, status, headers}
end
defp get_body(url, len, client, {handler, params, opts, mode} = h, acc) when len >= byte_size(acc) do
case :hackney.stream_body(client) do
{:ok, data} ->
get_body(url, len, client, h, << acc::binary, data::binary >>)
:done ->
body = case mode do
:body -> acc
:file ->
{:ok, tmpfile} = Plug.Upload.random_file("linkplugin")
File.write!(tmpfile, acc)
tmpfile
end
handler.post_expand(url, body, params, opts)
{:error, reason} ->
{:ok, "failed to fetch body: #{inspect reason}"}
end
end
defp get_body(_, len, client, h, _acc) do
:hackney.close(client)
IO.inspect(h)
{:ok, "Error: file over 30"}
end
def expand_default(acc = [uri = %URI{scheme: scheme} | _]) when scheme in ["http", "https"] do
Logger.debug("link: expanding #{uri} with default")
headers = [{"user-agent", "DmzBot (like TwitterBot)"}]
options = [follow_redirect: false, max_body_length: 30_000_000]
case get(URI.to_string(uri), headers, options) do
{:ok, text} ->
{:ok, acc, text}
{:redirect, link} ->
new_uri = URI.parse(link)
#new_uri = %URI{new_uri | scheme: scheme, authority: uri.authority, host: uri.host, port: uri.port}
expand_link([new_uri | acc])
{:error, status, _headers} ->
text = Plug.Conn.Status.reason_phrase(status)
{:ok, acc, "Error: HTTP #{text} (#{status})"}
{:error, {:tls_alert, {:handshake_failure, err}}} ->
{:ok, acc, "TLS Error: #{to_string(err)}"}
{:error, reason} ->
{:ok, acc, "Error: #{to_string(reason)}"}
end
end
# Unsupported scheme, came from a redirect.
def expand_default(acc = [uri | _]) do
{:ok, [uri], "-> #{URI.to_string(uri)}"}
end
defp human_size(bytes) do
bytes
|> FileSize.new(:b)
|> FileSize.scale()
|> FileSize.format()
end
end
diff --git a/lib/lsg_irc/link_plugin/imgur.ex b/lib/lsg_irc/link_plugin/imgur.ex
index af0b36a..5d74956 100644
--- a/lib/lsg_irc/link_plugin/imgur.ex
+++ b/lib/lsg_irc/link_plugin/imgur.ex
@@ -1,96 +1,96 @@
defmodule Nola.IRC.LinkPlugin.Imgur do
@behaviour Nola.IRC.LinkPlugin
@moduledoc """
# Imgur link preview
No options.
Needs to have a Imgur API key configured:
```
- config :lsg, :imgur,
+ config :nola, :imgur,
client_id: "xxxxxxxx",
client_secret: "xxxxxxxxxxxxxxxxxxxx"
```
"""
@impl true
def match(uri = %URI{host: "imgur.io"}, arg) do
match(%URI{uri | host: "imgur.com"}, arg)
end
def match(uri = %URI{host: "i.imgur.io"}, arg) do
match(%URI{uri | host: "i.imgur.com"}, arg)
end
def match(uri = %URI{host: "imgur.com", path: "/a/"<>album_id}, _) do
{true, %{album_id: album_id}}
end
def match(uri = %URI{host: "imgur.com", path: "/gallery/"<>album_id}, _) do
{true, %{album_id: album_id}}
end
def match(uri = %URI{host: "i.imgur.com", path: "/"<>image}, _) do
[hash, _] = String.split(image, ".", parts: 2)
{true, %{image_id: hash}}
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
def expand(_uri, %{album_id: album_id}, opts) do
expand_imgur_album(album_id, opts)
end
def expand(_uri, %{image_id: image_id}, opts) do
expand_imgur_image(image_id, opts)
end
def expand_imgur_image(image_id, opts) do
- client_id = Keyword.get(Application.get_env(:lsg, :imgur, []), :client_id, "42")
+ client_id = Keyword.get(Application.get_env(:nola, :imgur, []), :client_id, "42")
headers = [{"Authorization", "Client-ID #{client_id}"}]
options = []
case HTTPoison.get("https://api.imgur.com/3/image/#{image_id}", headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
data = json["data"]
title = String.slice(data["title"] || data["description"], 0, 180)
nsfw = if data["nsfw"], do: "(NSFW) - ", else: " "
height = Map.get(data, "height")
width = Map.get(data, "width")
size = Map.get(data, "size")
{:ok, "image, #{width}x#{height}, #{size} bytes #{nsfw}#{title}"}
other ->
:error
end
end
def expand_imgur_album(album_id, opts) do
- client_id = Keyword.get(Application.get_env(:lsg, :imgur, []), :client_id, "42")
+ client_id = Keyword.get(Application.get_env(:nola, :imgur, []), :client_id, "42")
headers = [{"Authorization", "Client-ID #{client_id}"}]
options = []
case HTTPoison.get("https://api.imgur.com/3/album/#{album_id}", headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
data = json["data"]
title = data["title"]
nsfw = data["nsfw"]
nsfw = if nsfw, do: "(NSFW) - ", else: ""
if data["images_count"] == 1 do
[image] = data["images"]
title = if title || data["title"] do
title = [title, data["title"]] |> Enum.filter(fn(x) -> x end) |> Enum.uniq() |> Enum.join(" — ")
"#{title} — "
else
""
end
{:ok, "#{nsfw}#{title}#{image["link"]}"}
else
title = if title, do: title, else: "Untitled album"
{:ok, "#{nsfw}#{title} - #{data["images_count"]} images"}
end
other ->
:error
end
end
end
diff --git a/lib/lsg_irc/link_plugin/youtube.ex b/lib/lsg_irc/link_plugin/youtube.ex
index 1e3a0de..f7c7541 100644
--- a/lib/lsg_irc/link_plugin/youtube.ex
+++ b/lib/lsg_irc/link_plugin/youtube.ex
@@ -1,72 +1,72 @@
defmodule Nola.IRC.LinkPlugin.YouTube do
@behaviour Nola.IRC.LinkPlugin
@moduledoc """
# YouTube link preview
needs an API key:
```
- config :lsg, :youtube,
+ config :nola, :youtube,
api_key: "xxxxxxxxxxxxx"
```
options:
* `invidious`: Add a link to invidious.
"""
@impl true
def match(uri = %URI{host: yt, path: "/watch", query: "v="<>video_id}, _opts) when yt in ["youtube.com", "www.youtube.com"] do
{true, %{video_id: video_id}}
end
def match(%URI{host: "youtu.be", path: "/"<>video_id}, _opts) do
{true, %{video_id: video_id}}
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
@impl true
def expand(uri, %{video_id: video_id}, opts) do
- key = Application.get_env(:lsg, :youtube)[:api_key]
+ key = Application.get_env(:nola, :youtube)[:api_key]
params = %{
"part" => "snippet,contentDetails,statistics",
"id" => video_id,
"key" => key
}
headers = []
options = [params: params]
case HTTPoison.get("https://www.googleapis.com/youtube/v3/videos", [], options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
case Jason.decode(body) do
{:ok, json} ->
item = List.first(json["items"])
if item do
snippet = item["snippet"]
duration = item["contentDetails"]["duration"] |> String.replace("PT", "") |> String.downcase
date = snippet["publishedAt"]
|> DateTime.from_iso8601()
|> elem(1)
|> Timex.format("{relative}", :relative)
|> elem(1)
line = if host = Keyword.get(opts, :invidious) do
["-> https://#{host}/watch?v=#{video_id}"]
else
[]
end
{:ok, line ++ ["#{snippet["title"]}", "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}"
<> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes"]}
else
:error
end
_ -> :error
end
end
end
end
diff --git a/lib/lsg_irc/preums_plugin.ex b/lib/lsg_irc/preums_plugin.ex
index 9344ce9..f250e85 100644
--- a/lib/lsg_irc/preums_plugin.ex
+++ b/lib/lsg_irc/preums_plugin.ex
@@ -1,276 +1,276 @@
defmodule Nola.IRC.PreumsPlugin do
@moduledoc """
# preums !!!
* `!preums`: affiche le preums du jour
* `.preums`: stats des preums
"""
# WIP Scores
# L'idée c'est de donner un score pour mettre un peu de challenge en pénalisant les preums faciles.
#
# Un preums ne vaut pas 1 point, mais plutôt 0.10 ou 0.05, et on arrondi au plus proche. C'est un jeu sur le long
# terme. Un gros bonus pourrait apporter beaucoup de points.
#
# Il faudrait ces données:
# - moyenne des preums
# - activité récente du channel et par nb actifs d'utilisateurs
# (aggréger memberships+usertrack last_active ?)
# (faire des stats d'activité habituelle (un peu a la pisg) ?)
# - preums consécutifs
#
# Malus:
# - est proche de la moyenne en faible activité
# - trop consécutif de l'utilisateur sauf si activité
#
# Bonus:
# - plus le preums est éloigné de la moyenne
# - après 18h double
# - plus l'activité est élévée, exponentiel selon la moyenne
# - derns entre 4 et 6 (pourrait être adapté selon les stats d'activité)
#
# WIP Badges:
# - derns
# - streaks
# - faciles
# - ?
require Logger
@perfects [~r/preum(s|)/i]
# dets {{chan, day = {yyyy, mm, dd}}, nick, now, perfect?, text}
def all(dets) do
:dets.foldl(fn(i, acc) -> [i|acc] end, [], dets)
end
def all(dets, channel) do
fun = fn({{chan, date}, account_id, time, perfect, text}, acc) ->
if channel == chan do
[%{date: date, account_id: account_id, time: time, perfect: perfect, text: text} | acc]
else
acc
end
end
:dets.foldl(fun, [], dets)
end
def topnicks(dets, channel, options \\ []) do
sort_elem = case Keyword.get(options, :sort_by, :score) do
:score -> 1
:count -> 0
end
fun = fn(x = {{chan, date}, account_id, time, perfect, text}, acc) ->
if (channel == nil and chan) or (channel == chan) do
{count, points} = Map.get(acc, account_id, {0, 0})
score = score(chan, account_id, time, perfect, text)
Map.put(acc, account_id, {count + 1, points + score})
else
acc
end
end
:dets.foldl(fun, %{}, dets)
|> Enum.sort_by(fn({_account_id, value}) -> elem(value, sort_elem) end, &>=/2)
end
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def dets do
(Nola.data_path() <> "/preums.dets") |> String.to_charlist()
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "account", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts)
{:ok, dets} = :dets.open_file(dets(), [{:repair, :force}])
Util.ets_mutate_select_each(:dets, dets, [{:"$1", [], [:"$1"]}], fn(table, obj) ->
{key, nick, now, perfect, text} = obj
case key do
{{net, {bork,chan}}, date} ->
:dets.delete(table, key)
nick = if IRC.Account.get(nick) do
nick
else
if acct = IRC.Account.find_always_by_nick(net, nil, nick) do
acct.id
else
nick
end
end
:dets.insert(table, { { {net,chan}, date }, nick, now, perfect, text})
{{_net, nil}, _} ->
:dets.delete(table, key)
{{net, chan}, date} ->
if !IRC.Account.get(nick) do
if acct = IRC.Account.find_always_by_nick(net, chan, nick) do
:dets.delete(table, key)
:dets.insert(table, { { {net,chan}, date }, acct.id, now, perfect, text})
end
end
_ ->
Logger.debug("DID NOT FIX: #{inspect key}")
end
end)
{:ok, %{dets: dets}}
end
# Latest
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do
channelkey = {m.network, m.channel}
state = handle_preums(m, state)
tz = timezone(channelkey)
{:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
date = {now.year, now.month, now.day}
key = {channelkey, date}
chan_cache = Map.get(state, channelkey, %{})
item = if i = Map.get(chan_cache, date) do
i
else
case :dets.lookup(state.dets, key) do
[item = {^key, _account_id, _now, _perfect, _text}] -> item
_ -> nil
end
end
if item do
{_, account_id, date, _perfect, text} = item
h = "#{date.hour}:#{date.minute}:#{date.second}"
account = IRC.Account.get(account_id)
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
m.replyfun.("preums: #{nick} à #{h}: “#{text}”")
end
{:noreply, state}
end
# Stats
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :dot}}}, state) do
channel = {m.network, m.channel}
state = handle_preums(m, state)
top = topnicks(state.dets, channel, sort_by: :score)
|> Enum.map(fn({account_id, {count, score}}) ->
account = IRC.Account.get(account_id)
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
"#{nick}: #{score} (#{count})"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
msg = unless top == "" do
"top preums: #{top}"
else
"vous êtes tous nuls"
end
m.replyfun.(msg)
{:noreply, state}
end
# Help
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :query}}}, state) do
state = handle_preums(m, state)
msg = "!preums - preums du jour, .preums top preumseurs"
m.replymsg.(msg)
{:noreply, state}
end
# Trigger fallback
def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
state = handle_preums(m, state)
{:noreply, state}
end
# Message fallback
def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
{:noreply, handle_preums(m, state)}
end
# Account
def handle_info({:account_change, old_id, new_id}, state) do
spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
rename_object_owner(table, obj, new_id)
end)
{:noreply, state}
end
# Account: move from nick to account id
# FIXME: Doesn't seem to work.
def handle_info({:accounts, accounts}, state) do
for x={:account, _net, _chan, _nick, _account_id} <- accounts do
handle_info(x, state)
end
{:noreply, state}
end
def handle_info({:account, _net, _chan, nick, account_id}, state) do
nick = String.downcase(nick)
spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("account:: merging #{nick} -> #{account_id}")
rename_object_owner(table, obj, account_id)
end)
{:noreply, state}
end
def handle_info(_, dets) do
{:noreply, dets}
end
defp rename_object_owner(table, object = {key, _, now, perfect, time}, new_id) do
:dets.delete_object(table, key)
:dets.insert(table, {key, new_id, now, perfect, time})
end
defp timezone(channel) do
- env = Application.get_env(:lsg, Nola.IRC.PreumsPlugin, [])
+ env = Application.get_env(:nola, Nola.IRC.PreumsPlugin, [])
channels = Keyword.get(env, :channels, %{})
channel_settings = Map.get(channels, channel, [])
default = Keyword.get(env, :default_tz, "Europe/Paris")
Keyword.get(channel_settings, :tz, default) || default
end
defp handle_preums(%IRC.Message{channel: nil}, state) do
state
end
defp handle_preums(m = %IRC.Message{text: text, sender: sender}, state) do
channel = {m.network, m.channel}
tz = timezone(channel)
{:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
date = {now.year, now.month, now.day}
key = {channel, date}
chan_cache = Map.get(state, channel, %{})
unless i = Map.get(chan_cache, date) do
case :dets.lookup(state.dets, key) do
[item = {^key, _nick, _now, _perfect, _text}] ->
# Preums lost, but wasn't cached
Map.put(state, channel, %{date => item})
[] ->
# Preums won!
perfect? = Enum.any?(@perfects, fn(perfect) -> Regex.match?(perfect, text) end)
item = {key, m.account.id, now, perfect?, text}
:dets.insert(state.dets, item)
:dets.sync(state.dets)
Map.put(state, channel, %{date => item})
{:error, _} = error ->
Logger.error("#{__MODULE__} dets lookup failed: #{inspect error}")
state
end
else
state
end
end
def score(_chan, _account, _time, _perfect, _text) do
1
end
end
diff --git a/lib/lsg_irc/sms_plugin.ex b/lib/lsg_irc/sms_plugin.ex
index 2bbf13e..d8f7387 100644
--- a/lib/lsg_irc/sms_plugin.ex
+++ b/lib/lsg_irc/sms_plugin.ex
@@ -1,165 +1,165 @@
defmodule Nola.IRC.SmsPlugin do
@moduledoc """
## sms
* **!sms `<nick>` `<message>`** envoie un SMS.
"""
def short_irc_doc, do: false
def irc_doc, do: @moduledoc
require Logger
def incoming(from, "enable "<>key) do
key = String.trim(key)
account = IRC.Account.find_meta_account("sms-validation-code", String.downcase(key))
if account do
net = IRC.Account.get_meta(account, "sms-validation-target")
IRC.Account.put_meta(account, "sms-number", from)
IRC.Account.delete_meta(account, "sms-validation-code")
IRC.Account.delete_meta(account, "sms-validation-number")
IRC.Account.delete_meta(account, "sms-validation-target")
IRC.Connection.broadcast_message(net, account, "SMS Number #{from} added!")
send_sms(from, "Yay! Number linked to account #{account.name}")
end
end
def incoming(from, message) do
account = IRC.Account.find_meta_account("sms-number", from)
if account do
reply_fun = fn(text) ->
send_sms(from, text)
end
trigger_text = if Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(message, trigger) end) do
message
else
"!"<>message
end
message = %IRC.Message{
id: FlakeId.get(),
transport: :sms,
network: "sms",
channel: nil,
text: message,
account: account,
sender: %ExIRC.SenderInfo{nick: account.name},
replyfun: reply_fun,
trigger: IRC.Connection.extract_trigger(trigger_text)
}
Logger.debug("converted sms to message: #{inspect message}")
IRC.Connection.publish(message, ["messages:sms"])
message
end
end
def my_number() do
- Keyword.get(Application.get_env(:lsg, :sms, []), :number, "+33000000000")
+ Keyword.get(Application.get_env(:nola, :sms, []), :number, "+33000000000")
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def path() do
- account = Keyword.get(Application.get_env(:lsg, :sms), :account)
+ account = Keyword.get(Application.get_env(:nola, :sms), :account)
"https://eu.api.ovh.com/1.0/sms/#{account}"
end
def path(rest) do
Path.join(path(), rest)
end
def send_sms(number, text) do
url = path("/virtualNumbers/#{my_number()}/jobs")
body = %{
"message" => text,
"receivers" => [number],
#"senderForResponse" => true,
#"noStopClause" => true,
"charset" => "UTF-8",
"coding" => "8bit"
} |> Poison.encode!()
headers = [{"content-type", "application/json"}] ++ sign("POST", url, body)
options = []
case HTTPoison.post(url, body, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200}} -> :ok
{:ok, %HTTPoison.Response{status_code: code} = resp} ->
Logger.error("SMS Error: #{inspect resp}")
{:error, code}
{:error, error} -> {:error, error}
end
end
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:sms", [plugin: __MODULE__])
:ok = register_ovh_callback()
{:ok, %{}}
:ignore
end
def handle_info({:irc, :trigger, "sms", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick | text]}}}, state) do
with \
{:tree, false} <- {:tree, m.sender.nick == "Tree"},
{_, %IRC.Account{} = account} <- {:account, IRC.Account.find_always_by_nick(m.network, m.channel, nick)},
{_, number} when not is_nil(number) <- {:number, IRC.Account.get_meta(account, "sms-number")}
do
text = Enum.join(text, " ")
sender = if m.channel do
"#{m.channel} <#{m.sender.nick}> "
else
"<#{m.sender.nick}> "
end
case send_sms(number, sender<>text) do
:ok -> m.replyfun.("sent!")
{:error, error} -> m.replyfun.("not sent, error: #{inspect error}")
end
else
{:tree, _} -> m.replyfun.("Tree: va en enfer")
{:account, _} -> m.replyfun.("#{nick} not known")
{:number, _} -> m.replyfun.("#{nick} have not enabled sms")
end
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
defp register_ovh_callback() do
url = path()
body = %{
"callBack" =>NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback),
"smsResponse" => %{
"cgiUrl" => NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback),
"responseType" => "cgi"
}
} |> Poison.encode!()
headers = [{"content-type", "application/json"}] ++ sign("PUT", url, body)
options = []
case HTTPoison.put(url, body, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200}} ->
:ok
error -> error
end
end
defp sign(method, url, body) do
ts = DateTime.utc_now() |> DateTime.to_unix()
as = env(:app_secret)
ck = env(:consumer_key)
sign = Enum.join([as, ck, String.upcase(method), url, body, ts], "+")
sign_hex = :crypto.hash(:sha, sign) |> Base.encode16(case: :lower)
headers = [{"X-OVH-Application", env(:app_key)}, {"X-OVH-Timestamp", ts},
{"X-OVH-Signature", "$1$"<>sign_hex}, {"X-Ovh-Consumer", ck}]
end
def parse_number(num) do
{:error, :todo}
end
defp env() do
- Application.get_env(:lsg, :sms)
+ Application.get_env(:nola, :sms)
end
defp env(key) do
Keyword.get(env(), key)
end
end
diff --git a/lib/lsg_irc/txt_plugin.ex b/lib/lsg_irc/txt_plugin.ex
index d2bb627..cab912a 100644
--- a/lib/lsg_irc/txt_plugin.ex
+++ b/lib/lsg_irc/txt_plugin.ex
@@ -1,556 +1,556 @@
defmodule Nola.IRC.TxtPlugin do
alias IRC.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(:lsg, __MODULE__, []), :markov_handler, Nola.IRC.TxtPlugin.Markov.Native)
+ markov_handler = Keyword.get(Application.get_env(:nola, __MODULE__, []), :markov_handler, Nola.IRC.TxtPlugin.Markov.Native)
{:ok, markov} = markov_handler.start_link()
{:ok, _} = Registry.register(IRC.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(:lsg, :data_path) <> "/irc.txt/"
+ 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)
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)
operator? = IRC.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/lsg_irc/txt_plugin/markov_py_markovify.ex b/lib/lsg_irc/txt_plugin/markov_py_markovify.ex
index cda7853..b610ea8 100644
--- a/lib/lsg_irc/txt_plugin/markov_py_markovify.ex
+++ b/lib/lsg_irc/txt_plugin/markov_py_markovify.ex
@@ -1,39 +1,39 @@
defmodule Nola.IRC.TxtPlugin.MarkovPyMarkovify do
def start_link() do
{:ok, nil}
end
def reload(_data, _markov) do
:ok
end
def sentence(_) do
{:ok, run()}
end
def complete_sentence(sentence, _) do
{:ok, run([sentence])}
end
defp run(args \\ []) do
{binary, script} = script()
args = [script, Path.expand(Nola.IRC.TxtPlugin.directory()) | args]
IO.puts "Args #{inspect args}"
case MuonTrap.cmd(binary, args) do
{response, 0} -> response
{response, code} -> "error #{code}: #{response}"
end
end
defp script() do
- default_script = to_string(:code.priv_dir(:lsg)) <> "/irc/txt/markovify.py"
- env = Application.get_env(:lsg, Nola.IRC.TxtPlugin, [])
+ default_script = to_string(:code.priv_dir(:nola)) <> "/irc/txt/markovify.py"
+ env = Application.get_env(:nola, Nola.IRC.TxtPlugin, [])
|> Keyword.get(:py_markovify, [])
{Keyword.get(env, :python, "python3"), Keyword.get(env, :script, default_script)}
end
end
diff --git a/lib/lsg_irc/wolfram_alpha_plugin.ex b/lib/lsg_irc/wolfram_alpha_plugin.ex
index b553e63..6ee06f0 100644
--- a/lib/lsg_irc/wolfram_alpha_plugin.ex
+++ b/lib/lsg_irc/wolfram_alpha_plugin.ex
@@ -1,47 +1,47 @@
defmodule Nola.IRC.WolframAlphaPlugin do
use GenServer
require Logger
@moduledoc """
# wolfram alpha
* **`!wa <requête>`** lance `<requête>` sur WolframAlpha
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:wa", [plugin: __MODULE__])
{:ok, nil}
end
def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: query}}}, state) do
query = Enum.join(query, " ")
params = %{
- "appid" => Keyword.get(Application.get_env(:lsg, :wolframalpha, []), :app_id, "NO_APP_ID"),
+ "appid" => Keyword.get(Application.get_env(:nola, :wolframalpha, []), :app_id, "NO_APP_ID"),
"units" => "metric",
"i" => query
}
url = "https://www.wolframalpha.com/input/?i=" <> URI.encode(query)
case HTTPoison.get("http://api.wolframalpha.com/v1/result", [], [params: params]) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
m.replyfun.(["#{query} -> #{body}", url])
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
error = case {code, body} do
{501, b} -> "input invalide: #{body}"
{code, error} -> "erreur #{code}: #{body || ""}"
end
m.replyfun.("wa: #{error}")
{:error, %HTTPoison.Error{reason: reason}} ->
m.replyfun.("wa: erreur http: #{to_string(reason)}")
_ ->
m.replyfun.("wa: erreur http")
end
{:noreply, state}
end
end
diff --git a/lib/lsg_irc/youtube_plugin.ex b/lib/lsg_irc/youtube_plugin.ex
index 3d2acfb..fb9bea2 100644
--- a/lib/lsg_irc/youtube_plugin.ex
+++ b/lib/lsg_irc/youtube_plugin.ex
@@ -1,104 +1,104 @@
defmodule Nola.IRC.YouTubePlugin do
require Logger
@moduledoc """
# youtube
* **!yt `<recherche>`**, !youtube `<recherche>`: retourne le premier résultat de la `<recherche>` YouTube
"""
defstruct client: nil
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
for t <- ["trigger:yt", "trigger:youtube"], do: {:ok, _} = Registry.register(IRC.PubSub, t, [plugin: __MODULE__])
{:ok, %__MODULE__{}}
end
def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
irc_search(Enum.join(args, " "), message)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
defp irc_search(query, message) do
case search(query) do
{:ok, %{"items" => [item | _]}} ->
url = "https://youtube.com/watch?v=" <> item["id"]
snippet = item["snippet"]
duration = item["contentDetails"]["duration"] |> String.replace("PT", "") |> String.downcase
date = snippet["publishedAt"]
|> DateTime.from_iso8601()
|> elem(1)
|> Timex.format("{relative}", :relative)
|> elem(1)
info_line = "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}"
<> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes,"
<> " #{item["statistics"]["dislikeCount"]} dislikes"
message.replyfun.("#{snippet["title"]} — #{url}")
message.replyfun.(info_line)
{:error, error} ->
message.replyfun.("Erreur YouTube: "<>error)
_ ->
nil
end
end
defp search(query) do
query = query
|> String.strip
- key = Application.get_env(:lsg, :youtube)[:api_key]
+ key = Application.get_env(:nola, :youtube)[:api_key]
params = %{
"key" => key,
"maxResults" => 1,
"part" => "id",
"safeSearch" => "none",
"type" => "video",
"q" => query,
}
url = "https://www.googleapis.com/youtube/v3/search"
case HTTPoison.get(url, [], params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
item = List.first(json["items"])
if item do
video_id = item["id"]["videoId"]
params = %{
"part" => "snippet,contentDetails,statistics",
"id" => video_id,
"key" => key
}
headers = []
options = [params: params]
case HTTPoison.get("https://www.googleapis.com/youtube/v3/videos", [], options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
Jason.decode(body)
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
Logger.error "YouTube HTTP #{code}: #{inspect body}"
{:error, "http #{code}"}
error ->
Logger.error "YouTube http error: #{inspect error}"
:error
end
else
:error
end
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
Logger.error "YouTube HTTP #{code}: #{inspect body}"
{:error, "http #{code}"}
error ->
Logger.error "YouTube http error: #{inspect error}"
:error
end
end
end
diff --git a/lib/lsg_telegram/room.ex b/lib/lsg_telegram/room.ex
index 794cca3..ca8a437 100644
--- a/lib/lsg_telegram/room.ex
+++ b/lib/lsg_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(:lsg, :telegram, []), :key)
+ 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 IRC.Connection.get_network(net, chan) do
%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(IRC.PubSub, "#{net}/#{chan}:messages", plugin: __MODULE__)
{:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:triggers", plugin: __MODULE__)
{:ok, _} = Registry.register(IRC.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 = IRC.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
|> IRC.Account.new_account()
|> IRC.Account.update_account_name(name)
|> IRC.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 = IRC.Connection.get_network(state.net)
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 = IRC.Connection.get_network(state.net)
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 = %IRC.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(:lsg, :s3, []) |> Keyword.get(:bucket),
+ 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 = IRC.Connection.get_network(state.net)
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/lsg_telegram/telegram.ex b/lib/lsg_telegram/telegram.ex
index ef5c3b8..1c6a9a9 100644
--- a/lib/lsg_telegram/telegram.ex
+++ b/lib/lsg_telegram/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(:lsg, :telegram, []), :key)
+ 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 = IRC.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 = IRC.Account.find_meta_account("telegram-validation-code", String.downcase(key))
text = if account do
net = IRC.Account.get_meta(account, "telegram-validation-target")
IRC.Account.put_meta(account, "telegram-id", m["chat"]["id"])
IRC.Account.put_meta(account, "telegram-username", m["chat"]["username"])
IRC.Account.put_meta(account, "telegram-username", m["chat"]["username"])
IRC.Account.delete_meta(account, "telegram-validation-code")
IRC.Account.delete_meta(account, "telegram-validation-target")
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 = IRC.Account.find_meta_account("telegram-id", chat_id)
if account do
target = case String.split(target, "/") do
["everywhere"] -> IRC.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(:lsg, :s3, []) |> Keyword.get(:bucket),
+ 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)
"#{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 = IRC.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?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(text, trigger) end) ->
text
true ->
"!"<>text
end
message = %IRC.Message{
id: FlakeId.get(),
transport: :telegram,
network: "telegram",
channel: nil,
text: text,
account: account,
sender: %ExIRC.SenderInfo{nick: account.name},
replyfun: reply_fun,
trigger: IRC.Connection.extract_trigger(trigger_text),
at: nil
}
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 = IRC.Account.find_meta_account("telegram-id", id)
if account do
text = if(m["text"], do: m["text"], else: nil)
targets = IRC.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/lsg_web/controllers/irc_controller.ex b/lib/lsg_web/controllers/irc_controller.ex
index 90d9853..c617e78 100644
--- a/lib/lsg_web/controllers/irc_controller.ex
+++ b/lib/lsg_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([IRC.Account.AccountPlugin] ++ IRC.Plugin.enabled()) do
if is_atom(mod) do
identifier = Module.split(mod) |> List.last |> String.replace("Plugin", "") |> Macro.underscore
{identifier, mod.irc_doc()}
end
end
|> Enum.filter(& &1)
|> Enum.filter(fn({_, doc}) -> doc end)
members = cond do
network && channel -> Enum.map(IRC.UserTrack.channel(network, channel), fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end)
true ->
IRC.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.TxtPlugin.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(:lsg, :data_path) <> "/irc.txt/"
+ 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/lsg_web/controllers/open_id_controller.ex b/lib/lsg_web/controllers/open_id_controller.ex
index 94166eb..d3fef5d 100644
--- a/lib/lsg_web/controllers/open_id_controller.ex
+++ b/lib/lsg_web/controllers/open_id_controller.ex
@@ -1,64 +1,64 @@
defmodule NolaWeb.OpenIdController do
use NolaWeb, :controller
plug NolaWeb.ContextPlug, restrict: :public
require Logger
def login(conn, _) do
url = OAuth2.Client.authorize_url!(new_client(), scope: "openid", state: Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false))
redirect(conn, external: url)
end
def callback(conn, %{"error" => error_code, "error_description" => error}) do
Logger.warn("OpenId error: #{error_code} #{error}")
render(conn, "error.html", error: error)
end
def callback(conn, %{"code" => code, "state" => state}) do
with \
client = %{token: %OAuth2.AccessToken{access_token: json}} = OAuth2.Client.get_token!(new_client(), state: state, code: code),
{:ok, %{"access_token" => token}} <- Jason.decode(json),
client = %OAuth2.Client{client | token: %OAuth2.AccessToken{access_token: token}},
{:ok, %OAuth2.Response{body: body}} <- OAuth2.Client.get(client, "/userinfo"),
{:ok, %{"sub" => id, "preferred_username" => username}} <- Jason.decode(body)
do
if account = conn.assigns.account do
if !IRC.Account.get_meta(account, "identity-id") do # XXX: And oidc id not linked yet
IRC.Account.put_meta(account, "identity-id", id)
end
IRC.Account.put_meta(account, "identity-username", username)
conn
else
conn
end
conn
|> put_session(:oidc_id, id)
|> put_flash(:info, "Logged in!")
|> redirect(to: Routes.path(conn, "/"))
else
{:error, %OAuth2.Response{status_code: 401}} ->
Logger.error("OpenID: Unauthorized token")
render(conn, "error.html", error: "The token is invalid.")
{:error, %OAuth2.Error{reason: reason}} ->
Logger.error("Error: #{inspect reason}")
render(conn, "error.html", error: reason)
end
end
def callback(conn, _params) do
render(conn, "error.html", error: "Unspecified error.")
end
defp new_client() do
- config = Application.get_env(:lsg, :oidc)
+ config = Application.get_env(:nola, :oidc)
OAuth2.Client.new([
strategy: OAuth2.Strategy.AuthCode,
client_id: config[:client_id],
client_secret: config[:client_secret],
site: config[:base_url],
authorize_url: config[:authorize_url],
token_url: config[:token_url],
redirect_uri: Routes.open_id_url(NolaWeb.Endpoint, :callback)
])
end
end
diff --git a/lib/lsg_web/endpoint.ex b/lib/lsg_web/endpoint.ex
index d8bf962..b0cf9c5 100644
--- a/lib/lsg_web/endpoint.ex
+++ b/lib/lsg_web/endpoint.ex
@@ -1,62 +1,62 @@
defmodule NolaWeb.Endpoint do
use Sentry.PlugCapture
- use Phoenix.Endpoint, otp_app: :lsg
+ use Phoenix.Endpoint, otp_app: :nola
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug Plug.Static,
- at: "/", from: :lsg, gzip: false,
+ at: "/", from: :nola, gzip: false,
only: ~w(assets css js fonts images favicon.ico robots.txt)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if 42==43 && code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
end
plug Plug.RequestId
plug Plug.Logger
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Jason
plug Sentry.PlugContext
plug Plug.MethodOverride
plug Plug.Head
@session_options [store: :cookie,
key: "_lsg_key",
signing_salt: "+p7K3wrj"]
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [session: @session_options]]
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug Plug.Session, @session_options
plug NolaWeb.Router
@doc """
Callback invoked for dynamically configuring the endpoint.
It receives the endpoint configuration and checks if
configuration should be loaded from the system environment.
"""
def init(_key, config) do
if config[:load_from_system_env] do
port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
else
{:ok, config}
end
end
end
diff --git a/lib/lsg_web/gettext.ex b/lib/lsg_web/gettext.ex
index e9a46e9..a43cb0d 100644
--- a/lib/lsg_web/gettext.ex
+++ b/lib/lsg_web/gettext.ex
@@ -1,24 +1,24 @@
defmodule NolaWeb.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
import NolaWeb.Gettext
# Simple translation
gettext "Here is the string to translate"
# Plural translation
ngettext "Here is the string to translate",
"Here are the strings to translate",
3
# Domain-based translation
dgettext "errors", "Here is the error message to translate"
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
- use Gettext, otp_app: :lsg
+ use Gettext, otp_app: :nola
end
diff --git a/lib/open_ai.ex b/lib/open_ai.ex
index 81f12f4..cc0de27 100644
--- a/lib/open_ai.ex
+++ b/lib/open_ai.ex
@@ -1,20 +1,20 @@
defmodule OpenAi do
def post(path, data, options \\ []) do
- config = Application.get_env(:lsg, :openai, [])
+ config = Application.get_env(:nola, :openai, [])
url = "https://api.openai.com#{path}"
headers = [{"user-agent", "internal private experiment bot, href@random.sh"},
{"content-type", "application/json"},
{"authorization", "Bearer " <> Keyword.get(config, :key, "unset-api-key")}]
options = options ++ [timeout: :timer.seconds(180), recv_timeout: :timer.seconds(180)]
with {:ok, json} <- Poison.encode(data),
{:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.post(url, json, headers, options),
{:ok, data} <- Poison.decode(body) do
{:ok, data}
else
{:ok, %HTTPoison.Response{status_code: code}} -> {:error, Plug.Conn.Status.reason_atom(code)}
{:error, %HTTPoison.Error{reason: reason}} -> {:error, reason}
end
end
end
diff --git a/lib/untappd.ex b/lib/untappd.ex
index d5ac904..7ed3e66 100644
--- a/lib/untappd.ex
+++ b/lib/untappd.ex
@@ -1,94 +1,94 @@
defmodule Untappd do
@env Mix.env
@version Mix.Project.config[:version]
require Logger
def auth_url() do
client_id = Keyword.get(env(), :client_id)
url = NolaWeb.Router.Helpers.untappd_callback_url(NolaWeb.Endpoint, :callback)
"https://untappd.com/oauth/authenticate/?client_id=#{client_id}&response_type=code&redirect_url=#{URI.encode(url)}"
end
def auth_callback(code) do
client_id = Keyword.get(env(), :client_id)
client_secret = Keyword.get(env(), :client_secret)
url = NolaWeb.Router.Helpers.untappd_callback_url(NolaWeb.Endpoint, :callback)
params = %{
"client_id" => client_id,
"client_secret" => client_secret,
"response_type" => code,
"redirect_url" => url,
"code" => code
}
case HTTPoison.get("https://untappd.com/oauth/authorize", headers(), params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
json = Poison.decode!(body)
{:ok, get_in(json, ["response", "access_token"])}
error ->
Logger.error("Untappd auth callback failed: #{inspect error}")
:error
end
end
def maybe_checkin(account, beer_id) do
if token = IRC.Account.get_meta(account, "untappd-token") do
checkin(token, beer_id)
else
{:error, :no_token}
end
end
def checkin(token, beer_id) do
params = get_params(token: token)
|> Map.put("timezone", "CEST")
|> Map.put("bid", beer_id)
form_params = params
|> Enum.into([])
case HTTPoison.post("https://api.untappd.com/v4/checkin/add", {:form, form_params}, headers(), params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
body = Jason.decode!(body)
|> Map.get("response")
{:ok, body}
{:ok, resp = %HTTPoison.Response{status_code: code, body: body}} ->
Logger.warn "Untappd checkin error: #{inspect resp}"
{:error, {:http_error, code}}
{:error, error} -> {:error, {:http_error, error}}
end
end
def search_beer(query, params \\ []) do
params = get_params(params)
|> Map.put("q", query)
|> Map.put("limit", 10)
#|> Map.put("sort", "name")
case HTTPoison.get("https://api.untappd.com/v4/search/beer", headers(), params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Jason.decode!(body)}
error ->
Logger.error("Untappd search error: #{inspect error}")
end
end
def get_params(params) do
auth = %{"client_id" => Keyword.get(env(), :client_id), "client_secret" => Keyword.get(env(), :client_secret)}
if token = Keyword.get(params, :token) do
Map.put(auth, "access_token", token)
else
auth
end
end
def headers(extra \\ []) do
client_id = Keyword.get(env(), :client_id)
extra
++ [
{"user-agent", "dmzbot (#{client_id}; #{@version}-#{@env})"}
]
end
def env() do
- Application.get_env(:lsg, :untappd)
+ Application.get_env(:nola, :untappd)
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jun 8, 1:36 AM (21 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
47551
Default Alt Text
(115 KB)

Event Timeline