mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
Renaming (#112)
* First (not-yet-working) attempt at self-renaming * Actually working renames * last_renamed_at * Prevent renaming while banned * Move username changing from controller to model * Username change logging * Rate limiting for username changes * username -> name and format * add UBQ * modify interval Co-authored-by: Joey <joeyponi@gmail.com>
This commit is contained in:
parent
d03c1d7e5b
commit
af9e779c59
21 changed files with 298 additions and 3 deletions
|
@ -10,6 +10,7 @@ defmodule Philomena.Comments do
|
|||
alias Philomena.Elasticsearch
|
||||
alias Philomena.Reports.Report
|
||||
alias Philomena.Comments.Comment
|
||||
alias Philomena.Comments.ElasticsearchIndex, as: CommentIndex
|
||||
alias Philomena.Images.Image
|
||||
alias Philomena.Images
|
||||
alias Philomena.Notifications
|
||||
|
@ -188,6 +189,12 @@ defmodule Philomena.Comments do
|
|||
Comment.changeset(comment, %{})
|
||||
end
|
||||
|
||||
def user_name_reindex(old_name, new_name) do
|
||||
data = CommentIndex.user_name_update_by_query(old_name, new_name)
|
||||
|
||||
Elasticsearch.update_by_query(Comment, data.query, data.set_replacements, data.replacements)
|
||||
end
|
||||
|
||||
def reindex_comment(%Comment{} = comment) do
|
||||
spawn(fn ->
|
||||
Comment
|
||||
|
|
|
@ -60,4 +60,12 @@ defmodule Philomena.Comments.ElasticsearchIndex do
|
|||
body: comment.body
|
||||
}
|
||||
end
|
||||
|
||||
def user_name_update_by_query(old_name, new_name) do
|
||||
%{
|
||||
query: %{term: %{author: old_name}},
|
||||
replacements: [%{path: ["author"], old: old_name, new: new_name}],
|
||||
set_replacements: []
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule Philomena.Elasticsearch do
|
|||
alias Philomena.Repo
|
||||
require Logger
|
||||
import Ecto.Query
|
||||
import Elastix.HTTP
|
||||
|
||||
alias Philomena.Comments.Comment
|
||||
alias Philomena.Galleries.Gallery
|
||||
|
@ -106,6 +107,71 @@ defmodule Philomena.Elasticsearch do
|
|||
end)
|
||||
end
|
||||
|
||||
def update_by_query(module, query_body, set_replacements, replacements) do
|
||||
index = index_for(module)
|
||||
|
||||
url =
|
||||
elastic_url()
|
||||
|> prepare_url([index.index_name(), "_update_by_query"])
|
||||
|> append_query_string(%{conflicts: "proceed"})
|
||||
|
||||
# Elasticsearch "Painless" scripting language
|
||||
script = """
|
||||
// Replace values in "sets" (arrays in the source document)
|
||||
for (int i = 0; i < params.set_replacements.length; ++i) {
|
||||
def replacement = params.set_replacements[i];
|
||||
def path = replacement.path;
|
||||
def old_value = replacement.old;
|
||||
def new_value = replacement.new;
|
||||
def reference = ctx._source;
|
||||
|
||||
for (int j = 0; j < path.length; ++j) {
|
||||
reference = reference[path[j]];
|
||||
}
|
||||
|
||||
for (int j = 0; j < reference.length; ++j) {
|
||||
if (reference[j].equals(old_value)) {
|
||||
reference[j] = new_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace values in standalone fields
|
||||
for (int i = 0; i < params.replacements.length; ++i) {
|
||||
def replacement = params.replacements[i];
|
||||
def path = replacement.path;
|
||||
def old_value = replacement.old;
|
||||
def new_value = replacement.new;
|
||||
def reference = ctx._source;
|
||||
|
||||
// A little bit more complicated: go up to the last one before it
|
||||
// so that the value can actually be replaced
|
||||
|
||||
for (int j = 0; j < path.length - 1; ++j) {
|
||||
reference = reference[path[j]];
|
||||
}
|
||||
|
||||
if (reference[path[path.length - 1]] != null && reference[path[path.length - 1]].equals(old_value)) {
|
||||
reference[path[path.length - 1]] = new_value;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
body =
|
||||
Jason.encode!(%{
|
||||
script: %{
|
||||
source: script,
|
||||
params: %{
|
||||
set_replacements: set_replacements,
|
||||
replacements: replacements
|
||||
}
|
||||
},
|
||||
query: query_body
|
||||
})
|
||||
|
||||
{:ok, %{status_code: 200}} = Elastix.HTTP.post(url, body)
|
||||
end
|
||||
|
||||
def search(module, query_body) do
|
||||
index = index_for(module)
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Philomena.Galleries do
|
|||
alias Philomena.Elasticsearch
|
||||
alias Philomena.Galleries.Gallery
|
||||
alias Philomena.Galleries.Interaction
|
||||
alias Philomena.Galleries.ElasticsearchIndex, as: GalleryIndex
|
||||
alias Philomena.Notifications
|
||||
alias Philomena.Images
|
||||
|
||||
|
@ -122,6 +123,12 @@ defmodule Philomena.Galleries do
|
|||
Gallery.changeset(gallery, %{})
|
||||
end
|
||||
|
||||
def user_name_reindex(old_name, new_name) do
|
||||
data = GalleryIndex.user_name_update_by_query(old_name, new_name)
|
||||
|
||||
Elasticsearch.update_by_query(Gallery, data.query, data.set_replacements, data.replacements)
|
||||
end
|
||||
|
||||
def reindex_gallery(%Gallery{} = gallery) do
|
||||
spawn(fn ->
|
||||
Gallery
|
||||
|
|
|
@ -59,4 +59,15 @@ defmodule Philomena.Galleries.ElasticsearchIndex do
|
|||
description: gallery.description
|
||||
}
|
||||
end
|
||||
|
||||
def user_name_update_by_query(old_name, new_name) do
|
||||
old_name = String.downcase(old_name)
|
||||
new_name = String.downcase(new_name)
|
||||
|
||||
%{
|
||||
query: %{term: %{creator: old_name}},
|
||||
replacements: [%{path: ["creator"], old: old_name, new: new_name}],
|
||||
set_replacements: []
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,7 @@ defmodule Philomena.Images do
|
|||
alias Philomena.Images.Hider
|
||||
alias Philomena.Images.Uploader
|
||||
alias Philomena.Images.Tagging
|
||||
alias Philomena.Images.ElasticsearchIndex, as: ImageIndex
|
||||
alias Philomena.ImageFeatures.ImageFeature
|
||||
alias Philomena.SourceChanges.SourceChange
|
||||
alias Philomena.TagChanges.TagChange
|
||||
|
@ -522,6 +523,12 @@ defmodule Philomena.Images do
|
|||
Image.changeset(image, %{})
|
||||
end
|
||||
|
||||
def user_name_reindex(old_name, new_name) do
|
||||
data = ImageIndex.user_name_update_by_query(old_name, new_name)
|
||||
|
||||
Elasticsearch.update_by_query(Image, data.query, data.set_replacements, data.replacements)
|
||||
end
|
||||
|
||||
def reindex_image(%Image{} = image) do
|
||||
reindex_images([image.id])
|
||||
|
||||
|
|
|
@ -143,6 +143,38 @@ defmodule Philomena.Images.ElasticsearchIndex do
|
|||
}
|
||||
end
|
||||
|
||||
def user_name_update_by_query(old_name, new_name) do
|
||||
old_name = String.downcase(old_name)
|
||||
new_name = String.downcase(new_name)
|
||||
|
||||
%{
|
||||
query: %{
|
||||
bool: %{
|
||||
should: [
|
||||
%{term: %{uploader: old_name}},
|
||||
%{term: %{true_uploader: old_name}},
|
||||
%{term: %{deleted_by_user: old_name}},
|
||||
%{term: %{favourited_by_users: old_name}},
|
||||
%{term: %{hidden_by_users: old_name}},
|
||||
%{term: %{upvoters: old_name}},
|
||||
%{term: %{downvoters: old_name}}
|
||||
]
|
||||
}
|
||||
},
|
||||
replacements: [
|
||||
%{path: ["uploader"], old: old_name, new: new_name},
|
||||
%{path: ["true_uploader"], old: old_name, new: new_name},
|
||||
%{path: ["deleted_by_user"], old: old_name, new: new_name}
|
||||
],
|
||||
set_replacements: [
|
||||
%{path: ["favourited_by_users"], old: old_name, new: new_name},
|
||||
%{path: ["hidden_by_users"], old: old_name, new: new_name},
|
||||
%{path: ["upvoters"], old: old_name, new: new_name},
|
||||
%{path: ["downvoters"], old: old_name, new: new_name}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def wilson_score(%{upvotes_count: upvotes, downvotes_count: downvotes}) when upvotes > 0 do
|
||||
# Population size
|
||||
n = (upvotes + downvotes) / 1
|
||||
|
|
|
@ -11,6 +11,7 @@ defmodule Philomena.Posts do
|
|||
alias Philomena.Topics.Topic
|
||||
alias Philomena.Topics
|
||||
alias Philomena.Posts.Post
|
||||
alias Philomena.Posts.ElasticsearchIndex, as: PostIndex
|
||||
alias Philomena.Forums.Forum
|
||||
alias Philomena.Notifications
|
||||
alias Philomena.Versions
|
||||
|
@ -204,6 +205,12 @@ defmodule Philomena.Posts do
|
|||
Post.changeset(post, %{})
|
||||
end
|
||||
|
||||
def user_name_reindex(old_name, new_name) do
|
||||
data = PostIndex.user_name_update_by_query(old_name, new_name)
|
||||
|
||||
Elasticsearch.update_by_query(Post, data.query, data.set_replacements, data.replacements)
|
||||
end
|
||||
|
||||
def reindex_post(%Post{} = post) do
|
||||
spawn(fn ->
|
||||
Post
|
||||
|
|
|
@ -72,4 +72,15 @@ defmodule Philomena.Posts.ElasticsearchIndex do
|
|||
destroyed_content: post.destroyed_content
|
||||
}
|
||||
end
|
||||
|
||||
def user_name_update_by_query(old_name, new_name) do
|
||||
old_name = String.downcase(old_name)
|
||||
new_name = String.downcase(new_name)
|
||||
|
||||
%{
|
||||
query: %{term: %{author: old_name}},
|
||||
replacements: [%{path: ["author"], old: old_name, new: new_name}],
|
||||
set_replacements: []
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Philomena.Reports do
|
|||
|
||||
alias Philomena.Elasticsearch
|
||||
alias Philomena.Reports.Report
|
||||
alias Philomena.Reports.ElasticsearchIndex, as: ReportIndex
|
||||
alias Philomena.Polymorphic
|
||||
|
||||
@doc """
|
||||
|
@ -122,6 +123,12 @@ defmodule Philomena.Reports do
|
|||
|> Repo.update()
|
||||
end
|
||||
|
||||
def user_name_reindex(old_name, new_name) do
|
||||
data = ReportIndex.user_name_update_by_query(old_name, new_name)
|
||||
|
||||
Elasticsearch.update_by_query(Report, data.query, data.set_replacements, data.replacements)
|
||||
end
|
||||
|
||||
def reindex_reports(report_ids) do
|
||||
spawn(fn ->
|
||||
Report
|
||||
|
|
|
@ -65,6 +65,27 @@ defmodule Philomena.Reports.ElasticsearchIndex do
|
|||
}
|
||||
end
|
||||
|
||||
def user_name_update_by_query(old_name, new_name) do
|
||||
old_name = String.downcase(old_name)
|
||||
new_name = String.downcase(new_name)
|
||||
|
||||
%{
|
||||
query: %{
|
||||
bool: %{
|
||||
should: [
|
||||
%{term: %{user: old_name}},
|
||||
%{term: %{admin: old_name}}
|
||||
]
|
||||
}
|
||||
},
|
||||
replacements: [
|
||||
%{path: ["user"], old: old_name, new: new_name},
|
||||
%{path: ["admin"], old: old_name, new: new_name}
|
||||
],
|
||||
set_replacements: []
|
||||
}
|
||||
end
|
||||
|
||||
defp image_id(%{reportable_type: "Image", reportable_id: image_id}), do: image_id
|
||||
defp image_id(%{reportable_type: "Comment", reportable: %{image_id: image_id}}), do: image_id
|
||||
defp image_id(_report), do: nil
|
||||
|
|
|
@ -12,9 +12,9 @@ defmodule Philomena.UserNameChanges.UserNameChange do
|
|||
end
|
||||
|
||||
@doc false
|
||||
def changeset(user_name_change, attrs) do
|
||||
def changeset(user_name_change, old_name) do
|
||||
user_name_change
|
||||
|> cast(attrs, [])
|
||||
|> change(name: old_name)
|
||||
|> validate_required([])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,6 +12,12 @@ defmodule Philomena.Users do
|
|||
alias Philomena.{Forums, Forums.Forum}
|
||||
alias Philomena.Topics
|
||||
alias Philomena.Roles.Role
|
||||
alias Philomena.UserNameChanges.UserNameChange
|
||||
alias Philomena.Images
|
||||
alias Philomena.Comments
|
||||
alias Philomena.Posts
|
||||
alias Philomena.Galleries
|
||||
alias Philomena.Reports
|
||||
|
||||
use Pow.Ecto.Context,
|
||||
repo: Repo,
|
||||
|
@ -164,6 +170,33 @@ defmodule Philomena.Users do
|
|||
|> Repo.isolated_transaction(:serializable)
|
||||
end
|
||||
|
||||
def update_name(user, user_params) do
|
||||
old_name = user.name
|
||||
|
||||
name_change = UserNameChange.changeset(%UserNameChange{user_id: user.id}, user.name)
|
||||
account = User.name_changeset(user, user_params)
|
||||
|
||||
Multi.new()
|
||||
|> Multi.insert(:name_change, name_change)
|
||||
|> Multi.update(:account, account)
|
||||
|> Repo.isolated_transaction(:serializable)
|
||||
|> case do
|
||||
{:ok, %{account: %{name: new_name}}} = result ->
|
||||
spawn(fn ->
|
||||
Images.user_name_reindex(old_name, new_name)
|
||||
Comments.user_name_reindex(old_name, new_name)
|
||||
Posts.user_name_reindex(old_name, new_name)
|
||||
Galleries.user_name_reindex(old_name, new_name)
|
||||
Reports.user_name_reindex(old_name, new_name)
|
||||
end)
|
||||
|
||||
result
|
||||
|
||||
result ->
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def reactivate_user(%User{} = user) do
|
||||
user
|
||||
|> User.reactivate_changeset()
|
||||
|
|
|
@ -284,6 +284,12 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
|
|||
def can?(%User{id: id}, :edit_description, %User{id: id}), do: true
|
||||
def can?(%User{id: id}, :edit_title, %User{id: id}), do: true
|
||||
|
||||
# Edit their username
|
||||
def can?(%User{id: id}, :change_username, %User{id: id} = user) do
|
||||
time_ago = NaiveDateTime.utc_now() |> NaiveDateTime.add(-1 * 60 * 60 * 24 * 90)
|
||||
NaiveDateTime.diff(user.last_renamed_at, time_ago) < 0
|
||||
end
|
||||
|
||||
# View conversations they are involved in
|
||||
def can?(%User{id: id}, :show, %Conversation{to_id: id}), do: true
|
||||
def can?(%User{id: id}, :show, %Conversation{from_id: id}), do: true
|
||||
|
|
|
@ -250,6 +250,17 @@ defmodule Philomena.Users.User do
|
|||
|> cast(attrs, [:scratchpad])
|
||||
end
|
||||
|
||||
def name_changeset(user, attrs) do
|
||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
|
||||
user
|
||||
|> cast(attrs, [:name])
|
||||
|> validate_required([:name])
|
||||
|> put_slug
|
||||
|> unique_constraints()
|
||||
|> put_change(:last_renamed_at, now)
|
||||
end
|
||||
|
||||
def avatar_changeset(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [
|
||||
|
|
|
@ -6,7 +6,9 @@ defmodule PhilomenaWeb.ChannelController do
|
|||
alias Philomena.Repo
|
||||
import Ecto.Query
|
||||
|
||||
plug :load_and_authorize_resource, model: Channel, only: [:show, :new, :create, :edit, :update, :delete]
|
||||
plug :load_and_authorize_resource,
|
||||
model: Channel,
|
||||
only: [:show, :new, :create, :edit, :update, :delete]
|
||||
|
||||
def index(conn, params) do
|
||||
show_nsfw? = conn.cookies["chan_nsfw"] == "true"
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
defmodule PhilomenaWeb.Registration.NameController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias Philomena.Users
|
||||
|
||||
plug PhilomenaWeb.FilterBannedUsersPlug
|
||||
plug :verify_authorized
|
||||
|
||||
def edit(conn, _params) do
|
||||
changeset = Users.change_user(conn.assigns.current_user)
|
||||
|
||||
render(conn, "edit.html", title: "Editing Name", changeset: changeset)
|
||||
end
|
||||
|
||||
def update(conn, %{"user" => user_params}) do
|
||||
case Users.update_name(conn.assigns.current_user, user_params) do
|
||||
{:ok, %{account: user}} ->
|
||||
conn
|
||||
|> put_flash(:info, "Name successfully updated.")
|
||||
|> redirect(to: Routes.profile_path(conn, :show, user))
|
||||
|
||||
{:error, %{account: changeset}} ->
|
||||
render(conn, "edit.html", changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_authorized(conn, _opts) do
|
||||
case Canada.Can.can?(conn.assigns.current_user, :change_username, conn.assigns.current_user) do
|
||||
true -> conn
|
||||
_false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -91,6 +91,7 @@ defmodule PhilomenaWeb.Router do
|
|||
# Additional routes for TOTP
|
||||
scope "/registrations", Registration, as: :registration do
|
||||
resources "/totp", TotpController, only: [:edit, :update], singleton: true
|
||||
resources "/name", NameController, only: [:edit, :update], singleton: true
|
||||
end
|
||||
|
||||
scope "/sessions", Session, as: :session do
|
||||
|
|
|
@ -12,6 +12,11 @@ p
|
|||
' Looking to change your avatar?
|
||||
= link "Click here!", to: Routes.avatar_path(@conn, :edit)
|
||||
|
||||
= if can?(@conn, :change_username, @current_user) do
|
||||
p
|
||||
' Looking to change your username?
|
||||
= link "Click here!", to: Routes.registration_name_path(@conn, :edit)
|
||||
|
||||
h3 API Key
|
||||
p
|
||||
' Your API key is
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
h1 Editing Name
|
||||
|
||||
= form_for @changeset, Routes.registration_name_path(@conn, :update), [as: :user], fn f ->
|
||||
= if @changeset.action do
|
||||
.alert.alert-danger
|
||||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
p Enter your new name here. Usernames may only be changed once every 90 days. Please be aware that once you change your name, your previous name will be available for reuse, and someone else may claim it.
|
||||
|
||||
.field
|
||||
= text_input f, :name, class: "input", placeholder: "Name", required: true
|
||||
= error_tag f, :name
|
||||
|
||||
.action
|
||||
= submit "Save", class: "button"
|
||||
|
||||
p = link "Back", to: Routes.pow_registration_path(@conn, :edit)
|
3
lib/philomena_web/views/registration/name_view.ex
Normal file
3
lib/philomena_web/views/registration/name_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule PhilomenaWeb.Registration.NameView do
|
||||
use PhilomenaWeb, :view
|
||||
end
|
Loading…
Reference in a new issue