mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
Merge pull request #300 from philomena-dev/ad-extraction
Ads logic cleanup
This commit is contained in:
commit
1a7fbb29eb
7 changed files with 233 additions and 85 deletions
|
@ -7,53 +7,88 @@ defmodule Philomena.Adverts do
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
|
|
||||||
alias Philomena.Adverts.Advert
|
alias Philomena.Adverts.Advert
|
||||||
|
alias Philomena.Adverts.Restrictions
|
||||||
|
alias Philomena.Adverts.Server
|
||||||
alias Philomena.Adverts.Uploader
|
alias Philomena.Adverts.Uploader
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets an advert that is currently live.
|
||||||
|
|
||||||
|
Returns the advert, or nil if nothing was live.
|
||||||
|
|
||||||
|
iex> random_live()
|
||||||
|
nil
|
||||||
|
|
||||||
|
iex> random_live()
|
||||||
|
%Advert{}
|
||||||
|
|
||||||
|
"""
|
||||||
def random_live do
|
def random_live do
|
||||||
|
random_live_for_tags([])
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets an advert that is currently live, matching any tagging restrictions
|
||||||
|
for the given image.
|
||||||
|
|
||||||
|
Returns the advert, or nil if nothing was live.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> random_live(%Image{})
|
||||||
|
nil
|
||||||
|
|
||||||
|
iex> random_live(%Image{})
|
||||||
|
%Advert{}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def random_live(image) do
|
||||||
|
image
|
||||||
|
|> Repo.preload(:tags)
|
||||||
|
|> Map.get(:tags)
|
||||||
|
|> Enum.map(& &1.name)
|
||||||
|
|> random_live_for_tags()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp random_live_for_tags(tags) do
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
|
restrictions = Restrictions.tags(tags)
|
||||||
|
|
||||||
Advert
|
query =
|
||||||
|> where(live: true, restrictions: "none")
|
from a in Advert,
|
||||||
|> where([a], a.start_date < ^now and a.finish_date > ^now)
|
where: a.live == true,
|
||||||
|> order_by(asc: fragment("random()"))
|
where: a.restrictions in ^restrictions,
|
||||||
|> limit(1)
|
where: a.start_date < ^now and a.finish_date > ^now,
|
||||||
|> Repo.one()
|
order_by: [asc: fragment("random()")],
|
||||||
|
limit: 1
|
||||||
|
|
||||||
|
Repo.one(query)
|
||||||
end
|
end
|
||||||
|
|
||||||
def random_live_for(image) do
|
@doc """
|
||||||
image = Repo.preload(image, :tags)
|
Asynchronously records a new impression.
|
||||||
now = DateTime.utc_now()
|
|
||||||
|
|
||||||
Advert
|
## Example
|
||||||
|> where(live: true)
|
|
||||||
|> where([a], a.restrictions in ^restrictions(image))
|
iex> record_impression(%Advert{})
|
||||||
|> where([a], a.start_date < ^now and a.finish_date > ^now)
|
:ok
|
||||||
|> order_by(asc: fragment("random()"))
|
|
||||||
|> limit(1)
|
"""
|
||||||
|> Repo.one()
|
def record_impression(%Advert{id: id}) do
|
||||||
|
Server.record_impression(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sfw?(image) do
|
@doc """
|
||||||
image_tags = MapSet.new(image.tags |> Enum.map(& &1.name))
|
Asynchronously records a new click.
|
||||||
sfw_tags = MapSet.new(["safe", "suggestive"])
|
|
||||||
intersect = MapSet.intersection(image_tags, sfw_tags)
|
|
||||||
|
|
||||||
MapSet.size(intersect) > 0
|
## Example
|
||||||
end
|
|
||||||
|
|
||||||
defp nsfw?(image) do
|
iex> record_click(%Advert{})
|
||||||
image_tags = MapSet.new(image.tags |> Enum.map(& &1.name))
|
:ok
|
||||||
nsfw_tags = MapSet.new(["questionable", "explicit"])
|
|
||||||
intersect = MapSet.intersection(image_tags, nsfw_tags)
|
|
||||||
|
|
||||||
MapSet.size(intersect) > 0
|
"""
|
||||||
end
|
def record_click(%Advert{id: id}) do
|
||||||
|
Server.record_click(id)
|
||||||
defp restrictions(image) do
|
|
||||||
restrictions = ["none"]
|
|
||||||
restrictions = if nsfw?(image), do: ["nsfw" | restrictions], else: restrictions
|
|
||||||
restrictions = if sfw?(image), do: ["sfw" | restrictions], else: restrictions
|
|
||||||
restrictions
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -102,7 +137,7 @@ defmodule Philomena.Adverts do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates an advert.
|
Updates an Advert without updating its image.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -119,6 +154,18 @@ defmodule Philomena.Adverts do
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates the image for an Advert.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> update_advert_image(advert, %{image: new_value})
|
||||||
|
{:ok, %Advert{}}
|
||||||
|
|
||||||
|
iex> update_advert(advert, %{image: bad_value})
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
def update_advert_image(%Advert{} = advert, attrs) do
|
def update_advert_image(%Advert{} = advert, attrs) do
|
||||||
advert
|
advert
|
||||||
|> Advert.changeset(attrs)
|
|> Advert.changeset(attrs)
|
||||||
|
|
|
@ -1,33 +1,9 @@
|
||||||
defmodule PhilomenaWeb.AdvertUpdater do
|
defmodule Philomena.Adverts.Recorder do
|
||||||
alias Philomena.Adverts.Advert
|
alias Philomena.Adverts.Advert
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
def child_spec([]) do
|
def run(%{impressions: impressions, clicks: clicks}) do
|
||||||
%{
|
|
||||||
id: PhilomenaWeb.AdvertUpdater,
|
|
||||||
start: {PhilomenaWeb.AdvertUpdater, :start_link, [[]]}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def start_link([]) do
|
|
||||||
{:ok, spawn_link(&init/0)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def cast(type, advert_id) when type in [:impression, :click] do
|
|
||||||
pid = Process.whereis(:advert_updater)
|
|
||||||
if pid, do: send(pid, {type, advert_id})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp init do
|
|
||||||
Process.register(self(), :advert_updater)
|
|
||||||
run()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp run do
|
|
||||||
# Read impression counts from mailbox
|
|
||||||
{impressions, clicks} = receive_all()
|
|
||||||
|
|
||||||
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
||||||
|
|
||||||
# Create insert statements for Ecto
|
# Create insert statements for Ecto
|
||||||
|
@ -41,24 +17,7 @@ defmodule PhilomenaWeb.AdvertUpdater do
|
||||||
Repo.insert_all(Advert, impressions, on_conflict: impressions_update, conflict_target: [:id])
|
Repo.insert_all(Advert, impressions, on_conflict: impressions_update, conflict_target: [:id])
|
||||||
Repo.insert_all(Advert, clicks, on_conflict: clicks_update, conflict_target: [:id])
|
Repo.insert_all(Advert, clicks, on_conflict: clicks_update, conflict_target: [:id])
|
||||||
|
|
||||||
:timer.sleep(:timer.seconds(10))
|
:ok
|
||||||
|
|
||||||
run()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp receive_all(impressions \\ %{}, clicks \\ %{}) do
|
|
||||||
receive do
|
|
||||||
{:impression, advert_id} ->
|
|
||||||
impressions = Map.update(impressions, advert_id, 1, &(&1 + 1))
|
|
||||||
receive_all(impressions, clicks)
|
|
||||||
|
|
||||||
{:click, advert_id} ->
|
|
||||||
clicks = Map.update(clicks, advert_id, 1, &(&1 + 1))
|
|
||||||
receive_all(impressions, clicks)
|
|
||||||
after
|
|
||||||
0 ->
|
|
||||||
{impressions, clicks}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp impressions_insert_all({advert_id, impressions}, now) do
|
defp impressions_insert_all({advert_id, impressions}, now) do
|
47
lib/philomena/adverts/restrictions.ex
Normal file
47
lib/philomena/adverts/restrictions.ex
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
defmodule Philomena.Adverts.Restrictions do
|
||||||
|
@moduledoc """
|
||||||
|
Advert restriction application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@type restriction :: String.t()
|
||||||
|
@type restriction_list :: [restriction()]
|
||||||
|
@type tag_list :: [String.t()]
|
||||||
|
|
||||||
|
@nsfw_tags MapSet.new(["questionable", "explicit"])
|
||||||
|
@sfw_tags MapSet.new(["safe", "suggestive"])
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Calculates the restrictions available to a given tag list.
|
||||||
|
|
||||||
|
Returns a list containing `"none"`, and neither or one of `"sfw"`, `"nsfw"`.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> tags([])
|
||||||
|
["none"]
|
||||||
|
|
||||||
|
iex> tags(["safe"])
|
||||||
|
["sfw", "none"]
|
||||||
|
|
||||||
|
iex> tags(["explicit"])
|
||||||
|
["nsfw", "none"]
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec tags(tag_list()) :: restriction_list()
|
||||||
|
def tags(tags) do
|
||||||
|
tags = MapSet.new(tags)
|
||||||
|
|
||||||
|
["none"]
|
||||||
|
|> apply_if(tags, @nsfw_tags, "nsfw")
|
||||||
|
|> apply_if(tags, @sfw_tags, "sfw")
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec apply_if(restriction_list(), MapSet.t(), MapSet.t(), restriction()) :: restriction_list()
|
||||||
|
defp apply_if(restrictions, tags, test, new_restriction) do
|
||||||
|
if MapSet.disjoint?(tags, test) do
|
||||||
|
restrictions
|
||||||
|
else
|
||||||
|
[new_restriction | restrictions]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
94
lib/philomena/adverts/server.ex
Normal file
94
lib/philomena/adverts/server.ex
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
defmodule Philomena.Adverts.Server do
|
||||||
|
@moduledoc """
|
||||||
|
Advert impression and click aggregator.
|
||||||
|
|
||||||
|
Updating the impression count for adverts and clicks on every pageload is unnecessary
|
||||||
|
and slows down requests. This module collects the adverts and clicks and submits a batch
|
||||||
|
of updates to the database after every 10 seconds asynchronously, reducing the amount of
|
||||||
|
work to be done.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
alias Philomena.Adverts.Recorder
|
||||||
|
|
||||||
|
@type advert_id :: integer()
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts the GenServer.
|
||||||
|
|
||||||
|
See `GenServer.start_link/2` for more information.
|
||||||
|
"""
|
||||||
|
def start_link(_) do
|
||||||
|
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Asynchronously records a new impression.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> record_impression(advert.id)
|
||||||
|
:ok
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec record_impression(advert_id()) :: :ok
|
||||||
|
def record_impression(advert_id) do
|
||||||
|
GenServer.cast(__MODULE__, {:impressions, advert_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Asynchronously records a new click.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> record_click(advert.id)
|
||||||
|
:ok
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec record_click(advert_id()) :: :ok
|
||||||
|
def record_click(advert_id) do
|
||||||
|
GenServer.cast(__MODULE__, {:clicks, advert_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
# Used to force the GenServer to immediately sleep when no
|
||||||
|
# messages are available.
|
||||||
|
@timeout 0
|
||||||
|
@sleep :timer.seconds(10)
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
@doc false
|
||||||
|
def init(_) do
|
||||||
|
{:ok, initial_state(), @timeout}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
@doc false
|
||||||
|
def handle_cast({type, advert_id}, state) do
|
||||||
|
# Update the counter described by the message
|
||||||
|
state = update_in(state[type], &increment_counter(&1, advert_id))
|
||||||
|
|
||||||
|
# Return to GenServer event loop
|
||||||
|
{:noreply, state, @timeout}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
@doc false
|
||||||
|
def handle_info(:timeout, state) do
|
||||||
|
# Process all updates from state now
|
||||||
|
Recorder.run(state)
|
||||||
|
|
||||||
|
# Sleep for the specified delay
|
||||||
|
:timer.sleep(@sleep)
|
||||||
|
|
||||||
|
# Return to GenServer event loop
|
||||||
|
{:noreply, initial_state(), @timeout}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp increment_counter(map, advert_id) do
|
||||||
|
Map.update(map, advert_id, 1, &(&1 + 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp initial_state do
|
||||||
|
%{impressions: %{}, clicks: %{}}
|
||||||
|
end
|
||||||
|
end
|
|
@ -28,8 +28,10 @@ defmodule Philomena.Application do
|
||||||
node_name: valid_node_name(node())
|
node_name: valid_node_name(node())
|
||||||
]},
|
]},
|
||||||
|
|
||||||
|
# Advert update batching
|
||||||
|
Philomena.Adverts.Server,
|
||||||
|
|
||||||
# Start the endpoint when the application starts
|
# Start the endpoint when the application starts
|
||||||
PhilomenaWeb.AdvertUpdater,
|
|
||||||
PhilomenaWeb.UserFingerprintUpdater,
|
PhilomenaWeb.UserFingerprintUpdater,
|
||||||
PhilomenaWeb.UserIpUpdater,
|
PhilomenaWeb.UserIpUpdater,
|
||||||
PhilomenaWeb.Endpoint
|
PhilomenaWeb.Endpoint
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
defmodule PhilomenaWeb.AdvertController do
|
defmodule PhilomenaWeb.AdvertController do
|
||||||
use PhilomenaWeb, :controller
|
use PhilomenaWeb, :controller
|
||||||
|
|
||||||
alias PhilomenaWeb.AdvertUpdater
|
|
||||||
alias Philomena.Adverts.Advert
|
alias Philomena.Adverts.Advert
|
||||||
|
alias Philomena.Adverts
|
||||||
|
|
||||||
plug :load_resource, model: Advert
|
plug :load_resource, model: Advert
|
||||||
|
|
||||||
def show(conn, _params) do
|
def show(conn, _params) do
|
||||||
advert = conn.assigns.advert
|
advert = conn.assigns.advert
|
||||||
|
|
||||||
AdvertUpdater.cast(:click, advert.id)
|
Adverts.record_click(advert)
|
||||||
|
|
||||||
redirect(conn, external: advert.link)
|
redirect(conn, external: advert.link)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
defmodule PhilomenaWeb.AdvertPlug do
|
defmodule PhilomenaWeb.AdvertPlug do
|
||||||
alias PhilomenaWeb.AdvertUpdater
|
|
||||||
alias Philomena.Adverts
|
alias Philomena.Adverts
|
||||||
alias Plug.Conn
|
alias Plug.Conn
|
||||||
|
|
||||||
|
@ -19,7 +18,7 @@ defmodule PhilomenaWeb.AdvertPlug do
|
||||||
do: Conn.assign(conn, :advert, record_impression(Adverts.random_live()))
|
do: Conn.assign(conn, :advert, record_impression(Adverts.random_live()))
|
||||||
|
|
||||||
defp maybe_assign_ad(conn, image, true),
|
defp maybe_assign_ad(conn, image, true),
|
||||||
do: Conn.assign(conn, :advert, record_impression(Adverts.random_live_for(image)))
|
do: Conn.assign(conn, :advert, record_impression(Adverts.random_live(image)))
|
||||||
|
|
||||||
defp maybe_assign_ad(conn, _image, _false),
|
defp maybe_assign_ad(conn, _image, _false),
|
||||||
do: Conn.assign(conn, :advert, nil)
|
do: Conn.assign(conn, :advert, nil)
|
||||||
|
@ -33,7 +32,7 @@ defmodule PhilomenaWeb.AdvertPlug do
|
||||||
defp record_impression(nil), do: nil
|
defp record_impression(nil), do: nil
|
||||||
|
|
||||||
defp record_impression(advert) do
|
defp record_impression(advert) do
|
||||||
AdvertUpdater.cast(:impression, advert.id)
|
Adverts.record_impression(advert)
|
||||||
|
|
||||||
advert
|
advert
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue