Merge pull request #406 from philomena-dev/docs

Add doc comments to context modules
This commit is contained in:
liamwhite 2025-02-06 22:03:01 -05:00 committed by GitHub
commit d6e360a3a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1586 additions and 33 deletions

View file

@ -119,6 +119,20 @@ defmodule Philomena.Comments do
Repo.delete(comment)
end
@doc """
Hides a comment and handles associated reports.
## Parameters
- comment: The comment to hide
- attrs: Attributes for the hide operation
- user: The user performing the hide action
## Examples
iex> hide_comment(comment, %{staff_note: "Rule violation"}, user)
{:ok, %Comment{}}
"""
def hide_comment(%Comment{} = comment, attrs, user) do
report_query = Reports.close_report_query({"Comment", comment.id}, user)
comment = Comment.hide_changeset(comment, attrs, user)
@ -139,6 +153,15 @@ defmodule Philomena.Comments do
end
end
@doc """
Unhides a previously hidden comment.
## Examples
iex> unhide_comment(comment)
{:ok, %Comment{}}
"""
def unhide_comment(%Comment{} = comment) do
comment
|> Comment.unhide_changeset()
@ -154,12 +177,35 @@ defmodule Philomena.Comments do
end
end
@doc """
Marks a comment as destroyed and removes its text (hard deletion).
## Examples
iex> destroy_comment(comment)
{:ok, %Comment{}}
"""
def destroy_comment(%Comment{} = comment) do
comment
|> Comment.destroy_changeset()
|> Repo.update()
end
@doc """
Approves a comment, closes associated reports, and increments the user comments
posted count.
## Parameters
- comment: The comment to approve
- user: The user performing the approval
## Examples
iex> approve_comment(comment, user)
{:ok, %Comment{}}
"""
def approve_comment(%Comment{} = comment, user) do
report_query = Reports.close_report_query({"Comment", comment.id}, user)
comment = Comment.approve_changeset(comment)
@ -181,6 +227,23 @@ defmodule Philomena.Comments do
end
end
@doc """
Creates a system report for non-approved comments containing external images.
Returns false for already approved comments.
## Returns
- `false`: If the comment is already approved
- `{:ok, %Report{}}`: If a system report was created
## Examples
iex> report_non_approved(approved_comment)
false
iex> report_non_approved(unapproved_comment)
{:ok, %Report{}}
"""
def report_non_approved(%Comment{approved: true}), do: false
def report_non_approved(comment) do
@ -191,6 +254,20 @@ defmodule Philomena.Comments do
)
end
@doc """
Migrates comments from one image to another when handling duplicate images.
Returns the duplicate image parameter unchanged, for use in a pipeline.
## Parameters
- image: The source image whose comments will be moved
- duplicate_of_image: The target image that will receive the comments
## Examples
iex> migrate_comments(source_image, target_image)
%Image{}
"""
def migrate_comments(image, duplicate_of_image) do
{count, nil} =
Comment
@ -217,24 +294,62 @@ defmodule Philomena.Comments do
Comment.changeset(comment, %{})
end
@doc """
Updates comment search indices when a user's name changes.
## Examples
iex> user_name_reindex("old_username", "new_username")
:ok
"""
def user_name_reindex(old_name, new_name) do
data = CommentIndex.user_name_update_by_query(old_name, new_name)
Search.update_by_query(Comment, data.query, data.set_replacements, data.replacements)
end
@doc """
Queues a single comment for search index updates.
Returns the comment struct unchanged, for use in a pipeline.
## Examples
iex> reindex_comment(comment)
%Comment{}
"""
def reindex_comment(%Comment{} = comment) do
Exq.enqueue(Exq, "indexing", IndexWorker, ["Comments", "id", [comment.id]])
comment
end
@doc """
Queues all comments associated with an image for search index updates.
Returns the image struct unchanged, for use in a pipeline.
## Examples
iex> reindex_comments(image)
%Image{}
"""
def reindex_comments(image) do
Exq.enqueue(Exq, "indexing", IndexWorker, ["Comments", "image_id", [image.id]])
image
end
@doc """
Provides preload queries for comment indexing operations.
## Examples
iex> indexing_preloads()
[user: user_query, image: image_query]
"""
def indexing_preloads do
user_query = select(User, [u], map(u, [:id, :name]))
tag_query = select(Tag, [t], map(t, [:id]))
@ -250,6 +365,22 @@ defmodule Philomena.Comments do
]
end
@doc """
Performs a search reindex operation on comments matching the given criteria.
## Parameters
- column: The database column to filter on (e.g., :id, :image_id)
- condition: A list of values to match against the column
## Examples
iex> perform_reindex(:id, [1, 2, 3])
:ok
iex> perform_reindex(:image_id, [123])
:ok
"""
def perform_reindex(column, condition) do
Comment
|> preload(^indexing_preloads())

View file

@ -77,6 +77,18 @@ defmodule Philomena.DnpEntries do
|> Repo.update()
end
@doc """
Transitions a DNP entry to a new state.
## Examples
iex> transition_dnp_entry(dnp_entry, user, "acknowledged")
{:ok, %DnpEntry{}}
iex> transition_dnp_entry(dnp_entry, user, "invalid_state")
{:error, %Ecto.Changeset{}}
"""
def transition_dnp_entry(%DnpEntry{} = dnp_entry, user, new_state) do
dnp_entry
|> DnpEntry.transition_changeset(user, new_state)
@ -112,6 +124,19 @@ defmodule Philomena.DnpEntries do
DnpEntry.changeset(dnp_entry, %{})
end
@doc """
Returns the count of active DNP entries in requested, claimed,
or acknowledged state, if the user has permission to view them.
## Examples
iex> count_dnp_entries(admin)
42
iex> count_dnp_entries(user)
nil
"""
def count_dnp_entries(user) do
if Canada.Can.can?(user, :index, DnpEntry) do
DnpEntry

View file

@ -16,6 +16,18 @@ defmodule Philomena.DuplicateReports do
alias Philomena.Images.Image
alias Philomena.Images
@doc """
Generates automated duplicate reports for an image based on perceptual matching.
Takes a source image and generates duplicate reports for similar images based on
intensity and aspect ratio comparison.
## Examples
iex> generate_reports(source_image)
[{:ok, %DuplicateReport{}}, ...]
"""
def generate_reports(source) do
source = Repo.preload(source, :intensity)
@ -30,6 +42,23 @@ defmodule Philomena.DuplicateReports do
end)
end
@doc """
Query for potential duplicate images based on intensity values and aspect ratio.
Takes a tuple of {intensities, aspect_ratio} and optional options to control the search:
- `:aspect_dist` - Maximum aspect ratio difference (default: 0.05)
- `:limit` - Maximum number of results (default: 10)
- `:dist` - Maximum intensity difference per channel (default: 0.25)
## Examples
iex> find_duplicates({%{nw: 0.5, ne: 0.5, sw: 0.5, se: 0.5}, 1.0})
#Ecto.Query<...>
iex> find_duplicates({intensities, ratio}, dist: 0.3, limit: 20)
#Ecto.Query<...>
"""
def find_duplicates({intensities, aspect_ratio}, opts \\ []) do
aspect_dist = Keyword.get(opts, :aspect_dist, 0.05)
limit = Keyword.get(opts, :limit, 10)
@ -150,6 +179,21 @@ defmodule Philomena.DuplicateReports do
|> Repo.insert()
end
@doc """
Accepts a duplicate report and merges the duplicate image into the target image.
Takes an optional Ecto.Multi, the duplicate report to accept, and the user accepting the report.
Handles rejecting any other duplicate reports between the same images and merges the images.
## Examples
iex> accept_duplicate_report(nil, duplicate_report, user)
{:ok, %{duplicate_report: %DuplicateReport{}, ...}}
iex> accept_duplicate_report(existing_multi, duplicate_report, user)
%Ecto.Multi{}
"""
def accept_duplicate_report(multi \\ nil, %DuplicateReport{} = duplicate_report, user) do
duplicate_report = Repo.preload(duplicate_report, [:image, :duplicate_of_image])
@ -175,6 +219,18 @@ defmodule Philomena.DuplicateReports do
|> Images.merge_image(duplicate_report.image, duplicate_report.duplicate_of_image, user)
end
@doc """
Accepts a duplicate report in reverse, making the target image the duplicate instead.
Creates a new duplicate report with reversed image relationship if one doesn't exist,
rejects the original report, and accepts the reversed report.
## Examples
iex> accept_reverse_duplicate_report(duplicate_report, user)
{:ok, %{duplicate_report: %DuplicateReport{}, ...}}
"""
def accept_reverse_duplicate_report(%DuplicateReport{} = duplicate_report, user) do
new_report =
DuplicateReport
@ -204,18 +260,47 @@ defmodule Philomena.DuplicateReports do
|> accept_duplicate_report(new_report, user)
end
@doc """
Claims a duplicate report for review by a user.
## Examples
iex> claim_duplicate_report(duplicate_report, user)
{:ok, %DuplicateReport{}}
"""
def claim_duplicate_report(%DuplicateReport{} = duplicate_report, user) do
duplicate_report
|> DuplicateReport.claim_changeset(user)
|> Repo.update()
end
@doc """
Removes a user's claim on a duplicate report.
## Examples
iex> unclaim_duplicate_report(duplicate_report)
{:ok, %DuplicateReport{}}
"""
def unclaim_duplicate_report(%DuplicateReport{} = duplicate_report) do
duplicate_report
|> DuplicateReport.unclaim_changeset()
|> Repo.update()
end
@doc """
Rejects a duplicate report.
Updates the duplicate report's state to rejected and records the user who rejected it.
## Examples
iex> reject_duplicate_report(duplicate_report, user)
{:ok, %DuplicateReport{}}
"""
def reject_duplicate_report(%DuplicateReport{} = duplicate_report, user) do
duplicate_report
|> DuplicateReport.reject_changeset(user)
@ -251,6 +336,19 @@ defmodule Philomena.DuplicateReports do
DuplicateReport.changeset(duplicate_report, %{})
end
@doc """
Counts the number of duplicate reports in "open" state,
if the user has permission to view them.
## Examples
iex> count_duplicate_reports(admin)
42
iex> count_duplicate_reports(user)
nil
"""
def count_duplicate_reports(user) do
if Canada.Can.can?(user, :index, DuplicateReport) do
DuplicateReport

View file

@ -93,6 +93,17 @@ defmodule Philomena.Filters do
|> reindex_after_update()
end
@doc """
Makes a filter public.
Updates the filter to be publicly accessible by other users.
## Examples
iex> make_filter_public(filter)
{:ok, %Filter{}}
"""
def make_filter_public(%Filter{} = filter) do
filter
|> Filter.public_changeset()
@ -140,6 +151,21 @@ defmodule Philomena.Filters do
Filter.changeset(filter, %{})
end
@doc """
Returns a grouped list of recent and user filters.
Takes a user and returns a list of their recently used filters and personal filters,
grouped into "Recent Filters" and "Your Filters" categories.
## Examples
iex> recent_and_user_filters(user)
[
{"Recent Filters", [[key: "Filter 1", value: 1], ...]},
{"Your Filters", [[key: "Filter 2", value: 2], ...]}
]
"""
def recent_and_user_filters(user) do
recent_filter_ids =
[user.current_filter_id | user.recent_filter_ids]
@ -174,6 +200,17 @@ defmodule Philomena.Filters do
|> Enum.reverse()
end
@doc """
Adds a tag to a filter's hidden tags list.
Updates the filter to hide content with the specified tag.
## Examples
iex> hide_tag(filter, tag)
{:ok, %Filter{}}
"""
def hide_tag(filter, tag) do
hidden_tag_ids = Enum.uniq([tag.id | filter.hidden_tag_ids])
@ -183,6 +220,15 @@ defmodule Philomena.Filters do
|> reindex_after_update()
end
@doc """
Removes a tag from a filter's hidden tags list.
## Examples
iex> unhide_tag(filter, tag)
{:ok, %Filter{}}
"""
def unhide_tag(filter, tag) do
hidden_tag_ids = filter.hidden_tag_ids -- [tag.id]
@ -192,6 +238,15 @@ defmodule Philomena.Filters do
|> reindex_after_update()
end
@doc """
Adds a tag to a filter's spoilered tags list.
## Examples
iex> spoiler_tag(filter, tag)
{:ok, %Filter{}}
"""
def spoiler_tag(filter, tag) do
spoilered_tag_ids = Enum.uniq([tag.id | filter.spoilered_tag_ids])
@ -201,6 +256,15 @@ defmodule Philomena.Filters do
|> reindex_after_update()
end
@doc """
Removes a tag from a filter's spoilered tags list.
## Examples
iex> unspoiler_tag(filter, tag)
{:ok, %Filter{}}
"""
def unspoiler_tag(filter, tag) do
spoilered_tag_ids = filter.spoilered_tag_ids -- [tag.id]
@ -210,38 +274,92 @@ defmodule Philomena.Filters do
|> reindex_after_update()
end
defp reindex_after_update({:ok, filter}) do
reindex_filter(filter)
defp reindex_after_update(result) do
case result do
{:ok, filter} ->
reindex_filter(filter)
{:ok, filter}
{:ok, filter}
error ->
error
end
end
defp reindex_after_update(error) do
error
end
@doc """
Updates filter indexes when a user's name changes.
Updates search indexes to reflect a user's new name.
## Examples
iex> user_name_reindex("old_name", "new_name")
:ok
"""
def user_name_reindex(old_name, new_name) do
data = FilterIndex.user_name_update_by_query(old_name, new_name)
Search.update_by_query(Filter, data.query, data.set_replacements, data.replacements)
end
@doc """
Queues a single filter for search index updates.
Returns the filter struct unchanged, for use in a pipeline.
## Examples
iex> reindex_filter(filter)
%Filter{}
"""
def reindex_filter(%Filter{} = filter) do
Exq.enqueue(Exq, "indexing", IndexWorker, ["Filters", "id", [filter.id]])
filter
end
@doc """
Removes a filter from the search index.
## Examples
iex> unindex_filter(filter)
%Filter{}
"""
def unindex_filter(%Filter{} = filter) do
Search.delete_document(filter.id, Filter)
filter
end
@doc """
Returns a list of associations to preload when indexing filters.
## Examples
iex> indexing_preloads()
[:user]
"""
def indexing_preloads do
[:user]
end
@doc """
Performs a search reindex operation on filters matching the given criteria.
## Parameters
- column: The database column to filter on (e.g., :id)
- condition: A list of values to match against the column
## Examples
iex> perform_reindex(:id, [1, 2, 3])
{:ok, [%Filter{}, ...]}
"""
def perform_reindex(column, condition) do
Filter
|> preload(^indexing_preloads())

View file

@ -121,6 +121,15 @@ defmodule Philomena.Galleries do
Gallery.changeset(gallery, %{})
end
@doc """
Updates gallery search indices when a user's name changes.
## Examples
iex> user_name_reindex("old_username", "new_username")
:ok
"""
def user_name_reindex(old_name, new_name) do
data = GalleryIndex.user_name_update_by_query(old_name, new_name)
@ -137,22 +146,65 @@ defmodule Philomena.Galleries do
error
end
@doc """
Queues a gallery for reindexing.
Adds the gallery to the indexing queue to update its search index.
## Examples
iex> reindex_gallery(gallery)
%Gallery{}
"""
def reindex_gallery(%Gallery{} = gallery) do
Exq.enqueue(Exq, "indexing", IndexWorker, ["Galleries", "id", [gallery.id]])
gallery
end
@doc """
Removes a gallery from the search index.
Deletes the gallery's document from the search index.
## Examples
iex> unindex_gallery(gallery)
%Gallery{}
"""
def unindex_gallery(%Gallery{} = gallery) do
Search.delete_document(gallery.id, Gallery)
gallery
end
@doc """
Returns a list of associations to preload when indexing galleries.
## Examples
iex> indexing_preloads()
[:subscribers, :creator, :interactions]
"""
def indexing_preloads do
[:subscribers, :creator, :interactions]
end
@doc """
Reindexes galleries based on a column condition.
Updates the search index for all galleries matching the given column condition.
Used for batch reindexing of galleries.
## Examples
iex> perform_reindex(:id, [1, 2, 3])
{:ok, [%Gallery{}, ...]}
"""
def perform_reindex(column, condition) do
Gallery
|> preload(^indexing_preloads())
@ -160,6 +212,24 @@ defmodule Philomena.Galleries do
|> Search.reindex(Gallery)
end
@doc """
Adds the specified image to the gallery, updates image count, triggers
notifications, and performs necessary reindexing.
The image is added at the last position.
## Examples
iex> add_image_to_gallery(gallery, image)
{:ok,
%{
gallery: %Gallery{},
interaction: %Interaction{},
image_count: 1,
notification: %Notification{}
}}
"""
def add_image_to_gallery(gallery, image) do
Multi.new()
|> Multi.run(:gallery, fn repo, %{} ->
@ -202,6 +272,21 @@ defmodule Philomena.Galleries do
end
end
@doc """
Removes the specified image from the gallery, updates image count,
and performs necessary reindexing.
## Examples
iex> remove_image_from_gallery(gallery, image)
{:ok,
%{
gallery: %Gallery{},
interaction: 1,
image_count: 0
}}
"""
def remove_image_from_gallery(gallery, image) do
Multi.new()
|> Multi.run(:gallery, fn repo, %{} ->
@ -254,10 +339,35 @@ defmodule Philomena.Galleries do
|> Repo.aggregate(:max, :position)
end
@doc """
Queues a gallery reorder operation.
Returns the gallery struct unchanged, for use in a pipeline.
## Examples
iex> reorder_gallery(gallery, [1, 2, 3])
%Gallery{}
"""
def reorder_gallery(gallery, image_ids) do
Exq.enqueue(Exq, "indexing", GalleryReorderWorker, [gallery.id, image_ids])
gallery
end
@doc """
Performs the actual reordering of images in a gallery.
Reorders the gallery's images according to the provided image IDs list, updating
positions while maintaining relative order for unspecified images. Handles position
updates efficiently and reindexes only the affected images.
## Examples
iex> perform_reorder(gallery_id, [3, 1, 2])
:ok
"""
def perform_reorder(gallery_id, image_ids) do
gallery = get_gallery!(gallery_id)
@ -320,6 +430,8 @@ defmodule Philomena.Galleries do
# Now update all the associated images
Images.reindex_images(Map.keys(requested))
:ok
end
defp position_order(%{order_position_asc: true}), do: [asc: :position]

View file

@ -156,6 +156,16 @@ defmodule Philomena.Images do
Logger.error("Aborting upload of #{image.id} after #{retry_count} retries")
end
@doc """
Approves an image for public viewing.
This will make the image visible to users and update necessary statistics.
## Examples
iex> approve_image(image)
{:ok, %Image{}}
"""
def approve_image(image) do
image
|> Repo.preload(:user)
@ -197,6 +207,18 @@ defmodule Philomena.Images do
defp maybe_suggest_user_verification(_user), do: false
@doc """
Counts the number of images pending approval that a user can moderate.
## Examples
iex> count_pending_approvals(admin)
42
iex> count_pending_approvals(user)
nil
"""
def count_pending_approvals(user) do
if Canada.Can.can?(user, :approve, %Image{}) do
Image
@ -208,12 +230,36 @@ defmodule Philomena.Images do
end
end
@doc """
Marks the given image as the current featured image.
## Examples
iex> feature_image(user, image)
{:ok, %ImageFeature{}}
"""
def feature_image(featurer, %Image{} = image) do
%ImageFeature{user_id: featurer.id, image_id: image.id}
|> ImageFeature.changeset(%{})
|> Repo.insert()
end
@doc """
Destroys the contents of an image (hard deletion) by marking it as hidden
and deleting up associated files.
This will:
1. Mark the image as removed in the database
2. Purge associated files
3. Remove thumbnails
## Examples
iex> destroy_image(image)
{:ok, %Image{}}
"""
def destroy_image(%Image{} = image) do
image
|> Image.remove_image_changeset()
@ -230,36 +276,91 @@ defmodule Philomena.Images do
end
end
@doc """
Locks or unlocks comments on an image.
## Examples
iex> lock_comments(image, true)
{:ok, %Image{}}
"""
def lock_comments(%Image{} = image, locked) do
image
|> Image.lock_comments_changeset(locked)
|> Repo.update()
end
@doc """
Locks or unlocks the description of an image.
## Examples
iex> lock_description(image, true)
{:ok, %Image{}}
"""
def lock_description(%Image{} = image, locked) do
image
|> Image.lock_description_changeset(locked)
|> Repo.update()
end
@doc """
Locks or unlocks the tags on an image.
## Examples
iex> lock_tags(image, true)
{:ok, %Image{}}
"""
def lock_tags(%Image{} = image, locked) do
image
|> Image.lock_tags_changeset(locked)
|> Repo.update()
end
@doc """
Removes the original SHA-512 hash from an image, allowing users to upload
the same file again.
## Examples
iex> remove_hash(image)
{:ok, %Image{}}
"""
def remove_hash(%Image{} = image) do
image
|> Image.remove_hash_changeset()
|> Repo.update()
end
@doc """
Updates the scratchpad notes on an image.
## Examples
iex> update_scratchpad(image, %{"scratchpad" => "New notes"})
{:ok, %Image{}}
"""
def update_scratchpad(%Image{} = image, attrs) do
image
|> Image.scratchpad_changeset(attrs)
|> Repo.update()
end
@doc """
Removes all source change history for an image.
## Examples
iex> remove_source_history(image)
{:ok, %Image{}}
"""
def remove_source_history(%Image{} = image) do
image
|> Repo.preload(:source_changes)
@ -267,17 +368,49 @@ defmodule Philomena.Images do
|> Repo.update()
end
@doc """
Repairs an image by regenerating its thumbnails.
Returns the image struct unchanged, for use in a pipeline.
This will:
1. Mark the image as needing thumbnail regeneration
2. Queue the thumbnail generation job
## Examples
iex> repair_image(image)
%Image{}
"""
def repair_image(%Image{} = image) do
Image
|> where(id: ^image.id)
|> Repo.update_all(set: [thumbnails_generated: false, processed: false])
Exq.enqueue(Exq, queue(image.image_mime_type), ThumbnailWorker, [image.id])
image
end
defp queue("video/webm"), do: "videos"
defp queue(_mime_type), do: "images"
@doc """
Updates the file content of an image.
This will:
1. Update the image metadata
2. Save the new file
3. Generate new thumbnails
4. Purge old files
5. Reindex the image
## Examples
iex> update_file(image, %{"image" => upload})
{:ok, %Image{}}
"""
def update_file(%Image{} = image, attrs) do
image
|> Image.changeset(attrs)
@ -316,12 +449,48 @@ defmodule Philomena.Images do
|> Repo.update()
end
@doc """
Updates an image's description.
## Examples
iex> update_description(image, %{"description" => "New description"})
{:ok, %Image{}}
"""
def update_description(%Image{} = image, attrs) do
image
|> Image.description_changeset(attrs)
|> Repo.update()
end
@doc """
Updates an image's sources with attribution tracking.
Handles both added and removed sources. Automatically determines the user's
intended source changes based on the provided previous image state.
This will update the image's sources, create source change records
for tracking, and reindex the image.
## Examples
iex> update_sources(
...> image,
...> %{attribution: attrs},
...> %{
...> "old_sources" => %{},
...> "sources" => %{"0" => "http://example.com"}
...> }
...> )
{:ok,
%{
image: image,
added_source_changes: 1,
removed_source_changes: 0
}}
"""
def update_sources(%Image{} = image, attribution, attrs) do
old_sources = attrs["old_sources"]
new_sources = attrs["sources"]
@ -383,6 +552,17 @@ defmodule Philomena.Images do
}
end
@doc """
Updates the locked tags on an image.
Locked tags can only be added or removed by privileged users.
## Examples
iex> update_locked_tags(image, %{tag_input: "safe, validated"})
{:ok, %Image{}}
"""
def update_locked_tags(%Image{} = image, attrs) do
new_tags = Tags.get_or_create_tags(attrs["tag_input"])
@ -392,6 +572,33 @@ defmodule Philomena.Images do
|> Repo.update()
end
@doc """
Updates an image's tags with attribution tracking.
Handles both added and removed tags. Automatically determines the user's
intended tag changes based on the provided previous image state.
This will update the image's tags, create tag change records
for tracking, and reindex the image.
## Examples
iex> update_tags(
...> image,
...> %{attribution: attrs},
...> %{
...> old_tag_input: "safe",
...> tag_input: "safe, cute"
...> }
...> )
{:ok,
%{
image: image,
added_tag_changes: 1,
removed_tag_changes: 0
}}
"""
def update_tags(%Image{} = image, attribution, attrs) do
old_tags = Tags.get_or_create_tags(attrs["old_tag_input"])
new_tags = Tags.get_or_create_tags(attrs["tag_input"])
@ -486,14 +693,27 @@ defmodule Philomena.Images do
end
end
@doc """
Updates the tag change tracking after committing updates to an image.
This updates the rate limit counters for total tag change count and rating change count
based on the changes made to the image.
## Examples
iex> update_tag_change_limits_after_commit(image, %{user: user, ip: "127.0.0.1"})
:ok
"""
def update_tag_change_limits_after_commit(image, attribution) do
rating_changed_count = if(image.ratings_changed, do: 1, else: 0)
tag_changed_count = length(image.added_tags) + length(image.removed_tags)
user = attribution[:user]
ip = attribution[:ip]
Limits.update_tag_count_after_update(user, ip, tag_changed_count)
Limits.update_rating_count_after_update(user, ip, rating_changed_count)
:ok = Limits.update_tag_count_after_update(user, ip, tag_changed_count)
:ok = Limits.update_rating_count_after_update(user, ip, rating_changed_count)
:ok
end
defp tag_change_attributes(attribution, image, tag, added, user) do
@ -518,18 +738,48 @@ defmodule Philomena.Images do
}
end
@doc """
Changes the uploader of an image.
## Examples
iex> update_uploader(image, %{"username" => "Admin"})
{:ok, %Image{}}
"""
def update_uploader(%Image{} = image, attrs) do
image
|> Image.uploader_changeset(attrs)
|> Repo.update()
end
@doc """
Updates the anonymous status of an image.
## Examples
iex> update_anonymous(image, %{"anonymous" => "true"})
{:ok, %Image{}}
"""
def update_anonymous(%Image{} = image, attrs) do
image
|> Image.anonymous_changeset(attrs)
|> Repo.update()
end
@doc """
Updates the hide reason for an image.
## Examples
iex> update_hide_reason(image, %{hide_reason: "Duplicate of #1234"})
{:ok, %Image{}}
iex> update_hide_reason(image, %{hide_reason: ""})
{:ok, %Image{}}
"""
def update_hide_reason(%Image{} = image, attrs) do
image
|> Image.hide_reason_changeset(attrs)
@ -545,6 +795,28 @@ defmodule Philomena.Images do
end
end
@doc """
Hides an image from public view.
This will:
1. Mark the image as hidden
2. Close all reports and duplicate reports
3. Delete all gallery interactions containing the image
4. Decrement all tag counts with the image
5. Hide the image's thumbnails and purge them from the CDN
6. Reindex the image and all of its comments
## Examples
iex> hide_image(image, moderator, %{reason: "Rule violation"})
{:ok,
%{
image: image,
tags: tags,
reports: {count, reports}
}}
"""
def hide_image(%Image{} = image, user, attrs) do
duplicate_reports =
DuplicateReport
@ -560,6 +832,33 @@ defmodule Philomena.Images do
|> process_after_hide()
end
@doc """
Merges one image into another, combining their metadata and content.
This will:
1. Hide the source image
2. Update first_seen_at timestamp
3. Copy tags to the target image
4. Migrate sources, comments, subscriptions and interactions
5. Send merge notifications
6. Reindex both images and all of the comments
## Parameters
- multi: Optional `m:Ecto.Multi` for transaction handling
- image: The source image to merge from
- duplicate_of_image: The target image to merge into
- user: The user performing the merge
## Examples
iex> merge_image(nil, source_image, target_image, moderator)
{:ok,
%{
image: image,
tags: tags
}}
"""
def merge_image(multi \\ nil, %Image{} = image, duplicate_of_image, user) do
multi = multi || Multi.new()
@ -675,6 +974,26 @@ defmodule Philomena.Images do
{:ok, image}
end
@doc """
Unhides an image, making it visible to users again.
This will:
1. Remove the hidden status from the image
2. Increment tag counts
3. Unhide thumbnails
4. Reindex the image and related content
Returns {:ok, image} if successful, or returns the image unchanged if it's not hidden.
## Examples
iex> unhide_image(hidden_image)
{:ok, %Image{hidden_from_users: false}}
iex> unhide_image(visible_image)
{:ok, %Image{}}
"""
def unhide_image(%Image{hidden_from_users: true} = image) do
key = image.hidden_image_key
@ -711,6 +1030,24 @@ defmodule Philomena.Images do
def unhide_image(image), do: {:ok, image}
@doc """
Performs a batch update on multiple images, adding and removing tags.
This function efficiently updates tags for multiple images at once,
handling tag changes, tag counts, and reindexing in a single transaction.
## Parameters
- image_ids: List of image IDs to update
- added_tags: List of tags to add to all images
- removed_tags: List of tags to remove from all images
- tag_change_attributes: Attributes tag changes are created with
## Examples
iex> batch_update([1, 2], [tag1], [tag2], %{user_id: user.id})
{:ok, ...}
"""
def batch_update(image_ids, added_tags, removed_tags, tag_change_attributes) do
image_ids =
Image
@ -828,24 +1165,65 @@ defmodule Philomena.Images do
Image.changeset(image, %{})
end
@doc """
Updates image search indices when a user's name changes.
## Examples
iex> user_name_reindex("old_username", "new_username")
:ok
"""
def user_name_reindex(old_name, new_name) do
data = ImageIndex.user_name_update_by_query(old_name, new_name)
Search.update_by_query(Image, data.query, data.set_replacements, data.replacements)
end
@doc """
Queues a single image for search index updates.
Returns the image struct unchanged, for use in a pipeline.
## Examples
iex> reindex_image(image)
%Image{}
"""
def reindex_image(%Image{} = image) do
Exq.enqueue(Exq, "indexing", IndexWorker, ["Images", "id", [image.id]])
image
end
@doc """
Queues all listed image IDs for search index updates.
Returns the list unchanged, for use in a pipeline.
## Examples
iex> reindex_images([1, 2, 3])
[1, 2, 3]
"""
def reindex_images(image_ids) do
Exq.enqueue(Exq, "indexing", IndexWorker, ["Images", "id", image_ids])
image_ids
end
@doc """
Returns the preload configuration for image indexing.
Specifies which associations should be preloaded when indexing images,
optimizing the queries for better performance.
## Examples
iex> indexing_preloads()
[sources: query, user: query, ...]
"""
def indexing_preloads do
user_query = select(User, [u], map(u, [:id, :name]))
sources_query = select(Source, [s], map(s, [:image_id, :source]))
@ -869,6 +1247,19 @@ defmodule Philomena.Images do
]
end
@doc """
Performs a search reindex operation on images matching the given criteria.
## Parameters
- column: The database column to filter on (e.g., :id)
- condition: A list of values to match against the column
## Examples
iex> perform_reindex(:id, [1, 2, 3])
:ok
"""
def perform_reindex(column, condition) do
Image
|> preload(^indexing_preloads())
@ -876,6 +1267,17 @@ defmodule Philomena.Images do
|> Search.reindex(Image)
end
@doc """
Purges image files from the CDN.
Enqueues a job to purge both visible and hidden thumbnail paths for the given image.
## Examples
iex> purge_files(image, "hidden_key")
:ok
"""
def purge_files(image, hidden_key) do
files =
if is_nil(hidden_key) do
@ -888,6 +1290,17 @@ defmodule Philomena.Images do
Exq.enqueue(Exq, "indexing", ImagePurgeWorker, [files])
end
@doc """
Executes the actual purge operation for image files.
Calls the system purge-cache command to remove the specified files from the CDN cache.
## Examples
iex> perform_purge(["file1.jpg", "file2.jpg"])
:ok
"""
def perform_purge(files) do
{_out, 0} = System.cmd("purge-cache", [Jason.encode!(%{files: files})])
@ -896,6 +1309,29 @@ defmodule Philomena.Images do
alias Philomena.Images.Subscription
@doc """
Migrates subscriptions and notifications from one image to another.
This function is used during image merging to transfer all subscriptions
and notifications from the source image to the target image. It handles:
1. User subscriptions
2. Comment notifications
3. Merge notifications
Returns `{:ok, {comment_notification_count, merge_notification_count}}`.
## Parameters
- source: The source image to migrate from
- target: The target image to migrate to
## Examples
iex> migrate_subscriptions(source_image, target_image)
{:ok, {5, 2}}
"""
def migrate_subscriptions(source, target) do
subscriptions =
Subscription
@ -941,6 +1377,29 @@ defmodule Philomena.Images do
{:ok, {comment_notification_count, merge_notification_count}}
end
@doc """
Migrates source URLs from one image to another.
This function is used during image merging to combine source URLs from both images.
It will:
1. Combine sources from both images
2. Remove duplicates
3. Take up to 15 sources (the system limit)
4. Update the target image with the combined sources
Returns the result of updating the target image with the combined sources.
## Parameters
- source: The source image containing sources to migrate
- target: The target image to receive the combined sources
## Examples
iex> migrate_sources(source_image, target_image)
{:ok, %Image{}}
"""
def migrate_sources(source, target) do
sources =
(source.sources ++ target.sources)

View file

@ -8,6 +8,19 @@ defmodule Philomena.Interactions do
alias Philomena.Repo
alias Ecto.Multi
@doc """
Gets all interactions for a list of images for a given user.
Returns an empty list if no user is provided. Otherwise returns a list of maps containing:
- image_id: The ID of the image
- user_id: The ID of the user
- interaction_type: One of "hidden", "faved", or "voted"
- value: For votes, either "up" or "down". Empty string for other interaction types.
## Parameters
* images - List of images or image IDs to get interactions for
* user - The user to get interactions for, or nil
"""
def user_interactions(_images, nil),
do: []
@ -71,6 +84,18 @@ defmodule Philomena.Interactions do
|> Repo.all()
end
@doc """
Migrates all interactions from one image to another.
Copies all hides, faves, and votes from the source image to the target image.
Updates the target image's counters to reflect the new interactions.
All operations are performed in a single transaction.
## Parameters
* source - The source Image struct to copy interactions from
* target - The target Image struct to copy interactions to
"""
def migrate_interactions(source, target) do
now = DateTime.utc_now(:second)
source = Repo.preload(source, [:hiders, :favers, :upvoters, :downvoters])

View file

@ -111,6 +111,23 @@ defmodule Philomena.Posts do
Notifications.create_forum_post_notification(post.user, topic, post)
end
@doc """
Creates a system report for non-approved posts containing external images.
Returns false for already approved posts.
## Returns
- `false`: If the post is already approved
- `{:ok, %Report{}}`: If a system report was created
## Examples
iex> report_non_approved(approved_post)
false
iex> report_non_approved(unapproved_post)
{:ok, %Report{}}
"""
def report_non_approved(%Post{approved: true}), do: false
def report_non_approved(post) do
@ -176,6 +193,20 @@ defmodule Philomena.Posts do
Repo.delete(post)
end
@doc """
Hides a post and handles associated reports.
## Parameters
- post: The post to hide
- attrs: Attributes for the hide operation
- user: The user performing the hide action
## Examples
iex> hide_post(post, %{staff_note: "Rule violation"}, user)
{:ok, %Post{}}
"""
def hide_post(%Post{} = post, attrs, user) do
report_query = Reports.close_report_query({"Post", post.id}, user)
@ -209,6 +240,15 @@ defmodule Philomena.Posts do
end
end
@doc """
Unhides a previously hidden post.
## Examples
iex> unhide_post(post)
{:ok, %Post{}}
"""
def unhide_post(%Post{} = post) do
post
|> Post.unhide_changeset()
@ -216,6 +256,15 @@ defmodule Philomena.Posts do
|> reindex_after_update()
end
@doc """
Marks a post as destroyed and removes its text (hard deletion).
## Examples
iex> destroy_post(post)
{:ok, %Post{}}
"""
def destroy_post(%Post{} = post) do
post
|> Post.destroy_changeset()
@ -223,6 +272,20 @@ defmodule Philomena.Posts do
|> reindex_after_update()
end
@doc """
Approves a post, closes associated reports, and increments the user forum
posts count.
## Parameters
- post: The post to approve
- user: The user performing the approval
## Examples
iex> approve_comment(post, user)
{:ok, %Post{}}
"""
def approve_post(%Post{} = post, user) do
report_query = Reports.close_report_query({"Post", post.id}, user)
post = Post.approve_changeset(post)
@ -257,6 +320,15 @@ defmodule Philomena.Posts do
Post.changeset(post, %{})
end
@doc """
Updates post search indices when a user's name changes.
## Examples
iex> user_name_reindex("old_username", "new_username")
:ok
"""
def user_name_reindex(old_name, new_name) do
data = PostIndex.user_name_update_by_query(old_name, new_name)
@ -273,12 +345,31 @@ defmodule Philomena.Posts do
result
end
@doc """
Queues a single post for search index updates.
Returns the post struct unchanged, for use in a pipeline.
## Examples
iex> reindex_comment(post)
%Post{}
"""
def reindex_post(%Post{} = post) do
Exq.enqueue(Exq, "indexing", IndexWorker, ["Posts", "id", [post.id]])
post
end
@doc """
Provides preload queries for post indexing operations.
## Examples
iex> indexing_preloads()
[user: user_query, topic: topic_query]
"""
def indexing_preloads do
user_query = select(User, [u], map(u, [:id, :name]))
@ -293,6 +384,22 @@ defmodule Philomena.Posts do
]
end
@doc """
Performs a search reindex operation on posts matching the given criteria.
## Parameters
- column: The database column to filter on (e.g., :id, :topic_id)
- condition: A list of values to match against the column
## Examples
iex> perform_reindex(:id, [1, 2, 3])
:ok
iex> perform_reindex(:topic_id, [123])
:ok
"""
def perform_reindex(column, condition) do
Post
|> preload(^indexing_preloads())

View file

@ -1,29 +1,32 @@
defmodule Philomena.Slug do
# Generates a URL-safe slug from a string by removing nonessential
# information from it.
#
# The process for this is as follows:
#
# 1. Remove non-ASCII or non-printable characters.
#
# 2. Replace any runs of non-alphanumeric characters that were allowed
# through previously with hyphens.
#
# 3. Remove any starting or ending hyphens.
#
# 4. Convert all characters to their lowercase equivalents.
#
# This method makes no guarantee of creating unique slugs for unique inputs.
# In addition, for certain inputs, it will return empty strings.
#
# Example
#
# destructive_slug("Time-Wasting Thread 3.0 (SFW - No Explicit/Grimdark)")
# #=> "time-wasting-thread-3-0-sfw-no-explicit-grimdark"
#
# destructive_slug("~`!@#$%^&*()-_=+[]{};:'\" <>,./?")
# #=> ""
#
@moduledoc """
URL-safe string shortening.
"""
@doc """
Generates a URL-safe slug from a string by removing nonessential
information from it.
The process for this is as follows:
1. Remove non-ASCII or non-printable characters.
2. Replace any runs of non-alphanumeric characters that were allowed
through previously with hyphens.
3. Remove any starting or ending hyphens.
4. Convert all characters to their lowercase equivalents.
This method makes no guarantee of creating unique slugs for unique inputs.
In addition, for certain inputs, it will return empty strings.
## Example
iex> destructive_slug("Time-Wasting Thread 3.0 (SFW - No Explicit/Grimdark)")
"time-wasting-thread-3-0-sfw-no-explicit-grimdark"
iex> destructive_slug("~`!@#$%^&*()-_=+[]{};:'\" <>,./?")
""
"""
@spec destructive_slug(String.t()) :: String.t()
def destructive_slug(input) when is_binary(input) do
input
@ -39,6 +42,11 @@ defmodule Philomena.Slug do
def destructive_slug(_input), do: ""
@doc """
Generates a reversible slug from a string by replacing certain characters
with escaped (not URL-encoded) equivalents.
"""
@spec slug(String.t()) :: String.t()
def slug(string) when is_binary(string) do
string
|> String.replace("-", "-dash-")

View file

@ -24,6 +24,19 @@ defmodule Philomena.Tags do
alias Philomena.DnpEntries.DnpEntry
alias Philomena.Channels.Channel
@doc """
Gets existing tags or creates new ones from a tag list string.
Takes a string of comma-separated tag names, parses it into individual tags,
and either retrieves existing tags or creates new ones for tags that don't exist.
Also handles tag aliases by returning the aliased tag instead of the alias.
## Examples
iex> get_or_create_tags("safe, cute, pony")
[%Tag{name: "safe"}, %Tag{name: "cute"}, %Tag{name: "pony"}]
"""
@spec get_or_create_tags(String.t()) :: list()
def get_or_create_tags(tag_list) do
tag_names = Tag.parse_tag_list(tag_list)
@ -174,6 +187,18 @@ defmodule Philomena.Tags do
end
end
@doc """
Updates a tag's associated image.
Takes a tag and image upload attributes, analyzes the upload,
persists it, and removes the old tag image if successful.
## Examples
iex> update_tag_image(tag, %{"image" => upload})
{:ok, %Tag{}}
"""
def update_tag_image(%Tag{} = tag, attrs) do
tag
|> Uploader.analyze_upload(attrs)
@ -190,6 +215,17 @@ defmodule Philomena.Tags do
end
end
@doc """
Removes a tag's associated image.
Removes the image from the tag and deletes the persisted file.
## Examples
iex> remove_tag_image(tag)
{:ok, %Tag{}}
"""
def remove_tag_image(%Tag{} = tag) do
tag
|> Tag.remove_image_changeset()
@ -223,6 +259,18 @@ defmodule Philomena.Tags do
{:ok, tag}
end
@doc """
Performs the actual deletion of a tag.
Removes the tag from the database, deletes its search index,
and reindexes all images that were tagged with it.
## Examples
iex> perform_delete(123)
:ok
"""
def perform_delete(tag_id) do
tag = get_tag!(tag_id)
@ -243,6 +291,19 @@ defmodule Philomena.Tags do
|> Search.reindex(Image)
end
@doc """
Creates an alias from one tag to another.
Takes a source tag and target tag name, creating an alias relationship
where the source tag becomes an alias of the target tag. Once the alias
is created, a job is queued to finish processing the alias.
## Examples
iex> alias_tag(source_tag, %{"target_tag" => "destination"})
{:ok, %Tag{}}
"""
def alias_tag(%Tag{} = tag, attrs) do
target_tag = Repo.get_by(Tag, name: String.downcase(attrs["target_tag"]))
@ -261,6 +322,19 @@ defmodule Philomena.Tags do
end
end
@doc """
Performs the actual tag aliasing operation.
Transfers all associations from the source tag to the target tag,
including image taggings, filters, user watches, and other relationships.
Updates counters and reindexes affected records.
## Examples
iex> perform_alias(123, 456)
:ok
"""
def perform_alias(tag_id, target_tag_id) do
tag = get_tag!(tag_id)
target_tag = get_tag!(target_tag_id)
@ -315,14 +389,36 @@ defmodule Philomena.Tags do
# Finally, reindex
reindex_tag_images(target_tag)
reindex_tags([tag, target_tag])
:ok
end
@doc """
Enqueues reindexing of all images associated with a tag.
## Examples
iex> reindex_tag_images(tag)
{:ok, %Tag{}}
"""
def reindex_tag_images(%Tag{} = tag) do
Exq.enqueue(Exq, "indexing", TagReindexWorker, [tag.id])
{:ok, tag}
end
@doc """
Performs reindexing of all images associated with a tag.
Updates the tag's image count to reflect the current number of non-hidden images,
then reindexes all associated images and filters that reference this tag.
## Examples
iex> perform_reindex_images(123)
"""
def perform_reindex_images(tag_id) do
tag = get_tag!(tag_id)
@ -351,12 +447,32 @@ defmodule Philomena.Tags do
|> Search.reindex(Filter)
end
@doc """
Enqueues removal of a tag alias.
## Examples
iex> unalias_tag(tag)
{:ok, %Tag{}}
"""
def unalias_tag(%Tag{} = tag) do
Exq.enqueue(Exq, "indexing", TagUnaliasWorker, [tag.id])
{:ok, tag}
end
@doc """
Performs removal of a tag alias.
Removes the alias relationship between two tags and reindexes
the images of the formerly aliased tag.
## Examples
iex> perform_unalias(123)
{:ok, %Tag{}}
"""
def perform_unalias(tag_id) do
tag = get_tag!(tag_id)
former_alias = Repo.preload(tag, :aliased_tag).aliased_tag
@ -389,6 +505,18 @@ defmodule Philomena.Tags do
|> Repo.update_all([])
end
@doc """
Copies tags from one image to another.
Creates new taggings on the target image for all tags present on the source image,
updates tag counters, and returns the list of copied tags.
## Examples
iex> copy_tags(source_image, target_image)
[%Tag{}, ...]
"""
def copy_tags(source, target) do
# Ecto bug:
# ** (DBConnection.EncodeError) Postgrex expected a binary, got 5.
@ -437,22 +565,66 @@ defmodule Philomena.Tags do
Tag.changeset(tag, %{})
end
@doc """
Queues a single tag for search index updates.
Returns the tag struct unchanged, for use in a pipeline.
## Examples
iex> reindex_tag(tag)
%Tag{}
"""
def reindex_tag(%Tag{} = tag) do
Exq.enqueue(Exq, "indexing", IndexWorker, ["Tags", "id", [tag.id]])
tag
end
@doc """
Queues a list of tags for search index updates.
Returns the list of tags unchanged, for use in a pipeline.
## Examples
iex> reindex_tags([%Tag{}, %Tag{}, ...])
[%Tag{}, %Tag{}, ...]
"""
def reindex_tags(tags) do
Exq.enqueue(Exq, "indexing", IndexWorker, ["Tags", "id", Enum.map(tags, & &1.id)])
tags
end
@doc """
Returns the list of associations to preload for tag indexing.
## Examples
iex> indexing_preloads()
[:aliased_tag, :aliases, :implied_tags, :implied_by_tags]
"""
def indexing_preloads do
[:aliased_tag, :aliases, :implied_tags, :implied_by_tags]
end
@doc """
Performs reindexing of tags based on a column condition.
Takes a column name and a list of values to match against that column,
then reindexes all matching tags.
## Examples
iex> perform_reindex(:id, [1, 2, 3])
{:ok, []}
iex> perform_reindex(:name, ["safe", "suggestive"])
{:ok, []}
"""
def perform_reindex(column, condition) do
Tag
|> preload(^indexing_preloads())

View file

@ -138,26 +138,71 @@ defmodule Philomena.Topics do
Topic.changeset(topic, %{})
end
@doc """
Makes a topic sticky, appearing at the top of its forum.
## Examples
iex> stick_topic(topic)
{:ok, %Topic{}}
"""
def stick_topic(topic) do
Topic.stick_changeset(topic)
|> Repo.update()
end
@doc """
Removes sticky status from a topic.
## Examples
iex> unstick_topic(topic)
{:ok, %Topic{}}
"""
def unstick_topic(topic) do
Topic.unstick_changeset(topic)
|> Repo.update()
end
@doc """
Locks a topic to prevent further posting.
## Examples
iex> lock_topic(topic, %{"lock_reason" => "Off topic"}, user)
{:ok, %Topic{}}
"""
def lock_topic(%Topic{} = topic, attrs, user) do
Topic.lock_changeset(topic, attrs, user)
|> Repo.update()
end
@doc """
Unlocks a topic to allow posting again.
## Examples
iex> unlock_topic(topic)
{:ok, %Topic{}}
"""
def unlock_topic(%Topic{} = topic) do
Topic.unlock_changeset(topic)
|> Repo.update()
end
@doc """
Moves a topic to a different forum, updating post counts for both forums.
## Examples
iex> move_topic(topic, 123)
{:ok, %{topic: %Topic{}}}
"""
def move_topic(topic, new_forum_id) do
old_forum_id = topic.forum_id
topic_changes = Topic.move_changeset(topic, new_forum_id)
@ -183,6 +228,15 @@ defmodule Philomena.Topics do
|> Repo.transaction()
end
@doc """
Hides a topic and updates related forum data.
## Examples
iex> hide_topic(topic, "Violates rules", moderator)
{:ok, %Topic{}}
"""
def hide_topic(topic, deletion_reason, user) do
topic_changes = Topic.hide_changeset(topic, deletion_reason, user)
@ -205,11 +259,29 @@ defmodule Philomena.Topics do
end
end
@doc """
Unhides a previously hidden topic.
## Examples
iex> unhide_topic(topic)
{:ok, %Topic{}}
"""
def unhide_topic(topic) do
Topic.unhide_changeset(topic)
|> Repo.update()
end
@doc """
Updates a topic's title.
## Examples
iex> update_topic_title(topic, %{"title" => "New Title"})
{:ok, %Topic{}}
"""
def update_topic_title(topic, attrs) do
topic
|> Topic.title_changeset(attrs)

View file

@ -273,6 +273,12 @@ defmodule Philomena.Users do
@doc """
Unconditionally unlocks the given user.
## Examples
iex> unlock_user(user)
{:ok, %User{}}
"""
def unlock_user(user) do
user
@ -369,6 +375,20 @@ defmodule Philomena.Users do
load_with_roles(query)
end
@doc """
Checks if a TOTP token is valid for a given user.
Returns false if no user is provided.
## Examples
iex> user_totp_token_valid?(user, "123456")
true
iex> user_totp_token_valid?(nil, "123456")
false
"""
def user_totp_token_valid?(nil, _token) do
false
end
@ -503,6 +523,15 @@ defmodule Philomena.Users do
end
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user(user)
%Ecto.Changeset{data: %User{}}
"""
def change_user(%User{} = user) do
User.changeset(user, %{})
end
@ -544,30 +573,84 @@ defmodule Philomena.Users do
defp clean_roles(nil), do: []
defp clean_roles(roles), do: Enum.filter(roles, &("" != &1))
@doc """
Updates a user's spoiler type settings.
## Examples
iex> update_spoiler_type(user, %{spoiler_type: "click"})
{:ok, %User{}}
iex> update_spoiler_type(user, %{spoiler_type: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_spoiler_type(%User{} = user, attrs) do
user
|> User.spoiler_type_changeset(attrs)
|> Repo.update()
end
@doc """
Updates a user's general settings.
## Examples
iex> update_settings(user, %{"theme" => "dark"})
{:ok, %User{}}
iex> update_settings(user, %{"theme" => bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_settings(%User{} = user, attrs) do
user
|> User.settings_changeset(attrs)
|> Repo.update()
end
@doc """
Updates a user's profile description and personal title.
## Examples
iex> update_description(user, %{"description" => "Hello world"})
{:ok, %User{}}
iex> update_description(user, %{"personal_title" => "Site Admin"})
{:error, %Ecto.Changeset{}}
"""
def update_description(%User{} = user, attrs) do
user
|> User.description_changeset(attrs)
|> Repo.update()
end
@doc """
Updates a user's moderation scratchpad content.
## Examples
iex> update_scratchpad(user, %{"scratchpad" => "My notes"})
{:ok, %User{}}
"""
def update_scratchpad(%User{} = user, attrs) do
user
|> User.scratchpad_changeset(attrs)
|> Repo.update()
end
@doc """
Adds a tag to a user's watched tags list.
## Examples
iex> watch_tag(user, tag)
{:ok, %User{}}
"""
def watch_tag(%User{} = user, tag) do
watched_tag_ids = Enum.uniq([tag.id | user.watched_tag_ids])
@ -576,6 +659,15 @@ defmodule Philomena.Users do
|> Repo.update()
end
@doc """
Removes a tag from a user's watched tags list.
## Examples
iex> unwatch_tag(user, tag)
{:ok, %User{}}
"""
def unwatch_tag(%User{} = user, tag) do
watched_tag_ids = user.watched_tag_ids -- [tag.id]
@ -584,6 +676,17 @@ defmodule Philomena.Users do
|> Repo.update()
end
@doc """
Updates a user's avatar with the provided file.
Handles file analysis and persistence.
## Examples
iex> update_avatar(user, %{"avatar" => upload})
{:ok, %User{}}
"""
def update_avatar(%User{} = user, attrs) do
user
|> Uploader.analyze_upload(attrs)
@ -600,6 +703,15 @@ defmodule Philomena.Users do
end
end
@doc """
Removes a user's avatar.
## Examples
iex> remove_avatar(user)
{:ok, %User{}}
"""
def remove_avatar(%User{} = user) do
user
|> User.remove_avatar_changeset()
@ -615,6 +727,17 @@ defmodule Philomena.Users do
end
end
@doc """
Updates a user's name and records the change in history.
Triggers a background job to update references to the old username.
## Examples
iex> update_name(user, %{"name" => "new_name"})
{:ok, %User{}}
"""
def update_name(user, user_params) do
old_name = user.name
@ -636,6 +759,17 @@ defmodule Philomena.Users do
end
end
@doc """
Updates all search engine references to a user's old name with their new name.
This is called as a background job after a user requests a name change.
## Examples
iex> perform_rename("old_name", "new_name")
:ok
"""
def perform_rename(old_name, new_name) do
Images.user_name_reindex(old_name, new_name)
Comments.user_name_reindex(old_name, new_name)
@ -645,36 +779,96 @@ defmodule Philomena.Users do
Filters.user_name_reindex(old_name, new_name)
end
@doc """
Reactivates a previously deactivated user account.
## Examples
iex> reactivate_user(user)
{:ok, %User{}}
"""
def reactivate_user(%User{} = user) do
user
|> User.reactivate_changeset()
|> Repo.update()
end
@doc """
Deactivates a user account.
Takes a moderator who is recorded as performing the deactivation.
## Examples
iex> deactivate_user(moderator, user)
{:ok, %User{}}
"""
def deactivate_user(moderator, %User{} = user) do
user
|> User.deactivate_changeset(moderator)
|> Repo.update()
end
@doc """
Generates a new API key for the user.
## Examples
iex> reset_api_key(user)
{:ok, %User{}}
"""
def reset_api_key(%User{} = user) do
user
|> User.api_key_changeset()
|> Repo.update()
end
@doc """
Forces a specific filter on a user's account, which will be applied in
conjunction to the user's current filter.
## Examples
iex> force_filter(user, %{"forced_filter_id" => 123})
{:ok, %User{}}
iex> force_filter(user, %{"forced_filter_id" => bad_value})
{:error, %Ecto.Changeset{}}
"""
def force_filter(%User{} = user, user_params) do
user
|> User.force_filter_changeset(user_params)
|> Repo.update()
end
@doc """
Removes a forced filter from a user's account.
## Examples
iex> unforce_filter(user)
{:ok, %User{}}
"""
def unforce_filter(%User{} = user) do
user
|> User.unforce_filter_changeset()
|> Repo.update()
end
@doc """
Clears a user's recent filter history.
## Examples
iex> clear_recent_filters(user)
{:ok, %User{}}
"""
def clear_recent_filters(%User{} = user) do
user
|> User.clear_recent_filters_changeset()
@ -688,18 +882,50 @@ defmodule Philomena.Users do
|> setup_roles()
end
@doc """
Marks a user as verified for the purposes of automatically approving uploads,
and posting images in comments/posts/messages without moderator review.
## Examples
iex> verify_user(user)
{:ok, %User{}}
"""
def verify_user(%User{} = user) do
user
|> User.verify_changeset()
|> Repo.update()
end
@doc """
Unverifies a user, removing the automatic approval status.
## Examples
iex> unverify_user(user)
{:ok, %User{}}
"""
def unverify_user(%User{} = user) do
user
|> User.unverify_changeset()
|> Repo.update()
end
@doc """
Erases all changes associated with a user account, removing all personal
data and anonymizing the account.
This is primarily intended for use with spam accounts or other situations
where all of a user's data should be removed from the system.
## Examples
iex> erase_user(user, moderator)
{:ok, %User{}}
"""
def erase_user(%User{} = user, %User{} = moderator) do
# Deactivate to prevent the user from racing these changes
{:ok, user} = deactivate_user(moderator, user)