Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2018-05-30 14:27:21 +02:00
parent 2f0a29aa86
commit cac4dd3ca3
25 changed files with 669 additions and 46 deletions

View File

@@ -77,14 +77,14 @@ defmodule Eventos.Actors.Actor do
actor
|> Ecto.Changeset.cast(attrs, [:url, :outbox_url, :inbox_url, :following_url, :followers_url, :type, :name, :domain, :summary, :preferred_username, :public_key, :private_key, :manually_approves_followers, :suspended])
|> validate_required([:preferred_username, :public_key, :suspended, :url])
|> unique_constraint(:name, name: :actors_username_domain_index)
|> unique_constraint(:prefered_username, name: :actors_preferred_username_domain_index)
end
def registration_changeset(%Actor{} = actor, attrs) do
actor
|> Ecto.Changeset.cast(attrs, [:name, :domain, :display_name, :description, :private_key, :public_key, :suspended, :url])
|> validate_required([:preferred_username, :public_key, :suspended, :url])
|> unique_constraint(:name)
|> Ecto.Changeset.cast(attrs, [:preferred_username, :domain, :name, :summary, :private_key, :public_key, :suspended, :url, :type])
|> validate_required([:preferred_username, :public_key, :suspended, :url, :type])
|> unique_constraint(:prefered_username, name: :actors_preferred_username_domain_index)
end
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/

View File

@@ -196,7 +196,12 @@ defmodule Eventos.Actors do
end
def get_actor_by_name(name) do
Repo.get_by!(Actor, preferred_username: name)
actor = case String.split(name, "@") do
[name] ->
Repo.get_by(Actor, preferred_username: name)
[name, domain] ->
Repo.get_by(Actor, preferred_username: name, domain: domain)
end
end
def get_local_actor_by_name(name) do
@@ -231,6 +236,44 @@ defmodule Eventos.Actors do
end
end
@doc """
Find local users by it's username
"""
def find_local_by_username(username) do
actors = Repo.all from a in Actor, where: (ilike(a.preferred_username, ^like_sanitize(username)) or ilike(a.name, ^like_sanitize(username))) and is_nil(a.domain)
Repo.preload(actors, :organized_events)
end
@doc """
Find actors by their name or displayed name
"""
def find_actors_by_username(username) do
Repo.all from a in Actor, where: ilike(a.preferred_username, ^like_sanitize(username)) or ilike(a.name, ^like_sanitize(username))
end
@doc """
Sanitize the LIKE queries
"""
defp like_sanitize(value) do
"%" <> String.replace(value, ~r/([\\%_])/, "\\1") <> "%"
end
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
def search(name) do
case find_actors_by_username(name) do # find already saved accounts
[] ->
with true <- Regex.match?(@email_regex, name), # no accounts found, let's test if it's an username@domain.tld
{:ok, actor} <- ActivityPub.find_or_make_actor_from_nickname(name) do # creating the actor in that case
{:ok, [actor]}
else
false -> {:ok, []}
{:error, err} -> {:error, err} # error fingering the actor
end
actors = [_|_] ->
{:ok, actors} # actors already saved found !
end
end
@doc """
Get an user by email
"""
@@ -288,6 +331,32 @@ defmodule Eventos.Actors do
end
end
def register_bot_account(%{name: name, summary: summary}) do
key = :public_key.generate_key({:rsa, 2048, 65537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
{:ok, rsa_priv_key} = ExPublicKey.generate_key()
{:ok, rsa_pub_key} = ExPublicKey.public_key_from_private_key(rsa_priv_key)
actor = Eventos.Actors.Actor.registration_changeset(%Eventos.Actors.Actor{}, %{
preferred_username: name,
domain: nil,
private_key: pem,
public_key: "toto",
url: EventosWeb.Endpoint.url() <> "/@" <> name,
summary: summary,
type: :Service
})
try do
Eventos.Repo.insert!(actor)
rescue
e in Ecto.InvalidChangesetError ->
{:error, e}
end
end
@doc """
Creates a user.
@@ -450,4 +519,105 @@ defmodule Eventos.Actors do
Member.changeset(member, %{})
end
alias Eventos.Actors.Bot
@doc """
Returns the list of bots.
## Examples
iex> list_bots()
[%Bot{}, ...]
"""
def list_bots do
Repo.all(Bot)
end
@doc """
Gets a single bot.
Raises `Ecto.NoResultsError` if the Bot does not exist.
## Examples
iex> get_bot!(123)
%Bot{}
iex> get_bot!(456)
** (Ecto.NoResultsError)
"""
def get_bot!(id), do: Repo.get!(Bot, id)
@spec get_bot_by_actor(Actor.t) :: Bot.t
def get_bot_by_actor(%Actor{} = actor) do
Repo.get_by!(Bot, actor_id: actor.id)
end
@doc """
Creates a bot.
## Examples
iex> create_bot(%{field: value})
{:ok, %Bot{}}
iex> create_bot(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_bot(attrs \\ %{}) do
%Bot{}
|> Bot.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a bot.
## Examples
iex> update_bot(bot, %{field: new_value})
{:ok, %Bot{}}
iex> update_bot(bot, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_bot(%Bot{} = bot, attrs) do
bot
|> Bot.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a Bot.
## Examples
iex> delete_bot(bot)
{:ok, %Bot{}}
iex> delete_bot(bot)
{:error, %Ecto.Changeset{}}
"""
def delete_bot(%Bot{} = bot) do
Repo.delete(bot)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking bot changes.
## Examples
iex> change_bot(bot)
%Ecto.Changeset{source: %Bot{}}
"""
def change_bot(%Bot{} = bot) do
Bot.changeset(bot, %{})
end
end

25
lib/eventos/actors/bot.ex Normal file
View File

@@ -0,0 +1,25 @@
defmodule Eventos.Actors.Bot do
@moduledoc """
Represents a local bot
"""
use Ecto.Schema
import Ecto.Changeset
alias Eventos.Actors.{Actor, User, Bot}
schema "bots" do
field :source, :string
field :type, :string, default: :ics
belongs_to :actor, Actor
belongs_to :user, User
timestamps()
end
@doc false
def changeset(bot, attrs) do
bot
|> cast(attrs, [:source, :type, :actor_id, :user_id])
|> validate_required([:source])
end
end

View File

@@ -34,7 +34,7 @@ defmodule Eventos.Events do
offset: ^start,
preload: [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address]
events = Repo.all(query)
count_events = Repo.one(from e in Event, select: count(e.id))
count_events = Repo.one(from e in Event, select: count(e.id), where: e.organizer_actor_id == ^actor_id)
{:ok, events, count_events}
end
@@ -109,6 +109,21 @@ defmodule Eventos.Events do
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address])
end
@doc """
Find events by name
"""
def find_events_by_name(name) do
events = Repo.all from a in Event, where: ilike(a.title, ^like_sanitize(name))
Repo.preload(events, [:organizer_actor])
end
@doc """
Sanitize the LIKE queries
"""
defp like_sanitize(value) do
"%" <> String.replace(value, ~r/([\\%_])/, "\\1") <> "%"
end
@doc """
Creates a event.
@@ -205,6 +220,11 @@ defmodule Eventos.Events do
"""
def get_category!(id), do: Repo.get!(Category, id)
@spec get_category_by_title(String.t) :: tuple()
def get_category_by_title(title) when is_binary(title) do
Repo.get_by(Category, title: title)
end
@doc """
Creates a category.

View File

@@ -20,10 +20,11 @@ defmodule EventosWeb.ActorController do
render(conn, "show.json", actor: actor)
end
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
def search(conn, %{"name" => name}) do
with {:ok, actor} <- ActivityPub.make_actor_from_nickname(name) do
render(conn, "acccount_basic.json", actor: actor)
else
case Actors.search(name) do # find already saved accounts
{:ok, actors} ->
render(conn, "index.json", actors: actors)
{:error, err} -> json(conn, err)
end
end

View File

@@ -0,0 +1,46 @@
defmodule EventosWeb.BotController do
use EventosWeb, :controller
alias Eventos.Actors
alias Eventos.Actors.Bot
action_fallback EventosWeb.FallbackController
def index(conn, _params) do
bots = Actors.list_bots()
render(conn, "index.json", bots: bots)
end
def create(conn, %{"bot" => bot_params}) do
with user <- Guardian.Plug.current_resource,
bot_params <- Map.put(bot_params, "user_id", user.id),
{:ok, actor } <- Actors.register_bot_account(%{name: bot_params["name"], summary: bot_params["summary"]}),
bot_params <- Map.put(bot_params, "actor_id", actor.id),
{:ok, %Bot{} = bot} <- Actors.create_bot(bot_params) do
conn
|> put_status(:created)
|> put_resp_header("location", bot_path(conn, :show, bot))
|> render("show.json", bot: bot)
end
end
def show(conn, %{"id" => id}) do
bot = Actors.get_bot!(id)
render(conn, "show.json", bot: bot)
end
def update(conn, %{"id" => id, "bot" => bot_params}) do
bot = Actors.get_bot!(id)
with {:ok, %Bot{} = bot} <- Actors.update_bot(bot, bot_params) do
render(conn, "show.json", bot: bot)
end
end
def delete(conn, %{"id" => id}) do
bot = Actors.get_bot!(id)
with {:ok, %Bot{}} <- Actors.delete_bot(bot) do
send_resp(conn, :no_content, "")
end
end
end

View File

@@ -35,6 +35,11 @@ defmodule EventosWeb.EventController do
end
end
def search(conn, %{"name" => name}) do
events = Events.find_events_by_name(name)
render(conn, "index.json", events: events)
end
def show(conn, %{"username" => username, "slug" => slug}) do
event = Events.get_event_full_by_name_and_slug!(username, slug)
render(conn, "show.json", event: event)

View File

@@ -0,0 +1,20 @@
defmodule EventosWeb.SearchController do
@moduledoc """
Controller for Search
"""
use EventosWeb, :controller
alias Eventos.Events
alias Eventos.Actors
action_fallback EventosWeb.FallbackController
def search(conn, %{"name" => name}) do
events = Events.find_events_by_name(name)
case Actors.search(name) do # find already saved accounts
{:ok, actors} ->
render(conn, "search.json", events: events, actors: actors)
{:error, err} -> json(conn, err)
end
end
end

View File

@@ -39,11 +39,13 @@ defmodule EventosWeb.Router do
post "/login", UserSessionController, :sign_in
#resources "/groups", GroupController, only: [:index, :show]
get "/events", EventController, :index
get "/events/search/:name", EventController, :search
get "/events/:username/:slug", EventController, :show
get "/events/:username/:slug/ics", EventController, :export_to_ics
get "/events/:username/:slug/tracks", TrackController, :show_tracks_for_event
get "/events/:username/:slug/sessions", SessionController, :show_sessions_for_event
resources "/comments", CommentController, only: [:show]
get "/bots/:id", BotController, :view
get "/actors", ActorController, :index
get "/actors/search/:name", ActorController, :search
@@ -53,6 +55,8 @@ defmodule EventosWeb.Router do
resources "/sessions", SessionController, only: [:index, :show]
resources "/tracks", TrackController, only: [:index, :show]
resources "/addresses", AddressController, only: [:index, :show]
get "/search/:name", SearchController, :search
end
end
@@ -70,9 +74,10 @@ defmodule EventosWeb.Router do
patch "/events/:username/:slug", EventController, :update
put "/events/:username/:slug", EventController, :update
delete "/events/:username/:slug", EventController, :delete
resources "/comments", CommentController, except: [:new, :edit]
resources "/comments", CommentController, except: [:new, :edit, :show]
#post "/events/:id/request", EventRequestController, :create_for_event
resources "/participant", ParticipantController
resources "/bots", BotController, except: [:new, :edit, :show]
#resources "/requests", EventRequestController
#resources "/groups", GroupController, except: [:index, :show]
#post "/groups/:id/request", GroupRequestController, :create_for_group

View File

@@ -9,6 +9,7 @@ defmodule EventosWeb.ActivityPub.ActorView do
alias Eventos.Service.ActivityPub
alias Eventos.Service.ActivityPub.Transmogrifier
alias Eventos.Service.ActivityPub.Utils
alias Eventos.Activity
import Ecto.Query
def render("actor.json", %{actor: actor}) do
@@ -123,10 +124,10 @@ defmodule EventosWeb.ActivityPub.ActorView do
end
end
def render("activity.json", %{activity: activity}) do
def render("activity.json", %{activity: %Activity{local: local} = activity}) do
%{
"id" => activity.data.url <> "/activity",
"type" => "Create",
"type" => if local do "Create" else "Announce" end,
"actor" => activity.data.organizer_actor.url,
"published" => Timex.now(),
"to" => ["https://www.w3.org/ns/activitystreams#Public"],

View File

@@ -1,5 +1,6 @@
defmodule EventosWeb.ActivityPub.ObjectView do
use EventosWeb, :view
alias EventosWeb.ActivityPub.ObjectView
alias Eventos.Service.ActivityPub.Transmogrifier
@base %{
"@context" => [
@@ -22,7 +23,7 @@ defmodule EventosWeb.ActivityPub.ObjectView do
"type" => "Event",
"id" => event.url,
"name" => event.title,
"category" => %{"title" => event.category.title},
"category" => render_one(event.category, ObjectView, "category.json", as: :category),
"content" => event.description,
"mediaType" => "text/markdown",
"published" => Timex.format!(event.inserted_at, "{ISO:Extended}"),
@@ -32,6 +33,10 @@ defmodule EventosWeb.ActivityPub.ObjectView do
end
def render("category.json", %{category: category}) do
category
%{"title" => category.title}
end
def render("category.json", %{category: nil}) do
nil
end
end

View File

@@ -23,6 +23,7 @@ defmodule EventosWeb.ActorView do
domain: actor.domain,
display_name: actor.name,
description: actor.summary,
type: actor.type,
# public_key: actor.public_key,
suspended: actor.suspended,
url: actor.url,
@@ -35,6 +36,7 @@ defmodule EventosWeb.ActorView do
domain: actor.domain,
display_name: actor.name,
description: actor.summary,
type: actor.type,
# public_key: actor.public_key,
suspended: actor.suspended,
url: actor.url,

View File

@@ -0,0 +1,18 @@
defmodule EventosWeb.BotView do
use EventosWeb, :view
alias EventosWeb.BotView
def render("index.json", %{bots: bots}) do
%{data: render_many(bots, BotView, "bot.json")}
end
def render("show.json", %{bot: bot}) do
%{data: render_one(bot, BotView, "bot.json")}
end
def render("bot.json", %{bot: bot}) do
%{id: bot.id,
source: bot.source,
type: bot.type}
end
end

View File

@@ -34,6 +34,7 @@ defmodule EventosWeb.EventView do
organizer: %{
username: event.organizer_actor.preferred_username
},
type: "Event",
}
end
@@ -46,6 +47,7 @@ defmodule EventosWeb.EventView do
organizer: render_one(event.organizer_actor, ActorView, "acccount_basic.json"),
participants: render_many(event.participants, ActorView, "show_basic.json"),
address: render_one(event.address, AddressView, "address.json"),
type: "Event",
}
end
end

View File

@@ -0,0 +1,16 @@
defmodule EventosWeb.SearchView do
@moduledoc """
View for Events
"""
use EventosWeb, :view
alias EventosWeb.{EventView, ActorView, GroupView, AddressView}
def render("search.json", %{events: events, actors: actors}) do
%{
data: %{
events: render_many(events, EventView, "event_simple.json"),
actors: render_many(actors, ActorView, "acccount_basic.json"),
}
}
end
end

View File

@@ -0,0 +1,21 @@
defmodule Mix.Tasks.CreateBot do
use Mix.Task
alias Eventos.Actors
alias Eventos.Actors.Bot
alias Eventos.Repo
import Logger
@shortdoc "Register user"
def run([email, name, summary, type, url]) do
Mix.Task.run("app.start")
with user <- Actors.find_by_email(email),
actor <- Actors.register_bot_account(%{name: name, summary: summary}),
{:ok, %Bot{} = bot} <- Actors.create_bot(%{"type" => type, "source" => url, "actor_id" => actor.id, "user_id" => user.id}) do
bot
else
e -> Logger.error(inspect e)
end
end
end

View File

@@ -1,6 +1,6 @@
defmodule Eventos.Service.ActivityPub do
alias Eventos.Events
alias Eventos.Events.Event
alias Eventos.Events.{Event, Category}
alias Eventos.Service.ActivityPub.Transmogrifier
alias Eventos.Service.WebFinger
alias Eventos.Activity
@@ -174,6 +174,15 @@ defmodule Eventos.Service.ActivityPub do
end
end
@spec find_or_make_actor_from_nickname(String.t) :: tuple()
def find_or_make_actor_from_nickname(nickname) do
with %Actor{} = actor <- Actors.get_actor_by_name(nickname) do
{:ok, actor}
else
nil -> make_actor_from_nickname(nickname)
end
end
def make_actor_from_nickname(nickname) do
with {:ok, %{"url" => url}} when not is_nil(url) <- WebFinger.finger(nickname) do
make_actor_from_url(url)
@@ -288,19 +297,39 @@ defmodule Eventos.Service.ActivityPub do
end
@spec fetch_public_activities_for_actor(Actor.t, integer(), integer()) :: list()
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 10, limit \\ 10) do
{:ok, events, total} = Events.get_events_for_actor(actor, page, limit)
activities = Enum.map(events, fn event ->
{:ok, activity} = event_to_activity(event)
activity
end)
{activities, total}
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do
case actor.type do
:Person ->
{:ok, events, total} = Events.get_events_for_actor(actor, page, limit)
activities = Enum.map(events, fn event ->
{:ok, activity} = event_to_activity(event)
activity
end)
{activities, total}
:Service ->
bot = Actors.get_bot_by_actor(actor)
case bot.type do
"ics" ->
{:ok, %HTTPoison.Response{body: body} = _resp} = HTTPoison.get(bot.source)
ical_events = body
|> ExIcal.parse()
|> ExIcal.by_range(DateTime.utc_now(), DateTime.utc_now() |> Timex.shift(years: 1))
activities = ical_events
|> Enum.chunk_every(limit)
|> Enum.at(page - 1)
|> Enum.map(fn event ->
{:ok, activity } = ical_event_to_activity(event, actor, bot.source)
activity
end)
{activities, length(ical_events)}
end
end
end
defp event_to_activity(%Event{} = event) do
defp event_to_activity(%Event{} = event, local \\ true) do
activity = %Activity{
data: event,
local: true,
local: local,
actor: event.organizer_actor.url,
recipients: ["https://www.w3.org/ns/activitystreams#Public"]
}
@@ -309,4 +338,44 @@ defmodule Eventos.Service.ActivityPub do
#stream_out(activity)
{:ok, activity}
end
defp ical_event_to_activity(%ExIcal.Event{} = ical_event, %Actor{} = actor, source) do
# Logger.debug(inspect ical_event)
# TODO : refactor me !
category = if is_nil ical_event.categories do
nil
else
ical_category = ical_event.categories |> hd() |> String.downcase()
case ical_category |> Events.get_category_by_title() do
nil -> case Events.create_category(%{"title" => ical_category}) do
{:ok, %Category{} = category} -> category
_ -> nil
end
category -> category
end
end
{:ok, event} = Events.create_event(%{
begins_on: ical_event.start,
ends_on: ical_event.end,
inserted_at: ical_event.stamp,
updated_at: ical_event.stamp,
description: ical_event.description |> sanitize_ical_event_strings,
title: ical_event.summary |> sanitize_ical_event_strings,
organizer_actor: actor,
category: category,
})
event_to_activity(event, false)
end
defp sanitize_ical_event_strings(string) when is_binary(string) do
string
|> String.replace(~s"\r\n", "")
|> String.replace(~s"\\,", ",")
end
defp sanitize_ical_event_strings(nil) do
nil
end
end

View File

@@ -81,7 +81,7 @@ defmodule Eventos.Service.WebFinger do
address = "http://#{domain}/.well-known/webfinger?resource=acct:#{actor}"
Logger.debug(inspect address)
with response <- HTTPoison.get!(address, [Accept: "application/json, application/activity+json, application/jrd+json"],follow_redirect: true),
with {:ok, %HTTPoison.Response{} = response} <- HTTPoison.get(address, [Accept: "application/json, application/activity+json, application/jrd+json"],follow_redirect: true),
%{status_code: status_code, body: body} when status_code in 200..299 <- response do
{:ok, doc} = Jason.decode(body)
webfinger_from_json(doc)