Merge pull request #302 from philomena-dev/artist-link-extraction

Artist links logic cleanup
This commit is contained in:
liamwhite 2024-06-24 16:47:23 -04:00 committed by GitHub
commit 965e660e8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 204 additions and 69 deletions

View file

@ -9,39 +9,19 @@ defmodule Philomena.ArtistLinks do
alias Philomena.ArtistLinks.ArtistLink
alias Philomena.ArtistLinks.AutomaticVerifier
alias Philomena.Badges.Badge
alias Philomena.Badges.Award
alias Philomena.Tags.Tag
alias Philomena.ArtistLinks.BadgeAwarder
alias Philomena.Tags
@doc """
Check links pending verification to see if the user placed
the appropriate code on the page.
Updates all artist links pending verification, by transitioning to link verified state
or resetting next update time.
"""
def automatic_verify! do
now = DateTime.utc_now() |> DateTime.truncate(:second)
# Automatically retry in an hour if we don't manage to
# successfully verify any given link
recheck_time = DateTime.add(now, 3600, :second)
recheck_query =
from ul in ArtistLink,
where: ul.aasm_state == "unverified",
where: ul.next_check_at < ^now
recheck_query
|> Repo.all()
|> Enum.map(fn link ->
ArtistLink.automatic_verify_changeset(
link,
AutomaticVerifier.check_link(link, recheck_time)
)
end)
|> Enum.map(&Repo.update!/1)
Enum.each(AutomaticVerifier.generate_updates(), &Repo.update!/1)
end
@doc """
Gets a single artist_link.
Gets a single artist link.
Raises `Ecto.NoResultsError` if the Artist link does not exist.
@ -57,7 +37,7 @@ defmodule Philomena.ArtistLinks do
def get_artist_link!(id), do: Repo.get!(ArtistLink, id)
@doc """
Creates a artist_link.
Creates an artist link.
## Examples
@ -69,7 +49,7 @@ defmodule Philomena.ArtistLinks do
"""
def create_artist_link(user, attrs \\ %{}) do
tag = fetch_tag(attrs["tag_name"])
tag = Tags.get_tag_or_alias_by_name(attrs["tag_name"])
%ArtistLink{}
|> ArtistLink.creation_changeset(attrs, user, tag)
@ -77,7 +57,7 @@ defmodule Philomena.ArtistLinks do
end
@doc """
Updates a artist_link.
Updates an artist link.
## Examples
@ -89,47 +69,71 @@ defmodule Philomena.ArtistLinks do
"""
def update_artist_link(%ArtistLink{} = artist_link, attrs) do
tag = fetch_tag(attrs["tag_name"])
tag = Tags.get_tag_or_alias_by_name(attrs["tag_name"])
artist_link
|> ArtistLink.edit_changeset(attrs, tag)
|> Repo.update()
end
def verify_artist_link(%ArtistLink{} = artist_link, user) do
artist_link_changeset =
artist_link
|> ArtistLink.verify_changeset(user)
@doc """
Transitions an artist link to the verified state.
## Examples
iex> verify_artist_link(artist_link, verifying_user)
{:ok, %ArtistLink{}}
iex> verify_artist_link(artist_link, verifying_user)
:error
"""
def verify_artist_link(%ArtistLink{} = artist_link, verifying_user) do
artist_link_changeset = ArtistLink.verify_changeset(artist_link, verifying_user)
Multi.new()
|> Multi.update(:artist_link, artist_link_changeset)
|> Multi.run(:add_award, fn repo, _changes ->
now = DateTime.utc_now() |> DateTime.truncate(:second)
with badge when not is_nil(badge) <- repo.get_by(limit(Badge, 1), title: "Artist"),
nil <- repo.get_by(limit(Award, 1), badge_id: badge.id, user_id: artist_link.user_id) do
%Award{
badge_id: badge.id,
user_id: artist_link.user_id,
awarded_by_id: user.id,
awarded_on: now
}
|> Award.changeset(%{})
|> repo.insert()
else
_ ->
{:ok, nil}
end
end)
|> Multi.run(:add_award, fn _repo, _changes -> BadgeAwarder.award_badge(artist_link) end)
|> Repo.transaction()
|> case do
{:ok, %{artist_link: artist_link}} ->
{:ok, artist_link}
{:error, _operation, _value, _changes} ->
:error
end
end
@doc """
Transitions an artist link to the rejected state.
## Examples
iex> reject_artist_link(artist_link)
{:ok, %ArtistLink{}}
iex> reject_artist_link(artist_link)
{:error, %Ecto.Changeset{}}
"""
def reject_artist_link(%ArtistLink{} = artist_link) do
artist_link
|> ArtistLink.reject_changeset()
|> Repo.update()
end
@doc """
Transitions an artist link to the contacted state.
## Examples
iex> contact_artist_link(artist_link)
{:ok, %ArtistLink{}}
iex> contact_artist_link(artist_link)
{:error, %Ecto.Changeset{}}
"""
def contact_artist_link(%ArtistLink{} = artist_link, user) do
artist_link
|> ArtistLink.contact_changeset(user)
@ -137,7 +141,7 @@ defmodule Philomena.ArtistLinks do
end
@doc """
Deletes a ArtistLink.
Deletes an artist link.
## Examples
@ -153,7 +157,7 @@ defmodule Philomena.ArtistLinks do
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking artist_link changes.
Returns an `%Ecto.Changeset{}` for tracking artist link changes.
## Examples
@ -165,24 +169,26 @@ defmodule Philomena.ArtistLinks do
ArtistLink.changeset(artist_link, %{})
end
@doc """
Counts the number of artist links which are pending moderation action, or
nil if the user is not permitted to moderate artist links.
## Examples
iex> count_artist_links(normal_user)
nil
iex> count_artist_links(admin_user)
0
"""
def count_artist_links(user) do
if Canada.Can.can?(user, :index, %ArtistLink{}) do
ArtistLink
|> where([ul], ul.aasm_state in ^["unverified", "link_verified"])
|> Repo.aggregate(:count, :id)
|> Repo.aggregate(:count)
else
nil
end
end
defp fetch_tag(name) do
Tag
|> preload(:aliased_tag)
|> where(name: ^name)
|> Repo.one()
|> case do
nil -> nil
tag -> tag.aliased_tag || tag
end
end
end

View file

@ -1,5 +1,47 @@
defmodule Philomena.ArtistLinks.AutomaticVerifier do
def check_link(artist_link, recheck_time) do
@moduledoc """
Artist link automatic verification.
Artist links contain a random code which is generated when the link is created. If the user
places the code on their linked page and this verifier finds it, this expedites the process
of verifying a link for the moderator, as they can simply use the presence of the code in a
field controlled by the artist to ascertain the validity of the artist link.
"""
alias Philomena.ArtistLinks.ArtistLink
alias Philomena.Repo
import Ecto.Query
@doc """
Check links pending verification to see if the user placed the appropriate code on the page.
Polls each artist link in unverified state and generates a changeset to either set it to
link verified, if the code was found on the page, or reset the next check time, if the code
was not found.
Returns a list of changesets with updated links.
"""
def generate_updates do
# Automatically retry in an hour if we don't manage to
# successfully verify any given link
now = DateTime.utc_now(:second)
recheck_time = DateTime.add(now, 3600, :second)
Enum.map(links_to_check(now), fn link ->
ArtistLink.automatic_verify_changeset(link, check_link(link, recheck_time))
end)
end
defp links_to_check(now) do
recheck_query =
from ul in ArtistLink,
where: ul.aasm_state == "unverified",
where: ul.next_check_at < ^now
Repo.all(recheck_query)
end
defp check_link(artist_link, recheck_time) do
artist_link.uri
|> PhilomenaProxy.Http.get()
|> contains_verification_code?(artist_link.verification_code)

View file

@ -0,0 +1,28 @@
defmodule Philomena.ArtistLinks.BadgeAwarder do
@moduledoc """
Handles awarding a badge to the user of an associated artist link.
"""
alias Philomena.Badges
@badge_title "Artist"
@doc """
Awards a badge to an artist with a verified link.
If the badge with the title `"Artist"` does not exist, no award will be created.
If the user already has an award with that badge title, no award will be created.
Returns `{:ok, award}`, `{:ok, nil}`, or `{:error, changeset}`. The return value is
suitable for use as the return value to an `Ecto.Multi.run/3` callback.
"""
def award_badge(artist_link) do
with badge when not is_nil(badge) <- Badges.get_badge_by_title(@badge_title),
award when is_nil(award) <- Badges.get_badge_award_for(badge, artist_link.user) do
Badges.create_badge_award(artist_link.user, artist_link.user, %{badge_id: badge.id})
else
_ ->
{:ok, nil}
end
end
end

View file

@ -38,6 +38,22 @@ defmodule Philomena.Badges do
"""
def get_badge!(id), do: Repo.get!(Badge, id)
@doc """
Gets a single badge by its title.
Returns nil if the Badge does not exist.
## Examples
iex> get_badge_by_title("Artist")
%Badge{}
iex> get_badge_by_title("Nonexistent")
nil
"""
def get_badge_by_title(title), do: Repo.get_by(Badge, title: title)
@doc """
Creates a badge.
@ -162,6 +178,24 @@ defmodule Philomena.Badges do
"""
def get_badge_award!(id), do: Repo.get!(Award, id)
@doc """
Gets a the badge_award with the given badge type belonging to the user.
Raises nil if the Badge award does not exist.
## Examples
iex> get_badge_award_for(badge, user)
%Award{}
iex> get_badge_award_for(badge, user)
nil
"""
def get_badge_award_for(badge, user) do
Repo.get_by(Award, badge_id: badge.id, user_id: user.id)
end
@doc """
Creates a badge_award.

View file

@ -81,6 +81,31 @@ defmodule Philomena.Tags do
"""
def get_tag!(id), do: Repo.get!(Tag, id)
@doc """
Gets a single tag by its name, or the tag it is aliased to, if it is aliased.
Returns nil if the tag does not exist.
## Examples
iex> get_tag_or_alias_by_name("safe")
%Tag{}
iex> get_tag_or_alias_by_name("nonexistent")
nil
"""
def get_tag_or_alias_by_name(name) do
Tag
|> where(name: ^name)
|> preload(:aliased_tag)
|> Repo.one()
|> case do
nil -> nil
tag -> tag.aliased_tag || tag
end
end
@doc """
Creates a tag.

View file

@ -13,12 +13,12 @@ defmodule PhilomenaWeb.Admin.ArtistLink.VerificationController do
preload: [:user]
def create(conn, _params) do
{:ok, result} =
{:ok, artist_link} =
ArtistLinks.verify_artist_link(conn.assigns.artist_link, conn.assigns.current_user)
conn
|> put_flash(:info, "Artist link successfully verified.")
|> moderation_log(details: &log_details/2, data: result.artist_link)
|> moderation_log(details: &log_details/2, data: artist_link)
|> redirect(to: ~p"/admin/artist_links")
end