master
Thomas Citharel 4 years ago
parent a3852f26c1
commit 686cf04787
  1. 2
      lib/eventos/activity.ex
  2. 56
      lib/eventos/actors/actor.ex
  3. 40
      lib/eventos/actors/actors.ex
  4. 2
      lib/eventos/actors/member.ex
  5. 20
      lib/eventos/actors/service/reset_password.ex
  6. 12
      lib/eventos/events/comment.ex
  7. 16
      lib/eventos/events/event.ex
  8. 48
      lib/eventos/events/events.ex
  9. 8
      lib/eventos_web/controllers/activity_pub_controller.ex
  10. 10
      lib/eventos_web/controllers/actor_controller.ex
  11. 18
      lib/eventos_web/controllers/group_controller.ex
  12. 47
      lib/eventos_web/controllers/user_controller.ex
  13. 1
      lib/eventos_web/router.ex
  14. 11
      lib/eventos_web/views/activity_pub/actor_view.ex
  15. 13
      lib/eventos_web/views/activity_pub/object_view.ex
  16. 130
      lib/service/activity_pub/activity_pub.ex
  17. 23
      lib/service/activity_pub/transmogrifier.ex
  18. 31
      lib/service/activity_pub/utils.ex
  19. 1
      mix.exs
  20. 19
      priv/repo/migrations/20180816093446_add_primary_key_to_member.exs
  21. 125
      test/eventos/actors/actors_test.exs
  22. 140
      test/eventos/events/events_test.exs
  23. 26
      test/eventos/service/activitypub/activitypub_test.exs
  24. 116
      test/eventos_web/controllers/activity_pub_controller_test.exs
  25. 58
      test/eventos_web/controllers/actor_controller_test.exs
  26. 2
      test/eventos_web/controllers/address_controller_test.exs
  27. 139
      test/eventos_web/controllers/user_controller_test.exs
  28. 65
      test/fixtures/mastodon-post-activity.json
  29. 13
      test/support/factory.ex

@ -3,5 +3,5 @@ defmodule Eventos.Activity do
Represents an activity
"""
defstruct [:id, :data, :local, :actor, :recipients, :notifications]
defstruct [:id, :data, :local, :actor, :recipients, :notifications, :type]
end

@ -173,23 +173,9 @@ defmodule Eventos.Actors.Actor do
|> put_change(:local, true)
end
def get_or_fetch_by_url(url) do
if user = Actors.get_actor_by_url(url) do
user
else
case ActivityPub.make_actor_from_url(url) do
{:ok, user} ->
user
_ ->
{:error, "Could not fetch by AP id"}
end
end
end
@spec get_public_key_for_url(String.t()) :: {:ok, String.t()}
def get_public_key_for_url(url) do
with %Actor{} = actor <- get_or_fetch_by_url(url) do
with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(url) do
actor.keys
|> Eventos.Service.ActivityPub.Utils.pem_to_public_key()
else
@ -228,4 +214,44 @@ defmodule Eventos.Actors.Actor do
)
)
end
def get_groups_member_of(%Actor{id: actor_id}) do
Repo.all(
from(
a in Actor,
join: m in Member,
on: a.id == m.parent_id,
where: m.actor_id == ^actor_id
)
)
end
def get_members_for_group(%Actor{id: actor_id}) do
Repo.all(
from(
a in Actor,
join: m in Member,
on: a.id == m.actor_id,
where: m.parent_id == ^actor_id
)
)
end
def follow(%Actor{} = follower, %Actor{} = followed) do
# Check if actor is locked
# Check if followed has blocked follower
# Check if follower already follows followed
cond do
following?(follower, followed) ->
{:error,
"Could not follow actor: you are already following #{followed.preferred_username}"}
# true -> nil
# Follow the person
end
end
def following?(%Actor{} = follower, %Actor{followers: followers}) do
Enum.member?(followers, follower)
end
end

@ -265,13 +265,11 @@ defmodule Eventos.Actors do
def get_or_fetch_by_url(url) do
if actor = get_actor_by_url(url) do
actor
{:ok, actor}
else
ap_try = ActivityPub.make_actor_from_url(url)
case ap_try do
case ActivityPub.make_actor_from_url(url) do
{:ok, actor} ->
actor
{:ok, actor}
_ ->
{:error, "Could not fetch by AP id"}
@ -299,7 +297,7 @@ defmodule Eventos.Actors do
@doc """
Find actors by their name or displayed name
"""
def find_actors_by_username(username) do
def find_actors_by_username_or_name(username) do
Repo.all(
from(
a in Actor,
@ -320,7 +318,7 @@ defmodule Eventos.Actors do
@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
# find already saved accounts
case find_actors_by_username(name) do
case find_actors_by_username_or_name(name) do
[] ->
# no accounts found, let's test if it's an username@domain.tld
with true <- Regex.match?(@email_regex, name),
@ -457,6 +455,24 @@ defmodule Eventos.Actors do
|> Repo.insert()
end
@doc """
Gets an user by it's email
## Examples
iex> get_user_by_email(user, email)
{:ok, %User{}}
iex> get_user_by_email(user, wrong_email)
{:error, nil}
"""
def get_user_by_email(email) do
case Repo.get_by(User, email: email) do
nil -> {:error, nil}
user -> {:ok, user}
end
end
@doc """
Updates a user.
@ -548,10 +564,12 @@ defmodule Eventos.Actors do
"""
def create_member(attrs \\ %{}) do
%Member{}
|> Member.changeset(attrs)
|> Repo.insert!()
|> Repo.preload([:actor, :parent])
with {:ok, %Member{} = member} <-
%Member{}
|> Member.changeset(attrs)
|> Repo.insert() do
{:ok, Repo.preload(member, [:actor, :parent])}
end
end
@doc """

@ -7,7 +7,6 @@ defmodule Eventos.Actors.Member do
alias Eventos.Actors.Member
alias Eventos.Actors.Actor
@primary_key false
schema "members" do
field(:approved, :boolean, default: true)
# 0 : Member, 1 : Moderator, 2 : Admin
@ -23,5 +22,6 @@ defmodule Eventos.Actors.Member do
member
|> cast(attrs, [:role, :approved, :parent_id, :actor_id])
|> validate_required([:parent_id, :actor_id])
|> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
end
end

@ -11,16 +11,18 @@ defmodule Eventos.Actors.Service.ResetPassword do
"""
@spec check_reset_password_token(String.t(), String.t()) :: tuple
def check_reset_password_token(password, token) do
with %User{} = user <- Repo.get_by(User, reset_password_token: token) do
Repo.update(
User.password_reset_changeset(user, %{
"password" => password,
"reset_password_sent_at" => nil,
"reset_password_token" => nil
})
)
with %User{} = user <- Repo.get_by(User, reset_password_token: token),
{:ok, %User{} = user} <-
Repo.update(
User.password_reset_changeset(user, %{
"password" => password,
"reset_password_sent_at" => nil,
"reset_password_token" => nil
})
) do
{:ok, Repo.preload(user, :actors)}
else
_err ->
err ->
{:error, :invalid_token}
end
end

@ -21,17 +21,23 @@ defmodule Eventos.Events.Comment do
belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id)
belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id)
timestamps()
timestamps(type: :utc_datetime)
end
@doc false
def changeset(comment, attrs) do
uuid = Ecto.UUID.generate()
# TODO : really change me right away
url =
if Map.has_key?(attrs, "url"),
do: attrs["url"],
else: "#{EventosWeb.Endpoint.url()}/comments/#{uuid}"
comment
|> cast(attrs, [:url, :text, :actor_id, :event_id, :in_reply_to_comment_id, :attributed_to_id])
|> validate_required([:text, :actor_id])
|> put_change(:uuid, uuid)
|> put_change(:url, "#{EventosWeb.Endpoint.url()}/comments/#{uuid}")
|> put_change(:url, url)
|> validate_required([:text, :actor_id, :url])
end
end

@ -18,8 +18,11 @@ defmodule Eventos.Events.Event do
field(:description, :string)
field(:ends_on, Timex.Ecto.DateTimeWithTimezone)
field(:title, :string)
# ???
field(:state, :integer, default: 0)
# Event status: TENTATIVE 1, CONFIRMED 2, CANCELLED 3
field(:status, :integer, default: 0)
# If the event is public or private
field(:public, :boolean, default: true)
field(:thumbnail, :string)
field(:large_image, :string)
@ -42,9 +45,7 @@ defmodule Eventos.Events.Event do
@doc false
def changeset(%Event{} = event, attrs) do
uuid = Ecto.UUID.generate()
# TODO : check what's the use here. Tests ?
# TODO : Change all of this
actor_url =
if Map.has_key?(attrs, :organizer_actor) do
attrs.organizer_actor.preferred_username
@ -52,6 +53,13 @@ defmodule Eventos.Events.Event do
""
end
uuid = Ecto.UUID.generate()
url =
if Map.has_key?(attrs, "url"),
do: attrs["url"],
else: "#{EventosWeb.Endpoint.url()}/@#{actor_url}/#{uuid}"
event
|> Ecto.Changeset.cast(attrs, [
:title,
@ -74,7 +82,7 @@ defmodule Eventos.Events.Event do
|> cast_assoc(:tags)
|> cast_assoc(:physical_address)
|> put_change(:uuid, uuid)
|> put_change(:url, "#{EventosWeb.Endpoint.url()}/@#{actor_url}/#{uuid}")
|> put_change(:url, url)
|> validate_required([
:title,
:begins_on,

@ -110,10 +110,17 @@ defmodule Eventos.Events do
@doc """
Gets an event by it's URL
"""
def get_event_by_url!(url) do
def get_event_by_url(url) do
Repo.get_by(Event, url: url)
end
@doc """
Gets an event by it's URL
"""
def get_event_by_url!(url) do
Repo.get_by!(Event, url: url)
end
@doc """
Gets an event by it's UUID
"""
@ -175,7 +182,10 @@ defmodule Eventos.Events do
@doc """
Find events by name
"""
def find_events_by_name(name) when name == "", do: []
def find_events_by_name(name) do
name = String.trim(name)
events = Repo.all(from(a in Event, where: ilike(a.title, ^like_sanitize(name))))
Repo.preload(events, [:organizer_actor])
end
@ -780,6 +790,32 @@ defmodule Eventos.Events do
Repo.all(Comment)
end
def get_comments_for_actor(%Actor{id: actor_id}, page \\ 1, limit \\ 10) do
start = (page - 1) * limit
query =
from(
c in Comment,
where: c.actor_id == ^actor_id,
limit: ^limit,
order_by: [desc: :id],
offset: ^start,
preload: [
:actor,
:in_reply_to_comment,
:origin_comment,
:event
]
)
comments = Repo.all(query)
count_comments =
Repo.one(from(c in Comment, select: count(c.id), where: c.actor_id == ^actor_id))
{:ok, comments, count_comments}
end
@doc """
Gets a single comment.
@ -798,6 +834,16 @@ defmodule Eventos.Events do
def get_comment_with_uuid!(uuid), do: Repo.get_by!(Comment, uuid: uuid)
def get_comment_from_url(url), do: Repo.get_by(Comment, url: url)
def get_comment_from_url!(url), do: Repo.get_by!(Comment, url: url)
def get_comment_full_from_url!(url) do
with %Comment{} = comment <- Repo.get_by!(Comment, url: url) do
Repo.preload(comment, :actor)
end
end
@doc """
Creates a comment.

@ -18,10 +18,16 @@ defmodule EventosWeb.ActivityPubController do
end
def event(conn, %{"uuid" => uuid}) do
with %Event{} = event <- Events.get_event_full_by_uuid(uuid) do
with %Event{} = event <- Events.get_event_full_by_uuid(uuid),
true <- event.public do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("event.json", %{event: event}))
else
false ->
conn
|> put_status(404)
|> json("Not found")
end
end

@ -37,8 +37,12 @@ defmodule EventosWeb.ActorController do
end
def show(conn, %{"name" => name}) do
actor = Actors.get_actor_by_name_with_everything(name)
render(conn, "show.json", actor: actor)
with %Actor{} = actor <- Actors.get_actor_by_name_with_everything(name) do
render(conn, "show.json", actor: actor)
else
nil ->
send_resp(conn, :not_found, "")
end
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])?)*$/
@ -57,7 +61,7 @@ defmodule EventosWeb.ActorController do
actor = Actors.get_local_actor_by_name(name)
with {:ok, %Actor{} = actor} <- Actors.update_actor(actor, actor_params) do
render(conn, "show.json", actor: actor)
render(conn, "show_basic.json", actor: actor)
end
end

@ -15,15 +15,13 @@ defmodule EventosWeb.GroupController do
end
def create(conn, %{"group" => group_params}) do
with {:ok, %Actor{} = group} <- Actors.create_group(group_params) do
%Member{} =
_member =
Actors.create_member(%{
"parent_id" => group.id,
"actor_id" => Actors.get_local_actor_by_name(group_params["actor_admin"]).id,
"role" => 2
})
with {:ok, %Actor{} = group} <- Actors.create_group(group_params),
{:ok, %Member{} = member} <-
Actors.create_member(%{
"parent_id" => group.id,
"actor_id" => Actors.get_local_actor_by_name(group_params["actor_admin"]).id,
"role" => 2
}) do
conn
|> put_status(:created)
|> put_resp_header("location", actor_path(conn, :show, group))
@ -34,7 +32,7 @@ defmodule EventosWeb.GroupController do
def join(conn, %{"name" => group_name, "actor_name" => actor_name}) do
with %Actor{} = group <- Actors.get_group_by_name(group_name),
%Actor{} = actor <- Actors.get_local_actor_by_name(actor_name),
%Member{} = member <-
{:ok, %Member{} = member} <-
Actors.create_member(%{"parent_id" => group.id, "actor_id" => actor.id}) do
conn
|> put_status(:created)

@ -42,11 +42,15 @@ defmodule EventosWeb.UserController do
end
end
@time_before_resend 3600
def resend_confirmation(conn, %{"email" => email}) do
with {:ok, %User{} = user} <- Actors.find_by_email(email),
false <- is_nil(user.confirmation_token),
true <-
Timex.before?(Timex.shift(user.confirmation_sent_at, hours: 1), DateTime.utc_now()) do
Timex.before?(
Timex.shift(user.confirmation_sent_at, seconds: @time_before_resend),
DateTime.utc_now()
) do
Activation.resend_confirmation_email(user)
render(conn, "confirmation.json", %{user: user})
else
@ -58,7 +62,10 @@ defmodule EventosWeb.UserController do
_ ->
conn
|> put_status(:not_found)
|> json(%{"error" => "Unable to resend the validation token"})
|> json(%{
"error" =>
"Unable to resend the validation token. Please wait a while before you can ask for resending token"
})
end
end
@ -67,7 +74,7 @@ defmodule EventosWeb.UserController do
{:ok, _} <- ResetPassword.send_password_reset_email(user) do
render(conn, "password_reset.json", %{user: user})
else
{:error, :not_found} ->
{:error, nil} ->
conn
|> put_status(:not_found)
|> json(%{"errors" => "Unable to find an user with this email"})
@ -105,23 +112,23 @@ defmodule EventosWeb.UserController do
render(conn, "show_simple.json", user: user)
end
defp handle_changeset_errors(errors) do
errors
|> Enum.map(fn {field, detail} ->
"#{field} " <> render_detail(detail)
end)
|> Enum.join()
end
defp render_detail({message, values}) do
Enum.reduce(values, message, fn {k, v}, acc ->
String.replace(acc, "%{#{k}}", to_string(v))
end)
end
defp render_detail(message) do
message
end
# defp handle_changeset_errors(errors) do
# errors
# |> Enum.map(fn {field, detail} ->
# "#{field} " <> render_detail(detail)
# end)
# |> Enum.join()
# end
# defp render_detail({message, values}) do
# Enum.reduce(values, message, fn {k, v}, acc ->
# String.replace(acc, "%{#{k}}", to_string(v))
# end)
# end
# defp render_detail(message) do
# message
# end
def update(conn, %{"id" => id, "user" => user_params}) do
user = Actors.get_user!(id)

@ -122,6 +122,7 @@ defmodule EventosWeb.Router do
get("/@:name/following", ActivityPubController, :following)
get("/@:name/followers", ActivityPubController, :followers)
get("/events/:uuid", ActivityPubController, :event)
get("/comments/:uuid", ActivityPubController, :event)
post("/@:name/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox)
end

@ -134,10 +134,17 @@ defmodule EventosWeb.ActivityPub.ActorView do
else
"Announce"
end,
"actor" => activity.data.organizer_actor.url,
"actor" => activity.actor,
"published" => Timex.now(),
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => render_one(activity.data, ObjectView, "event.json", as: :event)
"object" =>
case activity.type do
:Event ->
render_one(activity.data, ObjectView, "event.json", as: :event)
:Comment ->
render_one(activity.data, ObjectView, "note.json", as: :note)
end
}
end

@ -32,6 +32,19 @@ defmodule EventosWeb.ActivityPub.ObjectView do
Map.merge(event, @base)
end
def render("note.json", %{note: note}) do
event = %{
"type" => "Note",
"id" => note.url,
"content" => note.text,
"mediaType" => "text/markdown",
"published" => Timex.format!(note.inserted_at, "{ISO:Extended}"),
"updated" => Timex.format!(note.updated_at, "{ISO:Extended}")
}
Map.merge(event, @base)
end
def render("category.json", %{category: category}) do
%{"title" => category.title}
end

@ -6,7 +6,7 @@ defmodule Eventos.Service.ActivityPub do
"""
alias Eventos.Events
alias Eventos.Events.{Event, Category}
alias Eventos.Events.{Event, Category, Comment}
alias Eventos.Service.ActivityPub.Transmogrifier
alias Eventos.Service.WebFinger
alias Eventos.Activity
@ -44,36 +44,65 @@ defmodule Eventos.Service.ActivityPub do
end
end
def fetch_object_from_url(url, :event), do: fetch_event_from_url(url)
def fetch_object_from_url(url, :note), do: fetch_note_from_url(url)
@spec fetch_object_from_url(String.t()) :: tuple()
def fetch_object_from_url(url) do
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
HTTPoison.get(
url,
[Accept: "application/activity+json"],
follow_redirect: true,
timeout: 10_000,
recv_timeout: 20_000
),
{:ok, data} <- Jason.decode(body),
nil <- Events.get_event_by_url(data["id"]),
nil <- Events.get_comment_from_url(data["id"]),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["attributedTo"],
"object" => data
},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
case data["type"] do
"Event" ->
{:ok, Events.get_event_by_url!(activity.data["object"]["id"])}
"Note" ->
{:ok, Events.get_comment_from_url!(activity.data["object"]["id"])}
end
else
object = %Event{} -> {:ok, object}
object = %Comment{} -> {:ok, object}
e -> {:error, e}
end
end
@spec fetch_object_from_url(String.t()) :: tuple()
def fetch_event_from_url(url) do
if object = Events.get_event_by_url!(url) do
{:ok, object}
with nil <- Events.get_event_by_url(url) do
Logger.info("Fetching #{url} via AP")
fetch_object_from_url(url)
else
%Event{} = comment ->
{:ok, comment}
end
end
@spec fetch_object_from_url(String.t()) :: tuple()
def fetch_note_from_url(url) do
with nil <- Events.get_comment_from_url(url) do
Logger.info("Fetching #{url} via AP")
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
HTTPoison.get(
url,
[Accept: "application/activity+json"],
follow_redirect: true,
timeout: 10_000,
recv_timeout: 20_000
),
{:ok, data} <- Jason.decode(body),
nil <- Events.get_event_by_url!(data["id"]),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["attributedTo"],
"object" => data
},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Events.get_event_by_url!(activity.data["object"]["id"])}
else
object = %Event{} -> {:ok, object}
e -> e
end
fetch_object_from_url(url)
else
%Comment{} = comment ->
{:ok, comment}
end
end
@ -127,7 +156,7 @@ defmodule Eventos.Service.ActivityPub do
end
end
def follow(follower, followed, activity_id \\ nil, local \\ true) do
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
@ -135,7 +164,9 @@ defmodule Eventos.Service.ActivityPub do
end
end
def delete(%Event{url: url, organizer_actor: actor} = event, local \\ true) do
def delete(object, local \\ true)
def delete(%Event{url: url, organizer_actor: actor} = event, local) do
data = %{
"type" => "Delete",
"actor" => actor.url,
@ -150,6 +181,21 @@ defmodule Eventos.Service.ActivityPub do
end
end
def delete(%Comment{url: url, actor: actor} = comment, local) do
data = %{
"type" => "Delete",
"actor" => actor.url,
"object" => url,
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
with Events.delete_comment(comment),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def create_public_activities(%Actor{} = actor) do
end
@ -285,13 +331,22 @@ defmodule Eventos.Service.ActivityPub do
case actor.type do
:Person ->
{:ok, events, total} = Events.get_events_for_actor(actor, page, limit)
{:ok, comments, total} = Events.get_comments_for_actor(actor, page, limit)
activities =
event_activities =
Enum.map(events, fn event ->
{:ok, activity} = event_to_activity(event)
activity
end)
comment_activities =
Enum.map(comments, fn comment ->
{:ok, activity} = comment_to_activity(comment)
activity
end)
activities = event_activities ++ comment_activities
{activities, total}
:Service ->
@ -322,6 +377,7 @@ defmodule Eventos.Service.ActivityPub do
defp event_to_activity(%Event{} = event, local \\ true) do
activity = %Activity{
type: :Event,
data: event,
local: local,
actor: event.organizer_actor.url,
@ -333,6 +389,20 @@ defmodule Eventos.Service.ActivityPub do
{:ok, activity}
end
defp comment_to_activity(%Comment{} = comment, local \\ true) do
activity = %Activity{
type: :Comment,
data: comment,
local: local,
actor: comment.actor.url,
recipients: ["https://www.w3.org/ns/activitystreams#Public"]
}
# Notification.create_notifications(activity)
# 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 !

@ -4,7 +4,7 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
"""
alias Eventos.Actors.Actor
alias Eventos.Actors
alias Eventos.Events.Event
alias Eventos.Events.{Event, Comment}
alias Eventos.Service.ActivityPub
import Ecto.Query
@ -77,9 +77,9 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
Logger.debug("Handle incoming to create notes")
Logger.info("Handle incoming to create notes")
with %Actor{} = actor <- Actor.get_or_fetch_by_url(data["actor"]) do
with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(data["actor"]) do
Logger.debug("found actor")
object = fix_object(data["object"])
@ -104,8 +104,8 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
) do
with %Actor{} = followed <- Actors.get_actor_by_url(followed),
%Actor{} = follower <- Actors.get_or_fetch_by_url(follower),
with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed),
{:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true})
@ -133,7 +133,7 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
) do
with %Actor{} = actor <- Actors.get_or_fetch_by_url(actor),
with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_event_from_url(object_id),
{:ok, activity, object} <- ActivityPub.announce(actor, object, id, false) do
@ -268,6 +268,17 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
{:ok, event}
end
def prepare_outgoing(%Comment{} = comment) do
comment =
comment
|> Map.from_struct()
|> Map.drop([:__meta__])
|> Map.put(:"@context", "https://www.w3.org/ns/activitystreams")
|> prepare_object
{:ok, comment}
end
#
# def maybe_fix_object_url(data) do
# if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do

@ -132,20 +132,23 @@ defmodule Eventos.Service.ActivityPub.Utils do
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data})
when is_map(object_data) and type == "Note" do
import Logger
Logger.debug("insert full object")
Logger.debug(inspect(object_data))
actor = Actors.get_actor_by_url(object_data["actor"])
data = %{
"text" => object_data["content"],
"url" => object_data["id"],
"actor_id" => actor.id,
"in_reply_to_comment_id" => object_data["inReplyTo"]
}
with {:ok, _} <- Events.create_comment(data) do
:ok
with {:ok, %Actor{id: actor_id}} <- Actors.get_or_fetch_by_url(object_data["actor"]) do
data = %{
"text" => object_data["content"],
"url" => object_data["id"],
"actor_id" => actor_id,
"in_reply_to_comment_id" => object_data["inReplyTo"]
}
require Logger
Logger.info("comment data ready to be inserted")
Logger.info(inspect(data))
with {:ok, comm} <- Events.create_comment(data) do
Logger.info("comment inserted")
Logger.info(inspect(comm))
:ok
end
end
end

@ -65,7 +65,6 @@ defmodule Eventos.Mixfile do
{:timex_ecto, "~> 3.0"},
{:icalendar, "~> 0.6"},
{:exgravatar, "~> 2.0.1"},
{:littlefinger, "~> 0.1"},
{:httpoison, "~> 1.0"},
{:json_ld, "~> 0.2"},
{:jason, "~> 1.0"},

@ -0,0 +1,19 @@
defmodule Eventos.Repo.Migrations.AddPrimaryKeyToMember do
use Ecto.Migration
def up do
execute("ALTER TABLE members DROP CONSTRAINT IF EXISTS members_pkey")
drop_if_exists index(:members, ["members_account_id_index"])
create unique_index(:members, [:actor_id, :parent_id], name: :members_actor_parent_unique_index)
alter table(:members) do
add :id, :serial, primary_key: true
end
end
def down do
drop index(:members, [:actor_id, :parent_id], name: :members_actor_parent_unique_index)
alter table(:members) do
remove :id
end
end
end

@ -9,7 +9,7 @@ defmodule Eventos.ActorsTest do
@valid_attrs %{
summary: "some description",
name: "some name",
name: "Bobby Blank",
domain: "some domain",
keys: "some keypair",
suspended: true,
@ -74,7 +74,7 @@ defmodule Eventos.ActorsTest do
end
test "get_actor_by_name/1 returns a remote actor" do
assert %Actor{} = actor = Actors.get_or_fetch_by_url(@remote_account_url)
assert {:ok, %Actor{} = actor} = Actors.get_or_fetch_by_url(@remote_account_url)
actor_found = Actors.get_actor_by_name("#{actor.preferred_username}@#{actor.domain}")
assert actor_found = actor
end
@ -107,7 +107,7 @@ defmodule Eventos.ActorsTest do
end
test "get_actor_by_name_with_everything!/1 returns the remote actor with it's organized events" do
assert %Actor{} = actor = Actors.get_or_fetch_by_url(@remote_account_url)
assert {:ok, %Actor{} = actor} = Actors.get_or_fetch_by_url(@remote_account_url)
assert Actors.get_actor_by_name_with_everything(
"#{actor.preferred_username}@#{actor.domain}"
@ -124,12 +124,15 @@ defmodule Eventos.ActorsTest do
test "get_or_fetch_by_url/1 returns the local actor for the url", %{
actor: actor
} do
assert Actors.get_or_fetch_by_url(actor.url).preferred_username == actor.preferred_username
assert Actors.get_or_fetch_by_url(actor.url).domain == nil
preferred_username = actor.preferred_username
assert {:ok, %Actor{preferred_username: preferred_username, domain: nil} = actor_found} =
Actors.get_or_fetch_by_url(actor.url)
end
test "get_or_fetch_by_url/1 returns the remote actor for the url" do
assert %Actor{preferred_username: @remote_account_username, domain: @remote_account_domain} =
assert {:ok,
%Actor{preferred_username: @remote_account_username, domain: @remote_account_domain}} =
Actors.get_or_fetch_by_url(@remote_account_url)
end
@ -141,12 +144,21 @@ defmodule Eventos.ActorsTest do
assert actors = [actor, actor2]
end
test "test find_actors_by_username/1 returns actors with similar usernames", %{actor: actor} do
%Actor{} = actor2 = Actors.get_or_fetch_by_url(@remote_account_url)
actors = Actors.find_actors_by_username("t")
test "test find_actors_by_username_or_name/1 returns actors with similar usernames", %{
actor: actor
} do
{:ok, %Actor{} = actor2} = Actors.get_or_fetch_by_url(@remote_account_url)
actors = Actors.find_actors_by_username_or_name("t")
assert actors = [actor, actor2]
end
test "test find_actors_by_username_or_name/1 returns actors with similar names", %{
actor: actor
} do
actors = Actors.find_actors_by_username_or_name("ohno")
assert actors == []
end
test "test search/1 returns accounts for search with existing accounts", %{actor: actor} do
assert {:ok, [actor]} = Actors.search("t")
end
@ -180,7 +192,7 @@ defmodule Eventos.ActorsTest do
test "create_actor/1 with valid data creates a actor" do
assert {:ok, %Actor{} = actor} = Actors.create_actor(@valid_attrs)
assert actor.summary == "some description"
assert actor.name == "some name"
assert actor.name == "Bobby Blank"
assert actor.domain == "some domain"
assert actor.keys == "some keypair"
assert actor.suspended
@ -484,4 +496,97 @@ defmodule Eventos.ActorsTest do
assert %Ecto.Changeset{} = Actors.change_follower(follower)
end
end
describe "members" do
alias Eventos.Actors.Member
alias Eventos.Actors.Actor
@valid_attrs %{approved: true, role: 0}
@update_attrs %{approved: false, role: 1}
@invalid_attrs %{approved: nil, role: nil}
setup do
actor = insert(:actor)
group = insert(:group)
{:ok, actor: actor, group: group}
end
defp create_member(%{actor: actor, group: group}) do
insert(:member, actor: actor, parent: group)
end
test "get_member!/1 returns the member with given id", context do
member = create_member(context)
assert member = Actors.get_member!(member.id)
end
test "create_member/1 with valid data creates a member", %{
actor: actor,
group: group
} do
valid_attrs =
@valid_attrs
|> Map.put(:actor_id, actor.id)
|> Map.put(:parent_id, group.id)
assert {:ok, %Member{} = member} = Actors.create_member(valid_attrs)
assert member.approved == true
assert member.role == 0
assert [group] = Actor.get_groups_member_of(actor)
assert [actor] = Actor.get_members_for_group(group)
end
test "create_member/1 with valid data but same actors fails to create a member", %{
actor: actor,
group: group
} do
create_member(%{actor: actor, group: group})
valid_attrs =
@valid_attrs
|> Map.put(:actor_id, actor.id)
|> Map.put(:parent_id, group.id)
assert {:error, _member} = Actors.create_member(valid_attrs)
end
test "create_member/1 with invalid data returns error changeset", %{
actor: actor,
group: group
} do
invalid_attrs =
@invalid_attrs
|> Map.put(:actor_id, nil)
|> Map.put(:parent_id, nil)
assert {:error, %Ecto.Changeset{}} = Actors.create_member(invalid_attrs)
end
test "update_member/2 with valid data updates the member", context do
member = create_member(context)
assert {:ok, member} = Actors.update_member(member, @update_attrs)
assert %Member{} = member
assert member.approved == false
assert member.role == 1
end
# This can't happen, since attrs are optional
# test "update_member/2 with invalid data returns error changeset", context do
# member = create_member(context)
# assert {:error, %Ecto.Changeset{}} = Actors.update_member(member, @invalid_attrs)
# assert member = Actors.get_member!(member.id)
# end
test "delete_member/1 deletes the member", context do
member = create_member(context)
assert {:ok, %Member{}} = Actors.delete_member(member)
assert_raise Ecto.NoResultsError, fn -> Actors.get_member!(member.id) end
end
test "change_member/1 returns a member changeset", context do
member = create_member(context)
assert %Ecto.Changeset{} = Actors.change_member(member)
end
end
end

@ -32,6 +32,12 @@ defmodule Eventos.EventsTest do
describe "events" do
alias Eventos.Events.Event
setup do
actor = insert(:actor)
event = insert(:event, organizer_actor: actor)
{:ok, actor: actor, event: event}
end
@valid_attrs %{
begins_on: "2010-04-17 14:00:00.000000Z",
description: "some description",
@ -46,14 +52,32 @@ defmodule Eventos.EventsTest do
}
@invalid_attrs %{begins_on: nil, description: nil, ends_on: nil, title: nil}
test "list_events/0 returns all events" do
event = event_fixture()
assert hd(Events.list_events()).title == event.title
test "list_events/0 returns all events", %{event: event} do
assert event.title == hd(Events.list_events()).title
end
test "get_event!/1 returns the event with given id" do
event = event_fixture()
test "get_event!/1 returns the event with given id", %{event: event} do
assert Events.get_event!(event.id).title == event.title
refute Ecto.assoc_loaded?(Events.get_event!(event.id).organizer_actor)
end
test "get_event_full!/1 returns the event with given id", %{event: event} do
assert Events.get_event_full!(event.id).organizer_actor.preferred_username ==
event.organizer_actor.preferred_username
assert Events.get_event_full!(event.id).participants == []
end
test "find_events_by_name/1 returns events for a given name", %{event: event} do
assert event.title == hd(Events.find_events_by_name(event.title)).title
event2 = insert(:event, title: "Special event")
assert event2.title == hd(Events.find_events_by_name("Special")).title
event2 = insert(:event, title: "Special event")
assert event2.title == hd(Events.find_events_by_name(" Special ")).title
assert [] == Events.find_events_by_name("")
end
test "create_event/1 with valid data creates a event" do
@ -79,8 +103,7 @@ defmodule Eventos.EventsTest do
assert {:error, %Ecto.Changeset{}} = Events.create_event(@invalid_attrs)
end
test "update_event/2 with valid data updates the event" do
event = event_fixture()
test "update_event/2 with valid data updates the event", %{event: event} do
assert {:ok, event} = Events.update_event(event, @update_attrs)
assert %Event{} = event
assert event.begins_on == DateTime.from_naive!(~N[2011-05-18 15:01:01.000000Z], "Etc/UTC")
@ -89,27 +112,44 @@ defmodule Eventos.EventsTest do
assert event.title == "some updated title"
end
test "update_event/2 with invalid data returns error changeset" do
event = event_fixture()
test "update_event/2 with invalid data returns error changeset", %{event: event} do
assert {:error, %Ecto.Changeset{}} = Events.update_event(event, @invalid_attrs)
assert event.title == Events.get_event!(event.id).title
end
test "delete_event/1 deletes the event" do
event = event_fixture()
test "delete_event/1 deletes the event", %{event: event} do
assert {:ok, %Event{}} = Events.delete_event(event)
assert_raise Ecto.NoResultsError, fn -> Events.get_event!(event.id) end
end
test "change_event/1 returns a event changeset" do
event = event_fixture()
test "change_event/1 returns a event changeset", %{event: event} do
assert %Ecto.Changeset{} = Events.change_event(event)
end
test "get_events_for_actor/1", %{actor: actor, event: event} do
assert {:ok, [event_found], 1} = Events.get_events_for_actor(actor)
assert event_found.title == event.title
end
test "get_events_for_actor/3", %{actor: actor, event: event} do
event1 = insert(:event, organizer_actor: actor)
assert {:ok, [event_found, event1_found], 2} = Events.get_events_for_actor(actor, 1, 10)
end
test "get_events_for_actor/3 with limited results", %{actor: actor, event: event} do
event1 = insert(:event, organizer_actor: actor)
assert {:ok, [event_found], 2} = Events.get_events_for_actor(actor, 1, 1)
end
end
describe "categories" do
alias Eventos.Events.Category
setup do
category = insert(:category)
{:ok, category: category}
end
@valid_attrs %{description: "some description", picture: "some picture", title: "some title"}
@update_attrs %{
description: "some updated description",
@ -118,16 +158,18 @@ defmodule Eventos.EventsTest do
}