static ghost ids
parent
ffb63ed2e0
commit
2e80974a74
|
@ -136,19 +136,25 @@ final class User(
|
|||
}
|
||||
|
||||
private def EnabledUser(username: String)(f: UserModel => Fu[Result])(implicit ctx: Context): Fu[Result] =
|
||||
env.user.repo named username flatMap {
|
||||
case None if isGranted(_.UserModView) => ctx.me.map(Holder) ?? { modC.searchTerm(_, username.trim) }
|
||||
case None => notFound
|
||||
case Some(u) if u.enabled || isGranted(_.UserModView) => f(u)
|
||||
case Some(u) =>
|
||||
negotiate(
|
||||
html = env.user.repo isErased u flatMap { erased =>
|
||||
if (erased.value) notFound
|
||||
else NotFound(html.user.show.page.disabled(u)).fuccess
|
||||
},
|
||||
api = _ => fuccess(NotFound(jsonError("No such user, or account closed")))
|
||||
)
|
||||
}
|
||||
if (UserModel.isGhost(username))
|
||||
negotiate(
|
||||
html = Ok(html.site.bits.ghost).fuccess,
|
||||
api = _ => notFoundJson("Deleted user")
|
||||
)
|
||||
else
|
||||
env.user.repo named username flatMap {
|
||||
case None if isGranted(_.UserModView) => ctx.me.map(Holder) ?? { modC.searchTerm(_, username.trim) }
|
||||
case None => notFound
|
||||
case Some(u) if u.enabled || isGranted(_.UserModView) => f(u)
|
||||
case Some(u) =>
|
||||
negotiate(
|
||||
html = env.user.repo isErased u flatMap { erased =>
|
||||
if (erased.value) notFound
|
||||
else NotFound(html.user.show.page.disabled(u)).fuccess
|
||||
},
|
||||
api = _ => fuccess(NotFound(jsonError("No such user, or account closed")))
|
||||
)
|
||||
}
|
||||
def showMini(username: String) =
|
||||
Open { implicit ctx =>
|
||||
OptionFuResult(env.user.repo named username) { user =>
|
||||
|
|
|
@ -150,13 +150,6 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
|
|||
player.rating.ifTrue(withRating) map { rating => s" ($rating)" },
|
||||
statusIcon
|
||||
)
|
||||
case Some(user) if user.id == "ghost" =>
|
||||
span(cls := "user-link")(
|
||||
"Ghost",
|
||||
(player.ratingDiff ifTrue withDiff) map { d =>
|
||||
frag(" ", showRatingDiff(d))
|
||||
}
|
||||
)
|
||||
case Some(user) =>
|
||||
frag(
|
||||
(if (link) a else span)(
|
||||
|
|
|
@ -53,4 +53,18 @@ object bits {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
def ghost(implicit ctx: Context) =
|
||||
views.html.base.layout(
|
||||
moreCss = cssTag("ghost"),
|
||||
title = "Deleted user"
|
||||
) {
|
||||
main(cls := "page-small box box-pad page")(
|
||||
h1("Deleted user"),
|
||||
div(
|
||||
p("This player account is gone!"),
|
||||
p("Nothing to see here, move along.")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,12 @@ if (typeof user == 'undefined') throw 'Usage: mongo lichess --eval \'user="usern
|
|||
|
||||
user = db.user4.findOne({ _id: user });
|
||||
|
||||
// if (!user || user.enabled || !user.erasedAt) throw 'Erase with lichess CLI first.';
|
||||
if (!user || user.enabled || !user.erasedAt) throw 'Erase with lichess CLI first.';
|
||||
|
||||
print(`\n\n Delete user ${user.username} and all references to their username!\n\n`);
|
||||
sleep(5000);
|
||||
|
||||
const ghostId = 'ghost';
|
||||
const newGhostId = () => {
|
||||
const idChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const idLength = 8;
|
||||
|
@ -31,9 +32,8 @@ const scrub = (collName, inDb) => f => {
|
|||
const byId = doc => ({ _id: doc._id });
|
||||
const deleteAllIn = (db, collName, field, value) => scrub(collName, db)(c => c.remove({ [field]: value || userId }));
|
||||
const deleteAll = (collName, field, value) => deleteAllIn(mainDb, collName, field, value);
|
||||
const setNewGhostId = (coll, doc, field) => coll.update(byId(doc), { $set: { [field]: newGhostId() } });
|
||||
const replaceWithNewGhostIds = (collName, field, inDb) =>
|
||||
scrub(collName, inDb)(c => c.find({ [field]: userId }, { _id: 1 }).forEach(doc => setNewGhostId(c, doc, field)));
|
||||
const replaceWithGhostId = (collName, field, inDb) =>
|
||||
scrub(collName, inDb)(c => c.updateMany({ [field]: userId }, { $set: { [field]: ghostId } }));
|
||||
|
||||
const userId = user._id;
|
||||
const tosViolation = user.marks && user.marks.length;
|
||||
|
@ -49,7 +49,7 @@ deleteAll('bookmark', 'u');
|
|||
deleteAll('challenge', 'challenger.id');
|
||||
deleteAll('challenge', 'destUser.id');
|
||||
|
||||
replaceWithNewGhostIds('clas_clas', 'created.by');
|
||||
replaceWithGhostId('clas_clas', 'created.by');
|
||||
scrub('clas_clas')(c => c.updateMany({ teachers: userId }, { $pull: { teachers: userId } }));
|
||||
|
||||
deleteAll('clas_student', 'userId');
|
||||
|
@ -64,20 +64,15 @@ deleteAll('coordinate_score', '_id');
|
|||
|
||||
deleteAll('crosstable2', '_id', new RegExp(`^${userId}/`));
|
||||
|
||||
scrub('f_post')(c =>
|
||||
c
|
||||
.find({ userId: userId }, { _id: 1 })
|
||||
.forEach(doc => coll.update(byId(doc), { $set: { userId: newGhostId(), text: '', erasedAt: new Date() } }))
|
||||
);
|
||||
scrub('f_post')(c => c.updateMany({ userId: userId }, { $set: { userId: ghostId, text: '', erasedAt: new Date() } }));
|
||||
|
||||
scrub('game5')(c =>
|
||||
c.find({ us: userId }, { us: 1, wid: 1 }).forEach(doc => {
|
||||
const gameGhostId = newGhostId();
|
||||
c.update(byId(doc), {
|
||||
$set: {
|
||||
[`us.${doc.us.indexOf(userId)}`]: gameGhostId, // replace player usernames
|
||||
...(doc.wid == userId ? { wid: gameGhostId } : {}), // replace winner username
|
||||
[`us.${doc.us.indexOf(userId)}`]: ghostId, // replace in player usernames
|
||||
},
|
||||
...(doc.wid == userId ? { $unset: { wid: 1 } } : {}), // remove winner username
|
||||
});
|
||||
})
|
||||
);
|
||||
|
@ -111,13 +106,13 @@ scrub('note')(c => {
|
|||
|
||||
deleteAll('notify', 'notifies');
|
||||
|
||||
replaceWithNewGhostIds('oauth_client', 'author', oauthDb);
|
||||
replaceWithGhostId('oauth_client', 'author', oauthDb);
|
||||
|
||||
deleteAllIn(oauthDb, 'oauth_access_token', 'user_id');
|
||||
|
||||
deleteAll('perf_stat', new RegExp(`^${userId}/`));
|
||||
|
||||
replaceWithNewGhostIds('plan_charge', 'userId');
|
||||
replaceWithGhostId('plan_charge', 'userId');
|
||||
|
||||
deleteAll('plan_patron', '_id');
|
||||
|
||||
|
@ -131,18 +126,7 @@ deleteAll('pref', '_id');
|
|||
|
||||
deleteAll('push_device', 'userId');
|
||||
|
||||
scrub(
|
||||
'puzzle2_puzzle',
|
||||
puzzleDb
|
||||
)(c =>
|
||||
c.find({ userId: userId }, { users: 1 }).forEach(doc =>
|
||||
c.update(byId(doc), {
|
||||
$set: {
|
||||
[`users.${doc.users.indexOf(userId)}`]: newGhostId(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
scrub('puzzle2_puzzle', puzzleDb)(c => c.updateMany({ users: userId }, { $set: { 'users.$': ghostId } }));
|
||||
|
||||
deleteAllIn(puzzleDb, 'puzzle2_round', '_id', new RegExp(`^${userId}:`));
|
||||
|
||||
|
@ -153,14 +137,13 @@ deleteAll('relation', 'u2');
|
|||
|
||||
scrub('report2')(c => {
|
||||
c.find({ 'atoms.by': userId }, { atoms: 1 }).forEach(doc => {
|
||||
const reportGhostId = newGhostId();
|
||||
const newAtoms = doc.atoms.map(a => ({
|
||||
...a,
|
||||
by: a.by == userId ? reportGhostId : a.by,
|
||||
by: a.by == userId ? ghostId : a.by,
|
||||
}));
|
||||
c.update(byId(doc), { $set: { atoms: newAtoms } });
|
||||
});
|
||||
!tosViolation && c.updateMany({ user: userId }, { $set: { user: newGhostId() } });
|
||||
!tosViolation && c.updateMany({ user: userId }, { $set: { user: ghostId } });
|
||||
});
|
||||
|
||||
if (!tosViolation) deleteAll('security', 'user');
|
||||
|
@ -169,15 +152,8 @@ deleteAll('seek', 'user.id');
|
|||
|
||||
deleteAll('shutup', '_id');
|
||||
|
||||
replaceWithNewGhostIds('simul', 'hostId');
|
||||
scrub('simul')(c =>
|
||||
c.find({ 'pairings.player.user': userId }, { pairings: 1 }).forEach(doc => {
|
||||
doc.pairings.forEach(p => {
|
||||
if (p.player.user == userId) p.player.user = newGhostId();
|
||||
});
|
||||
c.update(byId(doc), { $set: { pairings: doc.pairings } });
|
||||
})
|
||||
);
|
||||
replaceWithGhostId('simul', 'hostId');
|
||||
scrub('simul')(c => c.updateMany({ 'pairings.player.user': userId }, { $set: { 'pairings.$.player.user': ghostId } }));
|
||||
|
||||
deleteAll('storm_day', '_id', new RegExp(`^${userId}:`));
|
||||
|
||||
|
@ -198,7 +174,7 @@ scrub('study_chapter_flat', studyDb)(c => c.remove({ studyId: { $in: studyIds }
|
|||
|
||||
deleteAllIn(studyDb, 'study_user_topic', '_id');
|
||||
|
||||
replaceWithNewGhostIds('swiss', 'winnerId');
|
||||
replaceWithGhostId('swiss', 'winnerId');
|
||||
|
||||
const swissIds = scrub('swiss_player')(c => c.distinct('s', { u: userId }));
|
||||
|
||||
|
@ -217,15 +193,15 @@ if (swissIds.length) {
|
|||
scrub('swiss_pairing')(c => c.updateMany({ s: { $in: swissIds }, p: userId }, { $set: { 'p.$': swissGhostId } }));
|
||||
}
|
||||
|
||||
replaceWithNewGhostIds('team', 'createdBy');
|
||||
replaceWithGhostId('team', 'createdBy');
|
||||
scrub('team')(c => c.updateMany({ leaders: userId }, { $pull: { leaders: userId } }));
|
||||
|
||||
deleteAll('team_request', 'user');
|
||||
|
||||
deleteAll('team_member', 'user');
|
||||
|
||||
replaceWithNewGhostIds('tournament2', 'createdBy');
|
||||
replaceWithNewGhostIds('tournament2', 'winnerId');
|
||||
replaceWithGhostId('tournament2', 'createdBy');
|
||||
replaceWithGhostId('tournament2', 'winnerId');
|
||||
|
||||
const arenaIds = scrub('tournament_leaderboard')(c => c.distinct('t', { u: userId }));
|
||||
if (arenaIds.length) {
|
||||
|
|
|
@ -41,6 +41,7 @@ final private[api] class Cli(
|
|||
case None => "No such user."
|
||||
case Some(user) if user.enabled => "That user account is not closed. Can't erase."
|
||||
case Some(user) =>
|
||||
userRepo setErasedAt user
|
||||
Bus.publish(lila.user.User.GDPRErase(user), "gdprErase")
|
||||
s"Erasing all search data about ${user.username} now"
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ object LightUser {
|
|||
|
||||
private type UserID = String
|
||||
|
||||
val ghost = LightUser("ghost", "Ghost", none, false)
|
||||
val ghost = LightUser("ghost", "ghost", none, false)
|
||||
|
||||
implicit val lightUserWrites = OWrites[LightUser] { u =>
|
||||
writeNoId(u) + ("id" -> JsString(u.id))
|
||||
|
|
|
@ -106,8 +106,8 @@ final class PostRepo(val coll: Coll, filter: Filter = Safe)(implicit
|
|||
def allUserIdsByTopicId(topicId: String): Fu[List[User.ID]] =
|
||||
coll.distinctEasy[User.ID, List]("userId", $doc("topicId" -> topicId), ReadPreference.secondaryPreferred)
|
||||
|
||||
def cursor =
|
||||
def nonGhostCursor =
|
||||
coll
|
||||
.find($empty)
|
||||
.find($doc("userId" $ne User.ghostId))
|
||||
.cursor[Post](ReadPreference.secondaryPreferred)
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ final class ForumSearchApi(
|
|||
client match {
|
||||
case c: ESClientHttp =>
|
||||
c.putMapping >> {
|
||||
postRepo.cursor
|
||||
postRepo.nonGhostCursor
|
||||
.documentSource()
|
||||
.via(lila.common.LilaStream.logRate[Post]("forum index")(logger))
|
||||
.grouped(200)
|
||||
|
|
|
@ -64,7 +64,10 @@ final class SecurityForm(
|
|||
)
|
||||
)
|
||||
.verifying("usernameUnacceptable", u => !lameNameCheck.value || !LameName.username(u))
|
||||
.verifying("usernameAlreadyUsed", u => !userRepo.nameExists(u).await(3 seconds, "signupUsername"))
|
||||
.verifying(
|
||||
"usernameAlreadyUsed",
|
||||
u => !User.isGhost(u) && !userRepo.nameExists(u).await(3 seconds, "signupUsername")
|
||||
)
|
||||
|
||||
private val agreementBool = boolean.verifying(b => b)
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package lila.user
|
||||
|
||||
|
||||
import reactivemongo.api.bson._
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Success
|
||||
|
|
|
@ -163,6 +163,7 @@ object User {
|
|||
val anonymous = "Anonymous"
|
||||
val lichessId = "lichess"
|
||||
val broadcasterId = "broadcaster"
|
||||
val ghostId = "ghost"
|
||||
def isOfficial(username: String) = normalize(username) == lichessId || normalize(username) == broadcasterId
|
||||
|
||||
val seenRecently = 2.minutes
|
||||
|
@ -227,13 +228,13 @@ object User {
|
|||
val newUsernameChars = "(?i)^[a-z0-9_-]*$".r
|
||||
val newUsernameLetters = "(?i)^([a-z0-9][_-]?)+$".r
|
||||
|
||||
def couldBeUsername(str: User.ID) = historicalUsernameRegex.matches(str)
|
||||
def couldBeUsername(str: User.ID) = noGhost(str) && historicalUsernameRegex.matches(str)
|
||||
|
||||
def normalize(username: String) = username.toLowerCase
|
||||
|
||||
def validateId(name: String): Option[User.ID] = couldBeUsername(name) option normalize(name)
|
||||
|
||||
def isGhost(name: String) = name.headOption has '!'
|
||||
def isGhost(name: String) = normalize(name) == ghostId || name.headOption.has('!')
|
||||
|
||||
def noGhost(name: String) = !isGhost(name)
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ final class UserRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
|
|||
User.noGhost(id) ?? coll.one[User](disabledSelect ++ $id(id))
|
||||
|
||||
def named(username: String): Fu[Option[User]] =
|
||||
User.noGhost(username) ?? coll.byId[User](normalize(username)) recover {
|
||||
User.noGhost(username) ?? coll.byId[User](normalize(username)).recover {
|
||||
case _: reactivemongo.api.bson.exceptions.BSONValueNotFoundException => none // probably GDPRed user
|
||||
}
|
||||
|
||||
|
@ -668,6 +668,9 @@ final class UserRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
|
|||
ReadPreference.secondaryPreferred
|
||||
)
|
||||
|
||||
def setErasedAt(user: User) =
|
||||
coll.updateField($id(user.id), F.erasedAt, DateTime.now plusDays 1).void
|
||||
|
||||
private def newUser(
|
||||
username: String,
|
||||
passwordHash: HashedPassword,
|
||||
|
|
Loading…
Reference in New Issue