Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
liamwhite committed Jul 20, 2024
2 parents 3615859 + 844e0a3 commit e9fab5b
Show file tree
Hide file tree
Showing 16 changed files with 410 additions and 75 deletions.
70 changes: 67 additions & 3 deletions lib/philomena/duplicate_reports.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ defmodule Philomena.DuplicateReports do
alias Philomena.Repo

alias Philomena.DuplicateReports.DuplicateReport
alias Philomena.DuplicateReports.SearchQuery
alias Philomena.DuplicateReports.Uploader
alias Philomena.ImageIntensities.ImageIntensity
alias Philomena.Images.Image
alias Philomena.Images

def generate_reports(source) do
source = Repo.preload(source, :intensity)

duplicates_of(source.intensity, source.image_aspect_ratio, 0.2, 0.05)
{source.intensity, source.image_aspect_ratio}
|> find_duplicates(dist: 0.2)
|> where([i, _it], i.id != ^source.id)
|> Repo.all()
|> Enum.map(fn target ->
Expand All @@ -25,7 +28,11 @@ defmodule Philomena.DuplicateReports do
end)
end

def duplicates_of(intensities, aspect_ratio, dist \\ 0.25, aspect_dist \\ 0.05) do
def find_duplicates({intensities, aspect_ratio}, opts \\ []) do
aspect_dist = Keyword.get(opts, :aspect_dist, 0.05)
limit = Keyword.get(opts, :limit, 10)
dist = Keyword.get(opts, :dist, 0.25)

# for each color channel
dist = dist * 3

Expand All @@ -39,7 +46,64 @@ defmodule Philomena.DuplicateReports do
where:
i.image_aspect_ratio >= ^(aspect_ratio - aspect_dist) and
i.image_aspect_ratio <= ^(aspect_ratio + aspect_dist),
limit: 10
limit: ^limit
end

@doc """
Executes the reverse image search query from parameters.
## Examples
iex> execute_search_query(%{"image" => ..., "distance" => "0.25"})
{:ok, [%Image{...}, ....]}
iex> execute_search_query(%{"image" => ..., "distance" => "asdf"})
{:error, %Ecto.Changeset{}}
"""
def execute_search_query(attrs \\ %{}) do
%SearchQuery{}
|> SearchQuery.changeset(attrs)
|> Uploader.analyze_upload(attrs)
|> Ecto.Changeset.apply_action(:create)
|> case do
{:ok, search_query} ->
intensities = generate_intensities(search_query)
aspect = search_query.image_aspect_ratio
limit = search_query.limit
dist = search_query.distance

images =
{intensities, aspect}
|> find_duplicates(dist: dist, aspect_dist: dist, limit: limit)
|> preload([:user, :intensity, [:sources, tags: :aliases]])
|> Repo.all()

{:ok, images}

error ->
error
end
end

defp generate_intensities(search_query) do
analysis = SearchQuery.to_analysis(search_query)
file = search_query.uploaded_image

PhilomenaMedia.Processors.intensities(analysis, file)
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking search query changes.
## Examples
iex> change_search_query(search_query)
%Ecto.Changeset{source: %SearchQuery{}}
"""
def change_search_query(%SearchQuery{} = search_query) do
SearchQuery.changeset(search_query)
end

@doc """
Expand Down
59 changes: 59 additions & 0 deletions lib/philomena/duplicate_reports/search_query.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule Philomena.DuplicateReports.SearchQuery do
use Ecto.Schema
import Ecto.Changeset

embedded_schema do
field :distance, :float, default: 0.25
field :limit, :integer, default: 10

field :image_width, :integer
field :image_height, :integer
field :image_format, :string
field :image_duration, :float
field :image_mime_type, :string
field :image_is_animated, :boolean
field :image_aspect_ratio, :float
field :uploaded_image, :string, virtual: true
end

@doc false
def changeset(search_query, attrs \\ %{}) do
search_query
|> cast(attrs, [:distance, :limit])
|> validate_number(:distance, greater_than_or_equal_to: 0, less_than_or_equal_to: 1)
|> validate_number(:limit, greater_than_or_equal_to: 1, less_than_or_equal_to: 50)
end

@doc false
def image_changeset(search_query, attrs \\ %{}) do
search_query
|> cast(attrs, [
:image_width,
:image_height,
:image_format,
:image_duration,
:image_mime_type,
:image_is_animated,
:image_aspect_ratio,
:uploaded_image
])
|> validate_number(:image_width, greater_than: 0)
|> validate_number(:image_height, greater_than: 0)
|> validate_inclusion(
:image_mime_type,
~W(image/gif image/jpeg image/png image/svg+xml video/webm),
message: "(#{attrs["image_mime_type"]}) is invalid"
)
end

@doc false
def to_analysis(search_query) do
%PhilomenaMedia.Analyzers.Result{
animated?: search_query.image_is_animated,
dimensions: {search_query.image_width, search_query.image_height},
duration: search_query.image_duration,
extension: search_query.image_format,
mime_type: search_query.image_mime_type
}
end
end
17 changes: 17 additions & 0 deletions lib/philomena/duplicate_reports/uploader.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Philomena.DuplicateReports.Uploader do
@moduledoc """
Upload and processing callback logic for SearchQuery images.
"""

alias Philomena.DuplicateReports.SearchQuery
alias PhilomenaMedia.Uploader

def analyze_upload(search_query, params) do
Uploader.analyze_upload(
search_query,
"image",
params["image"],
&SearchQuery.image_changeset/2
)
end
end
15 changes: 15 additions & 0 deletions lib/philomena/users.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Philomena.Users do
alias Philomena.Galleries
alias Philomena.Reports
alias Philomena.Filters
alias Philomena.UserEraseWorker
alias Philomena.UserRenameWorker

## Database getters
Expand Down Expand Up @@ -683,6 +684,20 @@ defmodule Philomena.Users do
|> Repo.update()
end

def erase_user(%User{} = user, %User{} = moderator) do
# Deactivate to prevent the user from racing these changes
{:ok, user} = deactivate_user(moderator, user)

# Rename to prevent usage for brand recognition SEO
random_hex = Base.encode16(:crypto.strong_rand_bytes(16), case: :lower)
{:ok, user} = update_user(user, %{name: "deactivated_#{random_hex}"})

# Enqueue a background job to perform the rest of the deletion
Exq.enqueue(Exq, "indexing", UserEraseWorker, [user.id, moderator.id])

{:ok, user}
end

defp setup_roles(nil), do: nil

defp setup_roles(user) do
Expand Down
125 changes: 125 additions & 0 deletions lib/philomena/users/eraser.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
defmodule Philomena.Users.Eraser do
import Ecto.Query
alias Philomena.Repo

alias Philomena.Bans
alias Philomena.Comments.Comment
alias Philomena.Comments
alias Philomena.Galleries.Gallery
alias Philomena.Galleries
alias Philomena.Posts.Post
alias Philomena.Posts
alias Philomena.Topics.Topic
alias Philomena.Topics
alias Philomena.Images
alias Philomena.SourceChanges.SourceChange

alias Philomena.Users

@reason "Site abuse"
@wipe_ip %Postgrex.INET{address: {127, 0, 1, 1}, netmask: 32}
@wipe_fp "ffff"

def erase_permanently!(user, moderator) do
# Erase avatar
{:ok, user} = Users.remove_avatar(user)

# Erase "about me" and personal title
{:ok, user} = Users.update_description(user, %{description: "", personal_title: ""})

# Delete all forum posts
Post
|> where(user_id: ^user.id)
|> Repo.all()
|> Enum.each(fn post ->
{:ok, post} = Posts.hide_post(post, %{deletion_reason: @reason}, moderator)
{:ok, _post} = Posts.destroy_post(post)
end)

# Delete all comments
Comment
|> where(user_id: ^user.id)
|> Repo.all()
|> Enum.each(fn comment ->
{:ok, comment} = Comments.hide_comment(comment, %{deletion_reason: @reason}, moderator)
{:ok, _comment} = Comments.destroy_comment(comment)
end)

# Delete all galleries
Gallery
|> where(creator_id: ^user.id)
|> Repo.all()
|> Enum.each(fn gallery ->
{:ok, _gallery} = Galleries.delete_gallery(gallery)
end)

# Delete all posted topics
Topic
|> where(user_id: ^user.id)
|> Repo.all()
|> Enum.each(fn topic ->
{:ok, _topic} = Topics.hide_topic(topic, @reason, moderator)
end)

# Revert all source changes
SourceChange
|> where(user_id: ^user.id)
|> order_by(desc: :created_at)
|> preload(:image)
|> Repo.all()
|> Enum.each(fn source_change ->
if source_change.added do
revert_added_source_change(source_change, user)
else
revert_removed_source_change(source_change, user)
end
end)

# Delete all source changes
SourceChange
|> where(user_id: ^user.id)
|> Repo.delete_all()

# Ban the user
{:ok, _ban} =
Bans.create_user(
moderator,
%{
"user_id" => user.id,
"reason" => @reason,
"valid_until" => "permanent"
}
)

# We succeeded
:ok
end

defp revert_removed_source_change(source_change, user) do
old_sources = %{}
new_sources = %{"0" => %{"source" => source_change.source_url}}

revert_source_change(source_change, user, old_sources, new_sources)
end

defp revert_added_source_change(source_change, user) do
old_sources = %{"0" => %{"source" => source_change.source_url}}
new_sources = %{}

revert_source_change(source_change, user, old_sources, new_sources)
end

defp revert_source_change(source_change, user, old_sources, new_sources) do
attrs = %{"old_sources" => old_sources, "sources" => new_sources}

attribution = [
user: user,
ip: @wipe_ip,
fingerprint: @wipe_fp,
user_agent: "",
referrer: ""
]

{:ok, _} = Images.update_sources(source_change.image, attribution, attrs)
end
end
11 changes: 11 additions & 0 deletions lib/philomena/workers/user_erase_worker.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Philomena.UserEraseWorker do
alias Philomena.Users.Eraser
alias Philomena.Users

def perform(user_id, moderator_id) do
moderator = Users.get_user!(moderator_id)
user = Users.get_user!(user_id)

Eraser.erase_permanently!(user, moderator)
end
end
Loading

0 comments on commit e9fab5b

Please sign in to comment.