Skip to content

Commit

Permalink
Merge branch 'master' into new-challenge-bug
Browse files Browse the repository at this point in the history
  • Loading branch information
johndoknjas committed Dec 19, 2024
2 parents a3a0288 + 12a889e commit 10708d4
Show file tree
Hide file tree
Showing 53 changed files with 401 additions and 347 deletions.
1 change: 0 additions & 1 deletion app/UiEnv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ object UiEnv
def netConfig = env.net
def contactEmailInClear = env.net.email.value
def picfitUrl = env.memo.picfitUrl
def socketTest = env.web.socketTest

given lila.core.config.NetDomain = env.net.domain
given (using ctx: PageContext): Option[Nonce] = ctx.nonce
Expand Down
18 changes: 18 additions & 0 deletions app/controllers/Clas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,24 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env):
else redirectTo(clas)
}

def studentMove(id: ClasId, username: UserStr) = Secure(_.Teacher) { ctx ?=> me ?=>
WithClassAndStudents(id): (clas, students) =>
WithStudent(clas, username): s =>
for
classes <- env.clas.api.clas.of(me)
others = classes.filter(_.id != clas.id)
res <- Ok.page(views.clas.student.move(clas, students, s, others))
yield res
}

def studentMovePost(id: ClasId, username: UserStr, to: ClasId) = SecureBody(_.Teacher) { ctx ?=> me ?=>
WithClassAndStudents(id): (clas, students) =>
WithStudent(clas, username): s =>
WithClass(to): toClas =>
for _ <- env.clas.api.student.move(s, toClas)
yield Redirect(routes.Clas.show(clas.id)).flashSuccess
}

def becomeTeacher = AuthBody { ctx ?=> me ?=>
couldBeTeacher.elseNotFound:
val perm = lila.core.perm.Permission.Teacher.dbKey
Expand Down
13 changes: 1 addition & 12 deletions app/controllers/Dev.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ final class Dev(env: Env) extends LilaController(env):
env.web.settings.noDelaySecret,
env.web.settings.prizeTournamentMakers,
env.web.settings.sitewideCoepCredentiallessHeader,
env.web.socketTest.distributionSetting,
env.tournament.reloadEndpointSetting,
env.tutor.nbAnalysisSetting,
env.tutor.parallelismSetting,
Expand Down Expand Up @@ -80,14 +79,4 @@ final class Dev(env: Env) extends LilaController(env):
env.mod.logApi.cli(command) >>
env.api.cli(command.split(" ").toList)

def socketTestResult = AuthBody(parse.json) { ctx ?=> me ?=>
ctx.body.body
.validate[JsArray]
.fold(
err => BadRequest(Json.obj("error" -> err.toString)),
results =>
env.web.socketTest
.put(Json.obj(me.userId.toString -> results))
.inject(jsonOkResult)
)
}
end Dev
7 changes: 4 additions & 3 deletions app/controllers/Puzzle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -535,12 +535,13 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env):
Auth { ctx ?=> me ?=>
meOrFetch(username)
.flatMapz: user =>
(fuccess(isGranted(_.CheatHunter)) >>|
(fuccess(user.is(me) || isGranted(_.CheatHunter)) >>|
user.enabled.yes.so(env.clas.api.clas.isTeacherOf(me, user.id))).map {
_.option(user)
}
.dmap(_ | me.value)
.flatMap(f(_))
.flatMap:
case Some(user) => f(user)
case None => Redirect(routes.Puzzle.dashboard(Days(30), "home", none))
}

def WithPuzzlePerf[A](f: Perf ?=> Fu[A])(using Option[Me]): Fu[A] =
Expand Down
45 changes: 24 additions & 21 deletions app/controllers/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ final class User(
env.game.cached
.lastPlayedPlayingId(username.id)
.orElse(env.game.gameRepo.quickLastPlayedId(username.id))
.flatMap {
.flatMap:
case None => NotFound("No ongoing game")
case Some(gameId) => gameC.exportGame(gameId)
}

private def apiGames(u: UserModel, filter: String, page: Int)(using BodyContext[?]) =
userGames(u, filter, page).flatMap(env.game.userGameApi.jsPaginator).map { res =>
Expand Down Expand Up @@ -177,11 +176,12 @@ final class User(
ctx.userId.soFu(env.game.crosstableApi(user.id, _)),
ctx.isAuth.so(env.pref.api.followable(user.id))
).flatMapN: (blocked, crosstable, followable) =>
val ping = env.socket.isOnline.exec(user.id).so(env.socket.getLagRating(user.id))
negotiate(
html = (ctx.isnt(user)).so(currentlyPlaying(user.user)).flatMap { pov =>
Ok.snip(views.user.mini(user, pov, blocked, followable, relation, ping, crosstable))
.map(_.withHeaders(CACHE_CONTROL -> "max-age=5"))
html = ctx.isnt(user).so(currentlyPlaying(user.user)).flatMap { pov =>
val ping = env.socket.isOnline.exec(user.id).so(env.socket.getLagRating(user.id))
Ok.snip(
views.user.mini(user, pov, blocked, followable, relation, ping, crosstable)
).map(_.withHeaders(CACHE_CONTROL -> "max-age=5"))
},
json =
import lila.game.JsonView.given
Expand Down Expand Up @@ -553,21 +553,24 @@ final class User(
}

def perfStat(username: UserStr, perfKey: PerfKey) = Open:
Found(env.perfStat.api.data(username, perfKey)): data =>
negotiate(
Ok.async:
env.history
.ratingChartApi(data.user.user)
.map:
views.user.perfStatPage(data, _)
,
JsonOk:
getBool("graph")
.soFu:
env.history.ratingChartApi.singlePerf(data.user.user, data.stat.perfType.key)
.map: graph =>
env.perfStat.jsonView(data).add("graph", graph)
)
PerfType
.isLeaderboardable(perfKey)
.so:
Found(env.perfStat.api.data(username, perfKey)): data =>
negotiate(
Ok.async:
env.history
.ratingChartApi(data.user.user)
.map:
views.user.perfStatPage(data, _)
,
JsonOk:
getBool("graph")
.soFu:
env.history.ratingChartApi.singlePerf(data.user.user, data.stat.perfType.key)
.map: graph =>
env.perfStat.jsonView(data).add("graph", graph)
)

def autocomplete = OpenOrScoped(): ctx ?=>
NoTor:
Expand Down
10 changes: 5 additions & 5 deletions app/views/base/page.scala
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ object page:
"kid" -> ctx.kid.yes,
"mobile" -> lila.common.HTTPRequest.isMobileBrowser(ctx.req),
"playing fixed-scroll" -> p.playing,
"no-rating" -> !pref.showRatings,
"no-rating" -> (!pref.showRatings || (p.playing && pref.hideRatingsInGame)),
"no-flair" -> !pref.flairs,
"zen" -> (pref.isZen || (p.playing && pref.isZenAuto)),
"zenable" -> p.zenable,
Expand All @@ -111,10 +111,10 @@ object page:
dataDev,
dataVapid := (ctx.isAuth && env.security.lilaCookie.isRememberMe(ctx.req))
.option(env.push.vapidPublicKey),
dataUser := ctx.userId,
dataSoundSet := pref.currentSoundSet.toString,
attr("data-socket-domains") := socketTest.socketEndpoints(netConfig).mkString(","),
attr("data-socket-test-running") := socketTest.isUserInTestBucket(),
dataUser := ctx.userId,
dataSoundSet := pref.currentSoundSet.toString,
attr("data-socket-domains") := (if ~pref.usingAltSocket then netConfig.socketAlts
else netConfig.socketDomains).mkString(","),
dataAssetUrl,
dataAssetVersion := assetVersion,
dataNonce := ctx.nonce.ifTrue(sameAssetDomain).map(_.value),
Expand Down
2 changes: 1 addition & 1 deletion app/views/clas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object student:
lazy val formUi = lila.clas.ui.StudentFormUi(helpers, views.clas.ui, ui)

export ui.{ invite }
export formUi.{ newStudent as form, many as manyForm, edit, release, close }
export formUi.{ newStudent as form, many as manyForm, edit, release, close, move }

def show(
clas: Clas,
Expand Down
7 changes: 6 additions & 1 deletion app/views/game/side.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ object side:
game.players.mapList: p =>
frag(
div(cls := s"player color-icon is ${p.color.name} text")(
playerLink(p, withOnline = false, withDiff = true, withBerserk = true)
playerLink(
p,
withOnline = false,
withDiff = true,
withBerserk = true
)
),
tour.flatMap(_.teamVs).map(_.teams(p.color)).map {
teamLink(_, withIcon = false)(cls := "team")
Expand Down
122 changes: 68 additions & 54 deletions bin/mongodb/recap-notif.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,49 @@
const year = 2024;
const dry = false;

let count = 0;
let countAll = 0;
let countSent = 0;
let lastPrinted = 0;

const hasPuzzles = userId => db.user_perf.count({ _id: userId, 'puzzle.nb': { $gt: 0 } });
let hasRecap = new Set();
function reloadHasRecap() {
print('Loading existing recaps...');
hasRecap = new Set(db.recap_report.distinct('_id'));
print('Loaded ' + hasRecap.size + ' recaps');
}
reloadHasRecap();
setInterval(reloadHasRecap, 1000 * 60 * 10);

function sendToUser(user) {
if (!user.enabled) {
print('------------- ' + user._id + ' is closed');
return;
}
const exists = db.notify.countDocuments({ notifies: user._id, 'content.type': 'recap', }, { limit: 1 });
if (exists) {
print('------------- ' + user._id + ' already sent');
return;
}
if (user.seenAt < new Date('2024-01-01')) {
print('------------- ' + user._id + ' not seen in 2024');
return;
}
if (!user.count?.game && !hasPuzzles(user._id)) {
print('------------- ' + user._id + ' no games or puzzles');
return;
const hasPuzzles = userId => db.user_perf.countDocuments({ _id: userId, 'puzzle.nb': { $gt: 0 } }, { limit: 1 });

// only keeps users that don't yet have a recap notification for the year
// and don't have yet loaded their recap from another link
const filterNewUsers = users => {
const noRecap = users.filter(u => !hasRecap.has(u._id));
const hasNotif = new Set(db.notify.distinct('notifies', {
notifies: { $in: noRecap.map(u => u._id) }, 'content.type': 'recap', 'content.year': year
}));
return noRecap.filter(u => !hasNotif.has(u._id));
}

function* group(size) {
let batch = [];
while (true) {
const element = yield;
if (!element) {
yield batch;
return;
}
batch.push(element);
if (batch.length >= size) {
let element = yield batch;
batch = [element];
}
}
};

function sendToUser(user) {
if (!user.count?.game && !hasPuzzles(user._id)) return;
if (!dry) db.notify.insertOne({
_id: Math.random().toString(36).substring(2, 10),
notifies: user._id,
Expand All @@ -33,45 +54,38 @@ function sendToUser(user) {
read: false,
createdAt: new Date(),
});
count++;
print(count + ' ' + user._id);
}

function sendToUserId(userId) {
const user = db.user4.findOne({ _id: userId });
if (!user) {
print('------------- ' + userId + ' not found');
return;
}
sendToUser(user);
}

function sendToRoleOwners() {
db.user4.find({ enabled: true, roles: { $exists: 1, $ne: [] } }).forEach(user => {
roles = user.roles.filter(r => r != 'ROLE_COACH' && r != 'ROLE_TEACHER' && r != 'ROLE_VERIFIED' && r != 'ROLE_BETA');
if (roles.length) {
sendTo(user);
}
});
}

function sendToTeamMembers(teamId) {
db.team_member.find({ team: teamId }, { user: 1, _id: 0 }).forEach(member => {
sendToUserId(member.user);
});
}

function sendToRandomOnlinePlayers() {
db.user4.find({ enabled: true, 'count.game': { $gt: 10 }, seenAt: { $gt: new Date(Date.now() - 1000 * 60 * 2) } }).sort({ seenAt: -1 }).limit(5_000).forEach(sendToUser);
countSent++;
}

function sendToRandomOfflinePlayers() {
db.user4.find({
enabled: true, 'count.game': { $gt: 10 }, seenAt: {
$gt: new Date(Date.now() - 1000 * 60 * 60 * 24),
$lt: new Date(Date.now() - 1000 * 60 * 60)
const grouper = group(100);
grouper.next();
const process = user => {
countAll++;
const batch = grouper.next(user).value;
if (batch) {
const newUsers = filterNewUsers(batch);
newUsers.forEach(sendToUser);
if (countAll % 1000 == 0) {
print(`+ ${countSent - lastPrinted} = ${countSent} / ${countAll} | ${user.createdAt.toLocaleDateString('fr')}`);
lastPrinted = countSent;
}
sleep(10 * newUsers.length);
}
}).limit(25_000).forEach(sendToUser);
}
db.user4.find({
enabled: true,
createdAt: { $lt: new Date(year, 9, 1) },
seenAt: {
$gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30 * 3),
// $lt: new Date(Date.now() - 1000 * 60 * 20) // avoid the lila notif cache!
},
marks: { $nin: ['boost', 'engine', 'troll'] }
}).forEach(process);
process(); // flush the generator
}

sendToRandomOfflinePlayers();

print('Scan: ' + countAll);
print('Sent: ' + countSent);
2 changes: 2 additions & 0 deletions conf/clas.routes
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ POST /class/$id<\w{8}>/student/:username/release controllers.clas.Clas.studentR
GET /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClose(id: ClasId, username: UserStr)
POST /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClosePost(id: ClasId, username: UserStr)
POST /class/$id<\w{8}>/invitation/revoke controllers.clas.Clas.invitationRevoke(id: ClasInviteId)
GET /class/$id<\w{8}>/student/:username/move controllers.clas.Clas.studentMove(id: ClasId, username: UserStr)
POST /class/$id<\w{8}>/student/:username/move/$to<\w{8}> controllers.clas.Clas.studentMovePost(id: ClasId, username: UserStr, to: ClasId)
1 change: 0 additions & 1 deletion conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,6 @@ GET /dev/settings controllers.Dev.settings
POST /dev/settings/:id controllers.Dev.settingsPost(id)

GET /prometheus-metrics/:key controllers.Main.prometheusMetrics(key: String)
POST /dev/socket-test controllers.Dev.socketTestResult

# Push
POST /mobile/register/:platform/:deviceId controllers.Push.mobileRegister(platform, deviceId)
Expand Down
1 change: 0 additions & 1 deletion modules/api/src/main/Context.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ final class LoginContext(
val oauth: Option[TokenScopes]
):
export me.{ isDefined as isAuth, isEmpty as isAnon }
def myId: Option[MyId] = me.map(_.myId)
def user: Option[User] = Me.raw(me)
def userId: Option[UserId] = user.map(_.id)
def username: Option[UserName] = user.map(_.username)
Expand Down
11 changes: 11 additions & 0 deletions modules/clas/src/main/ClasApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,17 @@ final class ClasApi(
sendWelcomeMessage(teacher.id, user, clas)).inject(Student.WithPassword(student, password))
}

def move(s: Student.WithUser, toClas: Clas)(using teacher: Me): Fu[Option[Student]] = for
_ <- closeAccount(s)
stu = s.student.copy(id = Student.makeId(s.user.id, toClas.id), clasId = toClas.id)
moved <- colls.student.insert
.one(stu)
.inject(stu.some)
.recoverWith(lila.db.recoverDuplicateKey { _ =>
student.get(toClas, s.user.id)
})
yield moved

def manyCreate(
clas: Clas,
data: ClasForm.ManyNewStudent,
Expand Down
2 changes: 1 addition & 1 deletion modules/clas/src/main/ClasMatesCache.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class ClasMatesCache(colls: ClasColls, cacheApi: CacheApi, studentCache: C
def get(studentId: UserId): Fu[Set[UserId]] =
studentCache.isStudent(studentId).so(cache.get(studentId))

private val cache = cacheApi[UserId, Set[UserId]](256, "clas.mates"):
private val cache = cacheApi[UserId, Set[UserId]](64, "clas.mates"):
_.expireAfterWrite(5 minutes)
.buildAsyncFuture(fetchMatesAndTeachers)

Expand Down
6 changes: 4 additions & 2 deletions modules/clas/src/main/ui/DashboardUi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,13 @@ final class DashboardUi(helpers: Helpers, ui: ClasUi)(using NetDomain):
tr(
td(userIdLink(i.userId.some)),
td(i.realName),
td(if i.accepted.has(false) then "Declined" else "Pending"),
td(
if i.accepted.has(false) then trans.clas.declined.txt() else trans.clas.pending.txt()
),
td(momentFromNow(i.created.at)),
td:
postForm(action := routes.Clas.invitationRevoke(i.id)):
submitButton(cls := "button button-red button-empty")("Revoke")
submitButton(cls := "button button-red button-empty")(trans.site.delete())
)
)
val archivedBox =
Expand Down
Loading

0 comments on commit 10708d4

Please sign in to comment.