mirror of
https://github.com/FloatTech/ZeroBot-Plugin.git
synced 2025-12-19 13:59:39 +08:00
优化代码结构
This commit is contained in:
parent
f18c809355
commit
d4a057e21a
@ -1291,7 +1291,7 @@ print("run[CQ:image,file="+j["img"]+"]")
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>WarframeAPI</summary>
|
<summary>星际战甲</summary>
|
||||||
|
|
||||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/warframeapi"`
|
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/warframeapi"`
|
||||||
|
|
||||||
|
|||||||
94
plugin/warframeapi/api.go
Normal file
94
plugin/warframeapi/api.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package warframeapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/FloatTech/floatbox/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
const wfapiurl = "https://api.warframestat.us/pc" // 星际战甲API
|
||||||
|
const wfitemurl = "https://api.warframe.market/v1/items" // 星际战甲游戏品信息列表URL
|
||||||
|
|
||||||
|
// 从WFapi获取数据
|
||||||
|
func newwfapi() (w wfapi, err error) {
|
||||||
|
var data []byte
|
||||||
|
data, err = web.GetData(wfapiurl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取Warframe市场的售价表,并进行排序,cn_name为物品中文名称,onlyMaxRank表示只取最高等级的物品,返回物品售价表,物品信息,物品英文
|
||||||
|
func getitemsorder(cnName string, onlyMaxRank bool) (od orders, it *itemsInSet, n string, err error) {
|
||||||
|
var wfapiio wfAPIItemsOrders
|
||||||
|
data, err := web.RequestDataWithHeaders(&http.Client{}, fmt.Sprintf("https://api.warframe.market/v1/items/%s/orders?include=item", cnName), "GET", func(request *http.Request) error {
|
||||||
|
request.Header.Add("Accept", "application/json")
|
||||||
|
request.Header.Add("Platform", "pc")
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &wfapiio)
|
||||||
|
if len(wfapiio.Payload.Orders) <= 0 {
|
||||||
|
err = errors.New("no such name")
|
||||||
|
}
|
||||||
|
od = make(orders, 0, len(wfapiio.Payload.Orders))
|
||||||
|
// 遍历市场物品列表
|
||||||
|
for _, v := range wfapiio.Payload.Orders {
|
||||||
|
// 取其中类型为售卖,且去掉不在线的玩家
|
||||||
|
if v.OrderType == "sell" && v.User.Status != "offline" {
|
||||||
|
if !onlyMaxRank {
|
||||||
|
od = append(od, v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.ModRank == wfapiio.Include.Item.ItemsInSet[0].ModMaxRank {
|
||||||
|
od = append(od, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 对报价表进行排序,由低到高
|
||||||
|
sort.Sort(od)
|
||||||
|
// 获取物品信息
|
||||||
|
for i, v := range wfapiio.Include.Item.ItemsInSet {
|
||||||
|
if v.URLName == cnName {
|
||||||
|
it = &wfapiio.Include.Item.ItemsInSet[i]
|
||||||
|
n = v.En.ItemName
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it = &wfapiio.Include.Item.ItemsInSet[0]
|
||||||
|
n = wfapiio.Include.Item.ItemsInSet[0].En.ItemName
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newwm() (wmitems map[string]items, itemNames []string) {
|
||||||
|
var itemapi wfAPIItem // WarFrame市场的数据实例
|
||||||
|
|
||||||
|
data, err := web.RequestDataWithHeaders(&http.Client{}, wfitemurl, "GET", func(request *http.Request) error {
|
||||||
|
request.Header.Add("Accept", "application/json")
|
||||||
|
request.Header.Add("Language", "zh-hans")
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &itemapi)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wmitems = make(map[string]items, len(itemapi.Payload.Items)*4)
|
||||||
|
itemNames = make([]string, len(itemapi.Payload.Items))
|
||||||
|
for i, v := range itemapi.Payload.Items {
|
||||||
|
wmitems[v.ItemName] = v
|
||||||
|
itemNames[i] = v.ItemName
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@ -1,79 +0,0 @@
|
|||||||
package warframeapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/davidscholberg/go-durationfmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 游戏时间模拟
|
|
||||||
type gameTime struct {
|
|
||||||
rwm sync.RWMutex
|
|
||||||
Name string `json:"name"` // 时间名称
|
|
||||||
NextTime time.Time `json:"time"` // 下次更新时间
|
|
||||||
Status bool `json:"status"` // 状态
|
|
||||||
StatusTrueDes string `json:"true_des"` // 状态说明
|
|
||||||
StatusFalseDes string `json:"false_des"` // 状态说明
|
|
||||||
DayTime int `json:"day"` // 白天时长
|
|
||||||
NightTime int `json:"night"` // 夜间时长
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
gameTimes [3]*gameTime
|
|
||||||
)
|
|
||||||
|
|
||||||
// TimeString 根据传入的世界编号,获取对应的游戏时间文本
|
|
||||||
func (t *gameTime) String() string {
|
|
||||||
return "平原时间:" + t.daynight() + "\n" +
|
|
||||||
"下次更新:" + t.remaintime()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前游戏时间状态(白天/夜晚)
|
|
||||||
func (t *gameTime) daynight() string {
|
|
||||||
t.rwm.RLock()
|
|
||||||
defer t.rwm.RUnlock()
|
|
||||||
if t.Status {
|
|
||||||
return t.StatusTrueDes
|
|
||||||
}
|
|
||||||
return t.StatusFalseDes
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取下一次时间状态更新的剩余游戏时间(x分x秒)
|
|
||||||
func (t *gameTime) remaintime() string {
|
|
||||||
t.rwm.RLock()
|
|
||||||
d := time.Until(t.NextTime)
|
|
||||||
t.rwm.RUnlock()
|
|
||||||
durStr, _ := durationfmt.Format(d, "%m分%s秒后")
|
|
||||||
return durStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据API返回内容修正游戏时间
|
|
||||||
func loadTime(api wfAPI) {
|
|
||||||
gameTimes = [3]*gameTime{
|
|
||||||
{Name: "地球平原", NextTime: api.CetusCycle.Expiry.Local(), Status: api.CetusCycle.IsDay, StatusTrueDes: "白天", StatusFalseDes: "夜晚", DayTime: 100 * 60, NightTime: 50 * 60},
|
|
||||||
{Name: "金星平原", NextTime: api.VallisCycle.Expiry.Local(), Status: api.VallisCycle.IsWarm, StatusTrueDes: "温暖", StatusFalseDes: "寒冷", DayTime: 400, NightTime: 20 * 60},
|
|
||||||
{Name: "火卫二平原", NextTime: api.CambionCycle.Expiry.Local(), Status: api.CambionCycle.Active == "fass", StatusTrueDes: "fass", StatusFalseDes: "vome", DayTime: 100 * 60, NightTime: 50 * 60},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// timeDet游戏时间更新
|
|
||||||
func timeDet() {
|
|
||||||
for _, v := range gameTimes {
|
|
||||||
// 当前时间对比下一次游戏状态更新时间,看看还剩多少秒
|
|
||||||
nt := time.Until(v.NextTime).Seconds()
|
|
||||||
// 已经过了游戏时间状态更新时间
|
|
||||||
if nt < 0 {
|
|
||||||
v.rwm.Lock()
|
|
||||||
// 更新游戏状态,如果是白天就切换到晚上,反之亦然
|
|
||||||
if v.Status {
|
|
||||||
// 计算下次的晚上更新时间
|
|
||||||
v.NextTime = v.NextTime.Add(time.Duration(v.NightTime) * time.Second)
|
|
||||||
} else {
|
|
||||||
// 计算下次的白天更新时间
|
|
||||||
v.NextTime = v.NextTime.Add(time.Duration(v.DayTime) * time.Second)
|
|
||||||
}
|
|
||||||
v.rwm.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,120 +1,92 @@
|
|||||||
// Package warframeapi 百度内容审核
|
// Package warframeapi 星际战甲
|
||||||
package warframeapi
|
package warframeapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/FloatTech/floatbox/binary"
|
|
||||||
"github.com/FloatTech/floatbox/web"
|
|
||||||
ctrl "github.com/FloatTech/zbpctrl"
|
ctrl "github.com/FloatTech/zbpctrl"
|
||||||
"github.com/FloatTech/zbputils/control"
|
"github.com/FloatTech/zbputils/control"
|
||||||
"github.com/FloatTech/zbputils/img/text"
|
"github.com/FloatTech/zbputils/ctxext"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
zero "github.com/wdvxdr1123/ZeroBot"
|
zero "github.com/wdvxdr1123/ZeroBot"
|
||||||
"github.com/wdvxdr1123/ZeroBot/message"
|
"github.com/wdvxdr1123/ZeroBot/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var wmitems, itemNames = newwm()
|
||||||
wmitems map[string]items // WarFrame市场的中文名称对应的物品的字典
|
|
||||||
itmeNames []string // 物品名称列表
|
|
||||||
rt runtime
|
|
||||||
)
|
|
||||||
|
|
||||||
// 时间同步状态
|
|
||||||
type runtime struct {
|
|
||||||
rwm sync.RWMutex
|
|
||||||
enable bool // 是否启动
|
|
||||||
}
|
|
||||||
|
|
||||||
const wfapiurl = "https://api.warframestat.us/pc" // 星际战甲API
|
|
||||||
const wfitemurl = "https://api.warframe.market/v1/items" // 星际战甲游戏品信息列表URL
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
eng := control.Register("warframeapi", &ctrl.Options[*zero.Ctx]{
|
eng := control.Register("warframeapi", &ctrl.Options[*zero.Ctx]{
|
||||||
DisableOnDefault: false,
|
DisableOnDefault: false,
|
||||||
Help: "warframeapi\n" +
|
Brief: "星际战甲",
|
||||||
"- wf时间同步\n" +
|
Help: "- wf时间同步\n" +
|
||||||
"- [金星|地球|火卫二]平原时间\n" +
|
"- [金星|地球|火卫二]平原时间\n" +
|
||||||
"- .wm [物品名称]\n" +
|
"- .wm [物品名称]\n" +
|
||||||
"- 仲裁\n" +
|
"- wf仲裁\n" +
|
||||||
"- 警报\n" +
|
"- wf警报\n" +
|
||||||
"- 每日特惠",
|
"- wf每日特惠",
|
||||||
PrivateDataFolder: "warframeapi",
|
PrivateDataFolder: "warframeapi",
|
||||||
})
|
})
|
||||||
updateWM()
|
|
||||||
|
|
||||||
// 获取具体的平原时间,在触发后,会启动持续时间按5分钟的时间更新模拟,以此处理短时间内请求时,时间不会变化的问题
|
// 获取具体的平原时间, 在触发后, 会启动持续时间按5分钟的时间更新模拟, 以此处理短时间内请求时, 时间不会变化的问题
|
||||||
eng.OnSuffix("平原时间").SetBlock(true).
|
eng.OnSuffix("平原时间").SetBlock(true).
|
||||||
Handle(func(ctx *zero.Ctx) {
|
Handle(func(ctx *zero.Ctx) {
|
||||||
if !rt.enable { // 没有进行同步,就拉取一次服务器状态
|
if !gameWorld.hasSync() { // 没有进行同步,就拉取一次服务器状态
|
||||||
wfapi, err := wfapiGetData()
|
wfapi, err := newwfapi()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SendChain(message.Text("Error:获取服务器时间失败"))
|
ctx.SendChain(message.Text("ERROR: 获取服务器时间失败"))
|
||||||
}
|
}
|
||||||
loadTime(wfapi)
|
gameWorld.refresh(&wfapi)
|
||||||
}
|
}
|
||||||
|
var msg any
|
||||||
switch ctx.State["args"].(string) {
|
switch ctx.State["args"].(string) {
|
||||||
case "地球", "夜灵":
|
case "地球", "夜灵":
|
||||||
ctx.SendChain(message.Text(gameTimes[0]))
|
msg = gameWorld.w[0]
|
||||||
case "金星", "奥布山谷":
|
case "金星", "奥布山谷":
|
||||||
ctx.SendChain(message.Text(gameTimes[1]))
|
msg = gameWorld.w[1]
|
||||||
case "魔胎之境", "火卫二", "火卫":
|
case "魔胎之境", "火卫二", "火卫":
|
||||||
ctx.SendChain(message.Text(gameTimes[2]))
|
msg = gameWorld.w[2]
|
||||||
default:
|
default:
|
||||||
ctx.SendChain(message.Text("ERROR: 平原不存在"))
|
msg = "ERROR: 平原不存在"
|
||||||
}
|
}
|
||||||
|
ctx.SendChain(message.Text(msg))
|
||||||
// 是否正在进行同步,没有就开启同步,有就不开启
|
// 是否正在进行同步,没有就开启同步,有就不开启
|
||||||
if !rt.enable {
|
if !gameWorld.hasSync() {
|
||||||
// 设置标志位
|
if gameWorld.setsync() {
|
||||||
rt.rwm.Lock()
|
go func() {
|
||||||
if rt.enable { // 预检测,防止其他线程同时进来
|
// 30*10=300=5分钟
|
||||||
return
|
for i := 0; i < 30; i++ {
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
gameWorld.update() // 5分钟内每隔10秒更新一下时间
|
||||||
|
}
|
||||||
|
// 5分钟时间同步结束
|
||||||
|
_ = gameWorld.resetsync()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
rt.enable = true
|
|
||||||
rt.rwm.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// 30*10=300=5分钟
|
|
||||||
for i := 0; i < 30; i++ {
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
timeDet() // 5分钟内每隔10秒更新一下时间
|
|
||||||
}
|
|
||||||
// 5分钟时间同步结束
|
|
||||||
rt.rwm.Lock()
|
|
||||||
rt.enable = false
|
|
||||||
rt.rwm.Unlock()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
eng.OnFullMatch("警报").SetBlock(true).
|
eng.OnFullMatch("wf警报").SetBlock(true).
|
||||||
Handle(func(ctx *zero.Ctx) {
|
Handle(func(ctx *zero.Ctx) {
|
||||||
wfapi, err := wfapiGetData()
|
wfapi, err := newwfapi()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SendChain(message.Text("ERROR:", err.Error()))
|
ctx.SendChain(message.Text("ERROR: ", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 如果返回的wfapi中,警报数量>0
|
// 如果返回的wfapi中, 警报数量>0
|
||||||
if len(wfapi.Alerts) > 0 {
|
if len(wfapi.Alerts) > 0 {
|
||||||
// 遍历警报数据,打印警报信息
|
msgs := make(message.Message, len(wfapi.Alerts))
|
||||||
for _, v := range wfapi.Alerts {
|
// 遍历警报数据, 打印警报信息
|
||||||
// 如果警报处于激活状态
|
for i, v := range wfapi.Alerts {
|
||||||
if v.Active {
|
msgs[i] = ctxext.FakeSenderForwardNode(ctx, message.Text(
|
||||||
ctx.SendChain(stringArrayToImage([]string{
|
"激活: ", v.Active,
|
||||||
"节点:" + v.Mission.Node,
|
"\n节点: ", v.Mission.Node,
|
||||||
"类型:" + v.Mission.Type,
|
"\n类型: ", v.Mission.Type,
|
||||||
"敌人Lv:" + fmt.Sprint(v.Mission.MinEnemyLevel) + "~" + fmt.Sprint(v.Mission.MaxEnemyLevel),
|
"\n敌人等级: ", v.Mission.MinEnemyLevel, "~", v.Mission.MaxEnemyLevel,
|
||||||
"奖励:" + v.Mission.Reward.AsString,
|
"\n奖励: ", v.Mission.Reward.AsString,
|
||||||
"剩余时间:" + v.Eta,
|
"\n剩余时间:", v.Eta))
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ctx.SendChain(msgs...)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//TODO:订阅功能-等待重做
|
//TODO:订阅功能-等待重做
|
||||||
@ -203,38 +175,39 @@ func init() {
|
|||||||
// ctx.SendChain(msg...)
|
// ctx.SendChain(msg...)
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
eng.OnFullMatch("仲裁").SetBlock(true).
|
eng.OnFullMatch("wf仲裁").SetBlock(true).
|
||||||
Handle(func(ctx *zero.Ctx) {
|
Handle(func(ctx *zero.Ctx) {
|
||||||
// 通过wfapi获取仲裁信息
|
// 通过wfapi获取仲裁信息
|
||||||
wfapi, err := wfapiGetData()
|
wfapi, err := newwfapi()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SendChain(message.Text("ERROR:", err.Error()))
|
ctx.SendChain(message.Text("ERROR: ", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.SendChain(stringArrayToImage([]string{
|
ctx.SendChain(message.Text(
|
||||||
"节点:" + wfapi.Arbitration.Node,
|
"节点: ", wfapi.Arbitration.Node,
|
||||||
"类型:" + wfapi.Arbitration.Type,
|
"\n类型: ", wfapi.Arbitration.Type,
|
||||||
"阵营:" + wfapi.Arbitration.Enemy,
|
"\n阵营: ", wfapi.Arbitration.Enemy,
|
||||||
"剩余时间:" + fmt.Sprint(int(wfapi.Arbitration.Expiry.Sub(time.Now().UTC()).Minutes())) + "m",
|
"\n剩余时间: ", int(wfapi.Arbitration.Expiry.Sub(time.Now().UTC()).Minutes()), "m",
|
||||||
}))
|
))
|
||||||
})
|
})
|
||||||
eng.OnFullMatch("每日特惠").SetBlock(true).
|
eng.OnFullMatch("wf每日特惠").SetBlock(true).
|
||||||
Handle(func(ctx *zero.Ctx) {
|
Handle(func(ctx *zero.Ctx) {
|
||||||
wfapi, err := wfapiGetData()
|
wfapi, err := newwfapi()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SendChain(message.Text("ERROR:", err.Error()))
|
ctx.SendChain(message.Text("ERROR: ", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, dd := range wfapi.DailyDeals {
|
if len(wfapi.DailyDeals) > 0 {
|
||||||
ctx.SendChain(
|
msgs := make(message.Message, len(wfapi.DailyDeals))
|
||||||
message.Text(
|
for i, dd := range wfapi.DailyDeals {
|
||||||
"物品:", dd.Item, "\n",
|
msgs[i] = ctxext.FakeSenderForwardNode(ctx, message.Text(
|
||||||
"价格:", dd.OriginalPrice, "→", dd.SalePrice, "\n",
|
"物品: ", dd.Item,
|
||||||
"数量:(", dd.Total, "/", dd.Sold, ")\n",
|
"\n价格: ", dd.OriginalPrice, "→", dd.SalePrice,
|
||||||
"时间:", dd.Eta,
|
"\n数量: (", dd.Total, "/", dd.Sold, ")",
|
||||||
),
|
"\n时间: ", dd.Eta,
|
||||||
)
|
))
|
||||||
|
}
|
||||||
|
ctx.SendChain(msgs...)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// eng.OnRegex(`^入侵$`).SetBlock(true).
|
// eng.OnRegex(`^入侵$`).SetBlock(true).
|
||||||
@ -251,24 +224,23 @@ func init() {
|
|||||||
// })
|
// })
|
||||||
eng.OnFullMatch("wf时间同步").SetBlock(true).
|
eng.OnFullMatch("wf时间同步").SetBlock(true).
|
||||||
Handle(func(ctx *zero.Ctx) {
|
Handle(func(ctx *zero.Ctx) {
|
||||||
wfapi, err := wfapiGetData()
|
wfapi, err := newwfapi()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SendChain(message.Text("ERROR:", err.Error()))
|
ctx.SendChain(message.Text("ERROR: ", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadTime(wfapi)
|
gameWorld.refresh(&wfapi)
|
||||||
ctx.SendChain(message.Text("已拉取服务器时间并同步到本地模拟"))
|
ctx.SendChain(message.Text("已拉取服务器时间并同步到本地模拟"))
|
||||||
})
|
})
|
||||||
// 根据名称从Warframe市场查询物品售价
|
// 根据名称从Warframe市场查询物品售价
|
||||||
eng.OnPrefix(".wm ").SetBlock(true).
|
eng.OnPrefix(".wm ").SetBlock(true).
|
||||||
Handle(func(ctx *zero.Ctx) {
|
Handle(func(ctx *zero.Ctx) {
|
||||||
// 根据输入的名称,从游戏物品名称列表中进行模糊搜索
|
// 根据输入的名称, 从游戏物品名称列表中进行模糊搜索
|
||||||
sol := fuzzy.FindNormalizedFold(ctx.State["args"].(string), itmeNames)
|
sol := fuzzy.FindNormalizedFold(ctx.State["args"].(string), itemNames)
|
||||||
var msg []string
|
|
||||||
// 物品名称
|
// 物品名称
|
||||||
var name string
|
var name string
|
||||||
|
|
||||||
// 根据搜搜结果,打印找到的物品
|
// 根据搜搜结果, 打印找到的物品
|
||||||
switch len(sol) {
|
switch len(sol) {
|
||||||
case 0: // 没有搜索到任何东西
|
case 0: // 没有搜索到任何东西
|
||||||
ctx.SendChain(message.Text("无法查询到该物品"))
|
ctx.SendChain(message.Text("无法查询到该物品"))
|
||||||
@ -276,227 +248,142 @@ func init() {
|
|||||||
case 1: // 如果只搜索到了一个
|
case 1: // 如果只搜索到了一个
|
||||||
name = sol[0]
|
name = sol[0]
|
||||||
default: // 如果搜搜到了多个
|
default: // 如果搜搜到了多个
|
||||||
// 遍历搜索结果,并打印为图片展出
|
// 遍历搜索结果, 并打印为图片展出
|
||||||
|
msgs := make(message.Message, len(sol)+1)
|
||||||
|
msgs[0] = ctxext.FakeSenderForwardNode(ctx, message.Text("包含多个结果, 请输入编号查看(15s内),输入c直接结束会话"))
|
||||||
for i, v := range sol {
|
for i, v := range sol {
|
||||||
msg = append(msg, fmt.Sprintf("[%d] %s", i, v))
|
msgs[i+1] = ctxext.FakeSenderForwardNode(ctx, message.Text("[", i, "] ", v))
|
||||||
}
|
}
|
||||||
msg = append(msg, "包含多个结果,请输入编号查看(15s内),输入c直接结束会话")
|
ctx.SendChain(msgs...)
|
||||||
ctx.SendChain(stringArrayToImage(msg))
|
itemIndex := getitemnameindex(ctx)
|
||||||
msg = []string{}
|
if itemIndex < 0 {
|
||||||
|
return
|
||||||
itemIndex := itemNameFutureEvent(ctx, 2)
|
}
|
||||||
if itemIndex == -1 {
|
if itemIndex >= len(sol) || itemIndex < 0 {
|
||||||
|
ctx.SendChain(message.Text("ERROR: 编号超出范围"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name = sol[itemIndex]
|
name = sol[itemIndex]
|
||||||
}
|
}
|
||||||
Mf := false
|
onlymaxrank := false
|
||||||
|
msgs := message.Message{}
|
||||||
GETWM:
|
GETWM:
|
||||||
if Mf {
|
if onlymaxrank {
|
||||||
msg = []string{}
|
msgs = msgs[:0]
|
||||||
}
|
}
|
||||||
sells, itmeinfo, txt, err := wmItemOrders(wmitems[name].URLName, Mf)
|
sells, iteminfo, txt, err := getitemsorder(wmitems[name].URLName, onlymaxrank)
|
||||||
if !Mf {
|
if !onlymaxrank {
|
||||||
if itmeinfo.ZhHans.WikiLink == "" {
|
if iteminfo.ZhHans.WikiLink == "" {
|
||||||
ctx.Send([]message.MessageSegment{
|
msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx,
|
||||||
message.Image("https://warframe.market/static/assets/" + wmitems[name].Thumb),
|
message.Image("https://warframe.market/static/assets/"+wmitems[name].Thumb),
|
||||||
message.Text(wmitems[name].ItemName, "\n"),
|
message.Text("\n", wmitems[name].ItemName)))
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
ctx.Send([]message.MessageSegment{
|
msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx,
|
||||||
message.Image("https://warframe.market/static/assets/" + wmitems[name].Thumb),
|
message.Image("https://warframe.market/static/assets/"+wmitems[name].Thumb),
|
||||||
message.Text(wmitems[name].ItemName, "\n"),
|
message.Text("\n", wmitems[name].ItemName, "\nwiki: ", iteminfo.ZhHans.WikiLink)))
|
||||||
message.Text("wiki:", itmeinfo.ZhHans.WikiLink),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msg = append(msg, wmitems[name].ItemName)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Send(message.Text("Error:", err.Error()))
|
msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx, message.Text("ERROR: ", err)))
|
||||||
|
ctx.SendChain(msgs...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if sells == nil {
|
if sells == nil {
|
||||||
ctx.Send(message.Text("无可购买对象"))
|
msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx, message.Text("无可购买对象")))
|
||||||
|
ctx.SendChain(msgs...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ismod := false
|
ismod := iteminfo.ModMaxRank != 0
|
||||||
if itmeinfo.ModMaxRank != 0 {
|
|
||||||
ismod = true
|
|
||||||
}
|
|
||||||
|
|
||||||
max := 5
|
max := 5
|
||||||
if len(sells) <= max {
|
if len(sells) < max {
|
||||||
max = len(sells)
|
max = len(sells)
|
||||||
}
|
}
|
||||||
for i := 0; i < max; i++ {
|
if ismod {
|
||||||
if ismod {
|
if !onlymaxrank {
|
||||||
msg = append(msg, fmt.Sprintf("[%d](Rank:%d/%d) %dP - %s\n", i, sells[i].ModRank, itmeinfo.ModMaxRank, sells[i].Platinum, sells[i].User.IngameName))
|
msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx, message.Text("请输入编号选择, 或输入r获取满级报价(30s内)\n输入c直接结束会话")))
|
||||||
} else {
|
} else {
|
||||||
msg = append(msg, fmt.Sprintf("[%d] %dP -%s\n", i, sells[i].Platinum, sells[i].User.IngameName))
|
msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx, message.Text("请输入编号选择(30s内)\n输入c直接结束会话")))
|
||||||
|
}
|
||||||
|
for i := 0; i < max; i++ {
|
||||||
|
msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx,
|
||||||
|
message.Text(fmt.Sprintf("[%d] (Rank:%d/%d) %dP - %s\n", i, sells[i].ModRank, iteminfo.ModMaxRank, sells[i].Platinum, sells[i].User.IngameName))))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ismod && !Mf {
|
|
||||||
msg = append(msg, "请输入编号选择,或输入r获取满级报价(30s内)\n输入c直接结束会话")
|
|
||||||
} else {
|
} else {
|
||||||
msg = append(msg, "请输入编号选择(30s内)\n输入c直接结束会话")
|
for i := 0; i < max; i++ {
|
||||||
}
|
msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx,
|
||||||
ctx.SendChain(stringArrayToImage(msg))
|
message.Text(fmt.Sprintf("[%d] %dP -%s\n", i, sells[i].Platinum, sells[i].User.IngameName))))
|
||||||
|
|
||||||
GETNUM3:
|
|
||||||
next := zero.NewFutureEvent("message", 999, false, ctx.CheckSession()).Next()
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Second * 30):
|
|
||||||
ctx.SendChain(message.Text("会话已结束!"))
|
|
||||||
return
|
|
||||||
case e := <-next:
|
|
||||||
msg := e.Event.Message.ExtractPlainText()
|
|
||||||
// 重新获取报价
|
|
||||||
if msg == "r" {
|
|
||||||
Mf = true
|
|
||||||
goto GETWM
|
|
||||||
}
|
}
|
||||||
// 主动结束会话
|
}
|
||||||
if msg == "c" {
|
ctx.SendChain(msgs...)
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
next := zero.NewFutureEvent("message", 999, false, ctx.CheckSession()).Next()
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Second * 30):
|
||||||
ctx.SendChain(message.Text("会话已结束!"))
|
ctx.SendChain(message.Text("会话已结束!"))
|
||||||
return
|
return
|
||||||
}
|
case e := <-next:
|
||||||
i, err := strconv.Atoi(msg)
|
msg := e.Event.Message.ExtractPlainText()
|
||||||
if err != nil {
|
// 重新获取报价
|
||||||
ctx.SendChain(message.Text("请输入数字!(输入c结束会话)"))
|
if msg == "r" {
|
||||||
goto GETNUM3
|
onlymaxrank = true
|
||||||
}
|
goto GETWM
|
||||||
if err == nil {
|
|
||||||
if ismod {
|
|
||||||
ctx.Send(message.Text("/w ", sells[i].User.IngameName, " Hi! I want to buy: ", txt, "(Rank:", sells[i].ModRank, ") for ", sells[i].Platinum, " platinum. (warframe.market)"))
|
|
||||||
} else {
|
|
||||||
ctx.Send(message.Text("/w ", sells[i].User.IngameName, " Hi! I want to buy: ", txt, " for ", sells[i].Platinum, " platinum. (warframe.market)"))
|
|
||||||
}
|
}
|
||||||
|
// 主动结束会话
|
||||||
|
if msg == "c" {
|
||||||
|
ctx.SendChain(message.Text("会话已结束!"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(msg)
|
||||||
|
if err != nil {
|
||||||
|
ctx.SendChain(message.Text("请输入数字! (输入c结束会话)"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ismod {
|
||||||
|
ctx.SendChain(message.Text("/w ", sells[i].User.IngameName, " Hi! I want to buy: ", txt, "(Rank:", sells[i].ModRank, ") for ", sells[i].Platinum, " platinum. (warframe.market)"))
|
||||||
|
} else {
|
||||||
|
ctx.SendChain(message.Text("/w ", sells[i].User.IngameName, " Hi! I want to buy: ", txt, " for ", sells[i].Platinum, " platinum. (warframe.market)"))
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取搜索结果中的物品具体名称index的FutureEvent,传入ctx和一个递归次数上限,返回一个int,如果为返回内容为-1,说明会话超时,或主动结束,或超出递归
|
// 获取搜索结果中的物品具体名称index的FutureEvent
|
||||||
func itemNameFutureEvent(ctx *zero.Ctx, count int) int {
|
//
|
||||||
next := zero.NewFutureEvent("message", 999, false, ctx.CheckSession()).Next()
|
// 传入ctx和一个递归次数上限,返回一个int
|
||||||
select {
|
// 如果为返回内容为负, 说明
|
||||||
case <-time.After(time.Second * 15):
|
// -1 会话超时
|
||||||
// 超时15秒处理
|
// -2 主动结束
|
||||||
ctx.SendChain(message.Text("会话已超时!"))
|
// -3 连续3次错误
|
||||||
return -1
|
func getitemnameindex(ctx *zero.Ctx) int {
|
||||||
case e := <-next:
|
recv, cancel := zero.NewFutureEvent("message", 999, false, ctx.CheckSession()).Repeat()
|
||||||
msg := e.Event.Message.ExtractPlainText()
|
defer cancel()
|
||||||
// 输入c主动结束的处理
|
for i := 0; i < 3; i++ {
|
||||||
if msg == "c" {
|
select {
|
||||||
ctx.SendChain(message.Text("会话已结束!"))
|
case <-time.After(time.Second * 15):
|
||||||
|
// 超时15秒处理
|
||||||
|
ctx.SendChain(message.Text("会话已超时!"))
|
||||||
return -1
|
return -1
|
||||||
}
|
case e := <-recv:
|
||||||
// 尝试对输入进行数字转换
|
msg := e.Event.Message.ExtractPlainText()
|
||||||
num, err := strconv.Atoi(msg)
|
// 输入c主动结束的处理
|
||||||
// 如果出错,说明输入的并非数字,则重新触发该内容
|
if msg == "c" {
|
||||||
if err != nil {
|
ctx.SendChain(message.Text("会话已结束!"))
|
||||||
// 查看是否超时
|
return -2
|
||||||
if count == 0 {
|
|
||||||
ctx.SendChain(message.Text("连续输入错误,会话已结束!"))
|
|
||||||
return -1
|
|
||||||
}
|
}
|
||||||
ctx.SendChain(message.Text("请输入数字!(输入c结束会话)[", count, "]"))
|
// 尝试对输入进行数字转换
|
||||||
count--
|
num, err := strconv.Atoi(msg)
|
||||||
return itemNameFutureEvent(ctx, count)
|
if err != nil {
|
||||||
}
|
ctx.SendChain(message.Text("请输入数字! (输入c结束会话)"))
|
||||||
return num
|
continue
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数组字符串转图片
|
|
||||||
func stringArrayToImage(texts []string) message.MessageSegment {
|
|
||||||
b, err := text.RenderToBase64(strings.Join(texts, "\n"), text.FontFile, 400, 20)
|
|
||||||
if err != nil {
|
|
||||||
return message.Text("ERROR: ", err)
|
|
||||||
}
|
|
||||||
return message.Image("base64://" + binary.BytesToString(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从WFapi获取数据
|
|
||||||
func wfapiGetData() (wfAPI, error) {
|
|
||||||
var wfapi wfAPI // WarFrameAPI的数据实例
|
|
||||||
var data []byte
|
|
||||||
var err error
|
|
||||||
data, err = web.GetData(wfapiurl)
|
|
||||||
if err != nil {
|
|
||||||
return wfapi, err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(data, &wfapi)
|
|
||||||
if err != nil {
|
|
||||||
return wfapi, err
|
|
||||||
}
|
|
||||||
return wfapi, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从WF市场获取物品数据信息
|
|
||||||
func updateWM() {
|
|
||||||
var itmeapi wfAPIItem // WarFrame市场的数据实例
|
|
||||||
|
|
||||||
data, err := web.RequestDataWithHeaders(&http.Client{}, wfitemurl, "GET", func(request *http.Request) error {
|
|
||||||
request.Header.Add("Accept", "application/json")
|
|
||||||
request.Header.Add("Language", "zh-hans")
|
|
||||||
return nil
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(data, &itmeapi)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
loadToFuzzy(itmeapi)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取Warframe市场的售价表,并进行排序,cn_name为物品中文名称,onlyMaxRank表示只取最高等级的物品,返回物品售价表,物品信息,物品英文
|
|
||||||
func wmItemOrders(cnName string, onlyMaxRank bool) (orders, itemsInSet, string, error) {
|
|
||||||
var wfapiio wfAPIItemsOrders
|
|
||||||
data, err := web.RequestDataWithHeaders(&http.Client{}, fmt.Sprintf("https://api.warframe.market/v1/items/%s/orders?include=item", cnName), "GET", func(request *http.Request) error {
|
|
||||||
request.Header.Add("Accept", "application/json")
|
|
||||||
request.Header.Add("Platform", "pc")
|
|
||||||
return nil
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, itemsInSet{}, "", err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(data, &wfapiio)
|
|
||||||
var sellOrders orders
|
|
||||||
// 遍历市场物品列表
|
|
||||||
for _, v := range wfapiio.Payload.Orders {
|
|
||||||
// 取其中类型为售卖,且去掉不在线的玩家
|
|
||||||
if v.OrderType == "sell" && v.User.Status != "offline" {
|
|
||||||
// 如果需要满级报价
|
|
||||||
if onlyMaxRank && v.ModRank == wfapiio.Include.Item.ItemsInSet[0].ModMaxRank {
|
|
||||||
sellOrders = append(sellOrders, v)
|
|
||||||
} else if !onlyMaxRank {
|
|
||||||
sellOrders = append(sellOrders, v)
|
|
||||||
}
|
}
|
||||||
|
return num
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 对报价表进行排序,由低到高
|
ctx.SendChain(message.Text("连续输入错误, 会话已结束!"))
|
||||||
sort.Sort(sellOrders)
|
return -3
|
||||||
// 获取物品信息
|
|
||||||
for i, v := range wfapiio.Include.Item.ItemsInSet {
|
|
||||||
if v.URLName == cnName {
|
|
||||||
return sellOrders, wfapiio.Include.Item.ItemsInSet[i], wfapiio.Include.Item.ItemsInSet[i].En.ItemName, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sellOrders, wfapiio.Include.Item.ItemsInSet[0], wfapiio.Include.Item.ItemsInSet[0].En.ItemName, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadToFuzzy(wminfo wfAPIItem) {
|
|
||||||
wmitems = make(map[string]items)
|
|
||||||
itmeNames = []string{}
|
|
||||||
for _, v := range wminfo.Payload.Items {
|
|
||||||
wmitems[v.ItemName] = v
|
|
||||||
itmeNames = append(itmeNames, v.ItemName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package warframeapi
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type wfAPI struct {
|
type wfapi struct {
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
News []news `json:"news"`
|
News []news `json:"news"`
|
||||||
Events []events `json:"events"`
|
Events []events `json:"events"`
|
||||||
99
plugin/warframeapi/world.go
Normal file
99
plugin/warframeapi/world.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package warframeapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/davidscholberg/go-durationfmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 游戏时间模拟
|
||||||
|
type timezone struct {
|
||||||
|
sync.RWMutex `json:"-"`
|
||||||
|
Name string `json:"name"` // 时间名称
|
||||||
|
NextTime time.Time `json:"time"` // 下次更新时间
|
||||||
|
IsDay bool `json:"status"` // 状态
|
||||||
|
hasSync bool `json:"-"` // 是否已同步
|
||||||
|
DayDesc string `json:"true_des"` // 状态说明
|
||||||
|
NightDesc string `json:"false_des"` // 状态说明
|
||||||
|
DayLen int `json:"day"` // 白天时长
|
||||||
|
NightLen int `json:"night"` // 夜间时长
|
||||||
|
}
|
||||||
|
|
||||||
|
type world struct {
|
||||||
|
w [3]*timezone
|
||||||
|
hassync uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
gameWorld world
|
||||||
|
)
|
||||||
|
|
||||||
|
// String 根据传入的世界编号,获取对应的游戏时间文本
|
||||||
|
func (t *timezone) String() string {
|
||||||
|
t.RLock()
|
||||||
|
defer t.RUnlock()
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.WriteString("平原时间: ")
|
||||||
|
if t.IsDay {
|
||||||
|
sb.WriteString(t.DayDesc)
|
||||||
|
} else {
|
||||||
|
sb.WriteString(t.NightDesc)
|
||||||
|
}
|
||||||
|
sb.WriteString(", ")
|
||||||
|
sb.WriteString("下次更新: ")
|
||||||
|
d := time.Until(t.NextTime)
|
||||||
|
durStr, _ := durationfmt.Format(d, "%m分%s秒后")
|
||||||
|
sb.WriteString(durStr)
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *world) hasSync() bool {
|
||||||
|
return atomic.LoadUintptr(&w.hassync) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *world) setsync() bool {
|
||||||
|
return atomic.CompareAndSwapUintptr(&w.hassync, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *world) resetsync() bool {
|
||||||
|
return atomic.CompareAndSwapUintptr(&w.hassync, 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据API返回内容修正游戏时间
|
||||||
|
func (w *world) refresh(api *wfapi) {
|
||||||
|
for _, t := range w.w {
|
||||||
|
t.Lock()
|
||||||
|
}
|
||||||
|
w.w = [3]*timezone{
|
||||||
|
{Name: "地球平原", NextTime: api.CetusCycle.Expiry.Local(), IsDay: api.CetusCycle.IsDay, DayDesc: "白天", NightDesc: "夜晚", DayLen: 100 * 60, NightLen: 50 * 60},
|
||||||
|
{Name: "金星平原", NextTime: api.VallisCycle.Expiry.Local(), IsDay: api.VallisCycle.IsWarm, DayDesc: "温暖", NightDesc: "寒冷", DayLen: 400, NightLen: 20 * 60},
|
||||||
|
{Name: "火卫二平原", NextTime: api.CambionCycle.Expiry.Local(), IsDay: api.CambionCycle.Active == "fass", DayDesc: "fass", NightDesc: "vome", DayLen: 100 * 60, NightLen: 50 * 60},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 游戏时间更新
|
||||||
|
func (w *world) update() {
|
||||||
|
if !w.hasSync() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, t := range w.w {
|
||||||
|
t.Lock()
|
||||||
|
// 当前时间对比下一次游戏状态更新时间,看看还剩多少秒
|
||||||
|
nt := time.Until(t.NextTime).Seconds()
|
||||||
|
// 已经过了游戏时间状态更新时间
|
||||||
|
if nt < 0 {
|
||||||
|
// 更新游戏状态,如果是白天就切换到晚上,反之亦然
|
||||||
|
if t.IsDay {
|
||||||
|
// 计算下次的晚上更新时间
|
||||||
|
t.NextTime = t.NextTime.Add(time.Duration(t.NightLen) * time.Second)
|
||||||
|
} else {
|
||||||
|
// 计算下次的白天更新时间
|
||||||
|
t.NextTime = t.NextTime.Add(time.Duration(t.DayLen) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user