mirror of
https://github.com/nitezs/sub2clash.git
synced 2024-12-23 14:24:42 -05:00
feat: 增加重复节点检测
feat: 增加节点名称字符串替换 feat: 增加节点删除 feat: 增加短链密码设定 modify: 修改模板解析逻辑
This commit is contained in:
parent
06c9858866
commit
2339b7d256
36
API_README.md
Normal file
36
API_README.md
Normal file
@ -0,0 +1,36 @@
|
||||
# `/clash`, `/meta`
|
||||
|
||||
获取 Clash/Clash.Meta 配置链接
|
||||
|
||||
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||
|--------------|--------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接(可以输入多个,用 `,` 分隔) |
|
||||
| proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) |
|
||||
| refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
|
||||
| template | string | 否 | - | 外部模板链接或内部模板名称 |
|
||||
| ruleProvider | string | 否 | - | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集所走的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
|
||||
| rule | string | 否 | - | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
|
||||
| autoTest | bool | 否 | `false` | 国家策略组是否自动测速 |
|
||||
| lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
|
||||
| sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc` |
|
||||
| replace | string | 否 | - | 通过正则表达式重命名节点,格式 `[ReplaceKey,ReplaceTo]` |
|
||||
| remove | string | 否 | - | 通过正则表达式删除节点 |
|
||||
|
||||
# `/short`
|
||||
|
||||
获取短链,Content-Type 为 `application/json`
|
||||
具体参考使用可以参考 [api\templates\index.html](./api/templates/index.html)
|
||||
|
||||
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||
|----------|--------|------|-----|------------------|
|
||||
| url | string | 是 | - | 需要转换的 Query 参数部分 |
|
||||
| password | string | 否 | - | 短链密码 |
|
||||
|
||||
# `/s/:hash`
|
||||
|
||||
短链跳转
|
||||
`hash` 为动态路由参数,可以通过 `/short` 接口获取
|
||||
|
||||
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||
|----------|--------|------|-----|------|
|
||||
| password | string | 否 | - | 短链密码 |
|
24
README.md
24
README.md
@ -39,23 +39,17 @@
|
||||
|
||||
### API
|
||||
|
||||
#### `/clash`, `/meta`
|
||||
[API文档](./API_README.md)
|
||||
|
||||
获取 Clash/Clash.Meta 配置链接
|
||||
### 模板
|
||||
|
||||
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||
|--------------|--------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接(可以输入多个,用 `,` 分隔) |
|
||||
| proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) |
|
||||
| refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
|
||||
| template | string | 否 | - | 外部模板链接或内部模板名称 |
|
||||
| ruleProvider | string | 否 | - | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集所走的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
|
||||
| rule | string | 否 | - | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
|
||||
| autoTest | bool | 否 | `false` | 国家策略组是否自动测速 |
|
||||
| lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
|
||||
| sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc` |
|
||||
可以通过变脸自定义模板中的策略组代理节点
|
||||
解释的不太清楚,可以参考下方默认模板
|
||||
|
||||
## 默认模板
|
||||
- `<all>` 为添加所有节点
|
||||
- `<countries>` 为添加所有国家策略组
|
||||
|
||||
#### 默认模板
|
||||
|
||||
- [Clash](./templates/template_clash.yaml)
|
||||
- [Clash.Meta](./templates/template_meta.yaml)
|
||||
@ -63,5 +57,3 @@
|
||||
## 已知问题
|
||||
|
||||
[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue
|
||||
|
||||
## TODO
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/logger"
|
||||
"sub2clash/model"
|
||||
@ -88,6 +89,46 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
||||
proxyList = append(proxyList, sub.Proxies...)
|
||||
}
|
||||
}
|
||||
// 去重
|
||||
proxies := make(map[string]*model.Proxy)
|
||||
for i := range proxyList {
|
||||
key := proxyList[i].Server + ":" + strconv.Itoa(proxyList[i].Port) + ":" + proxyList[i].Type
|
||||
if _, exist := proxies[key]; exist {
|
||||
proxyList = append(proxyList[:i], proxyList[i+1:]...)
|
||||
}
|
||||
}
|
||||
// 重名检测
|
||||
names := make(map[string]bool)
|
||||
for i := range proxyList {
|
||||
if _, exist := names[proxyList[i].Name]; exist {
|
||||
proxyList[i].Name = proxyList[i].Name + "@" + proxyList[i].Server + ":" + strconv.Itoa(proxyList[i].Port)
|
||||
}
|
||||
names[proxyList[i].Name] = true
|
||||
}
|
||||
// 删除节点、改名
|
||||
if strings.TrimSpace(query.Remove) != "" {
|
||||
removeReg, err := regexp.Compile(query.Remove)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
|
||||
return nil, errors.New("remove 参数非法: " + err.Error())
|
||||
}
|
||||
replaceReg, err := regexp.Compile(query.ReplaceKey)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
|
||||
return nil, errors.New("replaceName 参数非法: " + err.Error())
|
||||
}
|
||||
newProxyList := make([]model.Proxy, 0, len(proxyList))
|
||||
for i := range proxyList {
|
||||
if removeReg.MatchString(proxyList[i].Name) {
|
||||
continue // 如果匹配到要删除的元素,跳过该元素,不添加到新切片中
|
||||
}
|
||||
if replaceReg.MatchString(proxyList[i].Name) {
|
||||
proxyList[i].Name = replaceReg.ReplaceAllString(proxyList[i].Name, query.ReplaceTo)
|
||||
}
|
||||
newProxyList = append(newProxyList, proxyList[i]) // 将要保留的元素添加到新切片中
|
||||
}
|
||||
proxyList = newProxyList
|
||||
}
|
||||
// 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
|
||||
var t = &model.Subscription{}
|
||||
utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
|
||||
@ -154,28 +195,28 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
|
||||
)
|
||||
}
|
||||
}
|
||||
var proxyNames []string
|
||||
for _, proxy := range sub.Proxies {
|
||||
proxyNames = append(proxyNames, proxy.Name)
|
||||
}
|
||||
// 将订阅中的节点添加到模板中
|
||||
temp.Proxies = append(temp.Proxies, sub.Proxies...)
|
||||
// 将订阅中的策略组添加到模板中
|
||||
skipGroups := []string{"全球直连", "广告拦截", "手动切换"}
|
||||
for i := range temp.ProxyGroups {
|
||||
skip := false
|
||||
for _, v := range skipGroups {
|
||||
if strings.Contains(temp.ProxyGroups[i].Name, v) {
|
||||
if v == "手动切换" {
|
||||
proxies := make([]string, 0, len(sub.Proxies))
|
||||
for _, p := range sub.Proxies {
|
||||
proxies = append(proxies, p.Name)
|
||||
}
|
||||
temp.ProxyGroups[i].Proxies = proxies
|
||||
}
|
||||
skip = true
|
||||
continue
|
||||
if temp.ProxyGroups[i].IsCountryGrop {
|
||||
continue
|
||||
}
|
||||
newProxies := make([]string, 0, len(temp.ProxyGroups[i].Proxies))
|
||||
for j := range temp.ProxyGroups[i].Proxies {
|
||||
if temp.ProxyGroups[i].Proxies[j] == "<all>" {
|
||||
newProxies = append(newProxies, proxyNames...)
|
||||
} else if temp.ProxyGroups[i].Proxies[j] == "<countries>" {
|
||||
newProxies = append(newProxies, countryGroupNames...)
|
||||
} else {
|
||||
newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
|
||||
}
|
||||
}
|
||||
if !skip {
|
||||
temp.ProxyGroups[i].Proxies = append(temp.ProxyGroups[i].Proxies, countryGroupNames...)
|
||||
}
|
||||
temp.ProxyGroups[i].Proxies = newProxies
|
||||
}
|
||||
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
|
||||
}
|
||||
|
@ -50,15 +50,20 @@ func ShortLinkGenHandler(c *gin.Context) {
|
||||
Hash: hash,
|
||||
Url: params.Url,
|
||||
LastRequestTime: -1,
|
||||
Password: params.Password,
|
||||
},
|
||||
)
|
||||
// 返回短链接
|
||||
if params.Password != "" {
|
||||
hash += "/?password=" + params.Password
|
||||
}
|
||||
c.String(200, hash)
|
||||
}
|
||||
|
||||
func ShortLinkGetHandler(c *gin.Context) {
|
||||
// 获取动态路由
|
||||
hash := c.Param("hash")
|
||||
password := c.Query("password")
|
||||
if strings.TrimSpace(hash) == "" {
|
||||
c.String(400, "参数错误")
|
||||
return
|
||||
@ -71,6 +76,10 @@ func ShortLinkGetHandler(c *gin.Context) {
|
||||
c.String(404, "未找到短链接")
|
||||
return
|
||||
}
|
||||
if shortLink.Password != "" && shortLink.Password != password {
|
||||
c.String(403, "密码错误")
|
||||
return
|
||||
}
|
||||
// 更新最后访问时间
|
||||
shortLink.LastRequestTime = time.Now().Unix()
|
||||
database.SaveShortLink(&shortLink)
|
||||
|
@ -168,6 +168,39 @@
|
||||
<option value="sizedesc">节点数量(降序)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Remove -->
|
||||
<div class="form-group mb-3">
|
||||
<label for="remove">删除节点:</label>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="remove"
|
||||
id="remove"
|
||||
placeholder="正则表达式"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Rename -->
|
||||
<div class="form-group mb-3">
|
||||
<label for="replaceKey">替换节点名称:</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="replace"
|
||||
id="replaceKey"
|
||||
placeholder="原字符串(正则表达式)"
|
||||
/>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="replace"
|
||||
id="replaceTo"
|
||||
placeholder="替换为"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Display the API Link -->
|
||||
@ -188,6 +221,12 @@
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="apiShortLink" readonly type="text" />
|
||||
<input
|
||||
class="form-control"
|
||||
id="password"
|
||||
type="text"
|
||||
placeholder="密码"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="generateShortLink()"
|
||||
@ -382,6 +421,24 @@
|
||||
// 获取排序策略
|
||||
const sort = document.getElementById("sort").value;
|
||||
queryParams.push(`sort=${sort}`);
|
||||
|
||||
// 获取删除节点的正则表达式
|
||||
const remove = document.getElementById("remove").value;
|
||||
if (remove.trim() !== "") {
|
||||
queryParams.push(`remove=${encodeURIComponent(remove)}`);
|
||||
}
|
||||
|
||||
// 获取替换节点名称的正则表达式
|
||||
const replaceKey = document.getElementById("replaceKey").value;
|
||||
const replaceTo = document.getElementById("replaceTo").value;
|
||||
if (replaceKey.trim() !== "" && replaceTo.trim() !== "") {
|
||||
queryParams.push(
|
||||
`replace=[${encodeURIComponent(replaceKey)},${encodeURIComponent(
|
||||
replaceTo,
|
||||
)}]`,
|
||||
);
|
||||
}
|
||||
|
||||
return `${endpoint}?${queryParams.join("&")}`;
|
||||
}
|
||||
|
||||
@ -396,6 +453,7 @@
|
||||
|
||||
function generateShortLink() {
|
||||
const apiShortLink = document.getElementById("apiShortLink");
|
||||
const password = document.getElementById("password");
|
||||
let uri = generateURI();
|
||||
if (uri === "") {
|
||||
return;
|
||||
@ -405,6 +463,7 @@
|
||||
"./short",
|
||||
{
|
||||
url: uri,
|
||||
password: password.value.trim(),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
|
@ -3,5 +3,6 @@ package model
|
||||
type ShortLink struct {
|
||||
Hash string `gorm:"primary_key"`
|
||||
Url string
|
||||
Password string
|
||||
LastRequestTime int64
|
||||
}
|
||||
|
@ -8,88 +8,104 @@ proxy-groups:
|
||||
- name: 节点选择
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: 手动切换
|
||||
type: select
|
||||
proxies:
|
||||
- <all>
|
||||
- name: 电报消息
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: OpenAi
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: 油管视频
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: 巴哈姆特
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: 哔哩哔哩
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 全球直连
|
||||
- name: 国外媒体
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: 国内媒体
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- DIRECT
|
||||
- 手动切换
|
||||
- name: 谷歌FCM
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- DIRECT
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- name: 微软云盘
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- DIRECT
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- name: 微软服务
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- DIRECT
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- name: 苹果服务
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- DIRECT
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- name: 游戏平台
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- DIRECT
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- name: 网易音乐
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- DIRECT
|
||||
- 节点选择
|
||||
- name: 全球直连
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- DIRECT
|
||||
- 节点选择
|
||||
- name: 广告拦截
|
||||
@ -105,6 +121,7 @@ proxy-groups:
|
||||
- name: 漏网之鱼
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- DIRECT
|
||||
- 手动切换
|
||||
@ -9579,4 +9596,4 @@ rules:
|
||||
- PROCESS-NAME,Weiyun.exe,全球直连
|
||||
- PROCESS-NAME,baidunetdisk.exe,全球直连
|
||||
- GEOIP,CN,全球直连
|
||||
- MATCH,漏网之鱼
|
||||
- MATCH,漏网之鱼
|
||||
|
@ -8,32 +8,38 @@ proxy-groups:
|
||||
- name: 节点选择
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: 手动切换
|
||||
type: select
|
||||
proxies:
|
||||
- <all>
|
||||
- name: 微软服务
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: 游戏平台
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: 巴哈姆特
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
- name: 哔哩哔哩
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
@ -50,6 +56,7 @@ proxy-groups:
|
||||
- name: 漏网之鱼
|
||||
type: select
|
||||
proxies:
|
||||
- <countries>
|
||||
- 节点选择
|
||||
- 手动切换
|
||||
- DIRECT
|
||||
@ -64,4 +71,4 @@ rules:
|
||||
- GEOSITE,geolocation-!cn,节点选择
|
||||
- GEOSITE,CN,全球直连
|
||||
- GEOIP,CN,全球直连
|
||||
- MATCH,漏网之鱼
|
||||
- MATCH,漏网之鱼
|
||||
|
14
utils/get.go
14
utils/get.go
@ -12,7 +12,19 @@ func Get(url string) (resp *http.Response, err error) {
|
||||
haveTried := 0
|
||||
retryDelay := time.Second // 延迟1秒再重试
|
||||
for haveTried < retryTimes {
|
||||
get, err := http.Get(url)
|
||||
client := &http.Client{}
|
||||
client.Timeout = time.Second * 10
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
haveTried++
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
}
|
||||
req.Header.Set(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
)
|
||||
get, err := client.Do(req)
|
||||
if err != nil {
|
||||
haveTried++
|
||||
time.Sleep(retryDelay)
|
||||
|
@ -31,7 +31,6 @@ func AddProxy(
|
||||
sub *model.Subscription, autotest bool,
|
||||
lazy bool, clashType model.ClashType, proxies ...model.Proxy,
|
||||
) {
|
||||
newCountryGroupNames := make([]string, 0)
|
||||
proxyTypes := model.GetSupportProxyTypes(clashType)
|
||||
// 添加节点
|
||||
for _, proxy := range proxies {
|
||||
@ -79,7 +78,6 @@ func AddProxy(
|
||||
}
|
||||
}
|
||||
sub.ProxyGroups = append(sub.ProxyGroups, newGroup)
|
||||
newCountryGroupNames = append(newCountryGroupNames, countryName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
package validator
|
||||
|
||||
type ShortLinkGenValidator struct {
|
||||
Url string `form:"url" binding:"required"`
|
||||
Url string `form:"url" binding:"required"`
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
||||
type ShortLinkGetValidator struct {
|
||||
Hash string `form:"hash" binding:"required"`
|
||||
Hash string `form:"hash" binding:"required"`
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
@ -25,6 +25,10 @@ type SubValidator struct {
|
||||
AutoTest bool `form:"autoTest,default=false" binding:""`
|
||||
Lazy bool `form:"lazy,default=false" binding:""`
|
||||
Sort string `form:"sort" binding:""`
|
||||
Remove string `form:"remove" binding:""`
|
||||
Replace string `form:"replace" binding:""`
|
||||
ReplaceKey string `form:"replaceKey" binding:""`
|
||||
ReplaceTo string `form:"replaceString" binding:""`
|
||||
}
|
||||
|
||||
type RuleProviderStruct struct {
|
||||
@ -135,5 +139,13 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
|
||||
} else {
|
||||
query.Rules = nil
|
||||
}
|
||||
if strings.TrimSpace(query.Replace) != "" {
|
||||
replace := strings.Split(strings.Trim(query.Replace, "[]"), ",")
|
||||
if len(replace) != 2 {
|
||||
return SubValidator{}, errors.New("参数错误: replace 格式错误")
|
||||
}
|
||||
query.ReplaceKey = replace[0]
|
||||
query.ReplaceTo = replace[1]
|
||||
}
|
||||
return query, nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user