* 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:
liamwhite 2020-05-02 18:17:55 -04:00 committed by GitHub
parent d03c1d7e5b
commit af9e779c59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 298 additions and 3 deletions

View file

@ -10,6 +10,7 @@ defmodule Philomena.Comments do
alias Philomena.Elasticsearch alias Philomena.Elasticsearch
alias Philomena.Reports.Report alias Philomena.Reports.Report
alias Philomena.Comments.Comment alias Philomena.Comments.Comment
alias Philomena.Comments.ElasticsearchIndex, as: CommentIndex
alias Philomena.Images.Image alias Philomena.Images.Image
alias Philomena.Images alias Philomena.Images
alias Philomena.Notifications alias Philomena.Notifications
@ -188,6 +189,12 @@ defmodule Philomena.Comments do
Comment.changeset(comment, %{}) Comment.changeset(comment, %{})
end 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 def reindex_comment(%Comment{} = comment) do
spawn(fn -> spawn(fn ->
Comment Comment

View file

@ -60,4 +60,12 @@ defmodule Philomena.Comments.ElasticsearchIndex do
body: comment.body body: comment.body
} }
end 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 end

View file

@ -3,6 +3,7 @@ defmodule Philomena.Elasticsearch do
alias Philomena.Repo alias Philomena.Repo
require Logger require Logger
import Ecto.Query import Ecto.Query
import Elastix.HTTP
alias Philomena.Comments.Comment alias Philomena.Comments.Comment
alias Philomena.Galleries.Gallery alias Philomena.Galleries.Gallery
@ -106,6 +107,71 @@ defmodule Philomena.Elasticsearch do
end) end)
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 def search(module, query_body) do
index = index_for(module) index = index_for(module)

View file

@ -10,6 +10,7 @@ defmodule Philomena.Galleries do
alias Philomena.Elasticsearch alias Philomena.Elasticsearch
alias Philomena.Galleries.Gallery alias Philomena.Galleries.Gallery
alias Philomena.Galleries.Interaction alias Philomena.Galleries.Interaction
alias Philomena.Galleries.ElasticsearchIndex, as: GalleryIndex
alias Philomena.Notifications alias Philomena.Notifications
alias Philomena.Images alias Philomena.Images
@ -122,6 +123,12 @@ defmodule Philomena.Galleries do
Gallery.changeset(gallery, %{}) Gallery.changeset(gallery, %{})
end 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 def reindex_gallery(%Gallery{} = gallery) do
spawn(fn -> spawn(fn ->
Gallery Gallery

View file

@ -59,4 +59,15 @@ defmodule Philomena.Galleries.ElasticsearchIndex do
description: gallery.description description: gallery.description
} }
end 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 end

View file

@ -14,6 +14,7 @@ defmodule Philomena.Images do
alias Philomena.Images.Hider alias Philomena.Images.Hider
alias Philomena.Images.Uploader alias Philomena.Images.Uploader
alias Philomena.Images.Tagging alias Philomena.Images.Tagging
alias Philomena.Images.ElasticsearchIndex, as: ImageIndex
alias Philomena.ImageFeatures.ImageFeature alias Philomena.ImageFeatures.ImageFeature
alias Philomena.SourceChanges.SourceChange alias Philomena.SourceChanges.SourceChange
alias Philomena.TagChanges.TagChange alias Philomena.TagChanges.TagChange
@ -522,6 +523,12 @@ defmodule Philomena.Images do
Image.changeset(image, %{}) Image.changeset(image, %{})
end 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 def reindex_image(%Image{} = image) do
reindex_images([image.id]) reindex_images([image.id])

View file

@ -143,6 +143,38 @@ defmodule Philomena.Images.ElasticsearchIndex do
} }
end 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 def wilson_score(%{upvotes_count: upvotes, downvotes_count: downvotes}) when upvotes > 0 do
# Population size # Population size
n = (upvotes + downvotes) / 1 n = (upvotes + downvotes) / 1

View file

@ -11,6 +11,7 @@ defmodule Philomena.Posts do
alias Philomena.Topics.Topic alias Philomena.Topics.Topic
alias Philomena.Topics alias Philomena.Topics
alias Philomena.Posts.Post alias Philomena.Posts.Post
alias Philomena.Posts.ElasticsearchIndex, as: PostIndex
alias Philomena.Forums.Forum alias Philomena.Forums.Forum
alias Philomena.Notifications alias Philomena.Notifications
alias Philomena.Versions alias Philomena.Versions
@ -204,6 +205,12 @@ defmodule Philomena.Posts do
Post.changeset(post, %{}) Post.changeset(post, %{})
end 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 def reindex_post(%Post{} = post) do
spawn(fn -> spawn(fn ->
Post Post

View file

@ -72,4 +72,15 @@ defmodule Philomena.Posts.ElasticsearchIndex do
destroyed_content: post.destroyed_content destroyed_content: post.destroyed_content
} }
end 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 end

View file

@ -8,6 +8,7 @@ defmodule Philomena.Reports do
alias Philomena.Elasticsearch alias Philomena.Elasticsearch
alias Philomena.Reports.Report alias Philomena.Reports.Report
alias Philomena.Reports.ElasticsearchIndex, as: ReportIndex
alias Philomena.Polymorphic alias Philomena.Polymorphic
@doc """ @doc """
@ -122,6 +123,12 @@ defmodule Philomena.Reports do
|> Repo.update() |> Repo.update()
end 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 def reindex_reports(report_ids) do
spawn(fn -> spawn(fn ->
Report Report

View file

@ -65,6 +65,27 @@ defmodule Philomena.Reports.ElasticsearchIndex do
} }
end 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: "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(%{reportable_type: "Comment", reportable: %{image_id: image_id}}), do: image_id
defp image_id(_report), do: nil defp image_id(_report), do: nil

View file

@ -12,9 +12,9 @@ defmodule Philomena.UserNameChanges.UserNameChange do
end end
@doc false @doc false
def changeset(user_name_change, attrs) do def changeset(user_name_change, old_name) do
user_name_change user_name_change
|> cast(attrs, []) |> change(name: old_name)
|> validate_required([]) |> validate_required([])
end end
end end

View file

@ -12,6 +12,12 @@ defmodule Philomena.Users do
alias Philomena.{Forums, Forums.Forum} alias Philomena.{Forums, Forums.Forum}
alias Philomena.Topics alias Philomena.Topics
alias Philomena.Roles.Role 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, use Pow.Ecto.Context,
repo: Repo, repo: Repo,
@ -164,6 +170,33 @@ defmodule Philomena.Users do
|> Repo.isolated_transaction(:serializable) |> Repo.isolated_transaction(:serializable)
end 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 def reactivate_user(%User{} = user) do
user user
|> User.reactivate_changeset() |> User.reactivate_changeset()

View file

@ -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_description, %User{id: id}), do: true
def can?(%User{id: id}, :edit_title, %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 # 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{to_id: id}), do: true
def can?(%User{id: id}, :show, %Conversation{from_id: id}), do: true def can?(%User{id: id}, :show, %Conversation{from_id: id}), do: true

View file

@ -250,6 +250,17 @@ defmodule Philomena.Users.User do
|> cast(attrs, [:scratchpad]) |> cast(attrs, [:scratchpad])
end 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 def avatar_changeset(user, attrs) do
user user
|> cast(attrs, [ |> cast(attrs, [

View file

@ -6,7 +6,9 @@ defmodule PhilomenaWeb.ChannelController do
alias Philomena.Repo alias Philomena.Repo
import Ecto.Query 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 def index(conn, params) do
show_nsfw? = conn.cookies["chan_nsfw"] == "true" show_nsfw? = conn.cookies["chan_nsfw"] == "true"

View file

@ -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

View file

@ -91,6 +91,7 @@ defmodule PhilomenaWeb.Router do
# Additional routes for TOTP # Additional routes for TOTP
scope "/registrations", Registration, as: :registration do scope "/registrations", Registration, as: :registration do
resources "/totp", TotpController, only: [:edit, :update], singleton: true resources "/totp", TotpController, only: [:edit, :update], singleton: true
resources "/name", NameController, only: [:edit, :update], singleton: true
end end
scope "/sessions", Session, as: :session do scope "/sessions", Session, as: :session do

View file

@ -12,6 +12,11 @@ p
' Looking to change your avatar? ' Looking to change your avatar?
= link "Click here!", to: Routes.avatar_path(@conn, :edit) = 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 h3 API Key
p p
' Your API key is ' Your API key is

View file

@ -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)

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.Registration.NameView do
use PhilomenaWeb, :view
end