feat: release v0.2.0 profiles, chat, freeze, and bots

Ship the product pass on the v0.1.3 schema with additive migrations, a replica migration lock, and bot accounts kept out of people lists.
This commit is contained in:
ahp
2026-10-10 16:42:35 +03:30
parent 8586bbafcc
commit 22d92c133d
85 changed files with 10510 additions and 1341 deletions
+8 -8
View File
@@ -77,6 +77,10 @@ func (s *Server) handleQuizSubmit(w http.ResponseWriter, r *http.Request) {
writeErr(w, http.StatusForbidden, service.ErrQuizNotUnlocked.Error())
return
}
if !pg.PassedQuiz && !u.Role.IsStaff() && u.Hearts <= 0 {
writeErr(w, http.StatusForbidden, "جان‌ها تمام شده است. تا بازگشت یک جان صبر کنید.")
return
}
var req quizSubmitRequest
if err := bodyJSON(r, &req); err != nil {
@@ -155,15 +159,11 @@ func (s *Server) handleQuizSubmit(w http.ResponseWriter, r *http.Request) {
}
if cert, created, cerr := s.issueCertificateIfEligible(r.Context(), activeUser); cerr == nil && created && cert != nil {
observability.Activity("certificate.issued", "user_id", u.ID, "public_id", cert.PublicID)
go func() {
_ = s.notify.Notify(context.Background(), u.ID, "progress", "certificate_issued",
"گواهینامه آماده است", "دوره را به پایان رساندید. گواهینامه دیجیتال شما صادر شد.", "/c/"+cert.PublicID, nil)
}()
_ = s.notify.Notify(r.Context(), u.ID, "progress", "certificate_issued",
"گواهینامه آماده است", "دوره را به پایان رساندید. گواهینامه دیجیتال شما صادر شد.", "/c/"+cert.PublicID, nil)
}
go func() {
s.notifyQuizPassed(u.ID, lesson.Title, id, activeUser.StreakCurrent)
_ = s.notifyUnlockedLessons(u.ID, id)
}()
s.notifyQuizPassed(u.ID, lesson.Title, id, activeUser.StreakCurrent)
_ = s.notifyUnlockedLessons(u.ID, id)
}
} else if pg.PassedQuiz {
// Already cleared — practice attempts do not burn hearts.