diff --git a/README.md b/README.md index e6d14178..41e97133 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,12 @@ zerobot [-h] [-t token] [-u url] [-n nickname] [-p prefix] [-d|w] [-g 监听地 - **煎蛋网无聊图** `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/jandan"` - [x] 来份屌图 - [x] 更新屌图 +- **月幕galgame图** `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ymgal"` + - [x] 随机galCG + - [x] 随机gal表情包 + - [x] galCG[xxx] + - [x] gal表情包[xxx] + - [x] 更新gal - **TODO...** ## 使用方法 diff --git a/main.go b/main.go index 2ff141a0..31f6d5d5 100644 --- a/main.go +++ b/main.go @@ -100,6 +100,7 @@ import ( _ "github.com/FloatTech/ZeroBot-Plugin/plugin/vtb_quotation" // vtb语录 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wangyiyun" // 网易云音乐热评 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordle" // 猜单词 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ymgal" // 月幕galgame // _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wtf" // 鬼东西 // _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bilibili_push" // b站推送 diff --git a/plugin/ai_reply/tts.go b/plugin/ai_reply/ai_tts.go similarity index 72% rename from plugin/ai_reply/tts.go rename to plugin/ai_reply/ai_tts.go index 86ab7e65..e9cbab18 100644 --- a/plugin/ai_reply/tts.go +++ b/plugin/ai_reply/ai_tts.go @@ -4,6 +4,7 @@ import ( "errors" "regexp" "strconv" + "sync" "github.com/pkumza/numcn" log "github.com/sirupsen/logrus" @@ -38,18 +39,26 @@ var ( ) type ttsInstances struct { + sync.RWMutex m map[string]tts.TTS l []string } func (t *ttsInstances) List() []string { - return t.l + t.RLock() + cl := make([]string, len(t.l)) + _ = copy(cl, t.l) + t.RUnlock() + return cl } func init() { engine := control.Register(ttsServiceName, order.AcquirePrio(), &control.Options{ - DisableOnDefault: false, - Help: "语音回复(包括拟声鸟和百度)\n- @Bot 任意文本(任意一句话回复)\n- 设置语音模式拟声鸟阿梓 | 设置语音模式拟声鸟药水哥 | 设置语音模式百度女声 | 设置语音模式百度男声| 设置语音模式百度度逍遥 | 设置语音模式百度度丫丫", + DisableOnDefault: true, + Help: "语音回复(包括拟声鸟和百度)\n" + + "- @Bot 任意文本(任意一句话回复)\n" + + "- 设置语音模式[拟声鸟阿梓 | 拟声鸟药水哥 | 百度女声 | 百度男声| 百度度逍遥 | 百度度丫丫]\n" + + "- 设置默认语音模式[拟声鸟阿梓 | 拟声鸟药水哥 | 百度女声 | 百度男声| 百度度逍遥 | 百度度丫丫]\n", }) engine.OnMessage(zero.OnlyToMe).SetBlock(true).Limit(ctxext.LimitByUser). Handle(func(ctx *zero.Ctx) { @@ -78,7 +87,13 @@ func init() { ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(err)) return } - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("成功")) + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("设置成功,当前模式为", param)) + }) + engine.OnRegex(`^设置默认语音模式(.*)$`, ctxext.FirstValueInList(t)).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + param := ctx.State["regex_matched"].([]string)[1] + t.setDefaultSoundMode(param) + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("设置成功,默认模式为", param)) }) } @@ -93,12 +108,14 @@ func (t *ttsInstances) setSoundMode(ctx *zero.Ctx, name string) error { gid = -ctx.Event.UserID } var index int64 + t.RLock() for i, s := range t.l { if s == name { index = int64(i) break } } + t.RUnlock() m, ok := control.Lookup(ttsServiceName) if !ok { return errors.New("no such plugin") @@ -113,6 +130,8 @@ func (t *ttsInstances) getSoundMode(ctx *zero.Ctx) (name string) { } m, ok := control.Lookup(ttsServiceName) if ok { + t.RLock() + defer t.RUnlock() index := m.GetData(gid) if int(index) < len(t.l) { return t.l[index] @@ -120,3 +139,18 @@ func (t *ttsInstances) getSoundMode(ctx *zero.Ctx) (name string) { } return "拟声鸟阿梓" } + +func (t *ttsInstances) setDefaultSoundMode(name string) { + var index int + t.RLock() + for _, s := range t.l { + if s == name { + break + } + index++ + } + t.RUnlock() + t.Lock() + t.l[0], t.l[index] = t.l[index], t.l[0] + t.Unlock() +} diff --git a/plugin/ymgal/model.go b/plugin/ymgal/model.go new file mode 100644 index 00000000..1ab94d31 --- /dev/null +++ b/plugin/ymgal/model.go @@ -0,0 +1,276 @@ +package ymgal + +import ( + "fmt" + "github.com/antchfx/htmlquery" + _ "github.com/fumiama/sqlite3" // import sql + "github.com/jinzhu/gorm" + log "github.com/sirupsen/logrus" + "math/rand" + "net/url" + "os" + "regexp" + "strconv" + "sync" + "time" +) + +// gdb 得分数据库 +var gdb *ymgaldb + +// ymgaldb galgame图片数据库 +type ymgaldb gorm.DB + +var mu sync.RWMutex + +// ymgal gal图片储存结构体 +type ymgal struct { + ID int64 `gorm:"column:id" ` + Title string `gorm:"column:title" ` + PictureType string `gorm:"column:picture_type" ` + PictureDescription string `gorm:"column:picture_description;type:varchar(1024)" ` + PictureList string `gorm:"column:picture_list;type:varchar(20000)" ` +} + +// TableName ... +func (ymgal) TableName() string { + return "ymgal" +} + +// initialize 初始化ymgaldb数据库 +func initialize(dbpath string) *ymgaldb { + var err error + if _, err = os.Stat(dbpath); err != nil || os.IsNotExist(err) { + // 生成文件 + f, err := os.Create(dbpath) + if err != nil { + return nil + } + defer f.Close() + } + gdb, err := gorm.Open("sqlite3", dbpath) + if err != nil { + panic(err) + } + gdb.AutoMigrate(&ymgal{}) + return (*ymgaldb)(gdb) +} + +func (gdb *ymgaldb) insertOrUpdateYmgalByID(id int64, title, pictureType, pictureDescription, pictureList string) (err error) { + db := (*gorm.DB)(gdb) + y := ymgal{ + ID: id, + Title: title, + PictureType: pictureType, + PictureDescription: pictureDescription, + PictureList: pictureList, + } + if err = db.Debug().Model(&ymgal{}).First(&y, "id = ? ", id).Error; err != nil { + if gorm.IsRecordNotFoundError(err) { + err = db.Debug().Model(&ymgal{}).Create(&y).Error // newUser not user + } + } else { + err = db.Debug().Model(&ymgal{}).Where("id = ? ", id).Update(map[string]interface{}{ + "title": title, + "picture_type": pictureType, + "picture_description": pictureDescription, + "picture_list": pictureList, + }).Error + } + return +} + +func (gdb *ymgaldb) getYmgalByID(id string) (y ymgal) { + db := (*gorm.DB)(gdb) + db.Debug().Model(&ymgal{}).Where("id = ?", id).Take(&y) + return +} + +func (gdb *ymgaldb) randomYmgal(pictureType string) (y ymgal) { + db := (*gorm.DB)(gdb) + var count int + s := db.Debug().Model(&ymgal{}).Where("picture_type = ?", pictureType).Count(&count) + if count == 0 { + return + } + s.Offset(rand.Intn(count)).Take(&y) + return +} + +func (gdb *ymgaldb) getYmgalByKey(pictureType, key string) (y ymgal) { + db := (*gorm.DB)(gdb) + var count int + s := db.Debug().Model(&ymgal{}).Where("picture_type = ? and (picture_description like ? or title like ?) ", pictureType, "%"+key+"%", "%"+key+"%").Count(&count) + if count == 0 { + return + } + s.Offset(rand.Intn(count)).Take(&y) + return +} + +const ( + webURL = "https://www.ymgal.com" + cgType = "Gal CG" + emoticonType = "其他" + webPicURL = webURL + "/co/picset/" + reNumber = `\d+` +) + +var ( + cgURL = webURL + "/search?type=picset&sort=default&category=" + url.QueryEscape(cgType) + "&page=" + emoticonURL = webURL + "/search?type=picset&sort=default&category=" + url.QueryEscape(emoticonType) + "&page=" + commonPageNumberExpr = "//*[@id='pager-box']/div/a[@class='icon item pager-next']/preceding-sibling::a[1]/text()" + cgIDList []string + emoticonIDList []string +) + +func initPageNumber() (maxCgPageNumber, maxEmoticonPageNumber int) { + doc, err := htmlquery.LoadURL(cgURL + "1") + if err != nil { + log.Errorln("[ymgal]:", err) + } + maxCgPageNumber, err = strconv.Atoi(htmlquery.FindOne(doc, commonPageNumberExpr).Data) + if err != nil { + log.Errorln("[ymgal]:", err) + } + doc, err = htmlquery.LoadURL(emoticonURL + "1") + if err != nil { + log.Errorln("[ymgal]:", err) + } + maxEmoticonPageNumber, err = strconv.Atoi(htmlquery.FindOne(doc, commonPageNumberExpr).Data) + if err != nil { + log.Errorln("[ymgal]:", err) + } + return +} + +func getPicID(pageNumber int, pictureType string) { + var picURL string + if pictureType == cgType { + picURL = cgURL + strconv.Itoa(pageNumber) + } else if pictureType == emoticonType { + picURL = emoticonURL + strconv.Itoa(pageNumber) + } + doc, err := htmlquery.LoadURL(picURL) + if err != nil { + log.Errorln("[ymgal]:", err) + } + list := htmlquery.Find(doc, "//*[@id='picset-result-list']/ul/div/div[1]/a") + for i := 0; i < len(list); i++ { + re := regexp.MustCompile(reNumber) + picID := re.FindString(list[i].Attr[0].Val) + if pictureType == cgType { + cgIDList = append(cgIDList, picID) + } else if pictureType == emoticonType { + emoticonIDList = append(emoticonIDList, picID) + } + } + +} + +func updatePic() { + maxCgPageNumber, maxEmoticonPageNumber := initPageNumber() + for i := 1; i <= maxCgPageNumber; i++ { + getPicID(i, cgType) + time.Sleep(time.Millisecond * 500) + } + for i := 1; i <= maxEmoticonPageNumber; i++ { + getPicID(i, emoticonType) + time.Sleep(time.Millisecond * 500) + } +CGLOOP: + for i := len(cgIDList) - 1; i >= 0; i-- { + mu.RLock() + y := gdb.getYmgalByID(cgIDList[i]) + mu.RUnlock() + if y.PictureList == "" { + mu.Lock() + storeCgPic(cgIDList[i]) + mu.Unlock() + } else { + break CGLOOP + } + time.Sleep(time.Millisecond * 500) + } +EMOTICONLOOP: + for i := len(emoticonIDList) - 1; i >= 0; i-- { + mu.RLock() + y := gdb.getYmgalByID(emoticonIDList[i]) + mu.RUnlock() + if y.PictureList == "" { + mu.Lock() + storeEmoticonPic(emoticonIDList[i]) + mu.Unlock() + } else { + break EMOTICONLOOP + } + time.Sleep(time.Millisecond * 500) + } +} + +func storeCgPic(picIDStr string) { + picID, err := strconv.ParseInt(picIDStr, 10, 64) + if err != nil { + log.Errorln("[ymgal]:", err) + } + pictureType := cgType + doc, err := htmlquery.LoadURL(webPicURL + picIDStr) + if err != nil { + log.Errorln("[ymgal]:", err) + } + title := htmlquery.FindOne(doc, "//meta[@name='name']").Attr[1].Val + pictureDescription := htmlquery.FindOne(doc, "//meta[@name='description']").Attr[1].Val + pictureNumberStr := htmlquery.FindOne(doc, "//div[@class='meta-info']/div[@class='meta-right']/span[2]/text()").Data + re := regexp.MustCompile(reNumber) + pictureNumber, err := strconv.Atoi(re.FindString(pictureNumberStr)) + if err != nil { + log.Errorln("[ymgal]:", err) + } + pictureList := "" + for i := 1; i <= pictureNumber; i++ { + picURL := htmlquery.FindOne(doc, fmt.Sprintf("//*[@id='main-picset-warp']/div/div[2]/div/div[@class='swiper-wrapper']/div[%d]", i)).Attr[1].Val + if i == 1 { + pictureList += picURL + } else { + pictureList += "," + picURL + } + } + err = gdb.insertOrUpdateYmgalByID(picID, title, pictureType, pictureDescription, pictureList) + if err != nil { + log.Errorln("[ymgal]:", err) + } + +} + +func storeEmoticonPic(picIDStr string) { + picID, err := strconv.ParseInt(picIDStr, 10, 64) + if err != nil { + log.Errorln("[ymgal]:", err) + } + pictureType := emoticonType + doc, err := htmlquery.LoadURL(webPicURL + picIDStr) + if err != nil { + log.Errorln("[ymgal]:", err) + } + title := htmlquery.FindOne(doc, "//meta[@name='name']").Attr[1].Val + pictureDescription := htmlquery.FindOne(doc, "//meta[@name='description']").Attr[1].Val + pictureNumberStr := htmlquery.FindOne(doc, "//div[@class='meta-info']/div[@class='meta-right']/span[2]/text()").Data + re := regexp.MustCompile(reNumber) + pictureNumber, err := strconv.Atoi(re.FindString(pictureNumberStr)) + if err != nil { + log.Errorln("[ymgal]:", err) + } + pictureList := "" + for i := 1; i <= pictureNumber; i++ { + picURL := htmlquery.FindOne(doc, fmt.Sprintf("//*[@id='main-picset-warp']/div/div[@class='stream-list']/div[%d]/img", i)).Attr[1].Val + if i == 1 { + pictureList += picURL + } else { + pictureList += "," + picURL + } + } + err = gdb.insertOrUpdateYmgalByID(picID, title, pictureType, pictureDescription, pictureList) + if err != nil { + log.Errorln("[ymgal]:", err) + } +} diff --git a/plugin/ymgal/ymgal.go b/plugin/ymgal/ymgal.go new file mode 100644 index 00000000..1f34bddb --- /dev/null +++ b/plugin/ymgal/ymgal.go @@ -0,0 +1,95 @@ +// Package ymgal 月幕galgame +package ymgal + +import ( + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/control/order" + "github.com/FloatTech/zbputils/ctxext" + "github.com/FloatTech/zbputils/file" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + "log" + "strings" +) + +func init() { + engine := control.Register("ymgal", order.AcquirePrio(), &control.Options{ + DisableOnDefault: false, + Help: "月幕galgame\n- 随机galCG\n- 随机gal表情包\n- galCG[xxx]\n- gal表情包[xxx]\n- 更新gal\n", + PublicDataFolder: "Ymgal", + }) + dbfile := engine.DataFolder() + "ymgal.db" + go func() { + defer order.DoneOnExit()() + _, _ = file.GetLazyData(dbfile, false, false) + gdb = initialize(dbfile) + log.Println("[ymgal]加载月幕gal数据库") + }() + engine.OnRegex("^随机gal(CG|表情包)$").Limit(ctxext.LimitByUser).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + ctx.Send("少女祈祷中......") + pictureType := ctx.State["regex_matched"].([]string)[1] + var y ymgal + if pictureType == "表情包" { + y = gdb.randomYmgal(emoticonType) + } else { + y = gdb.randomYmgal(cgType) + } + sendYmgal(y, ctx) + }) + engine.OnRegex("^gal(CG|表情包)([一-龥ぁ-んァ-ヶA-Za-z0-9]{1,25})$").Limit(ctxext.LimitByUser).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + ctx.Send("少女祈祷中......") + pictureType := ctx.State["regex_matched"].([]string)[1] + key := ctx.State["regex_matched"].([]string)[2] + var y ymgal + if pictureType == "CG" { + y = gdb.getYmgalByKey(cgType, key) + } else { + y = gdb.getYmgalByKey(emoticonType, key) + } + sendYmgal(y, ctx) + }) + engine.OnFullMatch("更新gal", zero.SuperUserPermission).SetBlock(true).Handle( + func(ctx *zero.Ctx) { + ctx.Send("少女祈祷中......") + updatePic() + ctx.Send("ymgal数据库已更新") + }) +} + +func sendYmgal(y ymgal, ctx *zero.Ctx) { + if y.PictureList == "" { + ctx.SendChain(message.Text(zero.BotConfig.NickName[0] + "暂时没有这样的图呢")) + return + } + m := message.Message{ + message.CustomNode( + ctx.Event.Sender.NickName, + ctx.Event.UserID, + y.Title, + )} + if y.PictureDescription != "" { + m = append(m, + message.CustomNode( + ctx.Event.Sender.NickName, + ctx.Event.UserID, + y.PictureDescription, + )) + } + for _, v := range strings.Split(y.PictureList, ",") { + m = append(m, + message.CustomNode( + ctx.Event.Sender.NickName, + ctx.Event.UserID, + []message.MessageSegment{ + message.Image(v), + }), + ) + } + if id := ctx.SendGroupForwardMessage( + ctx.Event.GroupID, + m).Get("message_id").Int(); id == 0 { + ctx.SendChain(message.Text("ERROR: 可能被风控了")) + } +}