package api import ( "net/http" "os" "path/filepath" "regexp" "github.com/mrleo1nid/pzmanager/internal/i18n" "github.com/mrleo1nid/pzmanager/internal/pzconfig" ) // idFromInput вытаскивает числовой идентификатор из того, что ввёл человек: // это может быть сам id или ссылка вида // https://steamcommunity.com/sharedfiles/filedetails/?id=2392709985. var idFromInput = regexp.MustCompile(`(\d{5,})`) // handleGetMods отдаёт списки модов профиля и каталог того, что уже скачано на // диск. Названия из мастерской здесь не запрашиваются: поход в Steam медленный, // для него есть отдельный эндпоинт lookup. func (s *Server) handleGetMods(w http.ResponseWriter, r *http.Request) { path := s.cfg.ServerConfigPath() ini, err := pzconfig.LoadINI(path) if os.IsNotExist(err) { writeJSON(w, http.StatusOK, map[string]any{ "exists": false, "path": path, "hint": i18n.T(i18n.FromRequest(r), "Файл конфигурации появится после первого запуска сервера"), }) return } if err != nil { writeError(w, r, http.StatusInternalServerError, err.Error()) return } mods, workshop := ini.Mods() writeJSON(w, http.StatusOK, map[string]any{ "exists": true, "path": path, "profile": s.cfg.ActiveProfile().ID, "mods": mods, "workshop_items": workshop, "map": pzconfig.SplitList(ini.Get(pzconfig.KeyMap)), "installed": s.installedMods(), }) } // installedMods собирает каталог скачанных модов. Сервер кладёт мастерскую // рядом с собой, но встречается и раскладка в ~/Zomboid. func (s *Server) installedMods() []pzconfig.InstalledMod { return pzconfig.ScanInstalledMods( []string{s.cfg.ServerDir, s.cfg.ZomboidDir}, filepath.Join(s.cfg.ZomboidDir, "mods"), ) } func (s *Server) handlePutMods(w http.ResponseWriter, r *http.Request) { var body struct { Mods []string `json:"mods"` WorkshopItems []string `json:"workshop_items"` Map []string `json:"map"` } if !decodeJSON(w, r, &body) { return } path := s.cfg.ServerConfigPath() ini, err := pzconfig.LoadINI(path) if err != nil { writeError(w, r, http.StatusBadRequest, "конфиг сервера недоступен: "+err.Error()) return } ini.SetMods(body.Mods, body.WorkshopItems) if len(body.Map) > 0 { ini.Set(pzconfig.KeyMap, pzconfig.JoinList(body.Map)) } if err := pzconfig.SaveINI(path, ini); err != nil { writeError(w, r, http.StatusInternalServerError, err.Error()) return } s.sup.AppendLog("manager", "Список модов обновлён. Сервер докачает моды из мастерской при следующем запуске.") writeJSON(w, http.StatusOK, map[string]any{"saved": true}) } // handleLookupMods спрашивает у Steam названия модов по Workshop ID. func (s *Server) handleLookupMods(w http.ResponseWriter, r *http.Request) { var body struct { IDs []string `json:"ids"` } if !decodeJSON(w, r, &body) { return } if len(body.IDs) == 0 { writeJSON(w, http.StatusOK, map[string]any{"items": []pzconfig.WorkshopItem{}}) return } if len(body.IDs) > 200 { writeError(w, r, http.StatusBadRequest, "слишком много идентификаторов за один запрос") return } items := s.workshop.Details(r.Context(), body.IDs) writeJSON(w, http.StatusOK, map[string]any{"items": items}) } // handleExpandCollection разворачивает коллекцию мастерской в список модов. // Принимает и голый id, и ссылку на страницу коллекции. func (s *Server) handleExpandCollection(w http.ResponseWriter, r *http.Request) { var body struct { ID string `json:"id"` } if !decodeJSON(w, r, &body) { return } match := idFromInput.FindString(body.ID) if match == "" { writeError(w, r, http.StatusBadRequest, "не похоже на идентификатор или ссылку на коллекцию") return } items, err := s.workshop.Collection(r.Context(), match) if err != nil { writeError(w, r, http.StatusBadGateway, err.Error()) return } // Названия подтягиваем сразу: иначе панель показала бы десяток голых // чисел и потребовала отдельного запроса. writeJSON(w, http.StatusOK, map[string]any{ "collection": match, "items": s.workshop.Details(r.Context(), items), }) }