修改 filterOptions 让接口更友好 (#260)
Co-authored-by: huruize <8985917+huruize007@user.noreply.gitee.com>
This commit is contained in:
@@ -120,7 +120,7 @@ func (s *AppServer) listFeedsHandler(c *gin.Context) {
|
||||
// searchFeedsHandler 搜索Feeds
|
||||
func (s *AppServer) searchFeedsHandler(c *gin.Context) {
|
||||
var keyword string
|
||||
var filters []xiaohongshu.FilterOption
|
||||
var filters xiaohongshu.FilterOption
|
||||
|
||||
switch c.Request.Method {
|
||||
case http.MethodPost:
|
||||
@@ -144,7 +144,7 @@ func (s *AppServer) searchFeedsHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 搜索 Feeds
|
||||
result, err := s.xiaohongshuService.SearchFeeds(c.Request.Context(), keyword, filters...)
|
||||
result, err := s.xiaohongshuService.SearchFeeds(c.Request.Context(), keyword, filters)
|
||||
if err != nil {
|
||||
respondError(c, http.StatusInternalServerError, "SEARCH_FEEDS_FAILED",
|
||||
"搜索Feeds失败", err.Error())
|
||||
|
||||
@@ -237,23 +237,18 @@ func (s *AppServer) handleSearchFeeds(ctx context.Context, args SearchFeedsArgs)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("MCP: 搜索Feeds - 关键词: %s, 筛选条件数量: %d", args.Keyword, len(args.Filters))
|
||||
var filters []xiaohongshu.FilterOption
|
||||
for _, filter := range args.Filters {
|
||||
filterOption, err := xiaohongshu.NewFilterOption(xiaohongshu.GetFilterGroupIndex(filter.FiltersIndex), filter.TagsIndex)
|
||||
if err != nil {
|
||||
return &MCPToolResult{
|
||||
Content: []MCPContent{{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("搜索Feeds失败: 筛选组 %v 的标签索引 %v 错误: %v",
|
||||
filter.FiltersIndex, filter.TagsIndex, err),
|
||||
}},
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
filters = append(filters, filterOption)
|
||||
logrus.Infof("MCP: 搜索Feeds - 关键词: %s", args.Keyword)
|
||||
|
||||
// 将 MCP 的 FilterOption 转换为 xiaohongshu.FilterOption
|
||||
filter := xiaohongshu.FilterOption{
|
||||
SortBy: args.Filters.SortBy,
|
||||
NoteType: args.Filters.NoteType,
|
||||
PublishTime: args.Filters.PublishTime,
|
||||
SearchScope: args.Filters.SearchScope,
|
||||
Location: args.Filters.Location,
|
||||
}
|
||||
result, err := s.xiaohongshuService.SearchFeeds(ctx, args.Keyword, filters...)
|
||||
|
||||
result, err := s.xiaohongshuService.SearchFeeds(ctx, args.Keyword, filter)
|
||||
if err != nil {
|
||||
return &MCPToolResult{
|
||||
Content: []MCPContent{{
|
||||
@@ -415,16 +410,16 @@ func (s *AppServer) handleLikeFeed(ctx context.Context, args map[string]interfac
|
||||
return &MCPToolResult{Content: []MCPContent{{Type: "text", Text: "操作失败: 缺少xsec_token参数"}}, IsError: true}
|
||||
}
|
||||
unlike, _ := args["unlike"].(bool)
|
||||
|
||||
|
||||
var res *ActionResult
|
||||
var err error
|
||||
|
||||
|
||||
if unlike {
|
||||
res, err = s.xiaohongshuService.UnlikeFeed(ctx, feedID, xsecToken)
|
||||
} else {
|
||||
res, err = s.xiaohongshuService.LikeFeed(ctx, feedID, xsecToken)
|
||||
}
|
||||
|
||||
|
||||
if err != nil {
|
||||
action := "点赞"
|
||||
if unlike {
|
||||
@@ -432,7 +427,7 @@ func (s *AppServer) handleLikeFeed(ctx context.Context, args map[string]interfac
|
||||
}
|
||||
return &MCPToolResult{Content: []MCPContent{{Type: "text", Text: action + "失败: " + err.Error()}}, IsError: true}
|
||||
}
|
||||
|
||||
|
||||
action := "点赞"
|
||||
if unlike {
|
||||
action = "取消点赞"
|
||||
@@ -451,16 +446,16 @@ func (s *AppServer) handleFavoriteFeed(ctx context.Context, args map[string]inte
|
||||
return &MCPToolResult{Content: []MCPContent{{Type: "text", Text: "操作失败: 缺少xsec_token参数"}}, IsError: true}
|
||||
}
|
||||
unfavorite, _ := args["unfavorite"].(bool)
|
||||
|
||||
|
||||
var res *ActionResult
|
||||
var err error
|
||||
|
||||
|
||||
if unfavorite {
|
||||
res, err = s.xiaohongshuService.UnfavoriteFeed(ctx, feedID, xsecToken)
|
||||
} else {
|
||||
res, err = s.xiaohongshuService.FavoriteFeed(ctx, feedID, xsecToken)
|
||||
}
|
||||
|
||||
|
||||
if err != nil {
|
||||
action := "收藏"
|
||||
if unfavorite {
|
||||
@@ -468,7 +463,7 @@ func (s *AppServer) handleFavoriteFeed(ctx context.Context, args map[string]inte
|
||||
}
|
||||
return &MCPToolResult{Content: []MCPContent{{Type: "text", Text: action + "失败: " + err.Error()}}, IsError: true}
|
||||
}
|
||||
|
||||
|
||||
action := "收藏"
|
||||
if unfavorite {
|
||||
action = "取消收藏"
|
||||
|
||||
@@ -30,13 +30,17 @@ type PublishVideoArgs struct {
|
||||
|
||||
// SearchFeedsArgs 搜索内容的参数
|
||||
type SearchFeedsArgs struct {
|
||||
Keyword string `json:"keyword" jsonschema:"搜索关键词"`
|
||||
Filters []FilterOption `json:"filters,omitempty" jsonschema:"筛选选项列表"`
|
||||
Keyword string `json:"keyword" jsonschema:"搜索关键词"`
|
||||
Filters FilterOption `json:"filters,omitempty" jsonschema:"筛选选项"`
|
||||
}
|
||||
|
||||
// FilterOption 筛选选项结构体
|
||||
type FilterOption struct {
|
||||
FiltersIndex string `json:"filters_index" jsonschema:"筛选索引 排序依据 笔记类型, 发布时间, 搜索范围, 位置距离"` //
|
||||
TagsIndex string `json:"tags_index" jsonschema:"筛选值 排序依据(综合、最新、最多点赞、最多评论、最多收藏)笔记类型(不限、视频、图文)发布时间(不限、一天内、一周内、半年内)搜索范围(不限、已看过、未看过、已关注)位置距离(不限、同城、附近)"`
|
||||
SortBy string `json:"sort_by,omitempty" jsonschema:"排序依据: 综合|最新|最多点赞|最多评论|最多收藏,默认为'综合'"`
|
||||
NoteType string `json:"note_type,omitempty" jsonschema:"笔记类型: 不限|视频|图文,默认为'不限'"`
|
||||
PublishTime string `json:"publish_time,omitempty" jsonschema:"发布时间: 不限|一天内|一周内|半年内,默认为'不限'"`
|
||||
SearchScope string `json:"search_scope,omitempty" jsonschema:"搜索范围: 不限|已看过|未看过|已关注,默认为'不限'"`
|
||||
Location string `json:"location,omitempty" jsonschema:"位置距离: 不限|同城|附近,默认为'不限'"`
|
||||
}
|
||||
|
||||
// FeedDetailArgs 获取Feed详情的参数
|
||||
|
||||
4
types.go
4
types.go
@@ -41,8 +41,8 @@ type FeedDetailRequest struct {
|
||||
}
|
||||
|
||||
type SearchFeedsRequest struct {
|
||||
Keyword string `json:"keyword" binding:"required"`
|
||||
Filters []xiaohongshu.FilterOption `json:"filters" binding:"required"`
|
||||
Keyword string `json:"keyword" binding:"required"`
|
||||
Filters xiaohongshu.FilterOption `json:"filters,omitempty"`
|
||||
}
|
||||
|
||||
// FeedDetailResponse Feed详情响应
|
||||
|
||||
@@ -17,14 +17,24 @@ type SearchResult struct {
|
||||
} `json:"search"`
|
||||
}
|
||||
|
||||
// FilterOption 筛选选项结构体
|
||||
type FilterOption struct {
|
||||
FiltersIndex int `json:"filters_index" jsonschema:"筛选组索引 1=排序依据, 2=笔记类型, 3=发布时间, 4=搜索范围, 5=位置距离"`
|
||||
TagsIndex int `json:"tags_index" jsonschema:"标签索引,根据不同的筛选组索引对应不同的选项: 1=排序依据(1-5), 2=笔记类型(1-3), 3=发布时间(1-4), 4=搜索范围(1-4), 5=位置距离(1-3)"`
|
||||
Text string `json:"text" jsonschema:"标签文本描述"`
|
||||
SortBy string `json:"sort_by,omitempty" jsonschema:"排序依据: 综合|最新|最多点赞|最多评论|最多收藏,默认为'综合'"`
|
||||
NoteType string `json:"note_type,omitempty" jsonschema:"笔记类型: 不限|视频|图文,默认为'不限'"`
|
||||
PublishTime string `json:"publish_time,omitempty" jsonschema:"发布时间: 不限|一天内|一周内|半年内,默认为'不限'"`
|
||||
SearchScope string `json:"search_scope,omitempty" jsonschema:"搜索范围: 不限|已看过|未看过|已关注,默认为'不限'"`
|
||||
Location string `json:"location,omitempty" jsonschema:"位置距离: 不限|同城|附近,默认为'不限'"`
|
||||
}
|
||||
|
||||
// 预定义的筛选选项映射表
|
||||
var FilterOptionsMap = map[int][]FilterOption{
|
||||
// internalFilterOption 内部使用的筛选选项(基于索引)
|
||||
type internalFilterOption struct {
|
||||
FiltersIndex int // 筛选组索引
|
||||
TagsIndex int // 标签索引
|
||||
Text string // 标签文本描述
|
||||
}
|
||||
|
||||
// 预定义的筛选选项映射表(内部使用)
|
||||
var filterOptionsMap = map[int][]internalFilterOption{
|
||||
1: { // 排序依据
|
||||
{FiltersIndex: 1, TagsIndex: 1, Text: "综合"},
|
||||
{FiltersIndex: 1, TagsIndex: 2, Text: "最新"},
|
||||
@@ -56,24 +66,83 @@ var FilterOptionsMap = map[int][]FilterOption{
|
||||
},
|
||||
}
|
||||
|
||||
// 定义筛选组索引到中文描述的映射
|
||||
var filterGroupMap = map[int]string{
|
||||
1: "排序依据",
|
||||
2: "笔记类型",
|
||||
3: "发布时间",
|
||||
4: "搜索范围",
|
||||
5: "位置距离",
|
||||
// convertToInternalFilters 将 FilterOption 转换为内部的 internalFilterOption 列表
|
||||
func convertToInternalFilters(filter FilterOption) ([]internalFilterOption, error) {
|
||||
var internalFilters []internalFilterOption
|
||||
|
||||
// 处理排序依据
|
||||
if filter.SortBy != "" {
|
||||
internal, err := findInternalOption(1, filter.SortBy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("排序依据错误: %w", err)
|
||||
}
|
||||
internalFilters = append(internalFilters, internal)
|
||||
}
|
||||
|
||||
// 处理笔记类型
|
||||
if filter.NoteType != "" {
|
||||
internal, err := findInternalOption(2, filter.NoteType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("笔记类型错误: %w", err)
|
||||
}
|
||||
internalFilters = append(internalFilters, internal)
|
||||
}
|
||||
|
||||
// 处理发布时间
|
||||
if filter.PublishTime != "" {
|
||||
internal, err := findInternalOption(3, filter.PublishTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发布时间错误: %w", err)
|
||||
}
|
||||
internalFilters = append(internalFilters, internal)
|
||||
}
|
||||
|
||||
// 处理搜索范围
|
||||
if filter.SearchScope != "" {
|
||||
internal, err := findInternalOption(4, filter.SearchScope)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("搜索范围错误: %w", err)
|
||||
}
|
||||
internalFilters = append(internalFilters, internal)
|
||||
}
|
||||
|
||||
// 处理位置距离
|
||||
if filter.Location != "" {
|
||||
internal, err := findInternalOption(5, filter.Location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("位置距离错误: %w", err)
|
||||
}
|
||||
internalFilters = append(internalFilters, internal)
|
||||
}
|
||||
|
||||
return internalFilters, nil
|
||||
}
|
||||
|
||||
// validateFilterOption 验证筛选选项是否在有效范围内
|
||||
func validateFilterOption(filter FilterOption) error {
|
||||
// findInternalOption 根据筛选组索引和文本查找内部筛选选项
|
||||
func findInternalOption(filtersIndex int, text string) (internalFilterOption, error) {
|
||||
options, exists := filterOptionsMap[filtersIndex]
|
||||
if !exists {
|
||||
return internalFilterOption{}, fmt.Errorf("筛选组 %d 不存在", filtersIndex)
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
if option.Text == text {
|
||||
return option, nil
|
||||
}
|
||||
}
|
||||
|
||||
return internalFilterOption{}, fmt.Errorf("在筛选组 %d 中未找到文本 '%s'", filtersIndex, text)
|
||||
}
|
||||
|
||||
// validateInternalFilterOption 验证内部筛选选项是否在有效范围内
|
||||
func validateInternalFilterOption(filter internalFilterOption) error {
|
||||
// 检查筛选组索引是否有效
|
||||
if filter.FiltersIndex < 1 || filter.FiltersIndex > 5 {
|
||||
return fmt.Errorf("无效的筛选组索引 %d,有效范围为 1-5", filter.FiltersIndex)
|
||||
}
|
||||
|
||||
// 检查标签索引是否在对应筛选组的有效范围内
|
||||
options, exists := FilterOptionsMap[filter.FiltersIndex]
|
||||
options, exists := filterOptionsMap[filter.FiltersIndex]
|
||||
if !exists {
|
||||
return fmt.Errorf("筛选组 %d 不存在", filter.FiltersIndex)
|
||||
}
|
||||
@@ -86,62 +155,6 @@ func validateFilterOption(filter FilterOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 便利函数:根据文本创建筛选选项
|
||||
func NewFilterOption(filtersIndex int, text string) (FilterOption, error) {
|
||||
options, exists := FilterOptionsMap[filtersIndex]
|
||||
if !exists {
|
||||
return FilterOption{}, fmt.Errorf("筛选组 %d 不存在", filtersIndex)
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
if option.Text == text {
|
||||
return option, nil
|
||||
}
|
||||
}
|
||||
|
||||
return FilterOption{}, fmt.Errorf("在筛选组 %d 中未找到文本 '%s'", filtersIndex, text)
|
||||
}
|
||||
|
||||
// 便利函数:创建常用的筛选选项
|
||||
func SortBy(text string) (FilterOption, error) {
|
||||
return NewFilterOption(1, text) // 排序依据
|
||||
}
|
||||
|
||||
func NoteType(text string) (FilterOption, error) {
|
||||
return NewFilterOption(2, text) // 笔记类型
|
||||
}
|
||||
|
||||
func TimeRange(text string) (FilterOption, error) {
|
||||
return NewFilterOption(3, text) // 发布时间
|
||||
}
|
||||
|
||||
func SearchScope(text string) (FilterOption, error) {
|
||||
return NewFilterOption(4, text) // 搜索范围
|
||||
}
|
||||
|
||||
func LocationDistance(text string) (FilterOption, error) {
|
||||
return NewFilterOption(5, text) // 位置距离
|
||||
}
|
||||
|
||||
// GetFilterGroupDescription 根据筛选组索引获取中文描述
|
||||
func GetFilterGroupDescription(index int) string {
|
||||
if desc, exists := filterGroupMap[index]; exists {
|
||||
return desc
|
||||
}
|
||||
return "未知筛选组"
|
||||
}
|
||||
|
||||
// GetFilterGroupIndex 根据中文描述获取筛选组索引
|
||||
func GetFilterGroupIndex(text string) int {
|
||||
// 通过遍历filterGroupMap获取对应的索引
|
||||
for index, description := range filterGroupMap {
|
||||
if description == text {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1 // 未找到匹配项时返回-1
|
||||
}
|
||||
|
||||
type SearchAction struct {
|
||||
page *rod.Page
|
||||
}
|
||||
@@ -163,9 +176,19 @@ func (s *SearchAction) Search(ctx context.Context, keyword string, filters ...Fi
|
||||
|
||||
// 如果有筛选条件,则应用筛选
|
||||
if len(filters) > 0 {
|
||||
// 验证所有筛选选项
|
||||
// 将所有 FilterOption 转换为内部筛选选项
|
||||
var allInternalFilters []internalFilterOption
|
||||
for _, filter := range filters {
|
||||
if err := validateFilterOption(filter); err != nil {
|
||||
internalFilters, err := convertToInternalFilters(filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("筛选选项转换失败: %w", err)
|
||||
}
|
||||
allInternalFilters = append(allInternalFilters, internalFilters...)
|
||||
}
|
||||
|
||||
// 验证所有内部筛选选项
|
||||
for _, filter := range allInternalFilters {
|
||||
if err := validateInternalFilterOption(filter); err != nil {
|
||||
return nil, fmt.Errorf("筛选选项验证失败: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -178,7 +201,7 @@ func (s *SearchAction) Search(ctx context.Context, keyword string, filters ...Fi
|
||||
page.MustWait(`() => document.querySelector('div.filter-panel') !== null`)
|
||||
|
||||
// 应用所有筛选条件
|
||||
for _, filter := range filters {
|
||||
for _, filter := range allInternalFilters {
|
||||
selector := fmt.Sprintf(`div.filter-panel div.filters:nth-child(%d) div.tags:nth-child(%d)`,
|
||||
filter.FiltersIndex, filter.TagsIndex)
|
||||
option := page.MustElement(selector)
|
||||
|
||||
@@ -17,7 +17,9 @@ func TestSearch(t *testing.T) {
|
||||
defer b.Close()
|
||||
|
||||
page := b.NewPage()
|
||||
defer page.Close()
|
||||
defer func() {
|
||||
_ = page.Close()
|
||||
}()
|
||||
|
||||
action := NewSearchAction(page)
|
||||
|
||||
@@ -35,75 +37,69 @@ func TestSearch(t *testing.T) {
|
||||
|
||||
func TestSearchWithFilters(t *testing.T) {
|
||||
|
||||
t.Skip("SKIP: 测试筛选功能")
|
||||
//t.Skip("SKIP: 测试筛选功能")
|
||||
|
||||
b := browser.NewBrowser(false)
|
||||
defer b.Close()
|
||||
|
||||
page := b.NewPage()
|
||||
defer page.Close()
|
||||
defer func() {
|
||||
_ = page.Close()
|
||||
}()
|
||||
|
||||
action := NewSearchAction(page)
|
||||
|
||||
// 方式1:直接使用索引
|
||||
filters1 := []FilterOption{
|
||||
{FiltersIndex: 2, TagsIndex: 3, Text: "图文"}, // 笔记类型 -> 图文
|
||||
{FiltersIndex: 3, TagsIndex: 2, Text: "一天内"}, // 发布时间 -> 一天内
|
||||
// 使用新的 FilterOption 结构
|
||||
filter := FilterOption{
|
||||
NoteType: "图文",
|
||||
PublishTime: "一天内",
|
||||
}
|
||||
|
||||
feeds1, err := action.Search(context.Background(), "dn432", filters1...)
|
||||
feeds, err := action.Search(context.Background(), "dn432", filter)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, feeds1, "feeds should not be empty")
|
||||
require.NotEmpty(t, feeds, "feeds should not be empty")
|
||||
|
||||
fmt.Printf("方式1 - 成功获取到 %d 个筛选后的 Feed\n", len(feeds1))
|
||||
fmt.Printf("成功获取到 %d 个筛选后的 Feed\n", len(feeds))
|
||||
|
||||
// 方式2:使用便利函数
|
||||
filter2, err := NoteType("图文")
|
||||
require.NoError(t, err)
|
||||
|
||||
filter3, err := TimeRange("一天内")
|
||||
require.NoError(t, err)
|
||||
|
||||
filters2 := []FilterOption{filter2, filter3}
|
||||
feeds2, err := action.Search(context.Background(), "dn432", filters2...)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, feeds2, "feeds should not be empty")
|
||||
|
||||
fmt.Printf("方式2 - 成功获取到 %d 个筛选后的 Feed\n", len(feeds2))
|
||||
|
||||
for _, feed := range feeds2 {
|
||||
for _, feed := range feeds {
|
||||
fmt.Printf("Feed ID: %s\n", feed.ID)
|
||||
fmt.Printf("Feed Title: %s\n", feed.NoteCard.DisplayTitle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterValidation(t *testing.T) {
|
||||
// 测试有效的筛选选项
|
||||
validFilter := FilterOption{FiltersIndex: 2, TagsIndex: 3, Text: "图文"}
|
||||
err := validateFilterOption(validFilter)
|
||||
// 测试有效的筛选选项转换
|
||||
validFilter := FilterOption{
|
||||
NoteType: "图文",
|
||||
PublishTime: "一天内",
|
||||
}
|
||||
internalFilters, err := convertToInternalFilters(validFilter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, internalFilters, 2)
|
||||
|
||||
// 测试无效的筛选组索引
|
||||
invalidFilterGroup := FilterOption{FiltersIndex: 6, TagsIndex: 1, Text: "无效"}
|
||||
err = validateFilterOption(invalidFilterGroup)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "无效的筛选组索引")
|
||||
// 验证转换后的内部筛选选项
|
||||
for _, filter := range internalFilters {
|
||||
err := validateInternalFilterOption(filter)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 测试无效的标签索引
|
||||
invalidTagIndex := FilterOption{FiltersIndex: 2, TagsIndex: 5, Text: "无效"}
|
||||
err = validateFilterOption(invalidTagIndex)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "标签索引 5 超出范围")
|
||||
|
||||
// 测试便利函数
|
||||
filter, err := NoteType("图文")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, filter.FiltersIndex)
|
||||
require.Equal(t, 3, filter.TagsIndex)
|
||||
require.Equal(t, "图文", filter.Text)
|
||||
|
||||
// 测试不存在的文本
|
||||
_, err = NoteType("不存在的类型")
|
||||
// 测试无效的筛选值
|
||||
invalidFilter := FilterOption{
|
||||
NoteType: "不存在的类型",
|
||||
}
|
||||
_, err = convertToInternalFilters(invalidFilter)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "未找到文本")
|
||||
|
||||
// 测试所有有效的筛选选项
|
||||
allFilters := FilterOption{
|
||||
SortBy: "最新",
|
||||
NoteType: "视频",
|
||||
PublishTime: "一周内",
|
||||
SearchScope: "已关注",
|
||||
Location: "同城",
|
||||
}
|
||||
internalFilters, err = convertToInternalFilters(allFilters)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, internalFilters, 5)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user