* 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.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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

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

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