diff --git a/lib/philomena/user_links.ex b/lib/philomena/user_links.ex index d94a63a9..48e21856 100644 --- a/lib/philomena/user_links.ex +++ b/lib/philomena/user_links.ex @@ -7,6 +7,7 @@ defmodule Philomena.UserLinks do alias Philomena.Repo alias Philomena.UserLinks.UserLink + alias Philomena.Tags.Tag @doc """ Returns the list of user_links. @@ -49,9 +50,11 @@ defmodule Philomena.UserLinks do {:error, %Ecto.Changeset{}} """ - def create_user_link(attrs \\ %{}) do + def create_user_link(user, attrs \\ %{}) do + tag = Repo.get_by(Tag, name: attrs["tag_name"]) + %UserLink{} - |> UserLink.changeset(attrs) + |> UserLink.creation_changeset(attrs, user, tag) |> Repo.insert() end diff --git a/lib/philomena/user_links/user_link.ex b/lib/philomena/user_links/user_link.ex index f934ee9d..ccdc078c 100644 --- a/lib/philomena/user_links/user_link.ex +++ b/lib/philomena/user_links/user_link.ex @@ -11,14 +11,14 @@ defmodule Philomena.UserLinks.UserLink do belongs_to :contacted_by_user, User belongs_to :tag, Tag - field :aasm_state, :string + field :aasm_state, :string, default: "unverified" field :uri, :string field :hostname, :string field :path, :string field :verification_code, :string field :public, :boolean, default: true - field :next_check_at, :naive_datetime - field :contacted_at, :naive_datetime + field :next_check_at, :utc_datetime + field :contacted_at, :utc_datetime timestamps(inserted_at: :created_at) end @@ -29,4 +29,37 @@ defmodule Philomena.UserLinks.UserLink do |> cast(attrs, []) |> validate_required([]) end + + def creation_changeset(user_link, attrs, user, tag) do + user_link + |> cast(attrs, [:uri]) + |> put_assoc(:tag, tag) + |> put_assoc(:user, user) + |> validate_required([:user, :uri]) + |> parse_uri() + |> put_verification_code() + |> put_next_check_at() + end + + defp parse_uri(changeset) do + string_uri = get_field(changeset, :uri) |> to_string() + uri = URI.parse(string_uri) + + changeset + |> change(hostname: uri.host, path: uri.path) + end + + defp put_verification_code(changeset) do + code = :crypto.strong_rand_bytes(5) |> Base.encode16() + change(changeset, verification_code: "DERPI-LINKVALIDATION-#{code}") + end + + defp put_next_check_at(changeset) do + time = + DateTime.utc_now() + |> DateTime.add(60 * 2, :second) + |> DateTime.truncate(:second) + + change(changeset, next_check_at: time) + end end diff --git a/lib/philomena/users/ability.ex b/lib/philomena/users/ability.ex index 211c834d..ed2c036b 100644 --- a/lib/philomena/users/ability.ex +++ b/lib/philomena/users/ability.ex @@ -8,6 +8,7 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do alias Philomena.Posts.Post alias Philomena.Filters.Filter alias Philomena.DnpEntries.DnpEntry + alias Philomena.UserLinks.UserLink # Admins can do anything def can?(%User{role: "admin"}, _action, _model), do: true @@ -60,6 +61,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do # Edit filters they own def can?(%User{id: id}, action, %Filter{user_id: id}) when action in [:edit, :update], do: true + # View user links they've created + def can?(%User{id: id}, :show, %UserLink{user_id: id}), do: true + # View non-deleted images def can?(_user, action, Image) when action in [:new, :create, :index], diff --git a/lib/philomena_web/controllers/user_link_controller.ex b/lib/philomena_web/controllers/user_link_controller.ex new file mode 100644 index 00000000..13597df6 --- /dev/null +++ b/lib/philomena_web/controllers/user_link_controller.ex @@ -0,0 +1,45 @@ +defmodule PhilomenaWeb.UserLinkController do + use PhilomenaWeb, :controller + + alias Philomena.UserLinks + alias Philomena.UserLinks.UserLink + alias Philomena.Repo + import Ecto.Query + + plug PhilomenaWeb.FilterBannedUsersPlug when action in [:new, :create] + plug :load_and_authorize_resource, model: UserLink, only: [:show], preload: [:user, :tag, :contacted_by_user] + + def index(conn, _params) do + user = conn.assigns.current_user + user_links = + UserLink + |> where(user_id: ^user.id) + |> Repo.all() + + render(conn, "index.html", user_links: user_links) + end + + def new(conn, _params) do + changeset = UserLinks.change_user_link(%UserLink{}) + render(conn, "new.html", changeset: changeset) + end + + def create(conn, %{"user_link" => user_link_params}) do + user = conn.assigns.current_user + + case UserLinks.create_user_link(user, user_link_params) do + {:ok, user_link} -> + conn + |> put_flash(:info, "Link submitted! Please put '#{user_link.verification_code}' on your linked webpage now.") + |> redirect(to: Routes.user_link_path(conn, :show, user_link)) + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "new.html", changeset: changeset) + end + end + + def show(conn, _params) do + user_link = conn.assigns.user_link + render(conn, "show.html", user_link: user_link) + end +end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 31ffcceb..9e942f2c 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -117,6 +117,7 @@ defmodule PhilomenaWeb.Router do end resources "/reports", ReportController, only: [:index] + resources "/user_links", UserLinkController, only: [:index, :new, :create, :show] end scope "/", PhilomenaWeb do diff --git a/lib/philomena_web/templates/user_link/_form.html.slime b/lib/philomena_web/templates/user_link/_form.html.slime new file mode 100644 index 00000000..b0f69d3f --- /dev/null +++ b/lib/philomena_web/templates/user_link/_form.html.slime @@ -0,0 +1,38 @@ += form_for @changeset, @action, fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + .field + .field + p + label for="tag_name" + ' The tag, + em> specific + ' to you, usually + code> + | artist: + em artist name here + ' or a series name + p Should be blank only if your content isn't on the site, generally + = text_input f, :tag_name, class: "input", autocomplete: "off", placeholder: "artist:name", data: [ac: "true", ac_min_length: "3", ac_source: "/tags/autocomplete?term="] + .field + label for="uri" + ' URL of your art webpage + = url_input f, :uri, class: "input input--wide", placeholder: "https://www.deviantart.com/your-name"#, required: true + = error_tag f, :uri + .field + => radio_button f, :public, "true" + => label f, :public, "Visible to everyone" + .field + => radio_button f, :public, "false" + => label f, :public, "Visible only to site staff" + + h4 Instructions + p + strong Review details carefully as only site staff can edit later. + p + strong> For quick results, put the LINKVALIDATION code on your linked webpage after submission. + | We'll message you there otherwise. + .actions + = submit "Submit", class: "button" diff --git a/lib/philomena_web/templates/user_link/index.html.slime b/lib/philomena_web/templates/user_link/index.html.slime new file mode 100644 index 00000000..5c70e732 --- /dev/null +++ b/lib/philomena_web/templates/user_link/index.html.slime @@ -0,0 +1,23 @@ +h1 Your Links +p + a.button href=Routes.user_link_path(@conn, :new) + ' Create a link +p + ' User links associate your account on Derpibooru with tags about content you create and with accounts on sites elsewhere. This allows users to easily identify artists and admins to act more rapidly on takedown requests. + +table.table + thead + tr + th URI + th Options + th Verification Code + th Verified? + th Public + tbody + = for link <- @user_links do + tr + td = link link.uri, to: link.uri + td = link "View Details", to: Routes.user_link_path(@conn, :show, link) + td = link.verification_code + th = verified_as_string(link) + th = public_as_string(link) diff --git a/lib/philomena_web/templates/user_link/new.html.slime b/lib/philomena_web/templates/user_link/new.html.slime new file mode 100644 index 00000000..b6bd683c --- /dev/null +++ b/lib/philomena_web/templates/user_link/new.html.slime @@ -0,0 +1,2 @@ +h1 Create Link += render PhilomenaWeb.UserLinkView, "_form.html", changeset: @changeset, action: Routes.user_link_path(@conn, :create) \ No newline at end of file diff --git a/lib/philomena_web/templates/user_link/show.html.slime b/lib/philomena_web/templates/user_link/show.html.slime new file mode 100644 index 00000000..a15761b9 --- /dev/null +++ b/lib/philomena_web/templates/user_link/show.html.slime @@ -0,0 +1,59 @@ +h1 + ' Link to + = link @user_link.uri, to: @user_link.uri + +h3 Status += cond do + - verified?(@user_link) -> + p This link has been verified by a member of the administration team. + p You can now remove the verification text from your website if you have not done so already. + + - contacted?(@user_link) -> + p + strong This link is awaiting your reply on the linked website in order to be verified. + p + ' An administrator + => "(#{@user_link.contacted_by_user.name})" + ' has manually contacted you at the address above, as your verification code was not found on the website. Please respond to the message from the administrator to confirm your link. + p The verification code is: + p + code + h1 = @user_link.verification_code + + - link_verified?(@user_link) -> + p + strong This link is pending verification by a member of the administration team. + p We've now found the verification code on your website. An administrator still needs to check the tag list before verifying the link. Please leave the code on your website until verification is complete. + p If you need it again, your verification code is: + p + code + h1 = @user_link.verification_code + + - unverified?(@user_link) -> + p + strong This link is pending verification by a member of the administration team. + p + h3 To have your link verified as fast as possible, please place this text somewhere on the page you are linking. + p + code + h1 = @user_link.verification_code + p Otherwise, an administrator will have to contact you to verify your identity. + p Once the link has been verified you can remove the text; the text simply allows the team to directly check with your website rather than messaging you and waiting for a reply. + + - rejected?(@user_link) -> + p This link has been rejected by a member of the administration team; this is probably because you were not reachable in a timely manner (~1 week) to verify the link. + +h3 Visibility += if public?(@user_link) do + p This link is public, and will be shown around the site. +- else + p This link is not public, and will only be shown to administrators. + +h3 Associated tag += if @user_link.tag do + p + = render PhilomenaWeb.TagView, "_tag.html", tag: @user_link.tag +- else + p There is no tag associated with this link. + += link "Back", to: Routes.user_link_path(@conn, :index) diff --git a/lib/philomena_web/views/user_link_view.ex b/lib/philomena_web/views/user_link_view.ex new file mode 100644 index 00000000..49572b32 --- /dev/null +++ b/lib/philomena_web/views/user_link_view.ex @@ -0,0 +1,17 @@ +defmodule PhilomenaWeb.UserLinkView do + use PhilomenaWeb, :view + + def verified?(%{aasm_state: state}), do: state == "verified" + def contacted?(%{aasm_state: state}), do: state == "contacted" + def link_verified?(%{aasm_state: state}), do: state == "link_verified" + def unverified?(%{aasm_state: state}), do: state == "unverified" + def rejected?(%{aasm_state: state}), do: state == "rejected" + + def public?(%{public: public}), do: !!public + + def verified_as_string(%{aasm_state: "verified"}), do: "Yes" + def verified_as_string(_user_link), do: "No" + + def public_as_string(%{public: true}), do: "Yes" + def public_as_string(_user_link), do: "No" +end diff --git a/test/philomena_web/controllers/user_link_controller_test.exs b/test/philomena_web/controllers/user_link_controller_test.exs new file mode 100644 index 00000000..7c0ca68a --- /dev/null +++ b/test/philomena_web/controllers/user_link_controller_test.exs @@ -0,0 +1,88 @@ +defmodule PhilomenaWeb.UserLinkControllerTest do + use PhilomenaWeb.ConnCase + + alias Philomena.UserLinks + + @create_attrs %{} + @update_attrs %{} + @invalid_attrs %{} + + def fixture(:user_link) do + {:ok, user_link} = UserLinks.create_user_link(@create_attrs) + user_link + end + + describe "index" do + test "lists all user_links", %{conn: conn} do + conn = get(conn, Routes.user_link_path(conn, :index)) + assert html_response(conn, 200) =~ "Listing User links" + end + end + + describe "new user_link" do + test "renders form", %{conn: conn} do + conn = get(conn, Routes.user_link_path(conn, :new)) + assert html_response(conn, 200) =~ "New User link" + end + end + + describe "create user_link" do + test "redirects to show when data is valid", %{conn: conn} do + conn = post(conn, Routes.user_link_path(conn, :create), user_link: @create_attrs) + + assert %{id: id} = redirected_params(conn) + assert redirected_to(conn) == Routes.user_link_path(conn, :show, id) + + conn = get(conn, Routes.user_link_path(conn, :show, id)) + assert html_response(conn, 200) =~ "Show User link" + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = post(conn, Routes.user_link_path(conn, :create), user_link: @invalid_attrs) + assert html_response(conn, 200) =~ "New User link" + end + end + + describe "edit user_link" do + setup [:create_user_link] + + test "renders form for editing chosen user_link", %{conn: conn, user_link: user_link} do + conn = get(conn, Routes.user_link_path(conn, :edit, user_link)) + assert html_response(conn, 200) =~ "Edit User link" + end + end + + describe "update user_link" do + setup [:create_user_link] + + test "redirects when data is valid", %{conn: conn, user_link: user_link} do + conn = put(conn, Routes.user_link_path(conn, :update, user_link), user_link: @update_attrs) + assert redirected_to(conn) == Routes.user_link_path(conn, :show, user_link) + + conn = get(conn, Routes.user_link_path(conn, :show, user_link)) + assert html_response(conn, 200) + end + + test "renders errors when data is invalid", %{conn: conn, user_link: user_link} do + conn = put(conn, Routes.user_link_path(conn, :update, user_link), user_link: @invalid_attrs) + assert html_response(conn, 200) =~ "Edit User link" + end + end + + describe "delete user_link" do + setup [:create_user_link] + + test "deletes chosen user_link", %{conn: conn, user_link: user_link} do + conn = delete(conn, Routes.user_link_path(conn, :delete, user_link)) + assert redirected_to(conn) == Routes.user_link_path(conn, :index) + assert_error_sent 404, fn -> + get(conn, Routes.user_link_path(conn, :show, user_link)) + end + end + end + + defp create_user_link(_) do + user_link = fixture(:user_link) + {:ok, user_link: user_link} + end +end