feat:添加月幕galgame网站图 (#138)

* feat:添加月幕galgame网站图

* fix:修lint

* fix:不重复更新

* fix:增加命令提醒语

* fix:增加日语匹配

* fix:换位置加锁

* fix:隐式声明

* fix:copy数组

* Update ai_tts.go

Co-authored-by: 源文雨 <41315874+fumiama@users.noreply.github.com>
This commit is contained in:
himawari 2022-02-27 18:57:52 +08:00 committed by GitHub
parent a469000d7a
commit 5c620d6268
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 416 additions and 4 deletions

View File

@ -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...**
## 使用方法

View File

@ -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站推送

View File

@ -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()
}

276
plugin/ymgal/model.go Normal file
View File

@ -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)
}
}

95
plugin/ymgal/ymgal.go Normal file
View File

@ -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: 可能被风控了"))
}
}