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
+11 -55
View File
@@ -33,6 +33,7 @@ func (s *Server) handleAdminGetUser(w http.ResponseWriter, r *http.Request) {
writeErr(w, http.StatusNotFound, "کاربر پیدا نشد")
return
}
u = s.settleAccount(r.Context(), u)
writeJSON(w, http.StatusOK, map[string]any{"user": u})
}
@@ -442,6 +443,16 @@ func (s *Server) handleAdminDeleteUser(w http.ResponseWriter, r *http.Request) {
writeErr(w, http.StatusForbidden, "دسترسی کافی ندارید")
return
}
target, err := s.store.GetUserByID(r.Context(), id)
if err != nil {
writeErr(w, http.StatusNotFound, "کاربر پیدا نشد")
return
}
target = s.settleAccount(r.Context(), target)
if !target.IsFrozen && !target.IsClosed {
writeErr(w, http.StatusConflict, msgDeleteNotFrozen)
return
}
if err := s.store.DeleteUser(r.Context(), id); err != nil {
writeErr(w, http.StatusInternalServerError, "حذف کاربر ممکن نشد")
return
@@ -837,58 +848,3 @@ func validateAdminEvent(e *models.Event, create bool) string {
_ = create
return ""
}
// ---- announcements ----
func (s *Server) handleAdminAnnounce(w http.ResponseWriter, r *http.Request) {
var req struct {
Title string `json:"title"`
Body string `json:"body"`
Route string `json:"route"`
Category string `json:"category"`
}
if err := bodyJSON(r, &req); err != nil {
writeErr(w, http.StatusBadRequest, "دادهٔ ارسالی نامعتبر است")
return
}
req.Title = strings.TrimSpace(req.Title)
req.Body = strings.TrimSpace(req.Body)
req.Route = strings.TrimSpace(req.Route)
if utf8.RuneCountInString(req.Title) < 3 {
writeErr(w, http.StatusBadRequest, "عنوان اطلاعیه خیلی کوتاه است")
return
}
if utf8.RuneCountInString(req.Title) > 200 {
writeErr(w, http.StatusBadRequest, "عنوان اطلاعیه خیلی طولانی است")
return
}
if utf8.RuneCountInString(req.Body) < 8 {
writeErr(w, http.StatusBadRequest, "متن اطلاعیه خیلی کوتاه است")
return
}
if utf8.RuneCountInString(req.Body) > 500 {
writeErr(w, http.StatusBadRequest, "متن اطلاعیه خیلی طولانی است")
return
}
if req.Category == "" {
req.Category = "event"
}
switch req.Category {
case "event", "progress", "gamification", "mentor":
default:
writeErr(w, http.StatusBadRequest, "دستهٔ اطلاعیه نامعتبر است")
return
}
userIDs, err := s.store.AllActiveUserIDs(r.Context())
if err != nil {
writeErr(w, http.StatusInternalServerError, "بارگذاری کاربران ممکن نشد")
return
}
go func() {
for _, uid := range userIDs {
_ = s.notify.Notify(context.Background(), uid, req.Category, "announcement",
req.Title, req.Body, req.Route, nil)
}
}()
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "recipients": len(userIDs)})
}