diff --git a/setutime/pic_searcher.go b/setutime/pic_searcher.go new file mode 100644 index 00000000..9aa78bf5 --- /dev/null +++ b/setutime/pic_searcher.go @@ -0,0 +1,152 @@ +package setutime + +import ( + utils "bot/setutime/utils" + + zero "github.com/wdvxdr1123/ZeroBot" +) + +func init() { + zero.RegisterPlugin(picSearch{}) // 注册插件 +} + +type picSearch struct{} // pixivSearch 搜索P站插图 + +func (_ picSearch) GetPluginInfo() zero.PluginInfo { // 返回插件信息 + return zero.PluginInfo{ + Author: "kanri", + PluginName: "PicSearch", + Version: "0.0.1", + Details: "以图搜图", + } +} + +func (_ picSearch) Start() { // 插件主体 + // TODO 根据PID搜图 + zero.OnRegex(`搜图(\d+)`).SetBlock(true).SetPriority(30). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + id := utils.Str2Int(state["regex_matched"].([]string)[1]) + zero.Send(event, "少女祈祷中......") + // TODO 获取P站插图信息 + illust := &utils.Illust{} + if err := illust.IllustInfo(id); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + // TODO 下载P站插图 + if _, err := illust.PixivPicDown(CACHEPATH); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + // TODO 发送搜索结果 + zero.Send(event, illust.DetailPic) + return zero.FinishResponse + }) + // TODO 通过回复以图搜图 + zero.OnRegex(`\[CQ:reply,id=(.*?)\](.*)搜索图片`).SetBlock(true).SetPriority(32). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var pics []string // 图片搜索池子 + // TODO 获取回复的上文图片链接 + id := utils.Str2Int(state["regex_matched"].([]string)[1]) + for _, elem := range zero.GetMessage(id).Elements { + if elem.Type == "image" { + pics = append(pics, elem.Data["url"]) + } + } + // TODO 没有收到图片则向用户索取 + if len(pics) == 0 { + zero.Send(event, "请发送多张图片!") + next := matcher.FutureEvent("message", zero.CheckUser(event.UserID)) + recv, cancel := next.Repeat() + for e := range recv { // 循环获取channel发来的信息 + if len(e.Message) == 1 && e.Message[0].Type == "text" { + cancel() // 如果是纯文本则退出索取 + break + } + for _, elem := range e.Message { + if elem.Type == "image" { // 将信息中的图片添加到搜索池子 + pics = append(pics, elem.Data["url"]) + } + } + if len(pics) >= 5 { + cancel() // 如果是图片数量大于等于5则退出索取 + break + } + } + } + if len(pics) == 0 { + zero.Send(event, "没有收到图片,搜图结束......") + return zero.FinishResponse + } + // TODO 开始搜索图片 + zero.Send(event, "少女祈祷中......") + for _, pic := range pics { + if text, err := utils.SauceNaoSearch(pic); err == nil { + zero.Send(event, text) // 返回SauceNAO的结果 + continue + } else { + utils.SendError(event, err) + } + if text, err := utils.Ascii2dSearch(pic); err == nil { + zero.Send(event, text) // 返回Ascii2d的结果 + continue + } else { + utils.SendError(event, err) + } + } + return zero.FinishResponse + }) + // TODO 通过命令以图搜图 + zero.OnKeywordGroup([]string{"以图识图", "以图搜图", "搜索图片"}).SetBlock(true).SetPriority(33). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var pics []string // 图片搜索池子 + // TODO 获取信息中图片链接 + for _, elem := range event.Message { + if elem.Type == "image" { + pics = append(pics, elem.Data["url"]) + } + } + // TODO 没有收到图片则向用户索取 + if len(pics) == 0 { + zero.Send(event, "请发送多张图片!") + next := matcher.FutureEvent("message", zero.CheckUser(event.UserID)) + recv, cancel := next.Repeat() + for e := range recv { // 循环获取channel发来的信息 + if len(e.Message) == 1 && e.Message[0].Type == "text" { + cancel() // 如果是纯文本则退出索取 + break + } + for _, elem := range e.Message { + if elem.Type == "image" { // 将信息中的图片添加到搜索池子 + pics = append(pics, elem.Data["url"]) + } + } + if len(pics) >= 5 { + cancel() // 如果是图片数量大于等于5则退出索取 + break + } + } + } + if len(pics) == 0 { + zero.Send(event, "没有收到图片,搜图结束......") + return zero.FinishResponse + } + // TODO 开始搜索图片 + zero.Send(event, "少女祈祷中......") + for _, pic := range pics { + if text, err := utils.SauceNaoSearch(pic); err == nil { + zero.Send(event, text) // 返回SauceNAO的结果 + continue + } else { + utils.SendError(event, err) + } + if text, err := utils.Ascii2dSearch(pic); err == nil { + zero.Send(event, text) // 返回Ascii2d的结果 + continue + } else { + utils.SendError(event, err) + } + } + return zero.FinishResponse + }) +} diff --git a/setutime/setu_geter.go b/setutime/setu_geter.go new file mode 100644 index 00000000..77e65bf5 --- /dev/null +++ b/setutime/setu_geter.go @@ -0,0 +1,339 @@ +package setutime + +import ( + "errors" + "fmt" + + utils "bot/setutime/utils" + + zero "github.com/wdvxdr1123/ZeroBot" +) + +type setuGet struct{} // setuGet 来份色图 + +func (_ setuGet) GetPluginInfo() zero.PluginInfo { // 返回插件信息 + return zero.PluginInfo{ + Author: "kanri", + PluginName: "SetuGet", + Version: "0.0.1", + Details: "来份色图", + } +} + +var ( + BOTPATH = utils.PathExecute() // 当前bot运行目录 + DATAPATH = BOTPATH + "data/SetuTime/" // 数据目录 + DBPATH = DATAPATH + "SetuTime.db" // 数据库路径 + CACHEPATH = DATAPATH + "cache/" // 缓冲图片路径 + + DB = utils.Sqlite{DBPath: DBPATH} // 涩图数据库 + + pool = utils.PicsCache{Max: 10} // 图片缓冲池子 +) + +func init() { + zero.RegisterPlugin(setuGet{}) // 注册插件 + + utils.CACHE_GROUP = 868047498 // 图片缓冲群 + utils.CACHEPATH = CACHEPATH // 缓冲图片路径 + + utils.CreatePath(DBPATH) + utils.CreatePath(CACHEPATH) + + ecy := &ecy{} + setu := &setu{} + scenery := &scenery{} + DB.DBCreate(ecy) + DB.DBCreate(setu) + DB.DBCreate(scenery) +} + +// ecy 二次元 +type ecy struct { + utils.Illust +} + +// setu 涩图 +type setu struct { + utils.Illust +} + +// scenery 风景 +type scenery struct { + utils.Illust +} + +func (_ setuGet) Start() { // 插件主体 + zero.OnFullMatchGroup([]string{"来份涩图", "setu", "来份色图"}).SetBlock(true).SetPriority(20). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var ( + type_ = "setu" + illust = &setu{} + ) + // TODO 池子无图片则立刻下载 + length := illust.Len(type_, &pool) + if length == 0 { + zero.Send(event, "[SetuTime] 正在填充弹药......") + if err := DB.DBSelect(illust, "ORDER BY RANDOM() limit 1"); err != nil { + utils.SendError(event, err) // 查询出一张图片 + return zero.FinishResponse + } + if err := illust.Add(type_, &pool); err != nil { + utils.SendError(event, err) // 向缓冲池添加一张图片 + return zero.FinishResponse + } + } + // TODO 补充池子 + go func() { + times := utils.Min(pool.Max-length, 2) + for i := 0; i < times; i++ { + if err := DB.DBSelect(illust, "ORDER BY RANDOM() limit 1"); err != nil { + utils.SendError(event, err) // 查询出一张图片 + } + if err := illust.Add(type_, &pool); err != nil { + utils.SendError(event, err) // 向缓冲池添加一张图片 + } + } + }() + // TODO 从缓冲池里抽一张 + hash := illust.Get(type_, &pool) + if utils.XML { + if id := zero.Send(event, illust.BigPic(hash)); id == 0 { + utils.SendError(event, errors.New("可能被风控了")) + } + } else { + if id := zero.Send(event, illust.NormalPic()); id == 0 { + utils.SendError(event, errors.New("可能被风控了")) + } + } + return zero.FinishResponse + }) + zero.OnFullMatchGroup([]string{"二次元", "ecy", "来份二次元"}).SetBlock(true).SetPriority(20). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var ( + type_ = "ecy" + illust = &ecy{} + ) + // TODO 池子无图片则立刻下载 + length := illust.Len(type_, &pool) + if length == 0 { + zero.Send(event, "[SetuTime] 正在填充弹药......") + if err := DB.DBSelect(illust, "ORDER BY RANDOM() limit 1"); err != nil { + utils.SendError(event, err) // 查询出一张图片 + return zero.FinishResponse + } + if err := illust.Add(type_, &pool); err != nil { + utils.SendError(event, err) // 向缓冲池添加一张图片 + return zero.FinishResponse + } + } + // TODO 补充池子 + go func() { + times := utils.Min(pool.Max-length, 2) + for i := 0; i < times; i++ { + if err := DB.DBSelect(illust, "ORDER BY RANDOM() limit 1"); err != nil { + utils.SendError(event, err) // 查询出一张图片 + } + if err := illust.Add(type_, &pool); err != nil { + utils.SendError(event, err) // 向缓冲池添加一张图片 + } + } + }() + // TODO 从缓冲池里抽一张 + hash := illust.Get(type_, &pool) + if utils.XML { + if id := zero.Send(event, illust.BigPic(hash)); id == 0 { + utils.SendError(event, errors.New("可能被风控了")) + } + } else { + if id := zero.Send(event, illust.NormalPic()); id == 0 { + utils.SendError(event, errors.New("可能被风控了")) + } + } + return zero.FinishResponse + }) + zero.OnFullMatchGroup([]string{"风景", "来份风景"}).SetBlock(true).SetPriority(20). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var ( + type_ = "scenery" + illust = &scenery{} + ) + // TODO 池子无图片则立刻下载 + length := illust.Len(type_, &pool) + if length == 0 { + zero.Send(event, "[SetuTime] 正在填充弹药......") + if err := DB.DBSelect(illust, "ORDER BY RANDOM() limit 1"); err != nil { + utils.SendError(event, err) // 查询出一张图片 + return zero.FinishResponse + } + if err := illust.Add(type_, &pool); err != nil { + utils.SendError(event, err) // 向缓冲池添加一张图片 + return zero.FinishResponse + } + } + // TODO 补充池子 + go func() { + times := utils.Min(pool.Max-length, 2) + for i := 0; i < times; i++ { + if err := DB.DBSelect(illust, "ORDER BY RANDOM() limit 1"); err != nil { + utils.SendError(event, err) // 查询出一张图片 + } + if err := illust.Add(type_, &pool); err != nil { + utils.SendError(event, err) // 向缓冲池添加一张图片 + } + } + }() + // TODO 从缓冲池里抽一张 + hash := illust.Get(type_, &pool) + if utils.XML { + if id := zero.Send(event, illust.BigPic(hash)); id == 0 { + utils.SendError(event, errors.New("可能被风控了")) + } + } else { + if id := zero.Send(event, illust.NormalPic()); id == 0 { + utils.SendError(event, errors.New("可能被风控了")) + } + } + return zero.FinishResponse + }) + zero.OnRegex(`添加涩图(\d+)`, zero.SuperUserPermission).SetBlock(true).SetPriority(21). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var illust = &setu{} + zero.Send(event, "少女祈祷中......") + // TODO 查询P站插图信息 + id := utils.Str2Int(state["regex_matched"].([]string)[1]) + if err := illust.IllustInfo(id); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + // TODO 下载插画 + if _, err := illust.PixivPicDown(CACHEPATH); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + if id := zero.Send(event, illust.DetailPic()); id == 0 { + utils.SendError(event, errors.New("可能被风控了")) + return zero.FinishResponse + } + // TODO 添加插画到对应的数据库table + if err := DB.DBInsert(illust); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + zero.Send(event, "添加成功") + return zero.FinishResponse + }) + zero.OnRegex(`添加二次元(\d+)`, zero.SuperUserPermission).SetBlock(true).SetPriority(21). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var illust = &ecy{} + zero.Send(event, "少女祈祷中......") + // TODO 查询P站插图信息 + id := utils.Str2Int(state["regex_matched"].([]string)[1]) + if err := illust.IllustInfo(id); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + // TODO 下载插画 + if _, err := illust.PixivPicDown(CACHEPATH); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + if id := zero.Send(event, illust.DetailPic()); id == 0 { + utils.SendError(event, errors.New("可能被风控了")) + return zero.FinishResponse + } + // TODO 添加插画到对应的数据库table + if err := DB.DBInsert(illust); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + zero.Send(event, "添加成功") + return zero.FinishResponse + }) + zero.OnRegex(`添加风景(\d+)`, zero.SuperUserPermission).SetBlock(true).SetPriority(21). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var illust = &scenery{} + zero.Send(event, "少女祈祷中......") + // TODO 查询P站插图信息 + id := utils.Str2Int(state["regex_matched"].([]string)[1]) + if err := illust.IllustInfo(id); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + // TODO 下载插画 + if _, err := illust.PixivPicDown(CACHEPATH); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + if id := zero.Send(event, illust.DetailPic()); id == 0 { + utils.SendError(event, errors.New("可能被风控了")) + return zero.FinishResponse + } + // TODO 添加插画到对应的数据库table + if err := DB.DBInsert(illust); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + zero.Send(event, "添加成功") + return zero.FinishResponse + }) + zero.OnRegex(`删除涩图(\d+)`, zero.SuperUserPermission).SetBlock(true).SetPriority(22). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var illust = &setu{} + // TODO 查询数据库 + id := utils.Str2Int(state["regex_matched"].([]string)[1]) + if err := DB.DBDelete(illust, fmt.Sprintf("WHERE pid=%d", id)); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + zero.Send(event, "删除成功") + return zero.FinishResponse + }) + zero.OnRegex(`删除二次元(\d+)`, zero.SuperUserPermission).SetBlock(true).SetPriority(22). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var illust = &ecy{} + // TODO 查询数据库 + id := utils.Str2Int(state["regex_matched"].([]string)[1]) + if err := DB.DBDelete(illust, fmt.Sprintf("WHERE pid=%d", id)); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + zero.Send(event, "删除成功") + return zero.FinishResponse + }) + zero.OnRegex(`删除风景(\d+)`, zero.SuperUserPermission).SetBlock(true).SetPriority(22). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + var illust = &scenery{} + // TODO 查询数据库 + id := utils.Str2Int(state["regex_matched"].([]string)[1]) + if err := DB.DBDelete(illust, fmt.Sprintf("WHERE pid=%d", id)); err != nil { + utils.SendError(event, err) + return zero.FinishResponse + } + zero.Send(event, "删除成功") + return zero.FinishResponse + }) + // TODO 查询数据库涩图数量 + zero.OnFullMatchGroup([]string{"setu -s", "setu --status"}, zero.SuperUserPermission).SetBlock(true).SetPriority(23). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + setu, _ := DB.DBNum(&setu{}) + ecy, _ := DB.DBNum(&ecy{}) + scenery, _ := DB.DBNum(&scenery{}) + zero.Send(event, fmt.Sprintf("[SetuTime] \n风景:%d \n二次元:%d \n涩图:%d", scenery, ecy, setu)) + return zero.FinishResponse + }) + // TODO 开xml模式 + zero.OnFullMatchGroup([]string{"setu -x", "setu --xml"}, zero.AdminPermission).SetBlock(true).SetPriority(24). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + utils.XML = true + zero.Send(event, "[SetuTime] XML->ON") + return zero.FinishResponse + }) + // TODO 关xml模式 + zero.OnFullMatchGroup([]string{"setu -p", "setu --pic"}, zero.AdminPermission).SetBlock(true).SetPriority(24). + Handle(func(matcher *zero.Matcher, event zero.Event, state zero.State) zero.Response { + utils.XML = false + zero.Send(event, "[SetuTime] XML->OFF") + return zero.FinishResponse + }) +} diff --git a/setutime/utils/ascii2d.go b/setutime/utils/ascii2d.go new file mode 100644 index 00000000..6104a12d --- /dev/null +++ b/setutime/utils/ascii2d.go @@ -0,0 +1,100 @@ +package utils + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + xpath "github.com/antchfx/htmlquery" +) + +// Ascii2dSearch Ascii2d 以图搜图 +// 第一个参数 返回错误 +// 第二个参数 返回的信息 +func Ascii2dSearch(pic string) (text string, err error) { + var ( + api = "https://ascii2d.net/search/uri" + ) + transport := http.Transport{ + DisableKeepAlives: true, + } + client := &http.Client{ + Transport: &transport, + } + + // TODO 包装请求参数 + data := url.Values{} + data.Set("uri", pic) // 图片链接 + fromData := strings.NewReader(data.Encode()) + + // TODO 网络请求 + req, _ := http.NewRequest("POST", api, fromData) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0") + resp, err := client.Do(req) + if err != nil { + return "", err + } + // TODO 色合检索改变到特征检索 + var bovwUrl = strings.ReplaceAll(resp.Request.URL.String(), "color", "bovw") + bovwReq, _ := http.NewRequest("POST", bovwUrl, nil) + bovwReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + bovwReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36") + bovwResp, err := client.Do(bovwReq) + if err != nil { + return "", err + } + defer bovwResp.Body.Close() + // TODO 解析XPATH + doc, err := xpath.Parse(resp.Body) + if err != nil { + return "", err + } + // TODO 取出每个返回的结果 + list := xpath.Find(doc, `//div[@class="row item-box"]`) + var link string + // TODO 遍历取出第一个返回的PIXIV结果 + for _, n := range list { + linkPath := xpath.Find(n, `//div[2]/div[3]/h6/a[1]`) + picPath := xpath.Find(n, `//div[1]/img`) + if len(linkPath) != 0 && len(picPath) != 0 { + link = xpath.SelectAttr(linkPath[0], "href") + if strings.Contains(link, "www.pixiv.net") { + break + } + } + } + // TODO 链接取出PIXIV id + var index = strings.LastIndex(link, "/") + if link == "" || index == -1 { + return "", errors.New("Ascii2d not found") + } + var id = Str2Int(link[index+1:]) + if id == 0 { + return "", errors.New("convert to pid error") + } + // TODO 根据PID查询插图信息 + var illust = &Illust{} + if err := illust.IllustInfo(id); err != nil { + return "", err + } + if illust.AgeLimit != "all-age" { + return "", errors.New("Ascii2d not found") + } + // TODO 返回插图信息文本 + return fmt.Sprintf( + `[SetuTime] emmm大概是这个? +标题:%s +插画ID:%d +画师:%s +画师ID:%d +直链:https://pixivel.moe/detail?id=%d`, + illust.Title, + illust.Pid, + illust.UserName, + illust.UserId, + illust.Pid, + ), nil +} diff --git a/setutime/utils/download.go b/setutime/utils/download.go new file mode 100644 index 00000000..e133b74f --- /dev/null +++ b/setutime/utils/download.go @@ -0,0 +1,60 @@ +package utils + +import ( + "crypto/md5" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" + + zero "github.com/wdvxdr1123/ZeroBot" +) + +// urlCache 缓存并返回缓存路径 +func (this *Illust) PixivPicDown(path string) (savePath string, err error) { + url := this.ImageUrls + pid := this.Pid + url = strings.ReplaceAll(url, "i.pximg.net", "i.pixiv.cat") + url = strings.ReplaceAll(url, "img-original", "img-master") + url = strings.ReplaceAll(url, "_p0", "_p0_master1200") + url = strings.ReplaceAll(url, ".png", ".jpg") + // TODO 文件名为url的hash值 + savePath = path + Int2Str(pid) + ".jpg" + // TODO 文件存在或文件大小大于10kb + if PathExists(savePath) && FileSize(savePath) > 10240 { + return savePath, nil + } + zero.SendGroupMessage(CACHE_GROUP, "正在下载"+url) + // TODO 模拟QQ客户端请求 + client := &http.Client{} + reqest, _ := http.NewRequest("GET", url, nil) + reqest.Header.Add("User-Agent", "QQ/8.2.0.1296 CFNetwork/1126") + reqest.Header.Add("Net-Type", "Wifi") + + resp, err := client.Do(reqest) + if err != nil { + return "", err + } + fmt.Println(resp.StatusCode) + if code := resp.StatusCode; code != 200 { + return "", errors.New(fmt.Sprintf("Download failed, code %d", code)) + } + defer resp.Body.Close() + // TODO 写入文件 + data, _ := ioutil.ReadAll(resp.Body) + f, _ := os.OpenFile(savePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + defer f.Close() + f.Write(data) + + return savePath, err +} + +func PicHash(path string) string { + data, err := ioutil.ReadFile(path) + if err != nil { + return "" + } + return strings.ToUpper(fmt.Sprintf("%x", md5.Sum(data))) +} diff --git a/setutime/utils/pic_pool.go b/setutime/utils/pic_pool.go new file mode 100644 index 00000000..975c7c88 --- /dev/null +++ b/setutime/utils/pic_pool.go @@ -0,0 +1,163 @@ +package utils + +import ( + "errors" + "fmt" + "strings" + "sync" + + zero "github.com/wdvxdr1123/ZeroBot" +) + +var ( + CACHEPATH string // 图片缓存路径 + CACHE_GROUP int64 // 图片缓存群,用于上传图片到tx服务器 +) + +// PicsCache 图片缓冲池 +type PicsCache struct { + Lock sync.Mutex + Max int + ECY []string + IECY []Illust + SETU []string + ISETU []Illust + SCENERY []string + ISCENERY []Illust +} + +// Len 返回当前缓冲池的图片数量 +func (this *Illust) Len(type_ string, pool *PicsCache) (length int) { + switch type_ { + case "ecy": + return len(pool.ECY) + case "setu": + return len(pool.SETU) + case "scenery": + return len(pool.SCENERY) + } + return 0 +} + +// Add 添加图片到缓冲池,返回错误 +func (this Illust) Add(type_ string, pool *PicsCache) (err error) { + // TODO 下载图片 + path, err := this.PixivPicDown(CACHEPATH) + if err != nil { + return err + } + hash := PicHash(path) + // TODO 发送到缓存群以上传tx服务器 + if id := zero.SendGroupMessage(CACHE_GROUP, "[CQ:image,file=file:///"+path+"]"); id == 0 { + return errors.New("send failed") + } + // TODO 把hash和插图信息添加到缓冲池 + pool.Lock.Lock() + defer pool.Lock.Unlock() + switch type_ { + case "ecy": + pool.ECY = append(pool.ECY, hash) + pool.IECY = append(pool.IECY, this) + case "setu": + pool.SETU = append(pool.SETU, hash) + pool.ISETU = append(pool.ISETU, this) + case "scenery": + pool.SCENERY = append(pool.SCENERY, hash) + pool.ISCENERY = append(pool.ISCENERY, this) + } + return nil +} + +// Get 从缓冲池里取出一张,返回hash,illust值中Pid和UserName会被改变 +func (this *Illust) Get(type_ string, pool *PicsCache) (hash string) { + pool.Lock.Lock() + defer pool.Lock.Unlock() + switch type_ { + case "ecy": + if len(pool.ECY) > 0 { + hash := pool.ECY[0] + this.Pid = pool.IECY[0].Pid + this.Title = pool.IECY[0].Title + this.UserName = pool.IECY[0].UserName + pool.ECY = pool.ECY[1:] + pool.IECY = pool.IECY[1:] + return hash + } + case "setu": + if len(pool.SETU) > 0 { + hash := pool.SETU[0] + this.Pid = pool.ISETU[0].Pid + this.Title = pool.ISETU[0].Title + this.UserName = pool.ISETU[0].UserName + pool.SETU = pool.SETU[1:] + pool.ISETU = pool.ISETU[1:] + return hash + } + case "scenery": + if len(pool.SCENERY) > 0 { + hash := pool.SCENERY[0] + this.Pid = pool.ISCENERY[0].Pid + this.Title = pool.ISCENERY[0].Title + this.UserName = pool.ISCENERY[0].UserName + pool.SCENERY = pool.SCENERY[1:] + pool.ISCENERY = pool.ISCENERY[1:] + return hash + } + default: + // + } + return "" +} + +func GetCQcodePicLink(text string) (url string) { + text = strings.ReplaceAll(text, "{", "") + text = strings.ReplaceAll(text, "{", "") + text = strings.ReplaceAll(text, "-", "") + if index := strings.Index(text, "."); index != -1 { + if hash := text[:index]; len(hash) == 32 { + return fmt.Sprintf("http://gchat.qpic.cn/gchatpic_new//--%s/0", hash) + } + } + return "" +} + +// BigPic 返回一张XML大图CQ码 +func (this *Illust) BigPic(hash string) string { + return fmt.Sprintf(`[CQ:xml,data= + +]`, + hash, + hash, + hash, + this.Title, + this.Pid, + this.UserName, + ) +} + +// NormalPic 返回一张普通图CQ码 +func (this *Illust) NormalPic() string { + return fmt.Sprintf(`[CQ:image,file=file:///%s%d.jpg]`, CACHEPATH, this.Pid) +} + +// DetailPic 返回一张带详细信息的图片CQ码 +func (this *Illust) DetailPic() string { + return fmt.Sprintf(`[SetuTime] %s 标题:%s +插画ID:%d +画师:%s +画师ID:%d +直链:https://pixivel.moe/detail?id=%d`, + this.NormalPic(), + this.Title, + this.Pid, + this.UserName, + this.UserId, + this.Pid, + ) +} diff --git a/setutime/utils/pixiv_api.go b/setutime/utils/pixiv_api.go new file mode 100644 index 00000000..432092ec --- /dev/null +++ b/setutime/utils/pixiv_api.go @@ -0,0 +1,94 @@ +package utils + +import ( + "crypto/tls" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "strings" + + "github.com/tidwall/gjson" +) + +// Illust 插画信息 +type Illust struct { + Pid int64 `db:"pid"` + Title string `db:"title"` + Caption string `db:"caption"` + Tags string `db:"tags"` + ImageUrls string `db:"image_urls"` + AgeLimit string `db:"age_limit"` + CreatedTime string `db:"created_time"` + UserId int64 `db:"user_id"` + UserName string `db:"user_name"` +} + +// IllustInfo 根据p站插画id返回插画信息Illust +func (this *Illust) IllustInfo(id int64) (err error) { + api := fmt.Sprintf("https://pixiv.net/ajax/illust/%d", id) + transport := http.Transport{ + DisableKeepAlives: true, + // TODO 绕过sni审查 + TLSClientConfig: &tls.Config{ + ServerName: "-", + InsecureSkipVerify: true, + }, + // TODO 更改dns + Dial: func(network, addr string) (net.Conn, error) { + return net.Dial("tcp", "210.140.131.223:443") + }, + } + client := &http.Client{ + Transport: &transport, + } + + // TODO 网络请求 + req, err := http.NewRequest("GET", api, nil) + if err != nil { + return err + } + req.Header.Set("Host", "pixiv.net") + req.Header.Set("Referer", "pixiv.net") + req.Header.Set("Accept", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0") + + resp, err := client.Do(req) + if err != nil { + return err + } + if code := resp.StatusCode; code != 200 { + return errors.New(fmt.Sprintf("Search illust's info failed, status %d", code)) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + json := gjson.ParseBytes(body).Get("body") + + // TODO 如果有"R-18"tag则判断为R-18(暂时) + var ageLimit = "all-age" + for _, tag := range json.Get("tags.tags.#.tag").Array() { + if tag.Str == "R-18" { + ageLimit = "r18" + break + } + } + // TODO 解决json返回带html格式 + var caption = strings.ReplaceAll(json.Get("illustComment").Str, "
", "\n") + if index := strings.Index(caption, "<"); index != -1 { + caption = caption[:index] + } + // TODO 解析返回插画信息 + this.Pid = json.Get("illustId").Int() + this.Title = json.Get("illustTitle").Str + this.Caption = caption + this.Tags = fmt.Sprintln(json.Get("tags.tags.#.tag").Array()) + this.ImageUrls = json.Get("urls.original").Str + this.AgeLimit = ageLimit + this.CreatedTime = json.Get("createDate").Str + this.UserId = json.Get("userId").Int() + this.UserName = json.Get("userName").Str + return nil +} diff --git a/setutime/utils/saucenao.go b/setutime/utils/saucenao.go new file mode 100644 index 00000000..481d8c78 --- /dev/null +++ b/setutime/utils/saucenao.go @@ -0,0 +1,84 @@ +package utils + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/tidwall/gjson" +) + +// SauceNaoSearch SauceNao 以图搜图 需要链接 返回错误和信息 +func SauceNaoSearch(pic string) (text string, err error) { + var ( + api = "https://saucenao.com/search.php" + apiKey = "2cc2772ca550dbacb4c35731a79d341d1a143cb5" + + minSimilarity = 70.0 // 返回图片结果的最小相似度 + ) + + transport := http.Transport{ + DisableKeepAlives: true, + } + client := &http.Client{ + Transport: &transport, + } + + // TODO 包装请求参数 + data := url.Values{} + data.Set("url", pic) // 图片链接 + data.Set("api_key", apiKey) // api_key + data.Set("db", "5") // 只搜索Pixiv + data.Set("numres", "1") // 返回一个结果 + data.Set("output_type", "2") // 返回JSON格式数据 + fromData := strings.NewReader(data.Encode()) + + // TODO 网络请求 + req, err := http.NewRequest("POST", api, fromData) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0") + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if code := resp.StatusCode; code != 200 { + // 如果返回不是200则立刻抛出错误 + return "", errors.New(fmt.Sprintf("SauceNAO not found, code %d", code)) + } + content := gjson.ParseBytes(body) + if status := content.Get("header.status").Int(); status != 0 { + // 如果json信息返回status不为0则立刻抛出错误 + return "", errors.New(fmt.Sprintf("SauceNAO not found, status %d", status)) + } + if content.Get("results.0.header.similarity").Float() < minSimilarity { + return "", errors.New("SauceNAO not found") + } + // TODO 正常发送 + return fmt.Sprintf( + `[SetuTime] 我有把握是这个![CQ:image,file=%s]相似度:%s%% +标题:%s +插画ID:%d +画师:%s +画师ID:%d +直链:https://pixivel.moe/detail?id=%d`, + content.Get("results.0.header.thumbnail").Str, + content.Get("results.0.header.similarity").Str, + content.Get("results.0.data.title").Str, + content.Get("results.0.data.pixiv_id").Int(), + content.Get("results.0.data.member_name").Str, + content.Get("results.0.data.member_id").Int(), + content.Get("results.0.data.pixiv_id").Int(), + ), nil +} diff --git a/setutime/utils/sqlite.go b/setutime/utils/sqlite.go new file mode 100644 index 00000000..07bb4686 --- /dev/null +++ b/setutime/utils/sqlite.go @@ -0,0 +1,224 @@ +package utils + +import ( + "database/sql" + "errors" + "fmt" + "reflect" + + _ "github.com/mattn/go-sqlite3" +) + +type Sqlite struct { + DB *sql.DB + DBPath string +} + +// DBCreate 根据结构体生成数据库table,tag为"id"为主键,自增 +func (db *Sqlite) DBCreate(objptr interface{}) (err error) { + if db.DB == nil { + database, err := sql.Open("sqlite3", db.DBPath) + if err != nil { + return err + } + db.DB = database + } + + table := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (", Struct2name(objptr)) + for i, column := range strcut2columns(objptr) { + table += fmt.Sprintf(" %s %s NULL", column, column2type(objptr, column)) + if i+1 != len(strcut2columns(objptr)) { + table += "," + } else { + table += " );" + } + } + if _, err := db.DB.Exec(table); err != nil { + return err + } + return nil +} + +// DBInsert 根据结构体插入一条数据 +func (db *Sqlite) DBInsert(objptr interface{}) (err error) { + defer func() { + if err := recover(); err != nil { + panic(err) + } + }() + rows, err := db.DB.Query("SELECT * FROM " + Struct2name(objptr)) + if err != nil { + return err + } + defer rows.Close() + + columns, _ := rows.Columns() + index := -1 + names := "(" + insert := "(" + for i, column := range columns { + if column == "id" { + index = i + continue + } + if i != len(columns)-1 { + names += column + "," + insert += "?," + } else { + names += column + ")" + insert += "?)" + } + } + stmt, err := db.DB.Prepare("INSERT INTO " + Struct2name(objptr) + names + " values " + insert) + if err != nil { + return err + } + + value := []interface{}{} + if index == -1 { + value = append(value, struct2values(objptr, columns)...) + } else { + value = append(value, append(struct2values(objptr, columns)[:index], struct2values(objptr, columns)[index+1:]...)...) + } + _, err = stmt.Exec(value...) + if err != nil { + return err + } + return nil +} + +// DBSelect 根据结构体查询对应的表,cmd可为"WHERE id = 0 " +func (db *Sqlite) DBSelect(objptr interface{}, cmd string) (err error) { + rows, err := db.DB.Query(fmt.Sprintf("SELECT * FROM %s %s", Struct2name(objptr), cmd)) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + columns, err := rows.Columns() + if err != nil { + return err + } + err = rows.Scan(struct2addrs(objptr, columns)...) + if err != nil { + return err + } + return nil + } + return errors.New("Database no such elem") +} + +// DBDelete 删除struct对应表的一行,返回错误 +func (db *Sqlite) DBDelete(objptr interface{}, cmd string) (err error) { + stmt, err := db.DB.Prepare(fmt.Sprintf("DELETE FROM %s %s", Struct2name(objptr), cmd)) + if err != nil { + return err + } + _, err = stmt.Exec() + if err != nil { + return err + } + return nil +} + +// DBNum 查询struct对应表的行数,返回行数以及错误 +func (db *Sqlite) DBNum(objptr interface{}) (num int, err error) { + rows, err := db.DB.Query(fmt.Sprintf("SELECT * FROM %s", Struct2name(objptr))) + if err != nil { + return num, err + } + defer rows.Close() + for rows.Next() { + num++ + } + return num, nil +} + +// strcut2columns 反射得到结构体的 tag 数组 +func strcut2columns(objptr interface{}) []string { + var columns []string + elem := reflect.ValueOf(objptr).Elem() + // TODO 判断第一个元素是否为匿名字段 + if elem.Type().Field(0).Anonymous { + elem = elem.Field(0) + } + for i, flen := 0, elem.Type().NumField(); i < flen; i++ { + columns = append(columns, elem.Type().Field(i).Tag.Get("db")) + } + return columns +} + +// Struct2name 反射得到结构体的名字 +func Struct2name(objptr interface{}) string { + return reflect.ValueOf(objptr).Elem().Type().Name() +} + +// column2type 反射得到结构体对应 tag 的 数据库数据类型 +func column2type(objptr interface{}, column string) string { + type_ := "" + elem := reflect.ValueOf(objptr).Elem() + // TODO 判断第一个元素是否为匿名字段 + if elem.Type().Field(0).Anonymous { + elem = elem.Field(0) + } + for i, flen := 0, elem.Type().NumField(); i < flen; i++ { + if column == elem.Type().Field(i).Tag.Get("db") { + type_ = elem.Field(i).Type().String() + } + } + if column == "id" { + return "INTEGER PRIMARY KEY" + } + switch type_ { + case "int64": + return "INT" + case "string": + return "TEXT" + default: + return "TEXT" + } +} + +// struct2addrs 反射得到结构体对应数据库字段的属性地址 +func struct2addrs(objptr interface{}, columns []string) []interface{} { + var addrs []interface{} + elem := reflect.ValueOf(objptr).Elem() + // TODO 判断第一个元素是否为匿名字段 + if elem.Type().Field(0).Anonymous { + elem = elem.Field(0) + } + for _, column := range columns { + for i, flen := 0, elem.Type().NumField(); i < flen; i++ { + if column == elem.Type().Field(i).Tag.Get("db") { + addrs = append(addrs, elem.Field(i).Addr().Interface()) + } + } + } + return addrs +} + +// struct2values 反射得到结构体对应数据库字段的属性值 +func struct2values(objptr interface{}, columns []string) []interface{} { + var values []interface{} + elem := reflect.ValueOf(objptr).Elem() + // TODO 判断第一个元素是否为匿名字段 + if elem.Type().Field(0).Anonymous { + elem = elem.Field(0) + } + for _, column := range columns { + for i, flen := 0, elem.Type().NumField(); i < flen; i++ { + if column == elem.Type().Field(i).Tag.Get("db") { + switch elem.Field(i).Type().String() { + case "int64": + values = append(values, elem.Field(i).Int()) + case "string": + values = append(values, elem.Field(i).String()) + default: + values = append(values, elem.Field(i).String()) + } + } + } + } + return values +} diff --git a/setutime/utils/switch.go b/setutime/utils/switch.go new file mode 100644 index 00000000..c5774877 --- /dev/null +++ b/setutime/utils/switch.go @@ -0,0 +1,5 @@ +package utils + +var ( + XML = true // 图片XML开关,默认开启 +) diff --git a/setutime/utils/utils.go b/setutime/utils/utils.go new file mode 100644 index 00000000..32a8b420 --- /dev/null +++ b/setutime/utils/utils.go @@ -0,0 +1,80 @@ +package utils + +import ( + "fmt" + "os" + "strconv" + "strings" + + zero "github.com/wdvxdr1123/ZeroBot" +) + +// Str2Int string --> int64 +func Str2Int(str string) int64 { + val, _ := strconv.ParseInt(str, 10, 64) + return val +} + +// Int2Str int64 --> string +func Int2Str(val int64) string { + str := strconv.FormatInt(val, 10) + return str +} + +// PathExecute 返回当前运行目录 +func PathExecute() string { + dir, err := os.Getwd() + if err != nil { + panic(err) + } + return dir + "/" +} + +// CreatePath 生成路径或文件所对应的目录 +func CreatePath(path string) { + length := len(path) + switch { + case path[length:] != "/": + path = path[:strings.LastIndex(path, "/")] + case path[length:] != "\\": + path = path[:strings.LastIndex(path, "\\")] + default: + // + } + if !PathExists(path) { + err := os.MkdirAll(path, 0644) + if err != nil { + panic(err) + } + } +} + +// PathExists 判断路径或文件是否存在 +func PathExists(path string) bool { + _, err := os.Stat(path) + return err == nil || os.IsExist(err) +} + +// FileSize 获取文件大小 +func FileSize(file string) int64 { + if fi, err := os.Stat(file); err == nil { + return fi.Size() + } + return 0 +} + +// Min 返回两数最小值 +func Min(a, b int) int { + switch { + default: + return a + case a > b: + return b + case a < b: + return a + } +} + +func SendError(event zero.Event, err error) { + zero.Send(event, fmt.Sprintf("ERROR: %v", err)) +}