diff --git a/internal/api/v1/homepage/categories.go b/internal/api/v1/homepage/categories.go index 08e524cf..b1a676f6 100644 --- a/internal/api/v1/homepage/categories.go +++ b/internal/api/v1/homepage/categories.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/route/routes" ) @@ -18,5 +19,24 @@ import ( // @Failure 403 {object} apitypes.ErrorResponse // @Router /homepage/categories [get] func Categories(c *gin.Context) { - c.JSON(http.StatusOK, routes.HomepageCategories()) + c.JSON(http.StatusOK, HomepageCategories()) +} + +func HomepageCategories() []string { + check := make(map[string]struct{}) + categories := make([]string, 0) + categories = append(categories, homepage.CategoryAll) + categories = append(categories, homepage.CategoryFavorites) + for _, r := range routes.HTTP.Iter { + item := r.HomepageItem() + if item.Category == "" { + continue + } + if _, ok := check[item.Category]; ok { + continue + } + check[item.Category] = struct{}{} + categories = append(categories, item.Category) + } + return categories } diff --git a/internal/api/v1/homepage/items.go b/internal/api/v1/homepage/items.go index 60d1f986..36098c78 100644 --- a/internal/api/v1/homepage/items.go +++ b/internal/api/v1/homepage/items.go @@ -1,16 +1,23 @@ package homepageapi import ( + "fmt" "net/http" + "net/url" + "slices" + "strings" "github.com/gin-gonic/gin" + "github.com/lithammer/fuzzysearch/fuzzy" apitypes "github.com/yusing/go-proxy/internal/api/types" + "github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/route/routes" ) type HomepageItemsRequest struct { - Category string `form:"category" validate:"omitempty"` - Provider string `form:"provider" validate:"omitempty"` + SearchQuery string `form:"search" validate:"omitempty"` + Category string `form:"category" validate:"omitempty"` + Provider string `form:"provider" validate:"omitempty"` } // @name HomepageItemsRequest // @x-id "items" @@ -20,6 +27,7 @@ type HomepageItemsRequest struct { // @Tags homepage // @Accept json // @Produce json +// @Param search query string false "Search query" // @Param category query string false "Category filter" // @Param provider query string false "Provider filter" // @Success 200 {object} homepage.Homepage @@ -42,5 +50,75 @@ func Items(c *gin.Context) { hostname = host } - c.JSON(http.StatusOK, routes.HomepageItems(proto, hostname, request.Category, request.Provider)) + c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request)) +} + +func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage { + switch proto { + case "http", "https": + default: + proto = "http" + } + + hp := homepage.NewHomepageMap(routes.HTTP.Size()) + + if strings.Count(hostname, ".") > 1 { + _, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain + } + + for _, r := range routes.HTTP.Iter { + if request.Provider != "" && r.ProviderName() != request.Provider { + continue + } + item := r.HomepageItem() + if request.Category != "" && item.Category != request.Category { + continue + } + if request.SearchQuery != "" && !fuzzy.MatchFold(request.SearchQuery, item.Name) { + continue + } + + // clear url if invalid + _, err := url.Parse(item.URL) + if err != nil { + item.URL = "" + } + + // append hostname if provided and only if alias is not FQDN + if hostname != "" && item.URL == "" { + isFQDNAlias := strings.Contains(item.Alias, ".") + if !isFQDNAlias { + item.URL = fmt.Sprintf("%s://%s.%s", proto, item.Alias, hostname) + } else { + item.URL = fmt.Sprintf("%s://%s", proto, item.Alias) + } + } + + // prepend protocol if not exists + if !strings.HasPrefix(item.URL, "http://") && !strings.HasPrefix(item.URL, "https://") { + item.URL = fmt.Sprintf("%s://%s", proto, item.URL) + } + + hp.Add(&item) + } + + ret := hp.Values() + // sort items in each category + for _, category := range ret { + category.Sort() + } + // sort categories + overrides := homepage.GetOverrideConfig() + slices.SortStableFunc(ret, func(a, b *homepage.Category) int { + // if category is "Hidden", move it to the end of the list + if a.Name == homepage.CategoryHidden { + return 1 + } + if b.Name == homepage.CategoryHidden { + return -1 + } + // sort categories by order in config + return overrides.CategoryOrder[a.Name] - overrides.CategoryOrder[b.Name] + }) + return ret } diff --git a/internal/api/v1/homepage/overrides.go b/internal/api/v1/homepage/overrides.go index b67dc4af..77c107c0 100644 --- a/internal/api/v1/homepage/overrides.go +++ b/internal/api/v1/homepage/overrides.go @@ -1,7 +1,6 @@ package homepageapi import ( - "encoding/json" "net/http" "github.com/gin-gonic/gin" @@ -15,16 +14,22 @@ type ( Value homepage.ItemConfig `json:"value"` } // @name HomepageOverrideItemParams HomepageOverrideItemsBatchParams struct { - Value map[string]*homepage.ItemConfig `json:"value"` + Value map[string]homepage.ItemConfig `json:"value"` } // @name HomepageOverrideItemsBatchParams + HomepageOverrideCategoryOrderParams struct { Which string `json:"which"` Value int `json:"value"` } // @name HomepageOverrideCategoryOrderParams + HomepageOverrideItemSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemSortOrderParams + HomepageOverrideItemAllSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemAllSortOrderParams + HomepageOverrideItemFavSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemFavSortOrderParams + HomepageOverrideItemVisibleParams struct { Which []string `json:"which"` Value bool `json:"value"` } // @name HomepageOverrideItemVisibleParams + HomepageOverrideItemFavoriteParams HomepageOverrideItemVisibleParams // @name HomepageOverrideItemFavoriteParams ) // @x-id "set-item" @@ -46,7 +51,7 @@ func SetItem(c *gin.Context) { return } overrides := homepage.GetOverrideConfig() - overrides.OverrideItem(params.Which, ¶ms.Value) + overrides.OverrideItem(params.Which, params.Value) c.JSON(http.StatusOK, apitypes.Success("success")) } @@ -65,15 +70,8 @@ func SetItem(c *gin.Context) { func SetItemsBatch(c *gin.Context) { var params HomepageOverrideItemsBatchParams if err := c.ShouldBindJSON(¶ms); err != nil { - data, derr := c.GetRawData() - if derr != nil { - c.Error(apitypes.InternalServerError(derr, "failed to get raw data")) - return - } - if uerr := json.Unmarshal(data, ¶ms); uerr != nil { - c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr)) - return - } + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return } overrides := homepage.GetOverrideConfig() overrides.OverrideItems(params.Value) @@ -95,22 +93,107 @@ func SetItemsBatch(c *gin.Context) { func SetItemVisible(c *gin.Context) { var params HomepageOverrideItemVisibleParams if err := c.ShouldBindJSON(¶ms); err != nil { - data, derr := c.GetRawData() - if derr != nil { - c.Error(apitypes.InternalServerError(derr, "failed to get raw data")) - return - } - if uerr := json.Unmarshal(data, ¶ms); uerr != nil { - c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr)) - return - } + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return } overrides := homepage.GetOverrideConfig() - if params.Value { - overrides.UnhideItems(params.Which) - } else { - overrides.HideItems(params.Which) + overrides.SetItemsVisibility(params.Which, params.Value) + c.JSON(http.StatusOK, apitypes.Success("success")) +} + +// @x-id "set-item-favorite" +// @BasePath /api/v1 +// @Summary Set homepage item favorite +// @Description Set homepage item favorite. +// @Tags homepage +// @Accept json +// @Produce json +// @Param request body HomepageOverrideItemFavoriteParams true "Set item favorite" +// @Success 200 {object} apitypes.SuccessResponse +// @Failure 400 {object} apitypes.ErrorResponse +// @Failure 500 {object} apitypes.ErrorResponse +// @Router /homepage/set/item_favorite [post] +func SetItemFavorite(c *gin.Context) { + var params HomepageOverrideItemFavoriteParams + if err := c.ShouldBindJSON(¶ms); err != nil { + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return } + overrides := homepage.GetOverrideConfig() + overrides.SetItemsFavorite(params.Which, params.Value) + c.JSON(http.StatusOK, apitypes.Success("success")) +} + +// @x-id "set-item-sort-order" +// @BasePath /api/v1 +// @Summary Set homepage item sort order +// @Description Set homepage item sort order. +// @Tags homepage +// @Accept json +// @Produce json +// @Param request body HomepageOverrideItemSortOrderParams true "Set item sort order" +// @Success 200 {object} apitypes.SuccessResponse +// @Failure 400 {object} apitypes.ErrorResponse +// @Failure 500 {object} apitypes.ErrorResponse +// @Router /homepage/set/item_sort_order [post] +func SetItemSortOrder(c *gin.Context) { + var params HomepageOverrideItemSortOrderParams + if err := c.ShouldBindJSON(¶ms); err != nil { + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return + } + overrides := homepage.GetOverrideConfig() + overrides.SetSortOrder(params.Which, params.Value) + c.JSON(http.StatusOK, apitypes.Success("success")) +} + +// @x-id "set-item-all-sort-order" + +// @x-id "set-item-all-sort-order" +// @BasePath /api/v1 +// @Summary Set homepage item all sort order +// @Description Set homepage item all sort order. +// @Tags homepage +// @Accept json +// @Produce json +// @Param request body HomepageOverrideItemAllSortOrderParams true "Set item all sort order" +// @Success 200 {object} apitypes.SuccessResponse +// @Failure 400 {object} apitypes.ErrorResponse +// @Failure 500 {object} apitypes.ErrorResponse +// @Router /homepage/set/item_all_sort_order [post] +func SetItemAllSortOrder(c *gin.Context) { + var params HomepageOverrideItemAllSortOrderParams + if err := c.ShouldBindJSON(¶ms); err != nil { + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return + } + overrides := homepage.GetOverrideConfig() + overrides.SetAllSortOrder(params.Which, params.Value) + c.JSON(http.StatusOK, apitypes.Success("success")) +} + +// @x-id "set-item-fav-sort-order" + +// @x-id "set-item-fav-sort-order" +// @BasePath /api/v1 +// @Summary Set homepage item fav sort order +// @Description Set homepage item fav sort order. +// @Tags homepage +// @Accept json +// @Produce json +// @Param request body HomepageOverrideItemFavSortOrderParams true "Set item fav sort order" +// @Success 200 {object} apitypes.SuccessResponse +// @Failure 400 {object} apitypes.ErrorResponse +// @Failure 500 {object} apitypes.ErrorResponse +// @Router /homepage/set/item_fav_sort_order [post] +func SetItemFavSortOrder(c *gin.Context) { + var params HomepageOverrideItemFavSortOrderParams + if err := c.ShouldBindJSON(¶ms); err != nil { + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return + } + overrides := homepage.GetOverrideConfig() + overrides.SetFavSortOrder(params.Which, params.Value) c.JSON(http.StatusOK, apitypes.Success("success")) } @@ -129,15 +212,8 @@ func SetItemVisible(c *gin.Context) { func SetCategoryOrder(c *gin.Context) { var params HomepageOverrideCategoryOrderParams if err := c.ShouldBindJSON(¶ms); err != nil { - data, derr := c.GetRawData() - if derr != nil { - c.Error(apitypes.InternalServerError(derr, "failed to get raw data")) - return - } - if uerr := json.Unmarshal(data, ¶ms); uerr != nil { - c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr)) - return - } + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return } overrides := homepage.GetOverrideConfig() overrides.SetCategoryOrder(params.Which, params.Value) diff --git a/internal/api/v1/route/route.go b/internal/api/v1/route/route.go index d3171896..38d5a0e1 100644 --- a/internal/api/v1/route/route.go +++ b/internal/api/v1/route/route.go @@ -5,6 +5,7 @@ import ( "github.com/gin-gonic/gin" apitypes "github.com/yusing/go-proxy/internal/api/types" + config "github.com/yusing/go-proxy/internal/config/types" "github.com/yusing/go-proxy/internal/route/routes" ) @@ -35,7 +36,14 @@ func Route(c *gin.Context) { route, ok := routes.Get(request.Which) if ok { c.JSON(http.StatusOK, route) - } else { - c.JSON(http.StatusNotFound, nil) + return } + + // also search for excluded routes + route = config.GetInstance().SearchRoute(request.Which) + if route != nil { + c.JSON(http.StatusOK, route) + return + } + c.JSON(http.StatusNotFound, nil) } diff --git a/internal/route/routes/query.go b/internal/route/routes/query.go index deb8e5cb..923733e2 100644 --- a/internal/route/routes/query.go +++ b/internal/route/routes/query.go @@ -4,11 +4,8 @@ import ( "encoding/json" "fmt" "math" - "net/url" - "strings" "time" - "github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/types" ) @@ -78,71 +75,6 @@ func getHealthInfo(r types.Route) *HealthInfo { } } -func HomepageCategories() []string { - check := make(map[string]struct{}) - categories := make([]string, 0) - for _, r := range HTTP.Iter { - item := r.HomepageConfig() - if item == nil || item.Category == "" { - continue - } - if _, ok := check[item.Category]; ok { - continue - } - check[item.Category] = struct{}{} - categories = append(categories, item.Category) - } - return categories -} - -func HomepageItems(proto, hostname, categoryFilter, providerFilter string) homepage.Homepage { - switch proto { - case "http", "https": - default: - proto = "http" - } - - hp := make(homepage.Homepage) - - if strings.Count(hostname, ".") > 1 { - _, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain - } - - for _, r := range HTTP.Iter { - if providerFilter != "" && r.ProviderName() != providerFilter { - continue - } - item := *r.HomepageItem() - if categoryFilter != "" && item.Category != categoryFilter { - continue - } - - // clear url if invalid - _, err := url.Parse(item.URL) - if err != nil { - item.URL = "" - } - - // append hostname if provided and only if alias is not FQDN - if hostname != "" && item.URL == "" { - isFQDNAlias := strings.Contains(item.Alias, ".") - if !isFQDNAlias { - item.URL = fmt.Sprintf("%s://%s.%s", proto, item.Alias, hostname) - } else { - item.URL = fmt.Sprintf("%s://%s", proto, item.Alias) - } - } - - // prepend protocol if not exists - if !strings.HasPrefix(item.URL, "http://") && !strings.HasPrefix(item.URL, "https://") { - item.URL = fmt.Sprintf("%s://%s", proto, item.URL) - } - - hp.Add(&item) - } - return hp -} - func ByProvider() map[string][]types.Route { rts := make(map[string][]types.Route) for r := range Iter {